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:
@@ -15,6 +15,7 @@ from daemon.iface import (
|
||||
DELETE_AUTH_WEBAUTHN_CREDENTIAL,
|
||||
GET_AUTH_SESSION,
|
||||
GET_AUTH_USERS,
|
||||
GET_AUTH_WEBAUTHN_CAPABLE,
|
||||
GET_AUTH_WEBAUTHN_CREDENTIAL_COUNTS,
|
||||
GET_AUTH_WEBAUTHN_CREDENTIALS,
|
||||
POST_AUTH_LOGIN,
|
||||
@@ -219,6 +220,39 @@ def delete_user(username: str):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _resolve_webauthn_origin() -> tuple[str, str]:
|
||||
"""Extract WebAuthn origin and rp_id from the current request.
|
||||
|
||||
Returns (origin, rp_id) derived from the actual request, falling back
|
||||
to config values when the request metadata is unavailable.
|
||||
"""
|
||||
scheme = request.headers.get("X-Forwarded-Proto", request.scheme)
|
||||
host = request.headers.get("X-Forwarded-Host", request.host.split(":")[0])
|
||||
origin = f"{scheme}://{host}"
|
||||
# rp_id is the registered domain (strip port numbers)
|
||||
rp_id = host.split(":")[0]
|
||||
return origin, rp_id
|
||||
|
||||
|
||||
@bp.route("/webauthn/capable", methods=["GET"])
|
||||
def webauthn_capable():
|
||||
"""Check if WebAuthn is available on the current request domain.
|
||||
|
||||
Endpoint:
|
||||
GET /api/auth/webauthn/capable
|
||||
Returns:
|
||||
{ "enabled": true/false, "rp_id": "...", "rp_name": "...", "origin": "..." }
|
||||
or { "enabled": false, "reason": "..." }
|
||||
"""
|
||||
try:
|
||||
origin, rp_id = _resolve_webauthn_origin()
|
||||
body = {"webauthn_origin": origin, "webauthn_rp_id": rp_id}
|
||||
return _ok(get(GET_AUTH_WEBAUTHN_CAPABLE, body))
|
||||
except Exception as exc:
|
||||
logger.error("WebAuthn capable check failed: %s", exc)
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
@bp.route("/webauthn/register-begin", methods=["POST"])
|
||||
def webauthn_register_begin():
|
||||
"""Begin WebAuthn registration.
|
||||
@@ -232,6 +266,9 @@ def webauthn_register_begin():
|
||||
"""
|
||||
try:
|
||||
body = request.get_json(silent=True) or {}
|
||||
origin, rp_id = _resolve_webauthn_origin()
|
||||
body["webauthn_origin"] = origin
|
||||
body["webauthn_rp_id"] = rp_id
|
||||
return _ok(post(POST_AUTH_WEBAUTHN_REGISTER_BEGIN, body))
|
||||
except Exception as exc:
|
||||
logger.error("WebAuthn register begin failed: %s", exc)
|
||||
@@ -251,6 +288,9 @@ def webauthn_register_finish():
|
||||
"""
|
||||
try:
|
||||
body = request.get_json(silent=True) or {}
|
||||
origin, rp_id = _resolve_webauthn_origin()
|
||||
body["webauthn_origin"] = origin
|
||||
body["webauthn_rp_id"] = rp_id
|
||||
return _ok(post(POST_AUTH_WEBAUTHN_REGISTER_FINISH, body))
|
||||
except Exception as exc:
|
||||
logger.error("WebAuthn register finish failed: %s", exc)
|
||||
@@ -271,6 +311,8 @@ def webauthn_authenticate_begin():
|
||||
"""
|
||||
try:
|
||||
body = request.get_json(silent=True) or {}
|
||||
_, rp_id = _resolve_webauthn_origin()
|
||||
body["webauthn_rp_id"] = rp_id
|
||||
return _ok(post(POST_AUTH_WEBAUTHN_AUTHENTICATE_BEGIN, body))
|
||||
except Exception as exc:
|
||||
logger.error("WebAuthn authenticate begin failed: %s", exc)
|
||||
@@ -291,6 +333,9 @@ def webauthn_authenticate_finish():
|
||||
try:
|
||||
body = request.get_json(silent=True) or {}
|
||||
body["client_ip"] = request.headers.get("X-Real-IP") or request.remote_addr
|
||||
origin, rp_id = _resolve_webauthn_origin()
|
||||
body["webauthn_origin"] = origin
|
||||
body["webauthn_rp_id"] = rp_id
|
||||
return _ok(post(POST_AUTH_WEBAUTHN_AUTHENTICATE_FINISH, body))
|
||||
except Exception as exc:
|
||||
logger.error("WebAuthn authenticate finish failed: %s", exc)
|
||||
|
||||
Reference in New Issue
Block a user