Repository navigation
Conversation
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.
|
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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.urlreports the internalhttp://origin. Cookie options derivedSecurefrom that URL alone, so the session cookie (and PKCE verifier cookie) was issued withoutSecureeven though the app is served over HTTPS. PassingbaseURLtohandleAuth(), which is documented for exactly this deployment shape, only fixed the post-login redirect, not the cookies.The policy
One function (
isSecureOrigininsrc/cookie.ts) decidesSecurefor every AuthKit cookie: session, PKCE verifier, and theeagerAuthaccess-token cookie. A cookie isSecureif any origin signal is HTTPS:handleAuth'sbaseURL, the middlewareredirectUrioption (or itsx-redirect-urirequest header), an explicitgetSignInUrl({ redirectUri }), and the redirect URI sealed into the PKCE stateNEXT_PUBLIC_WORKOS_REDIRECT_URIMissing or unparseable signals fail closed to
Secure, andSameSite=Nonestill forces it. Adding a signal can only tighten the result.Set-cookie paths covered
handleAuth): session cookie, PKCE delete,onSuccess-failure clearbaseURL, redirect URI sealed in PKCE state, envupdateSession) andeagerAuthJWT cookieredirectUrioption, envhandleAuthkitProxy)x-redirect-uri, envrefreshSession/switchToOrganizationx-url,x-redirect-uri, envwithAuth({ ensureSignedIn })→ PKCE cookiex-url,x-redirect-uri, envgetSignInUrl/getSignUpUrl→ PKCE cookiex-url,x-redirect-uri, explicitredirectUri, envsaveSession(public, signature unchanged)getAuthorizationUrlseals 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.tsgetCookieOptions(urls, { expired })andgetPKCECookieOptions(urls, { expired })return structured options only, and a singleserializeCookie(name, value, options)builds rawSet-Cookiestrings. This removes theasStringoverloads and the regex patching of serialized PKCE cookies.getJwtCookieuses 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
Securecookie before still does.Secureis omitted only when every signal ishttp://(e.g. local dev onhttp://localhost). TheeagerAuthaccess-token cookie keeps its production floor (an opt-in branch of the same policy): in production builds it staysSecureunless every signal is localhost.Existing non-
Securesession cookies are replaced withSecureones on the next refresh, with no forced sign-out.Testing
Unit tests: cover each path in the table above with an internal
http://web:3000request and an HTTPS public origin. Each new test was confirmed to fail with its corresponding fix reverted. Negative controls confirm that all-httpsetups stay non-Secure.End-to-end:
examples/nextas a production build (next build && next start, Next 16.2.6) behind Caddy terminating TLS and forwarding plain HTTP withHost: web:3000, against a local fake WorkOS API:Set-Cookie: wos-sessionHttpOnly; SameSite=laxSecure; HttpOnly; SameSite=laxhttp://request to the same hostSet-Cookie: wos-sessionHttpOnly; SameSite=Lax…; Secure/loginSecurehandleAuth({ baseURL })redirect + cookiehttps://…/+SecureredirectUriset only onauthkitProxy,http://env URI, nobaseURLSecureon PKCE, session and refresh cookieshttp://localhostredirect URI (control)SecureSecureChecklist
pnpm testpasses (453 tests)pnpm run buildsucceedspnpm run lintpassesoxfmt --check srcpasses