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
+48
View File
@@ -827,6 +827,49 @@ class TestTokenValidationEdgeCases:
payload = validate_token(token, "refresh", session_id="wrongsession")
assert payload is None
def test_require_session_accepts_matching_session(self) -> None:
"""require_session=True validates when matching session_id provided."""
create_user("reqsess", "password123")
token = generate_refresh_token("reqsess", session_id="mytoken-session")
payload = validate_token(
token,
"refresh",
session_id="mytoken-session",
require_session=True,
)
assert payload is not None
def test_require_session_rejects_when_session_id_omitted(self) -> None:
"""require_session=True rejects when token has session_id but request passes None."""
create_user("reqsess2", "password123")
token = generate_refresh_token("reqsess2", session_id="mytoken-session")
payload = validate_token(
token,
"refresh",
session_id=None,
require_session=True,
)
assert payload is None
def test_require_session_rejects_wrong_session(self) -> None:
"""require_session=True rejects when session_id doesn't match."""
create_user("reqsess3", "password123")
token = generate_refresh_token("reqsess3", session_id="mytoken-session")
payload = validate_token(
token,
"refresh",
session_id="wrong-session",
require_session=True,
)
assert payload is None
def test_require_session_with_no_session_in_token(self) -> None:
"""require_session=True with a token that has no session_id claim works normally."""
create_user("reqsess4", "password123")
token = generate_refresh_token("reqsess4")
payload = validate_token(token, "refresh", require_session=True)
assert payload is not None
def test_refresh_token_generate_tokens_includes_session_id(self) -> None:
"""generate_tokens produces refresh token with session_id."""
create_user("tokuser", "password123")
@@ -966,6 +1009,11 @@ class TestMultiUserAdmin:
creds = list_credentials("wgscduser")
assert len(creds) == 0
def test_delete_builtin_admin_rejected(self) -> None:
"""Deleting the builtin admin user is rejected."""
with pytest.raises(ValueError, match="Cannot delete builtin admin"):
delete_user("admin")
def test_self_deletion_prevention(self) -> None:
"""Self-deletion is prevented at the Flask blueprint layer.