Skip to content

fix: start page authentication in the callback route - #482

Merged
mitch-fultz merged 5 commits into
mainfrom
fix/page-auth-redirect
Oct 5, 2026
Merged

mitch-fultz merged 5 commits into
mainfrom
fix/page-auth-redirect

Conversation

@mitch-fultz

@mitch-fultz mitch-fultz commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes DAX-3330 — Linear issue

Problem and discovery

Anonymous Ask WorkOS QA generated the documented Next.js integration: default authkitProxy() plus withAuth({ ensureSignedIn: true }) in a Server Component. We copied all six generated application files, including AuthKitProvider, into a real Next 16.2.1 app with published AuthKit 4.3.1, Node SDK 10.13.0, and React 19.2.4.

Fresh / returned 200, /dashboard returned 500 twice, and the explicit /login Route Handler returned 307. The dashboard error was:

Cookies can only be modified in a Server Action or Route Handler.

The SDK README and canonical docs recommend this pattern. This was an upstream SDK/context mismatch, not an invented API or an indexing bug.

Adjacent-commit checks in the same full Next fixture confirmed 4c3f27b redirects successfully and ebef6e7 (#388) fails when PKCE/state becomes mandatory. The later cookie-deferral change #432 is not the cause and is not reverted.

Minimal reproduction

In a normal Next 16 App Router app with its root layout/provider, use:

// proxy.ts
import { authkitProxy } from '@workos-inc/authkit-nextjs';
export default authkitProxy();
export const config = { matcher: ['/dashboard'] };

// app/dashboard/page.tsx
import { withAuth } from '@workos-inc/authkit-nextjs';
export default async function Dashboard() {
  const { user } = await withAuth({ ensureSignedIn: true });
  return <p>{user.email}</p>;
}

// app/callback/route.ts
import { handleAuth } from '@workos-inc/authkit-nextjs';
export const GET = handleAuth();

Set WORKOS_API_KEY, WORKOS_CLIENT_ID, a 32+ character WORKOS_COOKIE_PASSWORD, and NEXT_PUBLIC_WORKOS_REDIRECT_URI=http://127.0.0.1:3000/callback, then run the app and request:

curl -i -H 'Accept: text/html' http://127.0.0.1:3000/dashboard

Dummy credentials suffice to reproduce the failure; do not follow the eventual WorkOS redirect for this local check.

Change

  • The shared page guard redirects through the existing callback route instead of writing cookies during rendering. No additional customer route or configuration.
  • One sealed, purpose-tagged routing payload carries the configured callback URI, return path/query and sign-in/sign-up hint. This preserves the original URI even when Next normalizes the incoming host or query encoding. Routing data is not an OAuth credential and does not arbitrarily expire cached links.
  • Only an explicit document-navigation start creates fresh PKCE/state. Next RSC/router-prefetch requests receive cookie-free 200 non-Flight HTML; Next performs a full navigation when needed. Native browser Purpose/Sec-Purpose preloads receive cookie-free, no-store 503 so Chrome cannot reuse an empty successful callback page on navigation. Existing proxy cookie deferral remains intact.
  • Normal code/state requests retain callback validation, including malformed mixed requests. OAuth state cannot substitute for routing data. Schema failures use a generic error rather than exposing decrypted input through logs or onError. Per-flow cookie names and existing callback checks/cleanup remain unchanged; cookie options use the configured public URI, not an internal proxy origin.
  • README examples call the still-cookie-writing getSignInUrl() / getSignUpUrl() helpers in Route Handlers or Server Actions, not during rendering. No Ask-specific override or public getter API expansion.

Verification

  • Final head 8a1a3b5eea3cedbae8ae78b6a59c1a328d5ec6fc, tree b675aded98c8da3c476453e43e01a1f80ef35364, integrated with current main 3acb7e1c. Node 22.22.3 and 24.21.0 format, zero-warning lint, source/test typecheck and build pass; 461 tests pass on each Node version. Exact-head CI and all five required checks pass. Final Greptile completion remains a pre-merge gate; no prior-head result is substituted.
  • Final regression suite fails on the original implementation with Next's actual read-only cookie adapter. Targeted ablations also fail when routing validation, callback precedence, RSC handoff or cached-link lifetime behavior is removed.
  • Same six application files: /dashboard now returns a local 307 with no verifier cookie; its document start returns 307 with one matching cookie. Verified state/cookie equality, SHA-256 challenge/verifier equality, return path/query and byte-exact configured callback URI, including 127.0.0.1 and encoded query values.
  • Real production Next 16.2.1 Link prefetch/click in Chrome: no pre-click verifier cookie or authorization request; callback RSC request returns 200/no cookie, followed by document navigation returning 307/one cookie. The final unchanged six-application-file fixture passes 11 checks covering document/Next/browser preload handling, exact callback URI and return query, cached routing/sign-up hints, concurrent PKCE, callback security, Route Handler and Server Action.
  • Actual Chrome speculation prefetch and prerender-preload followed by normal navigation reproduce the old blank-callback defect on 905e8ccf: Chrome reuses empty callback 200 and makes no fresh document request. Final SDK 503/no-store/no verifier cookie instead yields PrefetchFailedNon2XX; real navigation then reaches document 307, one fresh cookie and independently validated PKCE at a loopback authorization sink. The proxy passes through actual SDK status/headers/body unchanged. DevTools disables detached prerendering, so full detached or omnibox prerender activation is not claimed. No hosted login or token exchange occurred. The final fixture was reconstructed from the documented six files; the original September fixture was unavailable.

Release and documentation handoff

The owner has authorized merge and delivery. This source PR still does not itself publish the fix: use the normal Release Please release PR and npm OIDC publish workflow, then qualify the actual registry tarball/source and unchanged Next fixture. A separate reviewed workos/workos docs PR must regenerate the AuthKit README through the canonical transform, align the guide/proxy examples and record the actual first fixed npm version before normal docs deployment. A complete SDK-source reindex is also required. Source merge/indexing alone is not npm or canonical-docs delivery. No package version is claimed until the actual release is published and qualified; no merge, release, docs deploy or index mutation has occurred as of this final source verification.

Fixes DAX-3330. Preserve page-level guards without render-time cookie writes or speculative PKCE cookies.
@mitch-fultz
mitch-fultz requested a review from a team as a code owner September 13, 2026 05:08
@linear-code

linear-code Bot commented Sep 13, 2026

Copy link
Copy Markdown

DAX-3330

@greptile-apps

greptile-apps Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Critical risk] Restructures authentication flow and PKCE verification.

