Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions core/auth/anonymous-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ export const anonymousSignIn = async (user: Partial<AnonymousUser> = { 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,
});
};
Expand Down
49 changes: 48 additions & 1 deletion core/auth/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -184,6 +185,7 @@ const config = {
session: {
strategy: 'jwt',
},
cookies: {},
pages: {
signIn: '/login',
signOut: '/logout',
Expand Down Expand Up @@ -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<typeof authSignIn>) => {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await authSignIn(...args);
} finally {
await patchSessionTokenCookies();
}
};

export const updateSession = async (...args: Parameters<typeof authUpdateSession>) => {
try {
return await authUpdateSession(...args);
} finally {
await patchSessionTokenCookies();
}
};

export const getSessionCustomerAccessToken = async () => {
try {
Expand Down
26 changes: 25 additions & 1 deletion core/proxies/with-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +15 to +17

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no scenario for a consent selection that won't expire the cookie on browser close?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, consent selection cookies are handled separately and aren't affected here. The stripSessionCookieExpiry function only modifies cookies that match SESSION_TOKEN_COOKIE_RE, which targets authjs.session-token specifically. Consent cookies have their own names and expiry, so they'll persist across browser sessions as expected.

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';
Expand Down Expand Up @@ -45,5 +67,7 @@ export const withAuth: ProxyFactory = (next) => {
// Continue the proxy chain
return next(req, event);
})(request, event);

return response ? stripSessionCookieExpiry(response) : response;
};
};
Loading