Skip to content

Clear the OAuth popup result from localStorage after handover - #1570

Open
GeiserX wants to merge 2 commits into
UsefulSoftwareCo:mainfrom
GeiserX:fix/oauth-popup-storage-residue
Open

Clear the OAuth popup result from localStorage after handover#1570
GeiserX wants to merge 2 commits into
UsefulSoftwareCo:mainfrom
GeiserX:fix/oauth-popup-storage-residue

Conversation

@GeiserX

@GeiserX GeiserX commented Aug 12, 2026

Copy link
Copy Markdown

TL;DR

The OAuth popup writes its result to localStorage as a fallback completion channel and never removes it. The payload carries the user's identity label — an email — and, on failure, the error preview. It stays parked in the browser profile.

This clears the entry once the handover has had time to land.


Why the entry exists at all

The popup has three ways to hand its result back to the opener, because each one fails in a different situation:

  • postMessage is severed when the provider's consent page sets COOP — window.opener becomes null.
  • BroadcastChannel can be partitioned, and can be raced by the popup auto-closing.
  • localStorage raises a storage event on the opener, and is the reliable fallback when the other two are gone.

So the write is doing real work and should stay.

What this changes

The entry is removed after the handover: shortly before the window closes on success, and after a longer delay on failure, where the window stays open for the user to read the error.

Clearing cannot cost a listener the result. A storage event captures newValue at dispatch, so an opener that has been notified already holds the payload in its handler — removing the key afterwards does not reach back and empty it.

Why it matters

Leaving it parks identity data in the user's browser profile indefinitely whenever nobody is listening — which is every abandoned flow and every opener-less flow, exactly the cases where no one will ever come back to consume it.

Tests

The added tests assert the clear happens on both the success and the failure path, and that it is scheduled after the message has been dispatched rather than alongside it.

Scope

Fully independent — one file, plus tests. It touches nothing else in this series.

The callback page writes its result to localStorage as the reliable same-origin
completion channel, and the opener removes it on pickup. When nothing is
listening — an abandoned flow, a reloaded opener, any caller not using the React
helper — nobody ever removes it, and the payload carries the identity label (an
email) and, on failure, the error preview. It sat in the user's browser profile
indefinitely.

The page now clears its own entry: on success just before the existing
auto-close, and on failure after a delay, since a failed flow deliberately keeps
the window up. This cannot cost a listener the result — a `storage` event
captures `newValue` at dispatch, so an opener that was notified already holds
it, and the only other reader polls `popup.closed`, not storage.

The tests RUN the generated script against stub globals rather than matching its
source, because a string assertion passes just as well on a script that never
executes, and what is at stake here is what the browser is left holding.
@GeiserX

GeiserX commented Aug 13, 2026

Copy link
Copy Markdown
Author

Context for this one: #1585 explains why this PR and twelve others exist — they came out of a single pass over credential handling, asking for each credential where it ends up, how long it stays, and who can read it once it's there.

This PR stands alone and doesn't depend on any of the others.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant