Auth: rate limiter, WebAuthn domain awareness, misc fixes
- Rate limiter tracks failures only; success resets counter - Record failures/successes after password verification, not before - WebAuthn rp_id/origin resolved dynamically from request domain - Management domains auto-discovered from nginx backend config - All WebAuthn operations validate domain against management list - Add GET /api/auth/webauthn/capable endpoint for frontend checks - Frontend checkWebAuthnCapable() function for domain-gated UI - Timing side-channel fix: pre-compute dummy hash at module load - Builtin admin seeded with random password (logged at WARNING) - Logout handler returns consistent response shape
This commit is contained in:
+8
-3
@@ -31,6 +31,10 @@ from lib.password import hash_password, needs_rehash, verify_password
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Pre-computed dummy hash for constant-time verification on nonexistent users.
|
||||
# Generated once at module load to avoid timing leaks from per-call hash generation.
|
||||
_DUMMY_HASH = hash_password(secrets.token_hex(32))
|
||||
|
||||
# Builtin admin — hardcoded, full access, cannot be modified/deleted
|
||||
BUILTIN_ADMIN_USERNAME = "admin"
|
||||
|
||||
@@ -113,9 +117,10 @@ def verify_user_password(username: str, password: str) -> dict[str, Any] | None:
|
||||
"""
|
||||
user = find_user(username)
|
||||
if user is None:
|
||||
# Run a dummy Argon2id verification to prevent timing-based user enumeration.
|
||||
# The timing for both paths is now equivalent.
|
||||
verify_password(password, hash_password(secrets.token_hex(32)))
|
||||
# Run a dummy Argon2id verification against a pre-computed hash to
|
||||
# prevent timing-based user enumeration. Uses module-level hash
|
||||
# so both paths take ~1 verify call (~200ms) instead of ~400ms.
|
||||
verify_password(password, _DUMMY_HASH)
|
||||
return None
|
||||
if not verify_password(password, user["password_hash"]):
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user