Skip to content

Store the token fallback under its own key so it can actually be read - #34868

Draft
hughns wants to merge 6 commits into
developfrom
hughns/fix-token-storage-fallback
Draft

Store the token fallback under its own key so it can actually be read#34868
hughns wants to merge 6 commits into
developfrom
hughns/fix-token-storage-fallback

Conversation

@hughns

@hughns hughns commented Aug 31, 2026

Copy link
Copy Markdown
Member

Fixes #34866

When an IndexedDB write failed, persistTokenInStorage() fell back to writing the token to localStorage under the primary storage key. But getStoredToken reads IndexedDB first and only consults localStorage when the IndexedDB returns no value. The fallback was therefore sometimes unreachable: the stale IndexedDB value shadowed it permanently.

This PR writes the fallback to a distinct <key>_fallback localStorage key instead, and prefers it on read. It is only ever written when an IndexedDB write failed, and is cleared as
soon as one succeeds, so it is always at least as new as IndexedDB. The primary key in localStorage keeps its existing meaning - a pre-IndexedDB legacy token, which may well be older than IndexedDB - so the migration path is unchanged and the change is safe on upgrade.

Also:

  • logs the write failure, which was previously silent — a session becoming doomed left no trace in rageshakes.
  • leaves the stale IndexedDB entry alone after a failed write. The service worker reads the access token from IndexedDB only, so deleting it would break authenticated media for the rest of the session.
  • sweeps up plaintext tokens left at the primary key by the old fallback. Done on the write path, since only a successful write proves the copy is spare rather than the last recoverable one.

Checklist

hughns and others added 2 commits August 31, 2026 15:55
When an IndexedDB write failed, persistTokenInStorage fell back to writing the
token to localStorage under the primary storage key. But getStoredToken reads
IndexedDB first and only consults localStorage when IndexedDB is empty - which,
for a rotation, it never is. The fallback was therefore unreachable: the stale
IndexedDB value shadowed it permanently.

With rotating refresh tokens this is fatal and silent. A single failed idbSave
leaves the client presenting an already-consumed refresh token on its next
start, the server rejects it with a 4xx, and the session is destroyed.

Write the fallback to a distinct `<key>_fallback` key instead, and prefer it on
read. It is only ever written when an IndexedDB write failed, and is cleared as
soon as one succeeds, so it is always at least as new as IndexedDB. The primary
key in localStorage keeps its existing meaning - a pre-IndexedDB legacy token,
which may well be older than IndexedDB - so the migration path is unchanged and
the change is safe on upgrade.

Also:
 - log the write failure. This was silent, so the moment a session became
   doomed left no trace in rageshakes at all.
 - clear any stale IndexedDB entry after a failed write, so a client that does
   not know about the fallback key reads "no token" rather than an outdated one.
 - write the fallback before clearing IndexedDB, so there is never a moment
   where neither store holds a token.
 - deep-copy the store in the tests' initIdbMock. Several tests share one
   fixture object, so the new idbDelete leaked deletions between tests.

Part of #34866

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Before the previous commit, a failed IndexedDB write put the token into
localStorage under the *primary* storage key. Those values are now unreachable:
getStoredToken prefers the dedicated fallback key, and otherwise reads
IndexedDB, only consulting the primary key when IndexedDB is empty.

So an affected profile is left with a token sitting in localStorage in plain
text, which nothing will ever read or remove - which is precisely what the
pickle key exists to avoid. Previously the legacy migration path would
eventually sweep these up; it no longer gets the chance.

Remove it once IndexedDB has answered, and log the fact.

We deliberately do not *use* it: the old code never cleared it after a later
successful write, so it may be older than the IndexedDB copy, and preferring it
could demote a working session to a dead token - the exact failure the previous
commit fixes.

The warning also acts as a marker for profiles which hit the old bug, so
prevalence can be measured from rageshakes rather than guessed at.

Part of #34866

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
// Write the fallback *before* touching IndexedDB below, so that there is never a moment
// where neither store holds a token.
if (token) {
localStorage.setItem(fallbackStorageKey, token);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behaviour to store the token in plaintext is existing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A failed IndexedDB token write can silently and permanently lose the session (and leave it orphaned on the server)

2 participants