Context
Surfaced in the #145 (PR #167) review (codex). Rotating single-use refresh tokens + reuse-detection (replay revokes the whole family) interact badly with multiple tabs/windows: two tabs of the SPA share one refresh cookie, and each can issue a refresh. If two refreshes race with the same cookie value, the second presents an already-rotated token → reuse-detection treats it as theft → revokes the whole family → both tabs are logged out.
This is pre-existing to the rotating-refresh design (pre-#145, two tabs shared the same localStorage refresh token with the same race). #145 makes it marginally more likely: the access token is memory-only, so every page load/tab-open now does an eager silent refresh (vs pre-#145 only refreshing on a 401). Single-flight refresh is per-tab, so it does not help across tabs.
Impact
Opening the app in two tabs, or reloading two tabs near-simultaneously, can spuriously log the user out of both. Annoying, not a security hole (the revocation fails safe).
Options (pick during triage)
- Cross-tab coordination: a Web Lock /
BroadcastChannel / localStorage-mutex so only one tab refreshes at a time and others wait for the result. Most correct; some complexity.
- Server-side grace window: briefly (a few seconds) accept the immediately-previous refresh token as a valid rotation rather than a replay, so concurrent refreshes don't trip theft-detection. Simpler server change; slightly weakens reuse-detection.
- Accept + document: treat multi-tab as "may require re-login."
Acceptance
Out of scope for #145 (that PR moved storage, not rotation semantics).
Context
Surfaced in the #145 (PR #167) review (codex). Rotating single-use refresh tokens + reuse-detection (replay revokes the whole family) interact badly with multiple tabs/windows: two tabs of the SPA share one refresh cookie, and each can issue a refresh. If two refreshes race with the same cookie value, the second presents an already-rotated token → reuse-detection treats it as theft → revokes the whole family → both tabs are logged out.
This is pre-existing to the rotating-refresh design (pre-#145, two tabs shared the same
localStoragerefresh token with the same race). #145 makes it marginally more likely: the access token is memory-only, so every page load/tab-open now does an eager silent refresh (vs pre-#145 only refreshing on a 401). Single-flight refresh is per-tab, so it does not help across tabs.Impact
Opening the app in two tabs, or reloading two tabs near-simultaneously, can spuriously log the user out of both. Annoying, not a security hole (the revocation fails safe).
Options (pick during triage)
BroadcastChannel/ localStorage-mutex so only one tab refreshes at a time and others wait for the result. Most correct; some complexity.Acceptance
Out of scope for #145 (that PR moved storage, not rotation semantics).