Context
There's a class of apps — particularly those with strict GDPR compliance requirements — that need the session-token cookie to be a browser-session cookie (no Expires/Max-Age).
Under GDPR and the ePrivacy Directive, strictly necessary cookies are exempt from consent requirements. Authentication cookies typically qualify. However, compliance teams and some European DPAs expect "strictly necessary" to also mean minimally scoped — and a persistent cookie with a 30-day Expires attribute is harder to defend as "strictly necessary" than one that disappears when the browser closes. The data minimization principle (Article 5(1)(c)) pushes in the same direction: don't retain longer than needed, and a browser session is often the right scope for an auth token.
This also comes up outside of GDPR — banking and government applications commonly prohibit persistent auth cookies by policy, and enterprise SSO setups sometimes require the auth cookie lifetime to be session-scoped even when the underlying token has a longer validity window.
Right now there's no first-class way to get this behavior from Auth.js. Apps that need it have to strip Expires/Max-Age out of the Set-Cookie header after Auth.js writes it. That means intercepting in middleware, in route handlers, and around signIn/updateSession — which write cookies via the framework's cookies() API rather than response headers, so they need a separate interception path. It's fragile, it depends on internal cookie names, and it breaks silently across upgrades.
Two reasonable places to put the config knob
Option A: session.maxAge: null
Auth({
session: { maxAge: null }
})
The problem here is that session.maxAge drives two separate things: the cookie's Expires attribute and the JWT exp claim (or the DB session expiry column). There's no such thing as a "session-scoped JWT" — jose's EncryptJWT requires an exp claim. So null would need to mean "no cookie expiry, but still use some fallback for exp", which conflates two concerns in one option. It's a little muddy.
Option B: cookies.sessionToken.options.maxAge: null (I think this is the right call)
Auth({
cookies: {
sessionToken: {
options: { maxAge: null }
}
}
})
This keeps the concerns separate. session.maxAge keeps controlling JWT exp and DB session expiry unchanged. The cookie config controls the cookie transport layer. Setting maxAge: null on the session token cookie means "omit Expires and Max-Age" — the existing cookies config already lets you customize the session token cookie, this just adds null as a valid value to signal a browser-session cookie.
The caveats worth documenting:
session.maxAge still enforces expiry via JWT exp (default 30 days). The session will become invalid after that window even if the browser never closed. That's actually correct behavior — it's the enforcement layer.
- Chromium restores session cookies on restart, so "gone on browser close" is a browser hint, not a hard guarantee. JWT exp is the real enforcement.
- For DB strategy, the
expires column is unaffected. The DB row still expires after session.maxAge. The cookie just becomes session-scoped.
I've got a PoC branch implementing Option B if it'd be useful as a reference: https://github.com/chanceaclark/next-auth/tree/feat/session-cookie-maxage-null
Happy to open a PR against this once y'all weigh in on the API surface. Totally open to a different shape if there's a better place for it.
Context
There's a class of apps — particularly those with strict GDPR compliance requirements — that need the session-token cookie to be a browser-session cookie (no
Expires/Max-Age).Under GDPR and the ePrivacy Directive, strictly necessary cookies are exempt from consent requirements. Authentication cookies typically qualify. However, compliance teams and some European DPAs expect "strictly necessary" to also mean minimally scoped — and a persistent cookie with a 30-day
Expiresattribute is harder to defend as "strictly necessary" than one that disappears when the browser closes. The data minimization principle (Article 5(1)(c)) pushes in the same direction: don't retain longer than needed, and a browser session is often the right scope for an auth token.This also comes up outside of GDPR — banking and government applications commonly prohibit persistent auth cookies by policy, and enterprise SSO setups sometimes require the auth cookie lifetime to be session-scoped even when the underlying token has a longer validity window.
Right now there's no first-class way to get this behavior from Auth.js. Apps that need it have to strip
Expires/Max-Ageout of theSet-Cookieheader after Auth.js writes it. That means intercepting in middleware, in route handlers, and aroundsignIn/updateSession— which write cookies via the framework'scookies()API rather than response headers, so they need a separate interception path. It's fragile, it depends on internal cookie names, and it breaks silently across upgrades.Two reasonable places to put the config knob
Option A:
session.maxAge: nullThe problem here is that
session.maxAgedrives two separate things: the cookie'sExpiresattribute and the JWTexpclaim (or the DB session expiry column). There's no such thing as a "session-scoped JWT" —jose'sEncryptJWTrequires anexpclaim. Sonullwould need to mean "no cookie expiry, but still use some fallback forexp", which conflates two concerns in one option. It's a little muddy.Option B:
cookies.sessionToken.options.maxAge: null(I think this is the right call)This keeps the concerns separate.
session.maxAgekeeps controlling JWTexpand DB session expiry unchanged. The cookie config controls the cookie transport layer. SettingmaxAge: nullon the session token cookie means "omitExpiresandMax-Age" — the existingcookiesconfig already lets you customize the session token cookie, this just addsnullas a valid value to signal a browser-session cookie.The caveats worth documenting:
session.maxAgestill enforces expiry via JWTexp(default 30 days). The session will become invalid after that window even if the browser never closed. That's actually correct behavior — it's the enforcement layer.expirescolumn is unaffected. The DB row still expires aftersession.maxAge. The cookie just becomes session-scoped.I've got a PoC branch implementing Option B if it'd be useful as a reference: https://github.com/chanceaclark/next-auth/tree/feat/session-cookie-maxage-null
Happy to open a PR against this once y'all weigh in on the API surface. Totally open to a different shape if there's a better place for it.