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:
2026-07-29 02:48:53 +00:00
parent 6d30f1387e
commit 48f8d0be18
11 changed files with 335 additions and 65 deletions
+8 -3
View File
@@ -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