[GH-ISSUE #3330] SAML SSO callback in Next.js App Router never issues final 302 redirect #9572

Closed
opened 2026-04-13 05:05:45 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @iamshadmirza on GitHub (Jul 10, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3330

When running an SP-initiated SAML flow on Next.js App Router, Better Auth correctly:

  1. Generates the SAMLRequest and returns { redirect: true, url: … }
  2. Validates the incoming SAMLResponse POST
  3. Runs my provisionUser hook

…but it never issues the final HTTP 302 back to my callbackURL. Instead the callback endpoint always returns JSON { redirect: true, url: "https://accounts.google.com/.../dashboard" } as if it were starting a new flow.


Environment

  • better-auth: "1.3.0-beta.7",
  • @better-auth/sso: "^1.3.0-beta.1",
  • next: "^15.3.0", App Router
  • Node: 20.x
  • Browser: reproduced in Chrome on localhost via ngrok
  • Google Workspace: SAML App configured with correct ACS URL & Entity ID

Steps to reproduce

  1. Register the SAML provider via the API, embedding both spMetadata.metadata and idpMetadata.metadata.

  2. On the frontend, call:

    const { redirect, url } = await authClient.signIn.sso({
      email,
      callbackURL: "/"
    });
    if (redirect) window.location.href = url;
    
  3. Browser navigates to Google, user logs in, clicks app tile → Google auto-POSTs SAMLResponse to:

    POST /api/auth/sso/saml2/callback/test-google-saml
    
  4. Server logs show:

    POST /api/auth/sign-in/sso 200 in 320ms  
    User signed in via SSO provider: test-google-saml  
    Successfully provisioned user shad@workspace.com from SSO provider test-google-saml  
    POST /api/auth/sso/saml2/callback/test-google-saml 200 in 3018ms
    
  5. Expected: callback response is HTTP 302 → / (with session cookie).
    Actual: callback response is JSON

    { "redirect": true, "url": "https://accounts.google.com/o/saml2?idpid=<idp-id>/dashboard" }
    

What I’ve tried

  • Ensured app/api/auth/[...all]/route.ts exists with:

    import { auth } from "@/lib/auth";
    import { toNextJsHandler } from "better-auth/next-js";
    
    export const { GET, POST } = toNextJsHandler(auth.handler);
    
  • Removed any pages/api/auth routes to avoid Next.js precedence conflicts.

  • Patched specific SSO callback route at app/api/auth/sso/saml2/callback/[providerId]/route.ts to:

    import { toNextJsHandler } from "better-auth/next-js";
    import { auth } from "@/lib/auth";
    
    export const POST = toNextJsHandler(auth.handler).POST;
    export const GET  = toNextJsHandler(auth.handler).GET;
    
  • Verified issuer, audience, and ACS URL match between SP metadata, Google App, and code.

  • Restarted dev server after every change.

Despite all of the above, the final 302 is never sent. It appears the SSO plugin’s redirect logic (which lives inside auth.handler) isn’t being invoked for the callback POST, and instead the generic “start‐flow” JSON is returned.


Request
Can you help identify why the SAML callback handler isn’t issuing its HTTP 302 redirect? Is there a missing configuration or a known issue with App Router catch-all vs. specific nested route handling? Any pointers or fixes would be greatly appreciated.

Originally created by @iamshadmirza on GitHub (Jul 10, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3330 When running an SP-initiated SAML flow on Next.js App Router, Better Auth correctly: 1. Generates the SAMLRequest and returns `{ redirect: true, url: … }` 2. Validates the incoming `SAMLResponse` POST 3. Runs my `provisionUser` hook …but it never issues the final HTTP 302 back to my `callbackURL`. Instead the callback endpoint always returns JSON `{ redirect: true, url: "https://accounts.google.com/.../dashboard" }` as if it were starting a new flow. --- **Environment** * **better-auth**: "1.3.0-beta.7", * **@better-auth/sso**: "^1.3.0-beta.1", * **next**: "^15.3.0", App Router * **Node**: 20.x * **Browser**: reproduced in Chrome on localhost via ngrok * **Google Workspace**: SAML App configured with correct ACS URL & Entity ID --- **Steps to reproduce** 1. Register the SAML provider via the API, embedding both `spMetadata.metadata` and `idpMetadata.metadata`. 2. On the frontend, call: ```ts const { redirect, url } = await authClient.signIn.sso({ email, callbackURL: "/" }); if (redirect) window.location.href = url; ``` 3. Browser navigates to Google, user logs in, clicks app tile → Google auto-POSTs SAMLResponse to: ``` POST /api/auth/sso/saml2/callback/test-google-saml ``` 4. Server logs show: ``` POST /api/auth/sign-in/sso 200 in 320ms User signed in via SSO provider: test-google-saml Successfully provisioned user shad@workspace.com from SSO provider test-google-saml POST /api/auth/sso/saml2/callback/test-google-saml 200 in 3018ms ``` 5. **Expected**: callback response is HTTP 302 → `/` (with session cookie). **Actual**: callback response is JSON ```json { "redirect": true, "url": "https://accounts.google.com/o/saml2?idpid=<idp-id>/dashboard" } ``` --- **What I’ve tried** * Ensured `app/api/auth/[...all]/route.ts` exists with: ```ts import { auth } from "@/lib/auth"; import { toNextJsHandler } from "better-auth/next-js"; export const { GET, POST } = toNextJsHandler(auth.handler); ``` * Removed any `pages/api/auth` routes to avoid Next.js precedence conflicts. * Patched specific SSO callback route at `app/api/auth/sso/saml2/callback/[providerId]/route.ts` to: ```ts import { toNextJsHandler } from "better-auth/next-js"; import { auth } from "@/lib/auth"; export const POST = toNextJsHandler(auth.handler).POST; export const GET = toNextJsHandler(auth.handler).GET; ``` * Verified `issuer`, `audience`, and ACS URL match between SP metadata, Google App, and code. * Restarted dev server after every change. Despite all of the above, the final 302 is never sent. It appears the SSO plugin’s redirect logic (which lives inside `auth.handler`) isn’t being invoked for the callback POST, and instead the generic “start‐flow” JSON is returned. --- **Request** Can you help identify why the SAML callback handler isn’t issuing its HTTP 302 redirect? Is there a missing configuration or a known issue with App Router catch-all vs. specific nested route handling? Any pointers or fixes would be greatly appreciated.
GiteaMirror added the locked label 2026-04-13 05:05:46 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jul 10, 2025):

Hi @iamshadmirza! I'm Dosu and I’m helping the better-auth team.

The open-source better-auth codebase doesn't implement SAML SSO callback logic or true HTTP 302 redirect handling for Next.js App Router. The toNextJsHandler is just a thin wrapper and doesn't convert control flow objects (like { redirect: true, url: ... }) into real HTTP redirects—so you get a JSON response instead of a 302. There’s also no SAML POST/ACS flow or SAML callback endpoint logic in the mainline code; all SSO plugin logic is OIDC/OAuth2-based, so any SAML behavior you see is coming from userland or external code, not the core framework. To get a real 302 after the SAML callback, you’ll need to add custom glue code in your route handler to detect these control flow results and return a Next.js redirect response yourself. Native SAML and redirect support would require more work in the OSS project itself.
Reference

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

<!-- gh-comment-id:3057326240 --> @dosubot[bot] commented on GitHub (Jul 10, 2025): <!-- Greeting --> Hi @iamshadmirza! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The open-source better-auth codebase doesn't implement SAML SSO callback logic or true HTTP 302 redirect handling for Next.js App Router. The toNextJsHandler is just a thin wrapper and doesn't convert control flow objects (like `{ redirect: true, url: ... }`) into real HTTP redirects—so you get a JSON response instead of a 302. There’s also no SAML POST/ACS flow or SAML callback endpoint logic in the mainline code; all SSO plugin logic is OIDC/OAuth2-based, so any SAML behavior you see is coming from userland or external code, not the core framework. To get a real 302 after the SAML callback, you’ll need to add custom glue code in your route handler to detect these control flow results and return a Next.js redirect response yourself. Native SAML and redirect support would require more work in the OSS project itself. [Reference](https://github.com/better-auth/better-auth/blob/1c6a1dcf9f6640c75516661490ee8cfe0d2b53fd/packages/better-auth/src/integrations/next-js.ts) <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/36276e8c-59fd-4d97-8da2-43c9878f9e70?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/36276e8c-59fd-4d97-8da2-43c9878f9e70?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/36276e8c-59fd-4d97-8da2-43c9878f9e70?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/36276e8c-59fd-4d97-8da2-43c9878f9e70?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/36276e8c-59fd-4d97-8da2-43c9878f9e70?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/36276e8c-59fd-4d97-8da2-43c9878f9e70?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/36276e8c-59fd-4d97-8da2-43c9878f9e70?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3330)
Author
Owner

@natetewelde commented on GitHub (Jul 11, 2025):

I'm having the exact same issue and from a conversation in the discord with @Bekacru, the SSO plugin SHOULD handle redirect. I've noticed in the base index code that there's a callbackSSO, and callbackSSOSAML function, where one seems to handle a redirect and the other just returns the json:

callbackSSO return: https://github.com/better-auth/better-auth/blob/v1.3/packages/sso/src/index.ts#L1166

callbackSSOSAML return: https://github.com/better-auth/better-auth/blob/v1.3/packages/sso/src/index.ts#L1331

<!-- gh-comment-id:3063224849 --> @natetewelde commented on GitHub (Jul 11, 2025): I'm having the exact same issue and from a conversation in the discord with @Bekacru, the SSO plugin SHOULD handle redirect. I've noticed in the base index code that there's a callbackSSO, and callbackSSOSAML function, where one seems to handle a redirect and the other just returns the json: callbackSSO return: https://github.com/better-auth/better-auth/blob/v1.3/packages/sso/src/index.ts#L1166 callbackSSOSAML return: https://github.com/better-auth/better-auth/blob/v1.3/packages/sso/src/index.ts#L1331
Author
Owner

@osdiab commented on GitHub (Jul 16, 2025):

this seems relevant. https://github.com/better-auth/better-auth/pull/3343

<!-- gh-comment-id:3077114538 --> @osdiab commented on GitHub (Jul 16, 2025): this seems relevant. https://github.com/better-auth/better-auth/pull/3343
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9572