From 9676a4e28837be808d06a90661bdd447bcf232b0 Mon Sep 17 00:00:00 2001 From: Ridanshi Date: Wed, 27 May 2026 01:07:59 +0530 Subject: [PATCH] fix(auth): rate limit login and signup --- backend/config/authRateLimit.js | 25 +++++++++++ backend/package.json | 1 + backend/routes/auth.js | 5 ++- package.json | 1 + spec/auth.rate-limit.spec.cjs | 73 +++++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 backend/config/authRateLimit.js create mode 100644 spec/auth.rate-limit.spec.cjs diff --git a/backend/config/authRateLimit.js b/backend/config/authRateLimit.js new file mode 100644 index 00000000..4dc990af --- /dev/null +++ b/backend/config/authRateLimit.js @@ -0,0 +1,25 @@ +const rateLimit = require('express-rate-limit'); + +const AUTH_RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; +const AUTH_RATE_LIMIT_MAX = 10; + +function createAuthRateLimiter(options = {}) { + return rateLimit({ + windowMs: AUTH_RATE_LIMIT_WINDOW_MS, + max: AUTH_RATE_LIMIT_MAX, + standardHeaders: true, + legacyHeaders: false, + ...options, + }); +} + +const loginLimiter = createAuthRateLimiter(); +const signupLimiter = createAuthRateLimiter(); + +module.exports = { + AUTH_RATE_LIMIT_MAX, + AUTH_RATE_LIMIT_WINDOW_MS, + createAuthRateLimiter, + loginLimiter, + signupLimiter, +}; diff --git a/backend/package.json b/backend/package.json index 74ab9dd7..21858d94 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,6 +17,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", + "express-rate-limit": "^7.5.0", "express-session": "^1.18.1", "mongoose": "^8.8.2", "passport": "^0.7.0", diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 7c2cda78..a3e260bf 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -3,10 +3,11 @@ const passport = require("passport"); const User = require("../models/User"); const { signupSchema, loginSchema } = require("../validators/authValidator"); const { validateRequest } = require("../validators/validationRequest"); +const { loginLimiter, signupLimiter } = require("../config/authRateLimit"); const router = express.Router(); // Signup route -router.post("/signup", validateRequest(signupSchema), async (req, res) => { +router.post("/signup", signupLimiter, validateRequest(signupSchema), async (req, res) => { const { username, email, password } = req.body; @@ -31,7 +32,7 @@ router.post("/signup", validateRequest(signupSchema), async (req, res) => { }); // Login route -router.post("/login", validateRequest(loginSchema), passport.authenticate('local'), (req, res) => { +router.post("/login", loginLimiter, validateRequest(loginSchema), passport.authenticate('local'), (req, res) => { res.status(200).json( { message: 'Login successful', user: req.user } ); }); diff --git a/package.json b/package.json index 43ad31cc..40a9b675 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@vitejs/plugin-react": "^4.3.3", "axios": "^1.7.7", "express": "^5.2.1", + "express-rate-limit": "^7.5.0", "framer-motion": "^12.23.12", "lucide-react": "^0.525.0", "mongoose": "^9.6.2", diff --git a/spec/auth.rate-limit.spec.cjs b/spec/auth.rate-limit.spec.cjs new file mode 100644 index 00000000..84f43212 --- /dev/null +++ b/spec/auth.rate-limit.spec.cjs @@ -0,0 +1,73 @@ +const express = require('express'); +const request = require('supertest'); + +const { + AUTH_RATE_LIMIT_MAX, + AUTH_RATE_LIMIT_WINDOW_MS, + createAuthRateLimiter, +} = require('../backend/config/authRateLimit'); + +function createRateLimitedAuthApp(max = 2) { + const app = express(); + + app.use(express.json()); + app.post('/api/auth/login', createAuthRateLimiter({ max }), (req, res) => { + res.status(200).json({ message: 'Login successful' }); + }); + app.post('/api/auth/signup', createAuthRateLimiter({ max }), (req, res) => { + res.status(201).json({ message: 'User created successfully' }); + }); + + return app; +} + +describe('Auth rate limiting', () => { + it('uses the configured production auth limiter baseline', () => { + expect(AUTH_RATE_LIMIT_MAX).toBe(10); + expect(AUTH_RATE_LIMIT_WINDOW_MS).toBe(15 * 60 * 1000); + }); + + it('allows legitimate login requests under the threshold', async () => { + const app = createRateLimitedAuthApp(); + + const res = await request(app) + .post('/api/auth/login') + .send({ email: 'user@example.com', password: 'password123' }); + + expect(res.status).toBe(200); + expect(res.body.message).toBe('Login successful'); + }); + + it('blocks excessive login requests with HTTP 429', async () => { + const app = createRateLimitedAuthApp(); + + await request(app).post('/api/auth/login').send({}); + await request(app).post('/api/auth/login').send({}); + const res = await request(app).post('/api/auth/login').send({}); + + expect(res.status).toBe(429); + }); + + it('blocks excessive signup requests with HTTP 429', async () => { + const app = createRateLimitedAuthApp(); + + await request(app).post('/api/auth/signup').send({}); + await request(app).post('/api/auth/signup').send({}); + const res = await request(app).post('/api/auth/signup').send({}); + + expect(res.status).toBe(429); + }); + + it('returns standard rate-limit headers without legacy headers', async () => { + const app = createRateLimitedAuthApp(); + + const res = await request(app) + .post('/api/auth/login') + .send({ email: 'user@example.com', password: 'password123' }); + + expect(res.headers['ratelimit-limit']).toBeDefined(); + expect(res.headers['ratelimit-remaining']).toBeDefined(); + expect(res.headers['ratelimit-reset']).toBeDefined(); + expect(res.headers['x-ratelimit-limit']).toBeUndefined(); + }); +});