feat(auth): add shared login handler with constant-time clamp
All checks were successful
Deploy to Homelab / deploy (push) Successful in 18s
All checks were successful
Deploy to Homelab / deploy (push) Successful in 18s
Single attemptLogin() orchestrates lockout check, bcrypt verify (against real or dummy hash), success/failure logging, and a configurable minimum response-time clamp. Both login routes will share this — no duplication. Adds three logger events for the operator's escalation pipeline: AUTH_LOCKOUT_TRIGGERED, AUTH_LOCKED_ATTEMPT, and AUTH_RATE_LIMITED. fail2ban filters can pick these up to escalate persistent attackers from in-app lockout to IP ban. Constant-time defense: unknown users still pay bcrypt cost (via dummy hash) and the clamp ensures locked-vs-unknown-vs-wrong-password aren't distinguishable by response time. Username is canonicalized (lowercase + trim) before lookup so attackers can't bypass lockout via case variation.
This commit is contained in:
@@ -12,9 +12,26 @@ interface FileNotFoundParams {
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
interface AuthLockoutTriggeredParams extends AuthLogParams {
|
||||
durationSeconds: number;
|
||||
}
|
||||
|
||||
interface AuthLockedAttemptParams extends AuthLogParams {
|
||||
retryAfterSeconds: number;
|
||||
}
|
||||
|
||||
interface AuthRateLimitedParams {
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
route: string;
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
authSuccess(params: AuthLogParams): Promise<void>;
|
||||
authFailure(params: AuthLogParams): Promise<void>;
|
||||
authLockoutTriggered(params: AuthLockoutTriggeredParams): Promise<void>;
|
||||
authLockedAttempt(params: AuthLockedAttemptParams): Promise<void>;
|
||||
authRateLimited(params: AuthRateLimitedParams): Promise<void>;
|
||||
fileNotFound(params: FileNotFoundParams): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -34,6 +51,12 @@ export function createLogger(logFile: string): Logger {
|
||||
return {
|
||||
authSuccess: (params) => write(authLine('AUTH_SUCCESS', params)),
|
||||
authFailure: (params) => write(authLine('AUTH_FAILURE', params)),
|
||||
authLockoutTriggered: ({ durationSeconds, ...auth }) =>
|
||||
write(`${authLine('AUTH_LOCKOUT_TRIGGERED', auth)} duration_seconds=${durationSeconds}`),
|
||||
authLockedAttempt: ({ retryAfterSeconds, ...auth }) =>
|
||||
write(`${authLine('AUTH_LOCKED_ATTEMPT', auth)} retry_after_seconds=${retryAfterSeconds}`),
|
||||
authRateLimited: ({ ip, userAgent, route }) =>
|
||||
write(`[${timestamp()}] AUTH_RATE_LIMITED ip=${ip} user-agent="${userAgent}" route="${route}"`),
|
||||
fileNotFound: ({ ip, userAgent, fileId }) =>
|
||||
write(`[${timestamp()}] FILE_NOT_FOUND ip=${ip} user-agent="${userAgent}" file_id="${fileId}"`),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user