feat(07.1): session revocation on privilege change — CR-01/CR-02/CR-03
- revoke_all_refresh_tokens: add skip_token_hash optional param (exclude
current session while revoking others)
- change_password, enable_totp, disable_totp: call revoke with skip hash
derived from refresh cookie; return sessions_revoked in response and
write to audit log metadata_
- 3 new tests: test_{change_password,enable_totp,disable_totp}_revokes_other_sessions
— all PASSED; 373 total passing, 0 regressions
- Frontend toasts: SettingsAccountTab + TotpEnrollment show
"Other sessions have been terminated." when sessions_revoked > 0
- Companion fixes: rate_limiting get_client_ip refactor, deps/auth.py
request.state.current_user, locustfile refresh-token task removal
- Version bump: 0.1.0 → 0.1.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
8d060a5da4
commit
c38c6b1c01
+20
-3
@@ -489,6 +489,10 @@ async def change_password(
|
||||
_ip = get_client_ip(request)
|
||||
user = await session.get(User, current_user.id)
|
||||
user.password_hash = auth_service.hash_password(body.new_password)
|
||||
# Revoke other sessions; keep current one alive via skip_token_hash (CR-01)
|
||||
raw_cookie = request.cookies.get("refresh_token")
|
||||
skip_hash = hashlib.sha256(raw_cookie.encode()).hexdigest() if raw_cookie else None
|
||||
revoked = await auth_service.revoke_all_refresh_tokens(session, current_user.id, skip_token_hash=skip_hash)
|
||||
# D-13: password changed event (flush within same transaction before commit)
|
||||
await write_audit_log(
|
||||
session,
|
||||
@@ -497,10 +501,11 @@ async def change_password(
|
||||
actor_id=current_user.id,
|
||||
resource_id=None,
|
||||
ip_address=_ip,
|
||||
metadata_={"sessions_revoked": revoked},
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
return {"message": "Password updated"}
|
||||
return {"message": "Password updated", "sessions_revoked": revoked}
|
||||
|
||||
|
||||
# ── Request models for new endpoints ─────────────────────────────────────────
|
||||
@@ -575,6 +580,11 @@ async def enable_totp(
|
||||
plain_codes = auth_service.generate_backup_codes(10)
|
||||
await auth_service.store_backup_codes(session, current_user.id, plain_codes)
|
||||
|
||||
# Revoke other sessions; keep current one alive via skip_token_hash (CR-02)
|
||||
raw_cookie = request.cookies.get("refresh_token")
|
||||
skip_hash = hashlib.sha256(raw_cookie.encode()).hexdigest() if raw_cookie else None
|
||||
revoked = await auth_service.revoke_all_refresh_tokens(session, current_user.id, skip_token_hash=skip_hash)
|
||||
|
||||
# D-13: TOTP enrolled event
|
||||
_ip = get_client_ip(request)
|
||||
await write_audit_log(
|
||||
@@ -584,10 +594,11 @@ async def enable_totp(
|
||||
actor_id=current_user.id,
|
||||
resource_id=None,
|
||||
ip_address=_ip,
|
||||
metadata_={"sessions_revoked": revoked},
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
return {"backup_codes": plain_codes}
|
||||
return {"backup_codes": plain_codes, "sessions_revoked": revoked}
|
||||
|
||||
|
||||
# ── DELETE /api/auth/totp ─────────────────────────────────────────────────────
|
||||
@@ -610,6 +621,11 @@ async def disable_totp(
|
||||
# Delete all backup codes for this user (including unused ones)
|
||||
await session.execute(delete(BackupCode).where(BackupCode.user_id == current_user.id))
|
||||
|
||||
# Revoke other sessions; keep current one alive via skip_token_hash (CR-03)
|
||||
raw_cookie = request.cookies.get("refresh_token")
|
||||
skip_hash = hashlib.sha256(raw_cookie.encode()).hexdigest() if raw_cookie else None
|
||||
revoked = await auth_service.revoke_all_refresh_tokens(session, current_user.id, skip_token_hash=skip_hash)
|
||||
|
||||
# D-13: TOTP revoked event
|
||||
await write_audit_log(
|
||||
session,
|
||||
@@ -618,10 +634,11 @@ async def disable_totp(
|
||||
actor_id=current_user.id,
|
||||
resource_id=None,
|
||||
ip_address=_ip,
|
||||
metadata_={"sessions_revoked": revoked},
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
return {"message": "TOTP disabled"}
|
||||
return {"message": "TOTP disabled", "sessions_revoked": revoked}
|
||||
|
||||
|
||||
# ── POST /api/auth/password-reset ─────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user