fix: close refresh token session binding bypass

Add require_session parameter to validate_token to enforce session_id
matching for refresh operations. Attacker with stolen refresh token can
no longer bypass session binding by omitting session_id from request.

Also adds backend guard against deleting builtin admin user (was only
blocked at Flask blueprint layer), and removes unused _ALL_RW variable.
This commit is contained in:
2026-08-12 20:06:05 +00:00
parent 9ae2cca801
commit c7593f8a1e
4 changed files with 70 additions and 8 deletions
+12 -3
View File
@@ -289,6 +289,7 @@ def validate_token(
token_string: str,
token_type: str = "access",
session_id: str | None = None,
require_session: bool = False,
) -> dict[str, Any] | None:
"""Validate a JWT token and check it against the blacklist.
@@ -298,8 +299,12 @@ def validate_token(
session_id: Must match the ``session_id`` claim in the token payload.
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).
the check is skipped unless ``require_session`` is True.
require_session: If True and the token payload contains a ``session_id``,
the request must provide a matching ``session_id``. Used by the
refresh handler to prevent session binding bypass. When False
(default), omitting ``session_id`` is acceptable even if the token
contains one.
Returns:
Payload dict including permissions, or None if invalid/blacklisted.
@@ -309,7 +314,11 @@ def validate_token(
return None
if payload.get("type") != token_type:
return None
if session_id is not None and session_id != payload.get("session_id"):
token_session_id = payload.get("session_id")
if require_session and token_session_id is not None:
if session_id is None or session_id != token_session_id:
return None
elif session_id is not None and session_id != token_session_id:
return None
jti = payload.get("jti")