fix: harden auth with refresh token session binding, logging, and router state

- Add session_id to refresh tokens and enforce it during validation,
  preventing stolen refresh tokens from being usable without the
  originating browser session
- Set router.isAuthenticated via auth:login event after successful
  login (previously only set at page load)
- Add console.warn logging to WS message parse/handler errors
- Improve _refreshPromise error handling in token refresh flow
- Document rate limiter in-memory limitation and CSP connect-src
  same-origin requirement
- Add 3 tests for session-bound refresh token validation
This commit is contained in:
2026-08-12 15:53:17 +00:00
parent 76300e281f
commit 6404508519
8 changed files with 95 additions and 38 deletions
+11 -3
View File
@@ -126,11 +126,15 @@ def generate_access_token(
return jwt.encode(payload, secret, algorithm=algorithm)
def generate_refresh_token(username: str) -> str:
def generate_refresh_token(username: str, session_id: str | None = None) -> str:
"""Generate a new refresh token for *username*.
Args:
username: The authenticated username.
session_id: Session binding ID included in the token payload.
When present, the refresh endpoint requires a matching session_id,
preventing a stolen refresh token from being usable without the
originating browser session.
Returns:
JWT refresh token string.
@@ -150,6 +154,8 @@ def generate_refresh_token(username: str) -> str:
"jti": str(uuid.uuid4()),
"type": "refresh",
}
if session_id:
payload["session_id"] = session_id
return jwt.encode(payload, secret, algorithm=algorithm)
@@ -165,7 +171,7 @@ def generate_tokens(username: str, permissions: dict[str, str]) -> dict[str, str
"""
session_id = secrets.token_urlsafe(16)
access_token = generate_access_token(username, permissions, session_id)
refresh_token = generate_refresh_token(username)
refresh_token = generate_refresh_token(username, session_id)
_persist_refresh_token(username, refresh_token)
return {
"access_token": access_token,
@@ -397,7 +403,9 @@ class RateLimiter:
del self.failures[key]
# Global rate limiters
# Global rate limiters — in-memory only. Counts reset on daemon restart
# (SIGHUP reload, process restart). Acceptable for a single-user appliance
# where restarts are rare; brute-force windows briefly reset post-restart.
_login_limiter = RateLimiter(max_attempts=10, window_seconds=300)
_webauthn_limiter = RateLimiter(max_attempts=5, window_seconds=600)