6.8 KiB
6.8 KiB
Phase 7.1: Security — Session Revocation on Privilege Change - Context
Gathered: 2026-06-05 Status: Ready for planning
## Phase BoundaryPhase 7.1 delivers exactly three security bug fixes: adding the missing revoke_all_refresh_tokens() call to change_password, enable_totp, and disable_totp in backend/api/auth.py. These are the three findings labeled CR-01, CR-02, and CR-03 in .planning/v0.1-MILESTONE-AUDIT.md. No other changes are in scope.
Scope
- D-01: Phase fixes only CR-01..03 — the three missing
revoke_all_refresh_tokens()calls. The other CONCERNS.md security issues (ES256, JTI, token fingerprinting, default secrets, etc.) are deferred to phases 7.2–7.4.
Session Revocation Behavior
- D-02: The calling user's own current refresh token is excluded from revocation. Other sessions (other devices/browsers) are revoked; the session making the request stays alive. The user does not need to re-login.
- D-03: To identify and exclude the current session: read the raw refresh token from the
refresh_tokencookie (request.cookies.get("refresh_token")), SHA-256 hash it (consistent with howrotate_refresh_tokenworks inservices/auth.py:189), and pass the hash as a new optionalskip_token_hashparameter torevoke_all_refresh_tokens. The function skips any row wheretoken_hash == skip_token_hash.
API Responses
- D-04: All three endpoints gain a
sessions_revoked: intfield in their response body, reporting how many OTHER sessions were terminated (not counting the current session). Example:{"message": "Password updated", "sessions_revoked": 2}.
Frontend UX
- D-05: When
sessions_revoked > 0is returned from any of the three endpoints, the frontend shows a brief toast notification: "Other sessions have been terminated." No redirect or re-login required (current session stays alive per D-02).
Audit Logging
- D-06: Log the revocation count in the existing audit log event's
metadata_field for all three operations (consistent with the pattern used inlogout_allatauth.py:419).
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
Target code — the three missing calls
backend/api/auth.py—change_passwordhandler (~line 447): add revoke beforesession.commit()at line 493backend/api/auth.py—enable_totphandler (~line 539): add revoke beforesession.commit()at line 580backend/api/auth.py—disable_totphandler (~line 588): add revoke beforesession.commit()at line 614
Existing revocation infrastructure
backend/services/auth.py:218—revoke_all_refresh_tokens(session, user_id): the function being extended with an optionalskip_token_hashparambackend/services/auth.py:154—create_refresh_token: shows token is stored as SHA-256 hash (token_hash = hashlib.sha256(raw.encode()).hexdigest())backend/services/auth.py:176—rotate_refresh_token: shows how raw token from cookie is hashed for DB lookup — same pattern needed for skip logicbackend/api/auth.py:394—logout_all: working reference for revoke + audit log pattern; D-04 responses follow this shape
Audit
.planning/v0.1-MILESTONE-AUDIT.md— CR-01, CR-02, CR-03 definitions with exact file/line references and required fixes.planning/codebase/CONCERNS.md— full security concern descriptions with fix approaches
CLAUDE.md security invariant
CLAUDE.md§"Login token hardening" — "Password change, TOTP enroll/revoke, and account deactivation immediately revoke all active sessions." This is the rule these fixes enforce.
Frontend toast
frontend/src/— check existing toast/notification component for the correct notification API before adding new toast calls
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
revoke_all_refresh_tokens(session, user_id)inservices/auth.py:218— already returnsintcount; needs one new optional param (skip_token_hash: Optional[str] = None) and a WHERE-clause addition to exclude the skip hash- SHA-256 cookie hashing pattern in
rotate_refresh_token(services/auth.py:189) — identical pattern needed in all three handlers to deriveskip_token_hashfrom the raw cookie value logout_allhandler (auth.py:401) — working reference for the revoke + audit log + response pattern that all three handlers will now mirror
Established Patterns
- Refresh token identity: raw token in cookie →
sha256(raw.encode()).hexdigest()→token_hashcolumn. No plaintext stored in DB. - Audit metadata:
metadata_={"sessions_revoked": count}passed towrite_audit_log(seelogout_allline 419) - Response shapes: all three endpoints currently return simple
{"message": "..."}dicts — D-04 addssessions_revokedto each
Integration Points
backend/api/auth.py— only file changed in the backend; no new routes, no new models, no migrations neededbackend/services/auth.py—revoke_all_refresh_tokensneeds the optionalskip_token_hashparam; this is the only service changefrontend/src/— toast notification triggered whensessions_revoked > 0in the response from these three endpoints
</code_context>
## Specific Ideas- The
skip_token_hashparam onrevoke_all_refresh_tokensshould beOptional[str] = None. WhenNone(the existinglogout_allcaller), behavior is unchanged — all tokens revoked. When set, the single WHERE clause addition isRefreshToken.token_hash != skip_token_hash. - If the refresh cookie is absent (e.g., request authenticated via access token only),
skip_token_hashisNoneand all tokens are revoked — safe fallback. - The
revoke_all_refresh_tokensbulk-UPDATE optimization noted in CONCERNS.md (one UPDATE per token → single bulk UPDATE) is out of scope for this phase; don't fix it here.
- Phase 7.2 — JTI claim + Redis access-token revocation: Closes the 15-min grace window; revoking refresh tokens still leaves the live access token valid until expiry. Tracked in CONCERNS.md §"No JTI Claim and No JTI Revocation in Redis".
- Phase 7.3 — ES256 algorithm upgrade: Replace HS256 with ECDSA P-256; generate a key pair, update
create_access_tokenand decode functions, rotate all refresh tokens. Tracked in CONCERNS.md §"JWT Algorithm Downgrade: HS256 Instead of ES256". - Phase 7.4 — Token fingerprinting / token binding: Add
fgp(fingerprint) claim = HMAC ofUser-Agent + Accept-Languageto access tokens; validate on every request. Tracked in CONCERNS.md §"No Token Fingerprint / Token Binding".
Phase: 07.1-security-session-revocation-on-privilege-change Context gathered: 2026-06-05