auth: make session binding optional, enforce WebAuthn credential ownership

- Make session_id parameter optional in validate_token — only enforced when
  provided, allowing WebSocket auth which cannot carry custom headers
- Remove decode_token round-trip from daemon WS handler
- Override WebAuthn registration username from JWT user_ctx to prevent users
  from registering credentials under another user's account
- Frontend no longer sends username for WebAuthn registration
- Redirect to login on 401 after token refresh fails for POST requests
- Remove redundant auth session check from login page load
- Add tests for session_id semantics and WebAuthn ownership guard
This commit is contained in:
2026-08-12 14:16:49 +00:00
parent 3654209b78
commit e01574c67e
7 changed files with 101 additions and 49 deletions
+3 -8
View File
@@ -373,7 +373,7 @@ async def _handle_ws(request: web.Request) -> web.Response:
1. WebSocket subprotocol header (Sec-WebSocket-Protocol: "Bearer <token>")
2. X-Auth-Token header (nginx-injected)
"""
from lib.auth import decode_token, validate_token
from lib.auth import validate_token
token_param = None
@@ -392,16 +392,11 @@ async def _handle_ws(request: web.Request) -> web.Response:
{"ok": False, "error": "authentication required"}, status=401
)
# Decode token to extract session_id from payload (browsers can't send
# X-Session-Id header on WebSocket connections, only subprotocols)
raw_payload = decode_token(token_param)
if raw_payload is None:
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
# Validate token. Session binding is skipped because browsers cannot send
# custom headers on WebSocket connections (no X-Session-Id available).
payload = validate_token(
token_param,
token_type="access",
session_id=raw_payload.get("session_id"),
)
if payload is None:
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)