From 7605b4344c17b2e4041db6e251a9e92b411083be Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Wed, 9 Apr 2025 23:33:44 +0200 Subject: [PATCH] feat: ensure cookie path is configurable --- src/server/client.ts | 6 ++- src/server/session/abstract-session-store.ts | 6 ++- .../session/stateful-session-store.test.ts | 44 +++++++++++++++++++ .../session/stateless-session-store.test.ts | 32 ++++++++++++++ src/server/transaction-store.test.ts | 34 ++++++++++++++ src/server/transaction-store.ts | 6 ++- 6 files changed, 124 insertions(+), 4 deletions(-) diff --git a/src/server/client.ts b/src/server/client.ts index 41af65d47..a79101059 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -200,13 +200,15 @@ export class Auth0Client { const sessionCookieOptions: SessionCookieOptions = { name: options.session?.cookie?.name ?? "__session", secure: options.session?.cookie?.secure ?? false, - sameSite: options.session?.cookie?.sameSite ?? "lax" + sameSite: options.session?.cookie?.sameSite ?? "lax", + path: options.session?.cookie?.path ?? "/" }; const transactionCookieOptions: TransactionCookieOptions = { prefix: options.transactionCookie?.prefix ?? "__txn_", secure: options.transactionCookie?.secure ?? false, - sameSite: options.transactionCookie?.sameSite ?? "lax" + sameSite: options.transactionCookie?.sameSite ?? "lax", + path: options.transactionCookie?.path ?? "/" }; if (appBaseUrl) { diff --git a/src/server/session/abstract-session-store.ts b/src/server/session/abstract-session-store.ts index f604e4f74..b052ac2c6 100644 --- a/src/server/session/abstract-session-store.ts +++ b/src/server/session/abstract-session-store.ts @@ -25,6 +25,10 @@ export interface SessionCookieOptions { * Default: depends on the protocol of the application's base URL. If the protocol is `https`, then `true`, otherwise `false`. */ secure?: boolean; + /** + * The path attribute of the session cookie. Will be set to '/' by default. + */ + path?: string; } export interface SessionConfiguration { @@ -103,7 +107,7 @@ export abstract class AbstractSessionStore { httpOnly: true, sameSite: cookieOptions?.sameSite ?? "lax", secure: cookieOptions?.secure ?? false, - path: "/" + path: cookieOptions?.path ?? "/" }; } diff --git a/src/server/session/stateful-session-store.test.ts b/src/server/session/stateful-session-store.test.ts index add110773..fab837bcd 100644 --- a/src/server/session/stateful-session-store.test.ts +++ b/src/server/session/stateful-session-store.test.ts @@ -595,6 +595,50 @@ describe("Stateful Session Store", async () => { expect(cookie?.secure).toEqual(false); }); + it("should apply the path to the cookie", 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().mockResolvedValue(session), + set: vi.fn(), + delete: vi.fn() + }; + + const requestCookies = new RequestCookies(new Headers()); + const responseCookies = new ResponseCookies(new Headers()); + + const sessionStore = new StatefulSessionStore({ + secret, + store, + rolling: true, + absoluteDuration: 3600, + inactivityDuration: 1800, + + cookieOptions: { + path: "/custom-path" + } + }); + await sessionStore.set(requestCookies, responseCookies, session); + + const cookie = responseCookies.get("__session"); + + expect(cookie).toBeDefined(); + expect(cookie?.path).toEqual("/custom-path"); + }); + it("should apply the cookie name", async () => { const currentTime = Date.now(); const createdAt = Math.floor(currentTime / 1000); diff --git a/src/server/session/stateless-session-store.test.ts b/src/server/session/stateless-session-store.test.ts index 1983efee7..24a427ed1 100644 --- a/src/server/session/stateless-session-store.test.ts +++ b/src/server/session/stateless-session-store.test.ts @@ -496,6 +496,38 @@ describe("Stateless Session Store", async () => { expect(cookie?.secure).toEqual(true); }); + it("should apply the path to the cookie", async () => { + 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: Math.floor(Date.now() / 1000) + } + }; + const requestCookies = new RequestCookies(new Headers()); + const responseCookies = new ResponseCookies(new Headers()); + + const sessionStore = new StatelessSessionStore({ + secret, + cookieOptions: { + path: '/custom-path' + } + }); + await sessionStore.set(requestCookies, responseCookies, session); + + const cookie = responseCookies.get("__session"); + + expect(cookie).toBeDefined(); + expect((await decrypt(cookie!.value, secret)).payload).toEqual(session); + expect(cookie?.path).toEqual("/custom-path"); + }); + it("should apply the cookie name", async () => { const secret = await generateSecret(32); const session: SessionData = { diff --git a/src/server/transaction-store.test.ts b/src/server/transaction-store.test.ts index ad4834e2c..94a3ea561 100644 --- a/src/server/transaction-store.test.ts +++ b/src/server/transaction-store.test.ts @@ -199,6 +199,40 @@ describe("Transaction Store", async () => { expect(cookie?.secure).toEqual(false); }); + it("should apply the path to the cookie", async () => { + const secret = await generateSecret(32); + const codeVerifier = oauth.generateRandomCodeVerifier(); + const nonce = oauth.generateRandomNonce(); + const state = oauth.generateRandomState(); + const transactionState: TransactionState = { + nonce, + maxAge: 3600, + codeVerifier: codeVerifier, + responseType: "code", + state, + returnTo: "/dashboard" + }; + const headers = new Headers(); + const responseCookies = new ResponseCookies(headers); + + const transactionStore = new TransactionStore({ + secret, + cookieOptions: { + path: "/custom-path" + } + }); + await transactionStore.save(responseCookies, transactionState); + + const cookieName = `__txn_${state}`; + const cookie = responseCookies.get(cookieName); + + expect(cookie).toBeDefined(); + expect((await decrypt(cookie!.value, secret)).payload).toEqual( + transactionState + ); + expect(cookie?.path).toEqual("/custom-path"); + }); + it("should apply the cookie prefix to the cookie name", async () => { const secret = await generateSecret(32); const codeVerifier = oauth.generateRandomCodeVerifier(); diff --git a/src/server/transaction-store.ts b/src/server/transaction-store.ts index 8e5a11260..9a20ea4e3 100644 --- a/src/server/transaction-store.ts +++ b/src/server/transaction-store.ts @@ -32,6 +32,10 @@ export interface TransactionCookieOptions { * Default: depends on the protocol of the application's base URL. If the protocol is `https`, then `true`, otherwise `false`. */ secure?: boolean; + /** + * The path attribute of the transaction cookie. Will be set to '/' by default. + */ + path?: string; } export interface TransactionStoreOptions { @@ -57,7 +61,7 @@ export class TransactionStore { httpOnly: true, sameSite: cookieOptions?.sameSite ?? "lax", // required to allow the cookie to be sent on the callback request secure: cookieOptions?.secure ?? false, - path: "/", + path: cookieOptions?.path ?? "/", maxAge: 60 * 60 // 1 hour in seconds }; }