Repository navigation
Two tabs refreshing at once sign the user out of every tab — a lost rotation race clears all session cookies #700
Copy link
Copy link
Closed
Labels
area:backendGo, /internal, /pkgGo, /internal, /pkgarea:foundationProduct and platform foundationsProduct and platform foundationsarea:frontendReact, /srcReact, /srcpriority:P1HighHighpriority:P1-highBlocks a milestoneBlocks a milestonestatus:in-reviewPR openPR opentier:0-trustTrust: security, isolation, evidence integrityTrust: security, isolation, evidence integritytype:bugSomething is brokenSomething is broken
Milestone
Description
Activity
- addedarea:foundationProduct and platform foundationsProduct and platform foundationspriority:P1HighHightype:bugSomething is brokenSomething is brokenarea:backendGo, /internal, /pkgGo, /internal, /pkgarea:frontendReact, /srcReact, /srcpriority:P1-highBlocks a milestoneBlocks a milestonestatus:readyMeets the ready definitionMeets the ready definitiontier:0-trustTrust: security, isolation, evidence integrityTrust: security, isolation, evidence integrity
on Sep 16, 2026 - addedstatus:in-progressAn agent is working itAn agent is working itand removedstatus:readyMeets the ready definitionMeets the ready definition
on Oct 7, 2026 - added 4 commits that reference this issue
on Oct 7, 2026 - linked a pull request that will close this issuetest(auth): prove two tabs refreshing at once stay signed in (#700) #896
on Oct 7, 2026 - addedstatus:in-reviewPR openPR openand removedstatus:in-progressAn agent is working itAn agent is working it
on Oct 7, 2026 backend-go — 2026-10-07
Done: no auth code changed. D-048 / #777 had already fixed the cause on master. This adds the proof and the tests the issue asked for (PR #896):
backend/internal/handler/auth/refresh_concurrency_e2e_test.go:TestRefreshHandler_ConcurrentBurst_Success,_UnknownToken_NotFound,_ReplayAfterWindow_Unauthorizedfrontend/e2e/session-tabs.spec.ts: two tabs,or_accessdropped then a simultaneous reload, plus a simultaneous/auth/refreshfrom both tabsbackend/internal/handler/auth/handler.go: stale comment onlydocs/700_CONCURRENT_REFRESH.md: spec and results
Verified: throwaway Postgres 16 + Redis 7, master
cefe453d. Each check was also run against the backend before #777 (9dd1c3ee) to show the tests fail there.- API, 2 concurrent
POST /auth/refreshon the same cookie, 3 runs: 3/3 both 200, sameor_refresh, no clearing, jaror_access, or_csrf, or_refreshintact,/auth/me200, next refresh 200. Before security(auth): a grace window so a concurrent refresh is not read as token theft #777:200+401 REFRESH_REUSE_DETECTED, jar empty,/auth/me401 (the issue's reproduction). - API, 8 concurrent: 8 × 200, one
or_refresh. In DB, each family has exactly 1 live token. - Original token replayed 11 s later:
401 REFRESH_REUSE_DETECTED, all 3 cookies cleared; the legitimate successor then gets 401 (family revoked). npx playwright test e2e/session-tabs.spec.ts --repeat-each 3 --workers 1: 6 passed on master, 6 failed before security(auth): a grace window so a concurrent refresh is not read as token theft #777.go test ./internal/handler/auth -run TestRefreshHandler -race: 3 passed. Before security(auth): a grace window so a concurrent refresh is not read as token theft #777, the burst test fails and the reuse test passes.go test ./internal/auth -run TestRefresh_ -race: 12 passed, all unchanged.go test ./...: 80 packages ok.
Criteria: 1 ✅ · 2 ✅ · 3 ✅ · 4 ✅ · 5 ✅ (needs #893 merged to run on master: the sign-in screen crashes there today; the run above used that one-line fix applied locally)
Next: merge PR #894 (#893) first, then PR #896. Once both are in,
session-tabs.spec.tsruns on master as is.Blocked on: #893 / PR #894 for the Playwright run on master. Nothing else.
- added a commit that references this issue
on Oct 7, 2026
Metadata
Metadata
Assignees
Labels
area:backendGo, /internal, /pkgGo, /internal, /pkgarea:foundationProduct and platform foundationsProduct and platform foundationsarea:frontendReact, /srcReact, /srcpriority:P1HighHighpriority:P1-highBlocks a milestoneBlocks a milestonestatus:in-reviewPR openPR opentier:0-trustTrust: security, isolation, evidence integrityTrust: security, isolation, evidence integritytype:bugSomething is brokenSomething is broken
Problem
A user with OpenRisk open in two tabs is signed out of every tab when both tabs renew their session at the same moment. The 15-minute access token expires in both tabs at once, so this happens whenever both tabs make a request within the same ~20 ms (a reload of both, two dashboards polling, a user switching back to a window with several tabs open). The refresh cookie is still valid for 30 days; nothing about the session is compromised. The user is simply dropped on the login page with "Session revoked. Please sign in again."
This survives the #691 fix (PR #692): #692 shares one refresh per tab, not across tabs.
Evidence (local stack,
masterabb4e02+ #697/#698/#699, 2026-09-16)POST /api/v1/auth/refreshsent at once with the same refresh cookie → one200, one401 {"code":"REFRESH_REUSE_DETECTED","error":"Session revoked. Please sign in again."}./auth/refreshat the same instant (Playwright), 3 runs out of 3:or_access, or_csrf, or_refresh;200, one401 REFRESH_REUSE_DETECTED;GET /auth/me→401; a further/auth/refresh→401.backend/internal/handler/auth/handler.go:427-438treats a lost concurrent-rotation race exactly like a stolen-token replay: it callsmiddleware.ClearSessionCookies(c)and answersREFRESH_REUSE_DETECTED. In a browser the cookie jar is shared by every tab, so the losing response deletes the cookies the winning response just set.frontend/src/lib/api.tsrefreshSession()shares one in-flight refresh per tab only (refreshInFlightis module state), and on a failed refresh setswindow.location.href = '/login'./auth/me 200, next refresh200). The logout is caused by the cookie clearing, not by revocation.Acceptance criteria
/login, and the cookie jar still holds a workingor_access/or_refreshpair.REFRESH_REUSE_DETECTED.Design note — owner decision may be needed
CLAUDE.md escalates redesigns touching auth. Candidate fixes differ in security trade-off:
BroadcastChannel/ Web Locks).The implementer should record the choice in
docs/DECISIONS.mdif it changes reuse-detection semantics.Definition of Done