Security HardeningLesson 5.1
How to add rate limiting to login endpoints
express-rate-limit, sliding window, brute force prevention, per-IP limiting, per-username limiting, lockout vs slowdown, 429 status code
Why Login Endpoints Need Rate Limiting
Without rate limiting, an attacker can make thousands of login attempts per second. bcrypt slows individual comparisons, but at scale that protection erodes. Rate limiting stops the attack before password verification even runs.
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // max 10 attempts per IP per window
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many login attempts, try again later' },
skipSuccessfulRequests: true // only count failures
});
app.post('/auth/login', loginLimiter, loginController);
Per-Username Rate Limiting
IP-based rate limiting fails against distributed attacks (botnet using many IPs). Supplement with per-username limiting: track failed attempts per email address in Redis or a database. After 5 failures on a specific account, require a CAPTCHA or temporary lock.
// Pseudo-code: check per-email failures before verifying password
const failures = await redis.incr(`login_fails:${email}`);
await redis.expire(`login_fails:${email}`, 900); // 15 min TTL
if (failures > 5) return res.status(429).json({ error: 'Account temporarily locked' });
Always return a 429 status with a Retry-After header so clients and browsers handle the response correctly.
