diff --git a/src/auth0-session/handlers/logout.ts b/src/auth0-session/handlers/logout.ts index 984301378..5806c85b6 100644 --- a/src/auth0-session/handlers/logout.ts +++ b/src/auth0-session/handlers/logout.ts @@ -9,6 +9,46 @@ const debug = createDebug('logout'); export type HandleLogout = (req: Auth0Request, res: Auth0Response, options?: LogoutOptions) => Promise; +/** + * Remove a cookie by creating a matching removal header with all possible attributes + */ +function removeCookie(res: Auth0Response, cookieName: string, cookieConfig: any = {}) { + let cookieString = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT`; + + // Add path (default to '/') + const path = cookieConfig.path || '/'; + cookieString += `; Path=${path}`; + + // Add domain if specified + if (cookieConfig.domain) { + cookieString += `; Domain=${cookieConfig.domain}`; + } + + // Add security attributes to match original cookie + if (cookieConfig.secure !== false) { + cookieString += '; Secure'; + } + + if (cookieConfig.httpOnly !== false) { + cookieString += '; HttpOnly'; + } + + if (cookieConfig.sameSite) { + cookieString += `; SameSite=${cookieConfig.sameSite}`; + } + + if (cookieConfig.partitioned) { + cookieString += '; Partitioned'; + } + + // Add to existing Set-Cookie headers + const existingCookies = res.res.getHeader('Set-Cookie') || []; + const cookieArray = Array.isArray(existingCookies) ? existingCookies : [existingCookies as string]; + cookieArray.push(cookieString); + + res.res.setHeader('Set-Cookie', cookieArray); +} + export default function logoutHandlerFactory( getConfig: GetConfig, getClient: GetClient, @@ -28,6 +68,7 @@ export default function logoutHandlerFactory( } const isAuthenticated = await sessionCache.isAuthenticated(req.req, res.res); + if (!isAuthenticated) { debug('end-user already logged out, redirecting to %s', returnURL); res.redirect(returnURL); @@ -35,8 +76,23 @@ export default function logoutHandlerFactory( } const idToken = await sessionCache.getIdToken(req.req, res.res); + await sessionCache.delete(req.req, res.res); + // Remove the session cookie with matching attributes + const cookieName = config.session?.name || 'appSession'; + + removeCookie(res, cookieName, config.session?.cookie); + + // Also remove with partitioned flag to ensure cleanup regardless of original cookie config + const cookieConfigWithPartitioned = { + ...config.session?.cookie, + partitioned: true + }; + removeCookie(res, cookieName, cookieConfigWithPartitioned); + + debug('session cookie cleared'); + if (!config.idpLogout) { debug('performing a local only logout, redirecting to %s', returnURL); res.redirect(returnURL); diff --git a/src/auth0-session/session/abstract-session.ts b/src/auth0-session/session/abstract-session.ts index 7b864f080..02f62c8fe 100644 --- a/src/auth0-session/session/abstract-session.ts +++ b/src/auth0-session/session/abstract-session.ts @@ -1,5 +1,9 @@ import createDebug from '../utils/debug'; import { CookieSerializeOptions } from 'cookie'; + +interface ExtendedCookieOptions extends CookieSerializeOptions { + partitioned?: boolean; +} import { Config, GetConfig } from '../config'; import { Auth0RequestCookies, Auth0ResponseCookies } from '../http'; @@ -51,14 +55,14 @@ export abstract class AbstractSession { uat: number, iat: number, exp: number, - cookieOptions: CookieSerializeOptions, + cookieOptions: ExtendedCookieOptions, isNewSession: boolean ): Promise; abstract deleteSession( req: Auth0RequestCookies, res: Auth0ResponseCookies, - cookieOptions: CookieSerializeOptions + cookieOptions: ExtendedCookieOptions ): Promise; public async read(req: Auth0RequestCookies): Promise<[Session?, number?]> { @@ -106,7 +110,7 @@ export abstract class AbstractSession { } = config.session; if (!session) { - await this.deleteSession(req, res, cookieConfig); + await this.deleteSession(req, res, { ...cookieConfig, partitioned: true }); return; } @@ -115,8 +119,9 @@ export abstract class AbstractSession { const iat = typeof createdAt === 'number' ? createdAt : uat; const exp = this.calculateExp(iat, uat, config); - const cookieOptions: CookieSerializeOptions = { - ...cookieConfig + const cookieOptions: ExtendedCookieOptions = { + ...cookieConfig, + partitioned: true }; if (!transient) { cookieOptions.expires = new Date(exp * 1000); diff --git a/src/auth0-session/session/stateful-session.ts b/src/auth0-session/session/stateful-session.ts index 8590a9642..c7dc67d86 100644 --- a/src/auth0-session/session/stateful-session.ts +++ b/src/auth0-session/session/stateful-session.ts @@ -1,4 +1,8 @@ import { CookieSerializeOptions } from 'cookie'; + +interface ExtendedCookieOptions extends CookieSerializeOptions { + partitioned?: boolean; +} import createDebug from '../utils/debug'; import { AbstractSession, SessionPayload } from './abstract-session'; import { generateCookieValue, getCookieValue } from '../utils/signed-cookies'; @@ -69,7 +73,7 @@ export class StatefulSession< uat: number, iat: number, exp: number, - cookieOptions: CookieSerializeOptions, + cookieOptions: ExtendedCookieOptions, isNewSession: boolean ): Promise { const config = await this.getConfig(req); @@ -103,7 +107,7 @@ export class StatefulSession< async deleteSession( req: Auth0RequestCookies, res: Auth0ResponseCookies, - cookieOptions: CookieSerializeOptions + cookieOptions: ExtendedCookieOptions ): Promise { const config = await this.getConfig(req); const { name: sessionName } = config.session; diff --git a/src/auth0-session/session/stateless-session.ts b/src/auth0-session/session/stateless-session.ts index fc4fb6787..bbf8ff170 100644 --- a/src/auth0-session/session/stateless-session.ts +++ b/src/auth0-session/session/stateless-session.ts @@ -1,5 +1,9 @@ import * as jose from 'jose'; import { CookieSerializeOptions, serialize } from 'cookie'; + +interface ExtendedCookieOptions extends CookieSerializeOptions { + partitioned?: boolean; +} import createDebug from '../utils/debug'; import { Config } from '../config'; import { encryption } from '../utils/hkdf'; @@ -31,7 +35,8 @@ export class StatelessSession< name: sessionName } = config.session; const cookieOptions: CookieSerializeOptions = { - ...cookieConfig + ...cookieConfig, + partitioned: true }; if (!transient) { cookieOptions.expires = new Date(); @@ -116,7 +121,7 @@ export class StatelessSession< uat: number, iat: number, exp: number, - cookieOptions: CookieSerializeOptions + cookieOptions: ExtendedCookieOptions ): Promise { const config = await this.getConfig(req); const { name: sessionName } = config.session; @@ -154,7 +159,7 @@ export class StatelessSession< async deleteSession( req: Auth0RequestCookies, res: Auth0ResponseCookies, - cookieOptions: CookieSerializeOptions + cookieOptions: ExtendedCookieOptions ): Promise { const config = await this.getConfig(req); const { name: sessionName } = config.session; diff --git a/src/auth0-session/utils/cookies.ts b/src/auth0-session/utils/cookies.ts index 60fce4b46..842d2b1f2 100644 --- a/src/auth0-session/utils/cookies.ts +++ b/src/auth0-session/utils/cookies.ts @@ -9,8 +9,7 @@ export abstract class Cookies { } set(name: string, value: string, options: CookieSerializeOptions = {}): void { - let cookieString = serialize(name, value, options); - cookieString += '; Partitioned'; + const cookieString = serialize(name, value, options); this.cookies.push(cookieString); }