Skip to content

feat: support browser-session cookies for the session token cookie #13456

@chanceaclark

Description

@chanceaclark

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    invalid reproductionThe issue did not have a detectable valid reproduction URL

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions