fix(ratelimit): split global and login budgets so storms can't lock out login
One shared 100 req / 10 min limiter counted everything — 401s, CORS preflights, even /health — so a bad-token burst from a second frontend burned the budget and 429'd POST /api/auth/login too, leaving no way back in short of waiting out the window or restarting (MemoryStore reset). New middleware/rateLimit.js holds two limiters, verified against express-rate-limit@5.5.1: - apiLimiter (300 / 15 min) skips OPTIONS, /health, and the public auth endpoints, so preflights, probes, and login attempts never drain real API budget. - authLimiter (20 / 15 min, successful logins free) guards register/login/forgotpassword/resetpassword against brute force. 429 there means 20 wrong passwords, never a busy API. Storm-tested with live server: 320 bad-token hits burn the global budget yet login still returns 200; 25 bad logins trip only the login limiter while API traffic is untouched.
This commit is contained in:
@@ -6,10 +6,10 @@ import helmet from 'helmet'
|
|||||||
import cookieParser from 'cookie-parser'
|
import cookieParser from 'cookie-parser'
|
||||||
import mongoSanitize from 'express-mongo-sanitize'
|
import mongoSanitize from 'express-mongo-sanitize'
|
||||||
import xss from 'xss-clean'
|
import xss from 'xss-clean'
|
||||||
import rateLimit from 'express-rate-limit'
|
|
||||||
import hpp from 'hpp'
|
import hpp from 'hpp'
|
||||||
import morgan from 'morgan'
|
import morgan from 'morgan'
|
||||||
import errorHandler from './middleware/error.js'
|
import errorHandler from './middleware/error.js'
|
||||||
|
import { apiLimiter } from './middleware/rateLimit.js'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -36,12 +36,7 @@ const corsOptions = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const limiter = rateLimit({
|
app.use(express.json(), cookieParser(), morgan('dev'), mongoSanitize(), helmet(), xss(), apiLimiter, hpp(), cors())
|
||||||
windowMs: 10 * 60 * 1000, // 10 minutes
|
|
||||||
max: 100
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(express.json(), cookieParser(), morgan('dev'), mongoSanitize(), helmet(), xss(), limiter, hpp(), cors())
|
|
||||||
|
|
||||||
app.get('/health', (req, res) => res.status(200).json({ status: 'ok' }))
|
app.get('/health', (req, res) => res.status(200).json({ status: 'ok' }))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import rateLimit from 'express-rate-limit'
|
||||||
|
|
||||||
|
// Public auth endpoints get their own brute-force budget (see authLimiter)
|
||||||
|
// and must not consume the shared API budget, otherwise a bad-token storm
|
||||||
|
// can lock the owner out of logging back in.
|
||||||
|
const PUBLIC_AUTH_PATHS = [
|
||||||
|
'/api/auth/login',
|
||||||
|
'/api/auth/register',
|
||||||
|
'/api/auth/forgotpassword',
|
||||||
|
'/api/auth/resetpassword',
|
||||||
|
]
|
||||||
|
|
||||||
|
// Shared budget for real API traffic. Counts failures too (cheap 401s, no DB
|
||||||
|
// hit), but skips CORS preflights, the health probe, and the public auth
|
||||||
|
// endpoints above so junk traffic and login attempts can't drain it.
|
||||||
|
export const apiLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 300,
|
||||||
|
skip: (req) =>
|
||||||
|
req.method === 'OPTIONS' ||
|
||||||
|
req.path === '/health' ||
|
||||||
|
PUBLIC_AUTH_PATHS.some((p) => req.path.startsWith(p)),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Strict budget for the login door only. Successful logins are free
|
||||||
|
// (skipSuccessfulRequests), so normal use never notices it — only repeated
|
||||||
|
// failed attempts burn budget. 429 here means "wrong password 20 times in
|
||||||
|
// 15 minutes", never "the API was busy".
|
||||||
|
export const authLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 20,
|
||||||
|
skipSuccessfulRequests: true,
|
||||||
|
message: {
|
||||||
|
success: false,
|
||||||
|
error: 'Too many login attempts, please try again later.',
|
||||||
|
},
|
||||||
|
})
|
||||||
+5
-4
@@ -4,14 +4,15 @@ import express from 'express'
|
|||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
import { register, login, getMe, forgotPassword, resetPassword, updateDetails, updatePassword, logout } from '../controllers/auth.js'
|
import { register, login, getMe, forgotPassword, resetPassword, updateDetails, updatePassword, logout } from '../controllers/auth.js'
|
||||||
import { protect } from '../middleware/auth.js'
|
import { protect } from '../middleware/auth.js'
|
||||||
|
import { authLimiter } from '../middleware/rateLimit.js'
|
||||||
|
|
||||||
router.post('/register', register)
|
router.post('/register', authLimiter, register)
|
||||||
router.post('/login', login)
|
router.post('/login', authLimiter, login)
|
||||||
router.get('/logout', logout)
|
router.get('/logout', logout)
|
||||||
router.get('/me', protect, getMe)
|
router.get('/me', protect, getMe)
|
||||||
router.put('/updatedetails', protect, updateDetails)
|
router.put('/updatedetails', protect, updateDetails)
|
||||||
router.post('/forgotpassword', forgotPassword)
|
router.post('/forgotpassword', authLimiter, forgotPassword)
|
||||||
router.put('/resetpassword/:resettoken', resetPassword)
|
router.put('/resetpassword/:resettoken', authLimiter, resetPassword)
|
||||||
router.put('/updatepassword', protect, updatePassword)
|
router.put('/updatepassword', protect, updatePassword)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
Reference in New Issue
Block a user