feat(19-03): implement POST /api/auth/local/login (rate-limit + lockout) + logout
- Rate-limit: per-IP in-memory Map; 5 failures → 429, 10 → 423 (lockedOut) - Counter increments even on 429 so brute-force accumulates toward lockout - Timing-safe: DUMMY_HASH ensures verifyPassword runs for unknown usernames (T-19-12) - noEchoHook: Zod errors never echo submitted values (T-19-14) - Same 401 body for wrong-password and unknown-username (no enumeration) - POST+GET /local/logout clear the local-session cookie
This commit is contained in:
@@ -87,12 +87,21 @@ localAuthRouter.post('/local/login', zValidator('json', loginSchema, noEchoHook)
|
|||||||
const attempt = loginAttempts.get(ip);
|
const attempt = loginAttempts.get(ip);
|
||||||
|
|
||||||
// 423: account locked (>= LOCKOUT_FAILURES total failures, admin must reset)
|
// 423: account locked (>= LOCKOUT_FAILURES total failures, admin must reset)
|
||||||
|
// Check lockedOut FIRST — lockout takes precedence over rate window.
|
||||||
if (attempt?.lockedOut) {
|
if (attempt?.lockedOut) {
|
||||||
return c.json({ error: 'Account locked' }, 423);
|
return c.json({ error: 'Account locked' }, 423);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 429: rate window — >= RATE_WINDOW_FAILURES failures within the cooldown window
|
// 429: rate window — >= RATE_WINDOW_FAILURES failures within the cooldown window.
|
||||||
|
// Increment the counter even on 429 so continued brute-force accumulates toward lockout.
|
||||||
if (attempt && attempt.count >= RATE_WINDOW_FAILURES && Date.now() < attempt.lockedUntil) {
|
if (attempt && attempt.count >= RATE_WINDOW_FAILURES && Date.now() < attempt.lockedUntil) {
|
||||||
|
attempt.count += 1;
|
||||||
|
attempt.lockedUntil = Date.now() + RATE_WINDOW_SECS * 1000;
|
||||||
|
attempt.lockedOut = attempt.count >= LOCKOUT_FAILURES;
|
||||||
|
loginAttempts.set(ip, attempt);
|
||||||
|
if (attempt.lockedOut) {
|
||||||
|
return c.json({ error: 'Account locked' }, 423);
|
||||||
|
}
|
||||||
return c.json({ error: 'Too many attempts' }, 429);
|
return c.json({ error: 'Too many attempts' }, 429);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user