Skip to content

fix: derive session cookie Secure flag from the browser-facing origin - #493

Open
nicknisi wants to merge 4 commits into
mainfrom
nicknisi/secure-cookie-behind-tls-proxy
Open

nicknisi wants to merge 4 commits into
mainfrom
nicknisi/secure-cookie-behind-tls-proxy

Conversation

@nicknisi

@nicknisi nicknisi commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Summary

When an app runs behind a proxy that terminates TLS and forwards plain HTTP to Next.js (CloudFront/ALB → container, Docker/Kubernetes ingress, etc.), NextRequest.url reports the internal http:// origin. Cookie options derived Secure from that URL alone, so the session cookie (and PKCE verifier cookie) was issued without Secure even though the app is served over HTTPS. Passing baseURL to handleAuth(), which is documented for exactly this deployment shape, only fixed the post-login redirect, not the cookies.

The policy

One function (isSecureOrigin in src/cookie.ts) decides Secure for every AuthKit cookie: session, PKCE verifier, and the eagerAuth access-token cookie. A cookie is Secure if any origin signal is HTTPS:

  • the request URL
  • the browser-facing URLs known at that call site: handleAuth's baseURL, the middleware redirectUri option (or its x-redirect-uri request header), an explicit getSignInUrl({ redirectUri }), and the redirect URI sealed into the PKCE state
  • NEXT_PUBLIC_WORKOS_REDIRECT_URI

Missing or unparseable signals fail closed to Secure, and SameSite=None still forces it. Adding a signal can only tighten the result.

Set-cookie paths covered

Path Origin signals
Callback (handleAuth): session cookie, PKCE delete, onSuccess-failure clear request URL, baseURL, redirect URI sealed in PKCE state, env
Middleware refresh / delete (updateSession) and eagerAuth JWT cookie request URL, redirectUri option, env
Middleware sign-in redirect PKCE cookie (handleAuthkitProxy) request URL, x-redirect-uri, env
refreshSession / switchToOrganization x-url, x-redirect-uri, env
withAuth({ ensureSignedIn }) → PKCE cookie x-url, x-redirect-uri, env
getSignInUrl / getSignUpUrl → PKCE cookie x-url, x-redirect-uri, explicit redirectUri, env
saveSession (public, signature unchanged) given request/URL, env

getAuthorizationUrl seals the redirect URI it used into the PKCE state. That way the callback knows the browser-facing origin even when the redirect URI was configured only on the middleware. The field is optional, so states sealed by earlier versions still parse.

Cleanup in cookie.ts

  • getCookieOptions(urls, { expired }) and getPKCECookieOptions(urls, { expired }) return structured options only, and a single serializeCookie(name, value, options) builds raw Set-Cookie strings. This removes the asString overloads and the regex patching of serialized PKCE cookies.
  • Origin URLs are a required array at every call site, so no caller can silently fall back to the request URL alone.
  • getJwtCookie uses the shared policy; its production/localhost floor is an opt-in branch of that policy instead of a separate inline derivation.

Behaviour changes

Strictly additive: every configuration that produced a Secure cookie before still does. Secure is omitted only when every signal is http:// (e.g. local dev on http://localhost). The eagerAuth access-token cookie keeps its production floor (an opt-in branch of the same policy): in production builds it stays Secure unless every signal is localhost.

Existing non-Secure session cookies are replaced with Secure ones on the next refresh, with no forced sign-out.

Testing

Unit tests: cover each path in the table above with an internal http://web:3000 request and an HTTPS public origin. Each new test was confirmed to fail with its corresponding fix reverted. Negative controls confirm that all-http setups stay non-Secure.

End-to-end: examples/next as a production build (next build && next start, Next 16.2.6) behind Caddy terminating TLS and forwarding plain HTTP with Host: web:3000, against a local fake WorkOS API:

