Files
games-express-api/middleware/rateLimit.js
T
john-okeefe 39f1c88255 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.
2026-09-05 22:48:48 -04:00

38 lines
1.3 KiB
JavaScript

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.',
},
})