fix: reuse the popup for the interactive retry — no more blocked second window / 'Open new window' dialog#13
fix: reuse the popup for the interactive retry — no more blocked second window / 'Open new window' dialog#13jeswr wants to merge 2 commits into
Conversation
The DPoP provider always tries `prompt=none` first, so a fresh login is: silent attempt → `login_required` → interactive retry. The popup element closed the window on EVERY callback message, so the retry had to open a NEW window — outside the original click's (already consumed) user activation. Popup blockers stop that, stranding the user in the "User interaction needed … Open new window" dialog on every first login (and on slower connections the first open can be blocked too). Now the popup is kept open when the callback carries one of the OIDC "user must interact" errors (login_required / interaction_required / consent_required) — exactly the errors token providers retry right away. The retry's `open()` then merely NAVIGATES the existing named window, which browsers always allow. Users see one popup that goes from the silent bounce straight to the login page, with no interstitial dialog. A code response or a terminal error (e.g. access_denied when the user denies consent) still closes the window as before, as do cancel/abort. Verified live with a Playwright-driven login (Chrome popup blocker enabled) against a deployed app + Solid-OIDC broker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the AuthorizationCodeFlow popup lifecycle so that when a silent prompt=none attempt returns an OIDC “user must interact” error (login_required, interaction_required, consent_required), the popup is kept open. This enables the subsequent interactive retry to reuse (navigate) the same named popup window, avoiding popup-blocker failures that occur when trying to open a second window without user activation.
Changes:
- Keep the authorization popup open on OIDC “interaction needed” error callbacks to allow interactive retry to navigate the existing named window.
- Add a helper (
needsInteraction) to detect the relevant OIDC error responses from the callback URL.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const onMessage = (message: MessageEvent) => { | ||
| signal.removeEventListener("abort", onAbort) | ||
| this.#switchModal.close() | ||
| this.#authorizationWindow?.close() | ||
|
|
||
| // When the server answered a silent (`prompt=none`) attempt with a "user |
There was a problem hiding this comment.
Fixed in 1ce81c1: the listener now ignores any message whose source is not the authorization popup, and removes itself on the matching message instead of using { once: true }.
Review follow-up: filter the window message listener by
message.source === the popup, so unrelated (or hostile) postMessage
traffic can neither resolve the flow early nor spoof an authorization
response. The listener now removes itself on the matching message
instead of using { once: true }.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
@langsamu thanks — answering your three points, with behavioural evidence rather than guessing where it's a runtime question. 1. Taking the second commit to 2. The UI element duplicating the token provider's error-message checks. This is a fair observation and I don't want to paper over it — it is genuine duplication. Both 3. "Is it correct to start with Short answer: yes, starting with
So "some require interaction, others don't" is the same server behaving differently depending on whether you already had a session — which is exactly the win Evidence (Playwright, on branch I sent an unauthenticated
All three decline the silent attempt, and the only difference is which of the three error strings they use (CSS/ESS say Two accuracy caveats I want to be straight about:
Also recorded the discovery metadata the provider keys its behaviour off (refresh_token grant, offline_access scope, PKCE, DPoP algs) for all three in |
…Solid server matrix Extends the PR #13 evidence (was CSS/ESS/broker) to every DISTINCT known Solid server codebase the server-agnostic reactive-fetch app could target: - CSS (solidcommunity.net, solidweb.me, + a local full-control instance) - Pivot (teamid.live) - ESS (login.inrupt.com) - NSS (solidweb.org, datapod.igrant.io) - Trinpod / TwinPod-Server (trinpod.us, trinpod.eu) - Manas, PHP Solid Server (pdsinterop), use.id, inrupt.net — recorded as unverified/unreachable (no public OIDC endpoint), skipped with a reason. oidc.ts gains a codebase-tagged MATRIX and a silentAttempt that CLASSIFIES the response (interaction-error / html-login / code / other) instead of only reading the redirect error. New table-driven specs (prompt-none-matrix, discovery-matrix) iterate the matrix, tolerate unreachable hosts, and assert observed behaviour. KEY FINDING: NSS and Trinpod IGNORE prompt=none and serve an interactive HTML login page (HTTP 200, no OIDC error redirect) rather than returning one of {login_required, interaction_required, consent_required}. Recorded as a divergence, not a failure — flagged for the library's retry classifier. Original specs (prompt-none.spec, discovery.spec, warm-session.spec) unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prompt=none evidence — expanded across the full Solid server matrixFollowing up on the earlier 3-server evidence: I broadened the probe to every distinct known Solid server codebase the reactive-fetch app could be pointed at (it's server-agnostic — the user types their issuer at runtime, so the target set is "all known Solid IdPs"). All probes are credential-free (dynamic public-client registration + an unauthenticated Tests pushed to
Codebases covered (live): CSS, Pivot, ESS, NSS, Trinpod — plus a local full-control CSS as a self-hosted control. So: is it correct to start with
|
Problem
DPoPTokenProvideralways triesprompt=nonefirst, so a fresh login is: silent attempt →login_required→ interactive retry.AuthorizationCodeFlowclosed the popup on every callback message, so the retry had toopen()a new window — outside the original click's (already consumed) user activation. Popup blockers stop that, stranding the user in the "User interaction needed to launch authorization code flow in new window → Open new window" dialog on every first login. This is the "unnecessary accepting of prompts to redirect" users report.Change
Keep the popup open when the callback message carries one of the OIDC "user must interact" errors (
login_required/interaction_required/consent_required) — exactly the errors token providers retry interactively right away. The retry'sopen(uri, sameWindowName)then merely navigates the existing named window, which browsers always allow without user activation.Result: the user sees one popup that goes from the silent bounce straight to the IdP login page — no blocked window, no interstitial dialog, no extra click.
A
coderesponse or a terminal error (e.g.access_deniedwhen the user denies consent) still closes the window exactly as before; cancel/abort paths are unchanged.Verification
npm run build(tsc): clean.--disable-popup-blockingremoved) against a deployed pod manager + Solid-OIDC broker (https://app.solid-test.jeswr.org). Before: silent popup closes → second window needed. After: one popup,prompt=nonebounce navigates in place to the Keycloak login, flow completes with zero interstitial dialogs.Future work (out of scope here): running the
prompt=noneattempt in a hidden iframe would remove the popup flash for purely silent re-auth; that needs acallback.htmlcontract change (parentvsopener), so it is left for a separate discussion.🤖 Generated with Claude Code