fix: make revocation generation cover refresh tokens (#2028) - #2388
Conversation
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThis is a well-scoped, well-tested security fix (extending the persisted revocation generation to cover refresh tokens, with fail-closed I/O handling). Let me verify the two things that could be residual defects: a new blocking call on the event loop, and whether any production caller is broken by the new The only production caller of No findings. [OPUS-REVIEWED] 7b2c5b5 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — ✅ PASSAdvisory design-level review of Design-Verdict: PASS Root-cause fix in the right seam: one persisted counter now governs both token kinds, mirroring the existing Suggestions
[DESIGN-REVIEWED] 7b2c5b5 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
70a115d to
d30dde2
Compare
|
Dispositions for review findings on GPT 5.6 BLOCKING — Design Review suggestion — bump from base 0 can overwrite a higher on-disk counter (5 → 1) → fixed by the same change: the bump refuses to proceed without a readable base, so a lower value can never be persisted over a higher one. |
d30dde2 to
0b32d16
Compare
|
Disposition for the finding on GPT 5.6 BLOCKING — |
0b32d16 to
7b2c5b5
Compare
|
Dispositions for findings on GPT 5.6 BLOCKING — SAST (Semgrep) — |
|
Disposition for the Design Review suggestion on No self-heal / remediation not surfaced for the unreadable-counter lockout → accepted-and-deferred: #2393. Rationale for deferring rather than amending this PR: the scenario requires state corrupted BEFORE this upgrade (post-upgrade writes are atomic via tmp + |
Problem
kirocrew logout(CLI →POST /api/logout→revoke_all_sessions()) ends access sessions but not refresh chains:validate_refresh_token()checks HMAC,kind,session_exp, and per-chain revocation, but never consults the revocation generation, andgenerate_refresh_token()embeds nogenclaim. A browser still holding a validmc_refresh_<port>cookie can callPOST /api/auth/refreshafter logout and mint a fresh access cookie carrying the new generation. There is also no global refresh-chain revocation path —revoke_chainis per-chain and only reachable from the target browser's own cookie.Why it matters
An operator running
kirocrew logoutreasonably expects every outstanding session to end. Sessions that silently survive the documented "revoke all sessions" control are a behavior/consistency gap (documented as a known limitation in the docs; this PR removes the gap the docs describe).Fix (symptom → root cause → change)
Symptom: sessions survive
kirocrew logoutvia the refresh path. Root cause: the persisted revocation-generation counter is embedded and checked only in access tokens. Change: make the counter authoritative over BOTH cookie types — the issue's preferred option, reusing the already-persisted counter with no chain enumeration.src/kiro_crew/dashboard/revocation_gen.py— the counter's canonical home (current_revocation_gen()/bump_revocation_gen()), extracted fromtoken_auth.pysorefresh_tokens.pycan consult it without recreating the knowntoken_auth↔refresh_tokensimport cycle (same seam astoken_secret.py). Loading is lazy + lock-guarded; both validators read the LIVE value through the accessor. A failed disk read is not memoized (answers 0 for that call, retries next call), so a transient startup read error cannot permanently un-revoke sessions.warm_auth_singletons()primes it off the event loop for both server entry points.token_auth.py— re-exports the old names (_REVOCATION_FILE,_load_revocation_gen,_bump_revocation_gen) for backwards compatibility;generate_token/validate_tokennow go through the accessor.refresh_tokens.py—generate_refresh_token()embeds"gen": current_revocation_gen();validate_refresh_token()rejectsgen < currentwith reason"session revoked", mirroring the access-cookie semantics. Also corrects the staleRefreshStateManager.clear_all()docstring (logout never calls it — and must not, since it would clear_revoked_chains).dashboard-token-auth.md,remote-and-mobile.md,slack-setup.md,cli.mdupdated in the same commit: the generation counter is authoritative over both cookie types; restart semantics unchanged (counter reloads unchanged, logs nobody out);POST /api/auth/logoutremains the narrower per-browser control.Upgrade consequence (deliberate fail-closed posture): refresh tokens minted before this claim existed default to
gen 0, so on installs where the operator has ever runkirocrew logout, existing refresh chains are rejected once and users re-mint via thekirocrew tokenURL. Installs that never ran a logout (gen still 0) are unaffected. Access-cookie behavior is unchanged.Tests
Extends
test/test_refresh_tokens.py(TR-U-28..32):revoke_all_sessions()→ invalid with reason"session revoked"(identity/claims still surfaced for audit).genclaim: valid while gen==0, rejected once gen>0 (fail-closed).POST /api/auth/refreshwith a pre-logout refresh cookie → 401.Existing chain-revocation, reuse-detection, and access-cookie gen tests pass; fixtures in
test_token_auth.py/test_refresh_tokens.pyupdated to pin the moved counter.Manual verification
N/A — unit coverage sufficient: the change is confined to token mint/validate paths exercised end-to-end by the handler-level test.
Closes #2028