fix(auth): preserve the OIDC authorize continuation across every login path#112
Merged
Conversation
…n path Starting a login from a client app (GET /connect/authorize) challenges to /login?redirect=<authorize-url>. Several paths authenticated the user without honoring that continuation, stranding them on the IdP dashboard while the client never received its callback. Login SPA: - Extract finishLogin + the same-origin guard into a shared useLoginRedirect composable; every session-establishing flow finishes through it. - External-IdP buttons read the continuation from ?redirect= (was the never-set ?returnUrl=) and apply the same-origin guard. - Magic-link, password-reset and self-registration thread the continuation through their e-mail round trips as a validated ?redirect= on the emailed URL. - Forward ?redirect= across every mid-flow detour (forgot-password, register, reset, verify-email, magic-login, back-to-login links). Consent deny/dead-ends: - Deny re-enters /connect/authorize with a subject-bound deny marker so OpenIddict emits the RFC 6749 access_denied error to the client, honoring response_mode + RFC 9207 iss (symmetric with the approve re-entry). - Expired/consumed tickets return a retryUrl; the subject-binding check runs before the consumed/expired branches so that URL only reaches its own subject. Open-redirect hardening: - Shared LoginRedirectGuard for all server-side continuations (external returnUrl, SAML RelayState, magic-link/reset/self-reg ReturnUrl); rejects //, /\ and control chars (a browser strips TAB/CR/LF, collapsing /\t/evil.com to //evil.com). Frontend guard mirrors it and type-guards array params. Tests: - LoginRedirectGuardTests pins the accept/reject cases incl. control-char smuggling. - e2e 07-oidc-continuation covers deny-returns-to-client, magic-link and password-reset continuations end to end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ebe0101 to
968d9cf
Compare
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.
Problem
Starting a login from a client app (
GET /connect/authorize) challenges the browser to/login?redirect=<authorize-url>. Several login paths authenticated the user without honoring that continuation, so the user landed on the Modgud dashboard while the client app never received its callback — silently breaking SSO for the affected methods.Fixes
Login SPA
finishLogin+ the same-origin guard into a shareduseLoginRedirectcomposable; every session-establishing flow (password, TOTP, email-OTP, passkey, magic-link, external IdP) finishes through it.?redirect=(previously the never-set?returnUrl=, so the continuation was always lost) and apply the same-origin guard.?redirect=on the emailed URL.?redirect=is forwarded across every mid-flow detour (forgot-password, register, reset, verify-email, magic-login, back-to-login links).Consent deny / dead-ends
/connect/authorizewith a subject-bound deny marker, so OpenIddict emits the RFC 6749access_deniederror to the client — honoring the client'sresponse_mode(query/fragment/form_post) and RFC 9207iss, symmetric with the approve re-entry. (This also removed awindow.location.assignsink: the SPA now only navigates a same-origin URL.)retryUrl; the subject-binding check runs before the consumed/expired branches so that URL only ever reaches the ticket's own subject.Open-redirect hardening
LoginRedirectGuardfor all server-side continuations (externalreturnUrl, SAML RelayState, magic-link/reset/self-regReturnUrl). Rejects//,/\and control characters — a browser strips TAB/CR/LF while resolving a redirect, collapsing/\t/evil.comto//evil.com(protocol-relative → external host). The frontend guard mirrors this and type-guards repeated (array)?redirect=params. Also tightens a pre-existing SAML RelayState open redirect.Review
This branch was reviewed by three independent no-prior-context agents (security, frontend correctness, backend correctness); their findings — the control-char smuggling, a
javascript:/data:consent-deny sink, and an array-param crash — are all addressed here.Tests
LoginRedirectGuardTestspins the accept/reject cases incl. control-char smuggling.e2e/07-oidc-continuation.spec.tscovers deny-returns-to-client, magic-link and password-reset continuations end to end (production-image rig).🤖 Generated with Claude Code