Skip to content

Session cookie not delivered on /api/auth/callback redirect (0.7.x–0.8.2, prod build only) #90

@scryer-Jim

Description

@scryer-Jim

Summary

In production builds on TanStack Start v1, the session cookie set by handleCallbackRoute is missing from the final response. Only the PKCE verifier-delete cookie reaches the browser. Sign-in completes successfully on the server (onSuccess fires with the right user + org), but the next request has no session, so the user gets bounced back to a "not authenticated" state.

Local dev (Vite SSR) is unaffected. The bug is only visible after pnpm build + serving via Node.

Versions

  • @workos/authkit-tanstack-react-start 0.8.2
  • @workos/authkit-session 0.5.1 (transitive)
  • @tanstack/react-start 1.151.x
  • @tanstack/start-server-core 1.167.x
  • Node 24.x
  • Deployed via a thin prod-server.js that wraps the built fetch handler in http.createServer

Pre-0.7 (0.6.0) does not exhibit this. Bug presumably introduced in 0.7.0 with the per-flow PKCE verifier flow (#68) and the ctx.__setPendingHeader / middleware pending-headers architecture. 0.7.x and 0.8.0/0.8.1 not individually tested but share the same code path.

Reproduction

  1. Multi-org user (3 orgs) signs in via WorkOS hosted picker
  2. Picks an org → WorkOS redirects to /api/auth/callback?code=...&state=...
  3. Callback onSuccess fires with the correct user and organizationId
  4. AuthKit redirects to returnPathname (/setup in our app)
  5. /setup loader calls getAuth()auth.user is null, no session
  6. User bounces back to setup/login screen

Diagnostic evidence

We added temporary logging:

Inside onSuccess — confirms WorkOS is returning the right shape:

DEBUG callback onSuccess
  userId: user_01KN…
  organizationId: org_01KN…   (Scryer)
  firstName: James

Wrapped handleCallbackRoute(...)(ctx) to log its response before middleware wrap:

DEBUG callback response shape (pre-middleware-wrap)
  status: 307
  location: https://tisket.com/setup
  setCookieCount: 0          ← AuthKit's returned response has zero Set-Cookies
  setCookieNames: []

DevTools Network tab on the FINAL response (after authkitMiddleware wraps):

Set-Cookie  wos-auth-verifier-a895956b=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax

Only the PKCE verifier-delete cookie is present. No wos-session=....

/setup loader sees:

DEBUG fetchSetupData auth shape (no user)
  authKeys: ["user"]

Suspected root cause

In AuthService.handleCallback:

const save = await this.storage.saveSession(response, encryptedSession);  // (1) session cookie
let clear = {};
if (cookieName) {
  clear = await this.storage.clearCookie(save.response ?? response, cookieName, clearOptions);  // (2) verifier delete
}
return {
  response: clear.response ?? save.response,
  headers: mergeHeaderBags(save.headers, clear.headers),
  ...
};

Both (1) and (2) flow into TanStackStartCookieSessionStorage.applyHeaders, which on the middleware-context path calls ctx.__setPendingHeader('Set-Cookie', header). The middleware appends each to a pendingHeaders = new Headers() and at the end wraps the response with [...pendingHeaders].

Both calls should land in pendingHeaders as separate Set-Cookie entries. Only the second one reaches the wrapped response.

We could not determine the exact mechanism — candidates we considered but did not confirm:

  • Multi-value Set-Cookie lost during [...pendingHeaders] iteration on certain Node/undici versions in prod (didn't observe in local Vite)
  • Object.entries(headers) in applyHeaders not iterating something it should (seems unlikely — the input is { 'Set-Cookie': string })
  • Some HTTP-layer per-header size cap silently dropping the larger Set-Cookie (the session cookie is several KB encrypted; the verifier delete is small)

The fact that local dev works strongly suggests it's an interaction with the TanStack Start v1 production-build response pipeline rather than AuthKit-only.

Workaround

Pinning back to 0.6.0 (which used the pre-PKCE flow with getSignInUrl() returning a bare URL string and no middleware-pending-cookie dance) restored working sign-in. Trade-off: loses the PKCE/CSRF protection added in 0.7.0 — but that protection is non-functional in 0.8.2 on prod anyway, since the cookie that backs it isn't being delivered.

What would help

  • Pointer to whether multi-value Set-Cookie via ctx.__setPendingHeader is expected to round-trip through the middleware wrap correctly under TanStack Start v1's compiled handler
  • Or guidance to fold the session Set-Cookie into the response directly (the way 0.6.0 did) instead of routing it through the middleware pending-header mechanism

Happy to provide more diagnostic data if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions