fix(oauth): serialize interactive authorization per provider session - #253
Merged
Conversation
Concurrent SDK auth() flows sharing one provider (background Streamable HTTP GET reconnect + bridged POST both receiving 401) each generated a PKCE challenge and opened a prompt, while only one persisted verifier could complete. redirectToAuthorization now claims a single interactive transaction per provider, suppresses overlapping redirects, re-persists the claimed flow's verifier, and clears the claim when the callback settles (with a 300s abandonment TTL). Keep-alive daemon operations no longer restart and replay on unauthorized errors, which duplicated authorization prompts. Fixes #247 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #247 with the decided join-in-flight contract: one interactive OAuth authorization transaction per shared provider/session.
Concurrent SDK
auth()flows sharing one provider (the background Streamable HTTP GET reconnect and a bridged POST both receiving 401) each generated a PKCE challenge and opened a prompt, while only one persisted verifier could complete.What changed
PersistentOAuthClientProvider.redirectToAuthorizationnow claims a single interactive transaction per provider (synchronously, before any await). Overlapping redirects are suppressed: no second browser prompt, no secondonAuthorizationUrl.saveCodeVerifierremembers each verifier by its S256 challenge; the claiming redirect re-persists its own flow's verifier, so a concurrent flow's save can never clobber the completable transaction.shouldRestartDaemonServernow consultsisUnauthorizedError): a restart cannot repair a credential, and the replay was a second source of duplicate prompts.tests/oauth-open-external.test.tsthat was breakingpnpm checkon main (CI's build job only compilestsconfig.build.json, which excludes tests).Tests
tests/oauth-bridge-get-post-concurrency.test.ts— the reporter's full regression harness from Serialize interactive OAuth authorization per shared provider/session #247, verbatim apart from strict-TS annotations. Fails on unpatched main withauthorizationCount: 2, uniqueChallenges: 2; passes with this change: one flow, synthetic completion succeeds, the retried Codex-shapedtools/listreturnsping.tests/oauth-session.test.ts— unit coverage for the single-flight semantics: one prompt for overlapping flows, persisted verifier matches the claimed challenge, pending saves cannot clobber it, and completion re-arms normal prompting.tests/keep-alive-runtime.test.ts— unauthorized daemon errors neither restart the server nor replay the operation.Proof (Node 24.18.0 / pnpm 10.33.2, macOS)
pnpm check— format, type-aware lint, typecheck clean.pnpm build— clean.pnpm test— 904 passed / 3 skipped.Thanks @mkaput for the exceptional report — the regression harness made this implementable without guesswork.
🤖 Generated with Claude Code