From c420f0a1cf7bd94e4a17394020f8f20e46a1daff Mon Sep 17 00:00:00 2001 From: indar suthar Date: Sat, 15 Nov 2025 19:16:34 +0530 Subject: [PATCH] feat: rate limiting on auth --- backend/package-lock.json | 29 +++++++++++++++++++++++ backend/package.json | 1 + backend/src/app.ts | 3 +++ backend/src/middleware/rateLimiter.ts | 30 +++++++++++++++++++++++ backend/src/routes/authRoutes.ts | 6 ++--- frontend/src/pages/Signin.tsx | 23 +++++++++++++----- frontend/src/pages/Signup.tsx | 34 +++++++++++++++++++-------- 7 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 backend/src/middleware/rateLimiter.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 86bcc7a..f3f7b8c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,6 +14,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", + "express-rate-limit": "^8.2.1", "jsonwebtoken": "^9.0.2", "mongodb": "^6.20.0", "mongoose": "^8.19.2", @@ -1044,6 +1045,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -1081,6 +1083,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -1352,6 +1372,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/backend/package.json b/backend/package.json index 603c6ef..2998a85 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,6 +17,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", + "express-rate-limit": "^8.2.1", "jsonwebtoken": "^9.0.2", "mongodb": "^6.20.0", "mongoose": "^8.19.2", diff --git a/backend/src/app.ts b/backend/src/app.ts index bdede31..870f460 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -12,6 +12,9 @@ import cors from "cors"; dotenv.config(); const app = express(); +if (process.env.TRUST_PROXY === "true") { + app.set("trust proxy", true); +} app.use(requestLogger); app.use(express.json()); app.use(cookieParser()); // <-- Add this middleware HERE diff --git a/backend/src/middleware/rateLimiter.ts b/backend/src/middleware/rateLimiter.ts new file mode 100644 index 0000000..3e85518 --- /dev/null +++ b/backend/src/middleware/rateLimiter.ts @@ -0,0 +1,30 @@ +import rateLimit from "express-rate-limit"; +import type { Request, Response } from "express"; +import logger from "../utils/logger.js"; + +export const authRateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 5, + standardHeaders: true, + legacyHeaders: false, + handler: (req: Request, res: Response) => { + const clientIP = req.ip || req.socket.remoteAddress || "unknown"; + logger.warn( + `Rate limit exceeded for IP: ${clientIP} on ${req.method} ${req.originalUrl}` + ); + + const retryAfter = req.rateLimit?.resetTime + ? Math.round((req.rateLimit.resetTime - Date.now()) / 1000) + : 900; + + res.status(429).json({ + success: false, + message: "Too many authentication attempts. Please try again after 15 minutes.", + retryAfter, + }); + }, + keyGenerator: (req: Request) => { + return req.ip || req.socket.remoteAddress || "unknown"; + }, +}); + diff --git a/backend/src/routes/authRoutes.ts b/backend/src/routes/authRoutes.ts index 1a72441..c23b49d 100644 --- a/backend/src/routes/authRoutes.ts +++ b/backend/src/routes/authRoutes.ts @@ -9,6 +9,7 @@ import { import passport from "passport"; import { Session } from "../models/sessionModel.js"; import { protect } from "../middleware/authMiddleware.js"; +import { authRateLimiter } from "../middleware/rateLimiter.js"; import { generateToken, generateRefreshToken } from "../utils/generateToken.js"; import jwt from "jsonwebtoken"; import dotenv from "dotenv"; @@ -20,9 +21,8 @@ dotenv.config(); const router = express.Router(); const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:5173"; -// REST endpoints -router.post("/signup", registerUser); -router.post("/signin", loginUser); +router.post("/signup", authRateLimiter, registerUser); +router.post("/signin", authRateLimiter, loginUser); router.post("/logout", logoutUser); router.get("/refresh", handleRefreshToken); router.get("/me", protect, getUserProfile); diff --git a/frontend/src/pages/Signin.tsx b/frontend/src/pages/Signin.tsx index 4d0107f..9cc3813 100644 --- a/frontend/src/pages/Signin.tsx +++ b/frontend/src/pages/Signin.tsx @@ -54,12 +54,23 @@ const SignIn = () => { reset(); navigate("/"); // redirect to home/dashboard } catch (error: any) { - toast({ - title: "Error", - description: - error.response?.data?.message || "Login failed. Please try again.", - variant: "destructive", - }); + // Handle rate limiting errors + if (error.response?.status === 429) { + const retryAfter = error.response?.data?.retryAfter || 15; + const minutes = Math.ceil(retryAfter / 60); + toast({ + title: "Too Many Attempts", + description: `Too many login attempts. Please try again after ${minutes} minute${minutes > 1 ? 's' : ''}.`, + variant: "destructive", + }); + } else { + toast({ + title: "Error", + description: + error.response?.data?.message || "Login failed. Please try again.", + variant: "destructive", + }); + } } finally { setIsLoading(false); } diff --git a/frontend/src/pages/Signup.tsx b/frontend/src/pages/Signup.tsx index 9d51ad9..ae80184 100644 --- a/frontend/src/pages/Signup.tsx +++ b/frontend/src/pages/Signup.tsx @@ -83,16 +83,30 @@ const SignUp = () => { reset(); setTimeout(() => navigate("/signin"), 1500); } catch (error: any) { - const msg = - error.response?.data?.message || - "Registration failed. Please try again."; - toast({ - title: "Error", - description: msg, - variant: "destructive", - }); - setServerMessage(msg); - setIsError(true); + // Handle rate limiting errors + if (error.response?.status === 429) { + const retryAfter = error.response?.data?.retryAfter || 15; + const minutes = Math.ceil(retryAfter / 60); + const msg = `Too many registration attempts. Please try again after ${minutes} minute${minutes > 1 ? 's' : ''}.`; + toast({ + title: "Too Many Attempts", + description: msg, + variant: "destructive", + }); + setServerMessage(msg); + setIsError(true); + } else { + const msg = + error.response?.data?.message || + "Registration failed. Please try again."; + toast({ + title: "Error", + description: msg, + variant: "destructive", + }); + setServerMessage(msg); + setIsError(true); + } } finally { setIsLoading(false); }