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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions