Phase 19: Local Auth (No-OIDC Mode) #23
@@ -97,6 +97,29 @@ export function resetLoginAttempts(username: string): void {
|
|||||||
loginAttempts.delete(username.trim());
|
loginAttempts.delete(username.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IN-03: evict stale rate-limit entries to bound the in-memory map size.
|
||||||
|
*
|
||||||
|
* An entry is stale (and safe to drop) when it is neither inside an active rate-limit
|
||||||
|
* window nor inside an active lockout TTL:
|
||||||
|
* - locked-out entries expire LOCKOUT_TTL_MS after lockedAt
|
||||||
|
* - non-locked entries expire once their lockedUntil window has passed
|
||||||
|
*
|
||||||
|
* Called opportunistically at the top of each login request. Dropping an entry is
|
||||||
|
* behaviourally identical to the entry having naturally expired, so eviction never
|
||||||
|
* weakens the brute-force defense — it only reclaims memory for identities no longer
|
||||||
|
* under active rate-limiting.
|
||||||
|
*/
|
||||||
|
function evictStaleLoginAttempts(now: number): void {
|
||||||
|
for (const [k, v] of loginAttempts) {
|
||||||
|
const lockoutExpired = v.lockedOut ? now - v.lockedAt >= LOCKOUT_TTL_MS : true;
|
||||||
|
const windowExpired = now >= v.lockedUntil;
|
||||||
|
if (lockoutExpired && windowExpired) {
|
||||||
|
loginAttempts.delete(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-computed dummy hash used to run verifyPassword on unknown-username paths
|
// Pre-computed dummy hash used to run verifyPassword on unknown-username paths
|
||||||
// (timing defense — prevents timing-oracle username enumeration, RESEARCH Pitfall 2).
|
// (timing defense — prevents timing-oracle username enumeration, RESEARCH Pitfall 2).
|
||||||
// WR-03: hashPassword is now async. Kick off the computation once at module load and keep
|
// WR-03: hashPassword is now async. Kick off the computation once at module load and keep
|
||||||
@@ -115,6 +138,9 @@ localAuthRouter.post('/local/login', zValidator('json', loginSchema, noEchoHook)
|
|||||||
const key = username; // loginSchema .trim()s the username; the map key matches resetLoginAttempts
|
const key = username; // loginSchema .trim()s the username; the map key matches resetLoginAttempts
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
// IN-03: opportunistically reclaim memory from entries that are no longer actively
|
||||||
|
// rate-limited or locked out. Cheap at household scale; bounds the map under input churn.
|
||||||
|
evictStaleLoginAttempts(now);
|
||||||
const attempt = loginAttempts.get(key);
|
const attempt = loginAttempts.get(key);
|
||||||
|
|
||||||
// 423: account locked (>= LOCKOUT_FAILURES failures). CR-04: the lockout auto-expires
|
// 423: account locked (>= LOCKOUT_FAILURES failures). CR-04: the lockout auto-expires
|
||||||
|
|||||||
Reference in New Issue
Block a user