Skip to content

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

Open
mitch-fultz wants to merge 2 commits into
mainfrom
fix/page-auth-redirect
Open

fix: start page authentication in the callback route#482
mitch-fultz wants to merge 2 commits into
mainfrom
fix/page-auth-redirect

Conversation

@mitch-fultz

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

Copy link
Copy Markdown

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. RSC/prefetch requests receive cookie-free, non-Flight HTML; Next performs a full navigation when needed. 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

  • Format, warning-free lint, typecheck and build pass; 430 tests pass.
  • 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 Link prefetch/click in Chrome: no pre-click cookie or authorization request; callback RSC request returns 200/no cookie, followed by document navigation returning 307/one cookie. A loopback authorization sink, not hosted login, verified the cookie/state/challenge and return path. No real login or token exchange was performed.

Release and documentation handoff

This PR does not publish the fix. npm consumers need the SDK release after merge. The scheduled workos/workos README sync opens a separate docs review PR; that needs merge/deployment before canonical docs change. A complete SDK-source reindex is also needed for retrieved source coverage. SDK merge/indexing alone does not update both npm and canonical docs. No merge, release, deploy or production reindex was performed here.

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

The PR appears safe to merge, with no outstanding correctness, security, or repository-rule violations identified.

Summary

  • Seals callback URI, return path, and screen hint into a purpose-specific routing payload.
  • Prevents RSC and prefetch requests from creating verifier cookies or starting authorization.
  • Preserves ordinary callback validation, per-flow PKCE cookies, and configured public callback URI behavior.
  • Updates documentation and adds focused regression coverage for routing validation, callback precedence, cached links, and passive requests.

Diagram

sequenceDiagram
    participant B as Browser
    participant P as Protected Server Component
    participant C as handleAuth callback route
    participant W as WorkOS authorization

    B->>P: Request protected page
    P-->>B: Redirect to callback with sealed routing payload
    alt RSC or prefetch request
        B->>C: Passive request
        C-->>B: 200 empty HTML, no PKCE cookie
        B->>C: Full document navigation
    else Document navigation
        B->>C: Authentication-start request
    end
    C->>C: Validate and unseal routing payload
    C->>C: Generate fresh PKCE verifier and OAuth state
    C-->>B: Set per-flow PKCE cookie and redirect
    B->>W: Authorization request
Loading

Reviews (2) · Last reviewed commit: "fix: keep rejected routing data out of e..."

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.

1 participant