The PR appears safe to merge based on the reviewed changes.

Summary

The PR moves PKCE initiation for signed-out Server Component guards into the existing callback route and updates the README examples accordingly.

  • The latest changes classify parameterized browser prefetch headers and return a cookie-free, non-cacheable response, while retaining the document-navigation path for starting authentication.
  • Regression tests cover passive requests and subsequent document starts.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A["Signed-out page guard"] --> B["Redirect to callback with sealed routing data"]
  B --> C{"Request type"}
  C -->|Browser prefetch| D["503; no PKCE cookie"]
  C -->|RSC or Next prefetch| E["200; no PKCE cookie"]
  C -->|Document navigation| F["Generate fresh PKCE and state"]
  F --> G["Set verifier cookie and redirect to AuthKit"]
Loading

Reviews (5) · Last reviewed commit: "fix: prevent browser preloads from consu..."

@nicknisi nicknisi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Looks good — approving.

Right fix for the Next 16 "cookies only in Server Action / Route Handler" failure when withAuth({ ensureSignedIn: true }) runs in a Server Component. Moving PKCE setup onto the existing handleAuth() callback (via sealed __authkit_start routing data) is clean, and the prefetch/RSC short-circuit is important so verifier cookies aren't created by Link prefetch.

Routing payload validation (purpose tag, return-path origin check, no OAuth-state substitution) and the new auth-start.spec.ts coverage look solid. README guidance to keep getSignInUrl / getSignUpUrl in Route Handlers is correct.

Local verification (this branch, workspace-linked examples/next)

  • pnpm test — 430/430 passed; lint / typecheck / build clean (Node 22)
  • Signed-out document GET /account → 307 to /callback?__authkit_start=…, no Set-Cookie, no 500
  • Document navigation to that start URL → 307 to WorkOS authorize + one wos-auth-verifier-* cookie (Max-Age=600)
  • RSC/prefetch GET to the same start URL → 200 empty body, no Set-Cookie

Didn't complete a live OAuth round-trip (dummy credentials); the signed-out start path that was 500ing is fixed.

Non-blocking: sealed __authkit_start query strings can get long — worth a quick thought on reverse-proxy URL limits in prod, but I wouldn't block on it.

@mitch-fultz

Copy link
Copy Markdown
Contributor Author

No further product change from this review. The approved fix conflicts with main, so it cannot land until it is rebased.

Integrate current callback failure cleanup and issuer validation without changing the accepted page-guard PKCE flow. Resolve README terminology and update the renamed sign-in route anchor.
Treat Purpose and Sec-Purpose prefetch tokens with parameters as passive requests. Cover Chrome prerender and whitespace variants at the existing callback boundary without changing ordinary document PKCE behavior.
@mitch-fultz
mitch-fultz merged commit 13938be into main Oct 5, 2026
7 checks passed
@mitch-fultz
mitch-fultz deleted the fix/page-auth-redirect branch October 5, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants