From 1cccdbdff1b727f062dcaeb4ad836e7926ca52a3 Mon Sep 17 00:00:00 2001 From: Marco Sadjadi Date: Mon, 25 May 2026 21:14:12 +0200 Subject: [PATCH] fix(auth): logout actually clears the session cookie in Chrome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/api/src/routes/auth.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index 9e81e92..a320bea 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -20,6 +20,29 @@ import { sendSms, smsConfigured } from '../lib/sms.js'; const SESSION_COOKIE = 'bmm_session'; 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({ iss: z.string(), aud: z.string(), @@ -229,7 +252,7 @@ export async function authRoutes(app: FastifyInstance): Promise { const token = req.cookies[SESSION_COOKIE]; const session = token ? await getSession(token) : null; if (token) await destroySession(token); - reply.clearCookie(SESSION_COOKIE, { path: '/' }); + reply.clearCookie(SESSION_COOKIE, sessionCookieOpts()); if (session) { await audit({ orgId: session.orgId,