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
+5 -10
View File
@@ -290,9 +290,10 @@ def validate_token(
token_string: The JWT token string.
token_type: Expected token type ("access" or "refresh").
session_id: Must match the ``session_id`` claim in the token payload.
Required for access tokens — prevents a stolen token from being
usable without the originating session. Ignored for refresh tokens
which carry no ``session_id`` claim.
When provided, enforces session binding to prevent a stolen token
from being usable without the originating session. When ``None``,
the check is skipped (used by WebSocket auth which cannot carry
the session ID header).
Returns:
Payload dict including permissions, or None if invalid/blacklisted.
@@ -302,13 +303,7 @@ def validate_token(
return None
if payload.get("type") != token_type:
return None
if token_type == "access" and session_id != payload.get("session_id"):
return None
if (
token_type != "access"
and session_id
and payload.get("session_id") != session_id
):
if session_id is not None and session_id != payload.get("session_id"):
return None
jti = payload.get("jti")