From a165248f97c5d3d7393a5e6764bb03097f36d8f5 Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Fri, 21 Aug 2026 17:06:05 -0600 Subject: [PATCH] Fix session persistence across page navigation --- apps/backend/src/auth/middleware.ts | 18 ++++++++++++++++-- apps/backend/tests/auth.test.ts | 22 +++++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/auth/middleware.ts b/apps/backend/src/auth/middleware.ts index 425bcee..04c9697 100644 --- a/apps/backend/src/auth/middleware.ts +++ b/apps/backend/src/auth/middleware.ts @@ -246,20 +246,34 @@ export function setSessionCookie( expiresAt: Date, secure: boolean ): void { + // Expire the former API-only scope before issuing the document-wide cookie. + clearSessionCookieAtPath(res, secure, "/api/v1"); res.cookie(SESSION_COOKIE_NAME, token, { httpOnly: true, sameSite: "strict", secure, - path: "/api/v1", + // Next.js authenticates document requests before rendering protected pages. + path: "/", expires: expiresAt }); } export function clearSessionCookie(res: Response, secure: boolean): void { + clearSessionCookieAtPath(res, secure, "/"); + // Older clients may still retain the scope used before page authentication. + clearSessionCookieAtPath(res, secure, "/api/v1"); +} + +/** Clears one exact cookie scope because browsers key cookies by name and path. */ +function clearSessionCookieAtPath( + res: Response, + secure: boolean, + path: string +): void { res.clearCookie(SESSION_COOKIE_NAME, { httpOnly: true, sameSite: "strict", secure, - path: "/api/v1" + path }); } diff --git a/apps/backend/tests/auth.test.ts b/apps/backend/tests/auth.test.ts index caf7d28..def7e62 100644 --- a/apps/backend/tests/auth.test.ts +++ b/apps/backend/tests/auth.test.ts @@ -610,7 +610,27 @@ test("session cookies follow the actual LAN or trusted-proxy HTTPS request", asy .set("Origin", "http://dvr.local") .send(payload); assert.equal(http.status, 200); - assert.doesNotMatch(http.headers["set-cookie"]?.[0] ?? "", /; Secure/i); + const setCookieHeader = http.headers["set-cookie"]; + const httpCookies = Array.isArray(setCookieHeader) + ? setCookieHeader + : setCookieHeader + ? [setCookieHeader] + : []; + const httpCookie = httpCookies.find((cookie) => + /; Path=\/(?:;|$)/i.test(cookie) + ); + assert.ok(httpCookie); + assert.doesNotMatch(httpCookie, /; Secure/i); + // Server-rendered page requests also need the session to preserve sign-in. + assert.match(httpCookie, /; Path=\/(?:;|$)/i); + // Remove the old API-only cookie so duplicate token names cannot survive migration. + assert.ok( + httpCookies.some( + (cookie) => + /; Path=\/api\/v1(?:;|$)/i.test(cookie) && + /Expires=Thu, 01 Jan 1970 00:00:00 GMT/i.test(cookie) + ) + ); const wrongScheme = await request(app) .post("/auth/login") .set("Host", "dvr.local")