fix(auth): make SAML SP-initiated login work end-to-end#405
Merged
Conversation
SAML single sign-on never completed in any configuration. Four independent defects, surfaced together behind a reverse proxy (Okta + k3s): - Redirects were built from the request host. With the standalone server bound to the pod HOSTNAME, this emitted internal URLs like https://<pod>:3000/... that don't resolve. Build absolute auth URLs from NEXTAUTH_URL via a shared getAppBaseUrl() helper. - The login lookup keyed on SamlConfiguration.id, but the sign-in UI passes the SsoProvider id. Look up by the unique providerId foreign key instead, which returns a 404 today. - The IdP posts the assertion to /api/auth/callback/saml, but the handler lived at /api/auth/saml/callback and no NextAuth saml provider exists, so the POST hit the catch-all and failed with "This action with HTTP POST is not supported by NextAuth.js". Move the validator to the path the IdP actually targets and hand off to /api/auth/saml/complete to mint the session. - Provider and destination were stored in SameSite=Lax cookies, which browsers withhold on the IdP's cross-site POST. Carry them in the SAML RelayState instead, backed by Valkey (single-use, short-lived, works across pods) with a signed-token fallback. Also seed access/isApi/passwordChangedAt into the completion token so middleware sees the user's access on the first request, and add unit tests covering each fix plus the post-login session handoff.
8fdf88f to
d0a3fc3
Compare
Contributor
Author
|
🎉 This PR is included in version 0.35.1 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
5 tasks
therealbrad
added a commit
that referenced
this pull request
Jun 5, 2026
…letion flow Follow-up to the SP-initiated SAML fix (#405), three changes: - IdP-initiated SSO: an Okta dashboard-tile launch posts an assertion with no RelayState. Rather than rejecting it, the ACS now picks the enabled SAML config whose certificate validates the assertion's signature (the stored issuer is the SP entity id, not a reliable IdP identifier) and defaults the post-login destination to /. - Enforce 2FA for SAML logins: the completion route now reads force2FAAllLogins and stamps the same twoFactorRequired / twoFactorSetupRequired claims the NextAuth jwt callback sets at sign-in, so SAML sessions hit the existing middleware 2FA gate instead of bypassing it. - Single-use completion token: the token handed to /api/auth/saml/complete rides in a redirect URL, so it now registers a single-use jti in Valkey (deleted on consume) and its lifetime is cut to two minutes — a replayed link is rejected.
therealbrad
added a commit
that referenced
this pull request
Jun 5, 2026
…letion flow (#406) Follow-up to the SP-initiated SAML fix (#405), three changes: - IdP-initiated SSO: an Okta dashboard-tile launch posts an assertion with no RelayState. Rather than rejecting it, the ACS now picks the enabled SAML config whose certificate validates the assertion's signature (the stored issuer is the SP entity id, not a reliable IdP identifier) and defaults the post-login destination to /. - Enforce 2FA for SAML logins: the completion route now reads force2FAAllLogins and stamps the same twoFactorRequired / twoFactorSetupRequired claims the NextAuth jwt callback sets at sign-in, so SAML sessions hit the existing middleware 2FA gate instead of bypassing it. - Single-use completion token: the token handed to /api/auth/saml/complete rides in a redirect URL, so it now registers a single-use jti in Valkey (deleted on consume) and its lifetime is cut to two minutes — a replayed link is rejected.
4 tasks
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.
Description
SAML single sign-on (SP-initiated) never completed in any configuration. This fixes four independent defects that surface together when the app runs behind a reverse proxy (reported case: Okta IdP, app on k3s):
output: "standalone"the server binds to the pod'sHOSTNAME, and Next buildsrequest.urlfrom that bound host rather than theHostheader — so SAML redirects emitted unreachable URLs likehttps://<pod>:3000/.... Absolute auth URLs are now built fromNEXTAUTH_URLvia a sharedgetAppBaseUrl()helper (also applied to the magic-link and logout/SLO redirects).SsoProviderid, but the handler queriedSamlConfiguration.id→ 404 "SAML provider not found or disabled". Now looks up by the uniqueproviderIdforeign key./api/auth/callback/saml, but the validator lived at/api/auth/saml/callbackand no NextAuthsamlprovider exists, so the POST hit the[...nextauth]catch-all and failed with "This action with HTTP POST is not supported by NextAuth.js". The validator now lives at the path the IdP targets and hands off to/api/auth/saml/completeto mint the session.RelayState, backed by Valkey (single-use, short-lived, works across pods) with a signed-token fallback when Valkey is absent.Also seeds
access/isApi/passwordChangedAtinto the completion session token so middleware sees the user's access level on the first request.Related Issue
Customer-reported SAML/Okta setup failure (no public issue).
Type of Change
Testing
/api/auth/saml/completeand decodes it with NextAuth's owndecode()to prove the post-login handoff yields a valid session (sub === userId).pnpm precommit(lint + format) clean;pnpm buildsucceeds with the new route registered;tsc --noEmitclean.NEXTAUTH_URLset to the public origin (already required for the ACS URL).Checklist
🤖 Generated with Claude Code