fix(auth): keep the pending OAuth request across sign-in - #237
Open
Waiel5 wants to merge 2 commits into
Open
Conversation
When a client begins an authorization and the browser has no session, the provider redirects to `/login` with the pending request in the query string, signed and time-bounded by the server. Both sign-in handlers then set `window.location.href = "/"`, discarding it. The user landed on the inbox, and the client that started the flow waited for a callback that never came. This is not an edge case: it is the path every third-party and native client takes on first connect, because such a client never has a browser session to begin with. It went unnoticed because every existing OAuth test signs in before calling authorize, so the logged-out branch was never exercised — and because `/login` and `/consent` sit outside AuthGuard, nothing else routes through it either. Sign-in now hands the same parameters back to `/api/auth/oauth2/authorize`, which resumes the request. This is not an open redirect: the destination is always our own authorize endpoint rather than a URL taken from the query, and the server re-verifies the signature and expiry before acting on it. An ordinary sign-in with no pending request still goes to `/`. Two layers of coverage, since the failure spans both: - `oauth-logged-out-authorize.test.ts` pins the server contract the page relies on — authorize redirects an unauthenticated request to the login page with signed, expiring parameters, replaying them with a session resumes the flow, and no code is ever issued to an unauthenticated caller. - `oauth-login-continuation.spec.ts` drives it in a browser, registering a client through dynamic registration exactly as an unknown client would. It fails against the previous handler with "sign-in dropped the pending authorization and landed on /", and carries a second case asserting an ordinary sign-in still reaches the inbox.
9 tasks
Owner
|
I would appreciate reproduction steps before attempting a PR for it, thanks! |
Contributor
Author
|
Repro, minimal:
This is the path every third-party client takes on first connect, since it always arrives logged out. The existing OAuth tests all authenticate before calling authorize, which is why the branch had no coverage — the PR adds an e2e that fails without the change. |
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
When a client starts an OAuth authorization and the browser has no session, the provider redirects to
/loginwith the pending request in the query string, signed and time-bounded by the server. Both sign-in handlers then runwindow.location.href = "/", discarding it. The user lands on the inbox and the client that started the flow waits for a callback that never arrives.This is the path every third-party and native client takes on first connect, since such a client never has a browser session to begin with.
Two reasons it went unnoticed: every existing OAuth test signs in before calling authorize (
runFlowinmcp-helpers.ts), so the logged-out branch has no coverage; and/loginand/consentsit outsideAuthGuard, so nothing else routes through it either.Changes
/api/auth/oauth2/authorizeinstead of navigating to/. Applies to both the passkey and the password path./, unchanged.Release impact
Patch — bug fix or internal change, no new behaviour
Bug Fix
Test plan
yarn test worker/src/__tests__/oauth-logged-out-authorize.test.ts— 4 passednpx playwright test e2e/specs/oauth-login-continuation.spec.ts— 2 passedsign-in dropped the pending authorization and landed on /prettier --checkcleanNotes for reviewers
On the redirect being safe. It is not an open redirect. The destination is always this deployment's own
/api/auth/oauth2/authorize, never a URL read out of the query; only the parameters are carried over, and the server re-verifies its ownsigandexpbefore acting on them. The gate is the presence of bothclient_idandsig, so an arbitrary query string does not trigger it.On the two test layers. The failure spans the browser and the server, so I covered both. The worker test pins the contract the page depends on — authorize redirects an unauthenticated request to the login page with signed, expiring parameters; replaying them with a session resumes rather than restarts; and no code is ever issued to an unauthenticated caller. Those pass on
maintoo, which is the point: they document a previously untested branch rather than chase the bug. The e2e spec is the one that actually fails without the fix.The e2e spec registers its client through RFC 7591 dynamic registration, exactly as an unknown client would, so it exercises the real path rather than a fixture.
Local environment note, in case it helps anyone else: running the e2e suite on macOS needed
CHOKIDAR_USEPOLLING=1— miniflare's file watcher hitsEMFILE: too many open filesagainstnode_modulesotherwise. CI is unaffected. Happy to add that to the README if you'd find it useful.Checklist
yarn db:generate) if the schema changed — n/aCHANGELOG.mdunder## [Unreleased]