diff --git a/src/server/session/stateful-session-store.test.ts b/src/server/session/stateful-session-store.test.ts index ec04b4d10..add110773 100644 --- a/src/server/session/stateful-session-store.test.ts +++ b/src/server/session/stateful-session-store.test.ts @@ -9,7 +9,7 @@ import { ResponseCookies, sign } from "../cookies"; -import { LegacySessionPayload } from "./normalize-session"; +import { LEGACY_COOKIE_NAME, LegacySessionPayload } from "./normalize-session"; import { StatefulSessionStore } from "./stateful-session-store"; describe("Stateful Session Store", async () => { @@ -645,6 +645,45 @@ describe("Stateful Session Store", async () => { expect(cookie?.secure).toEqual(false); }); }); + + + it("should remove the legacy cookie if it exists", async () => { + const currentTime = Date.now(); + const createdAt = Math.floor(currentTime / 1000); + const secret = await generateSecret(32); + const session: SessionData = { + user: { sub: "user_123" }, + tokenSet: { + accessToken: "at_123", + refreshToken: "rt_123", + expiresAt: 123456 + }, + internal: { + sid: "auth0-sid", + createdAt + } + }; + const store = { + get: vi.fn(), + set: vi.fn(), + delete: vi.fn() + }; + + const requestCookies = new RequestCookies(new Headers()); + const responseCookies = new ResponseCookies(new Headers()); + + const sessionStore = new StatefulSessionStore({ + secret, + store, + }); + + vi.spyOn(requestCookies, "has").mockReturnValue(true); + vi.spyOn(responseCookies, "delete"); + + await sessionStore.set(requestCookies, responseCookies, session); + + expect(responseCookies.delete).toHaveBeenCalledWith(LEGACY_COOKIE_NAME); + }); }); describe("delete", async () => { diff --git a/src/server/session/stateful-session-store.ts b/src/server/session/stateful-session-store.ts index 1fc5ae569..04206981f 100644 --- a/src/server/session/stateful-session-store.ts +++ b/src/server/session/stateful-session-store.ts @@ -147,6 +147,12 @@ export class StatefulSessionStore extends AbstractSessionStore { // to enable read-after-write in the same request for middleware reqCookies.set(this.sessionCookieName, jwe.toString()); + + // Any existing v3 cookie can also be deleted once we have set a v4 cookie. + // In stateful sessions, we do not have to worry about chunking. + if (reqCookies.has(LEGACY_COOKIE_NAME)) { + resCookies.delete(LEGACY_COOKIE_NAME); + } } async delete( diff --git a/src/server/session/stateless-session-store.test.ts b/src/server/session/stateless-session-store.test.ts index bc2860e0b..1983efee7 100644 --- a/src/server/session/stateless-session-store.test.ts +++ b/src/server/session/stateless-session-store.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { generateSecret } from "../../test/utils"; import { SessionData } from "../../types"; import { decrypt, encrypt, RequestCookies, ResponseCookies } from "../cookies"; -import { LegacySession } from "./normalize-session"; +import { LEGACY_COOKIE_NAME, LegacySession } from "./normalize-session"; import { StatelessSessionStore } from "./stateless-session-store"; describe("Stateless Session Store", async () => { @@ -313,6 +313,72 @@ describe("Stateless Session Store", async () => { expect(cookie?.maxAge).toEqual(0); // cookie should expire immediately expect(cookie?.secure).toEqual(false); }); + + it("should delete the legacy cookie if it exists", async () => { + const currentTime = Date.now(); + const createdAt = Math.floor(currentTime / 1000); + const secret = await generateSecret(32); + const session: SessionData = { + user: { sub: "user_123" }, + tokenSet: { + accessToken: "at_123", + refreshToken: "rt_123", + expiresAt: 123456 + }, + internal: { + sid: "auth0-sid", + createdAt + } + }; + const requestCookies = new RequestCookies(new Headers()); + const responseCookies = new ResponseCookies(new Headers()); + + const sessionStore = new StatelessSessionStore({ + secret, + }); + + vi.spyOn(responseCookies, "delete"); + vi.spyOn(requestCookies, "has").mockReturnValue(true); + + await sessionStore.set(requestCookies, responseCookies, session); + + expect(responseCookies.delete).toHaveBeenCalledWith(LEGACY_COOKIE_NAME); + }); + + it("should delete the legacy cookie chunks if they exists", async () => { + const currentTime = Date.now(); + const createdAt = Math.floor(currentTime / 1000); + const secret = await generateSecret(32); + const session: SessionData = { + user: { sub: "user_123" }, + tokenSet: { + accessToken: "at_123", + refreshToken: "rt_123", + expiresAt: 123456 + }, + internal: { + sid: "auth0-sid", + createdAt + } + }; + const requestCookies = new RequestCookies(new Headers()); + const responseCookies = new ResponseCookies(new Headers()); + + const sessionStore = new StatelessSessionStore({ + secret, + }); + + vi.spyOn(responseCookies, "delete"); + vi.spyOn(requestCookies, "getAll").mockReturnValue([ + { name: `${LEGACY_COOKIE_NAME}__0`, value: '' }, + { name: `${LEGACY_COOKIE_NAME}__1`, value: '' } + ]); + + await sessionStore.set(requestCookies, responseCookies, session); + + expect(responseCookies.delete).toHaveBeenCalledWith(`${LEGACY_COOKIE_NAME}__0`); + expect(responseCookies.delete).toHaveBeenCalledWith(`${LEGACY_COOKIE_NAME}__1`); + }); }); describe("with rolling sessions disabled", async () => { diff --git a/src/server/session/stateless-session-store.ts b/src/server/session/stateless-session-store.ts index e52da8da7..bdf628aa4 100644 --- a/src/server/session/stateless-session-store.ts +++ b/src/server/session/stateless-session-store.ts @@ -117,6 +117,10 @@ export class StatelessSessionStore extends AbstractSessionStore { ) ); } + + // Any existing v3 cookie can be deleted as soon as we have set a v4 cookie. + // In stateless sessions, we do have to ensure we delete all chunks. + cookies.deleteChunkedCookie(LEGACY_COOKIE_NAME, reqCookies, resCookies); } async delete(