Skip to content

🐛 fix: make the Microsoft sign-in survive one click - #80

Merged
edgarmesquita merged 1 commit into
mainfrom
fix/microsoft-popup-handshake
Sep 9, 2026
Merged

edgarmesquita merged 1 commit into
mainfrom
fix/microsoft-popup-handshake

Conversation

@edgarmesquita

Copy link
Copy Markdown
Contributor

Edgar signed in to OneDrive and it worked, but only after logging in three times before reaching the create-master-password screen. Two defects, both ending the same way: back on the sign-in screen instead of at the vault.

The window opened after an await

Building the authorize URL needs the PKCE challenge, and that is a real SHA-256 the platform settles off the event loop — not in a microtask. So a whole task passed between the click and window.open, and a browser is entitled to decide nobody asked for that window.

It now opens empty while the click is still fresh, and is pointed at Microsoft once the address exists.

The regression test is the shape of the bug, not a re-description of the fix:

const pending = new MicrosoftAuth('client-1').requestToken(true);
// Nem um await pelo meio: a janela já existe e ainda não tem endereço.
expect(win.opened).toHaveLength(1);
expect(win.navigated).toHaveLength(0);

The way home needed window.opener

A Cross-Origin-Opener-Policy on the sign-in page severs the opener, and Microsoft already sends it:

$ curl -sI 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?...'
HTTP/1.1 200 OK
Cross-Origin-Opener-Policy-Report-Only: same-origin; report-to="coop-endpoint"

Report-only does not sever today, but it is how a site announces what it is about to enforce. And with the opener gone the callback said "not ours" and the popup went on to boot an entire vault into itself — so what a person saw was the sign-in screen again, in a smaller window, which is a fair description of "I had to log in three times".

The state prefix is ours and nothing else writes it, so that alone identifies the callback now. The code comes back over a BroadcastChannel as well, which does not care who opened whom, only that both sides are this origin. If the browser then refuses to close the window, it says what it is instead of rendering a vault.

The closed-window watch had to learn the same difference: a severed handle reads as closed from the start, and treating that as a cancellation would abandon a sign-in in progress. One synchronous look at the moment of opening tells a stub from a real window — after which a real close is still noticed immediately, with no grace period to sit through.

Tests

24 in ms-auth.test.ts, up from 19. Four mutations, four failures, including putting yesterday's bug back exactly as it was:

Mutation Result
open the window after the digest again 2 failed
require opener in the callback again 2 failed
drop the BroadcastChannel, keep only postMessage 1 failed
give up on a handle that reads closed from the start 1 failed

npm test 383 green · build clean · smoke suite 229 green.

What I got wrong

The unit test that went flaky on CI last week — #79 — was this same fact about the digest, reported from the other side. I fixed the test and did not stop to ask what it was telling me about the product. It was telling me the window opens too late.

Still open, and it is a decision rather than a bug

The Microsoft refresh token is kept in memory only, so every page reload means a full interactive sign-in again. That was deliberate in #78 — a refresh token written to disk is a standing key to someone's files — but it is a trade worth making on purpose rather than by default, and it is the next thing Edgar will notice.

Two defects, both of which end the same way: back on the sign-in
screen instead of at the vault.

The window opened after an await. Building the authorize URL needs the
PKCE challenge, which is a real SHA-256 that the platform settles off
the event loop — so a whole task passed between the click and
`window.open`, and a browser is entitled to decide nobody asked for
that window. It opens empty now, while the click is still fresh, and
is pointed at Microsoft once the address exists.

The way home needed `window.opener`. A Cross-Origin-Opener-Policy on
the sign-in page severs it, and Microsoft already sends that header in
report-only mode — which is how a site announces what it is about to
enforce. With the opener gone the callback said "not ours" and the
popup went on to boot an entire vault into itself, so what a person saw
was the sign-in screen again, in a smaller window. The state prefix is
ours and nothing else writes it, so that alone identifies the callback
now, and the code comes back over a BroadcastChannel as well, which
does not care who opened whom.

The closed-window watch had to learn the difference too: a severed
handle reads as closed from the start, and treating that as a
cancellation would abandon a sign-in in progress. One synchronous look
at the moment of opening tells a stub from a real window, so a real
close is still noticed immediately.

The test that went flaky on CI last week was this same fact about the
digest, reported from the other side. I fixed the test and did not
listen to it.
@edgarmesquita
edgarmesquita merged commit 750500c into main Sep 9, 2026
2 checks passed
@edgarmesquita
edgarmesquita deleted the fix/microsoft-popup-handshake branch September 9, 2026 15:59
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