Before After
Callback Set-Cookie: wos-session HttpOnly; SameSite=lax Secure; HttpOnly; SameSite=lax
Same cookie sent on a plain http:// request to the same host yes no
Middleware refresh Set-Cookie: wos-session HttpOnly; SameSite=Lax …; Secure
PKCE verifier cookie from /login — Secure
handleAuth({ baseURL }) redirect + cookie — https://…/ + Secure
redirectUri set only on authkitProxy, http:// env URI, no baseURL — Secure on PKCE, session and refresh cookies
Local dev, http://localhost redirect URI (control) not Secure not Secure

Checklist

  • pnpm test passes (453 tests)
  • pnpm run build succeeds
  • pnpm run lint passes
  • oxfmt --check src passes
  • New code has colocated tests

Behind a proxy that terminates TLS and forwards plain HTTP to the app,
NextRequest.url reports an internal http:// origin. Cookie options were
derived from that URL alone, so session and PKCE cookies were issued
without the Secure attribute even though the app is served over HTTPS.

Cookie options now consider every known origin signal: the request URL,
the browser-facing URL (handleAuth's baseURL, the middleware redirectUri
option), and NEXT_PUBLIC_WORKOS_REDIRECT_URI. If any of them is HTTPS the
cookie is Secure. This applies to the callback, saveSession, middleware
session refresh/deletion, and PKCE verifier cookies.
@nicknisi
nicknisi requested a review from a team as a code owner October 1, 2026 17:27
@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Critical risk] Changes how session and PKCE cookies determine the Secure flag.

The PR appears safe to merge; no outstanding findings were identified.

Summary

The PR derives cookie security from the available browser-facing origins, carries the chosen redirect URI through PKCE state for callback cookies, and restores the production Secure floor for the eager-auth access-token cookie.

  • Session, PKCE, and access-token cookie paths now share origin-signal handling.
  • The latest change restores the access-token cookie’s production protection for non-localhost HTTP origins.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Request URL] --> D[Secure origin policy]
  B[Redirect URI, baseURL, and PKCE state] --> D
  C[Environment redirect URI] --> D
  D --> E[Session and PKCE cookies]
  D --> F[Access-token cookie with production floor]
Loading

Reviews (4) · Last reviewed commit: "fix: keep the production Secure floor fo..."

Comment thread src/authkit-callback-route.ts Outdated
When the browser-facing redirect URI was configured only on the
middleware (no baseURL, no HTTPS NEXT_PUBLIC_WORKOS_REDIRECT_URI), the
callback had no HTTPS signal and issued the session cookie without
Secure.

getAuthorizationUrl now seals the redirect URI it used into the PKCE
state, and the callback includes it when deriving cookie options. The
field is optional, so states sealed by earlier versions still parse.
… cookies

- getJwtCookie uses the same origin-signal policy as every other cookie
  instead of its own NODE_ENV/localhost heuristic, and the eagerAuth call
  sites pass the full origin signals.
- Cover the remaining set-cookie paths: refreshSession and
  redirectToSignIn read the middleware's x-redirect-uri, getSignInUrl /
  getSignUpUrl include an explicit redirectUri, and handleAuthkitProxy
  passes x-redirect-uri when re-adding the PKCE cookie for the AuthKit
  redirect.
- getCookieOptions / getPKCECookieOptions return structured options only
  (getXOptions(urls, { expired })); a single serializeCookie builds
  Set-Cookie strings. Removes the asString overloads and the regex
  patching of serialized PKCE cookies.
- Origin URLs are a required array everywhere, so no caller can silently
  fall back to the request URL alone.
Comment thread src/cookie.ts Outdated
…cookie

The shared Secure policy gains an opt-in production floor, used only by
the eagerAuth access token cookie: in production builds it stays Secure
unless every origin signal is localhost. This preserves the cookie's
previous behavior, so the change is strictly additive for every cookie.
@linear-code

linear-code Bot commented Oct 1, 2026

Copy link
Copy Markdown

VULN-3742

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