fix(auth): logout actually clears the session cookie in Chrome
All checks were successful
Deploy to Production / deploy (push) Successful in 53s

The clearCookie call on /v1/auth/logout was passing only {path:'/'},
missing the httpOnly + sameSite + secure flags the setCookie used. In
production (secure=true), Chrome treats a Set-Cookie clear directive
without Secure as a *different* cookie — it creates an empty insecure
cookie and leaves the original Secure session cookie in place. Result:
users who clicked "Sign out" stayed logged in for the full 30-day
session lifetime in the browser's view (DB session was destroyed
correctly; only the cookie persisted).

Now both setCookie and clearCookie pull from sessionCookieOpts() so
the attributes can't drift apart again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marco Sadjadi 2026-05-25 21:14:12 +02:00
parent 091454d273
commit 1cccdbdff1

View File

@ -20,6 +20,29 @@ import { sendSms, smsConfigured } from '../lib/sms.js';
const SESSION_COOKIE = 'bmm_session'; const SESSION_COOKIE = 'bmm_session';
const OAUTH_STATE_COOKIE = 'bmm_oauth_state'; const OAUTH_STATE_COOKIE = 'bmm_oauth_state';
/**
* Single source of truth for the session cookie's flags. setCookie AND
* clearCookie MUST agree on (path, sameSite, secure, httpOnly) when they
* drift, Chrome treats the clear directive as a brand-new cookie with
* different security attributes and silently leaves the original one in
* place. That's what bit us until now: logout looked successful (200 OK
* with a Set-Cookie clear) but the real session cookie persisted because
* the clear omitted Secure+HttpOnly.
*/
function sessionCookieOpts(): {
httpOnly: true;
sameSite: 'lax';
path: '/';
secure: boolean;
} {
return {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: config.NODE_ENV === 'production',
};
}
const GoogleClaims = z.object({ const GoogleClaims = z.object({
iss: z.string(), iss: z.string(),
aud: z.string(), aud: z.string(),
@ -229,7 +252,7 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
const token = req.cookies[SESSION_COOKIE]; const token = req.cookies[SESSION_COOKIE];
const session = token ? await getSession(token) : null; const session = token ? await getSession(token) : null;
if (token) await destroySession(token); if (token) await destroySession(token);
reply.clearCookie(SESSION_COOKIE, { path: '/' }); reply.clearCookie(SESSION_COOKIE, sessionCookieOpts());
if (session) { if (session) {
await audit({ await audit({
orgId: session.orgId, orgId: session.orgId,