diff --git a/core/auth/anonymous-session.ts b/core/auth/anonymous-session.ts index c5a4f98ca9..3ed7fee2b6 100644 --- a/core/auth/anonymous-session.ts +++ b/core/auth/anonymous-session.ts @@ -31,9 +31,6 @@ export const anonymousSignIn = async (user: Partial = { cartId: n cookieJar.set(`${cookiePrefix}${anonymousCookieName}`, jwt, { secure: useSecureCookies, sameSite: 'lax', - // We set the maxAge to 7 days as a good default for anonymous sessions. - // This can be adjusted based on your application's needs. - maxAge: 60 * 60 * 24 * 7, // 7 days httpOnly: true, }); }; diff --git a/core/auth/index.ts b/core/auth/index.ts index bf902fa2a8..e7a7aa8a6e 100644 --- a/core/auth/index.ts +++ b/core/auth/index.ts @@ -1,4 +1,5 @@ import { decodeJwt } from 'jose'; +import { cookies } from 'next/headers'; import NextAuth, { type NextAuthConfig, User } from 'next-auth'; import 'next-auth/jwt'; import CredentialsProvider from 'next-auth/providers/credentials'; @@ -184,6 +185,7 @@ const config = { session: { strategy: 'jwt', }, + cookies: {}, pages: { signIn: '/login', signOut: '/logout', @@ -318,7 +320,52 @@ const config = { ], } satisfies NextAuthConfig; -export const { handlers, auth, signIn, signOut, unstable_update: updateSession } = NextAuth(config); +const SESSION_TOKEN_NAME_RE = /^(__Secure-)?authjs\.session-token(\.\d+)?$/; + +// Auth.js sets Expires on session token cookies via cookies().set() when signIn/updateSession +// are called from server actions. Re-set those cookies without Expires so they comply with +// Essential classification (session cookies that expire when the browser closes). +async function patchSessionTokenCookies() { + const cookieJar = await cookies(); + + cookieJar.getAll().forEach(({ name, value }) => { + if (SESSION_TOKEN_NAME_RE.test(name) && value) { + cookieJar.set(name, value, { + httpOnly: true, + sameSite: 'lax' as const, + path: '/', + secure: name.startsWith('__Secure-'), + }); + } + }); +} + +const { + handlers, + auth, + signIn: authSignIn, + signOut, + unstable_update: authUpdateSession, +} = NextAuth(config); + +export { handlers, auth, signOut }; + +export const signIn = async (...args: Parameters) => { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return await authSignIn(...args); + } finally { + await patchSessionTokenCookies(); + } +}; + +export const updateSession = async (...args: Parameters) => { + try { + return await authUpdateSession(...args); + } finally { + await patchSessionTokenCookies(); + } +}; export const getSessionCustomerAccessToken = async () => { try { diff --git a/core/proxies/with-auth.ts b/core/proxies/with-auth.ts index 2371adcffb..472f48a2f5 100644 --- a/core/proxies/with-auth.ts +++ b/core/proxies/with-auth.ts @@ -6,14 +6,36 @@ import { type ProxyFactory } from './compose-proxies'; // Path matcher for any routes that require authentication const protectedPathPattern = new URLPattern({ pathname: `{/:locale}?/(account)/*` }); +const SESSION_TOKEN_COOKIE_RE = /^(__Secure-)?authjs\.session-token(\.\d+)?=/; function redirectToLogin(url: string) { return NextResponse.redirect(new URL('/login', url), { status: 302 }); } +// Auth.js always sets Expires on session cookies based on session.maxAge. We strip it here +// so the cookie becomes a session cookie (expires when the browser closes), which is required +// for Essential cookie classification compliance. +function stripSessionCookieExpiry(response: Response): Response { + const setCookies = response.headers.getSetCookie(); + const stripped = setCookies.map((cookie) => + SESSION_TOKEN_COOKIE_RE.test(cookie) + ? cookie.replace(/;\s*(?:expires|max-age)=[^;]+/gi, '') + : cookie, + ); + + if (stripped.every((c, i) => c === setCookies[i])) return response; + + const modified = new Response(response.body, response); + + modified.headers.delete('set-cookie'); + stripped.forEach((cookie) => modified.headers.append('set-cookie', cookie)); + + return modified; +} + export const withAuth: ProxyFactory = (next) => { return async (request, event) => { - return auth(async (req) => { + const response = await auth(async (req) => { const anonymousSession = await getAnonymousSession(); const isProtectedRoute = protectedPathPattern.test(req.nextUrl.toString().toLowerCase()); const isGetRequest = req.method === 'GET'; @@ -45,5 +67,7 @@ export const withAuth: ProxyFactory = (next) => { // Continue the proxy chain return next(req, event); })(request, event); + + return response ? stripSessionCookieExpiry(response) : response; }; };