From 2231e6aa6ec13fe9c2ae0a0caebd0e79b75e348b Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 4 Aug 2026 13:26:27 -0400 Subject: [PATCH 1/3] Simplify host-only cookie deletion --- frontend/src/middleware.ts | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index c42febd30..ff3f31ce0 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -34,30 +34,27 @@ export function middleware(request: NextRequest) { if (process.env.NEXT_PUBLIC_DEBUG !== "true") { const cookieNames = ["account_sess_token", "guest_sess_token"]; - const hasLegacyCookies = cookieNames.some((name) => + const existingCookies = cookieNames.filter((name) => request.cookies.has(name), ); - // If the user has auth cookies... - if (hasLegacyCookies) { - // We defensively try to DELETE the Host-Only (legacy) cookies on every request. - // By setting Max-Age=0 and omitting the Domain attribute, we target the Host-Only version. - // The valid Domain cookie (.example.com) will remain untouched because the browser - // sees them as different scopes. + // We defensively try to DELETE the Host-Only (legacy) cookies on every request. + // By setting Max-Age=0 and omitting the Domain attribute, we target the Host-Only version. + // The valid Domain cookie (.example.com) will remain untouched because the browser + // sees them as different scopes. - // This logic can only be removed AFTER February 15, 2027 (one year from now) just - // to be safe, since long session cookies have a 1 year lifetime + // This logic can only be removed AFTER February 15, 2027 (one year from now) just + // to be safe, since long session cookies have a 1 year lifetime - cookieNames.forEach((name) => { - // Because we initialized `response` with NextResponse.next() above, - // we can safely append headers to it here. If response was reassigned - // to a redirect, appending headers still works. - response.headers.append( - "Set-Cookie", - `${name}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`, - ); - }); - } + existingCookies.forEach((name) => { + // Because we initialized `response` with NextResponse.next() above, + // we can safely append headers to it here. If response was reassigned + // to a redirect, appending headers still works. + response.headers.append( + "Set-Cookie", + `${name}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`, + ); + }); } return response; From 488f290a0a0bab06c22833899ea21452660e007d Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 4 Aug 2026 16:39:13 -0400 Subject: [PATCH 2/3] Add cookie extension logic --- frontend/.env.example | 5 +++++ frontend/src/middleware.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/frontend/.env.example b/frontend/.env.example index 26ef4402c..dbdc794c4 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -4,3 +4,8 @@ NEXT_PUBLIC_DEBUG=true # The API URL for both the client and the Next.js server to communicate with the backend # Note the lack of a trailing slash NEXT_PUBLIC_API_URL=https://example.com + +# The domain for cookies to be extended on. This is for the temporary fix implemented in +# middleware.ts, and it MUST be the same as COOKIE_DOMAIN in the backend .env file. This +# is not needed for development, since it's not used when NEXT_PUBLIC_DEBUG is true. +COOKIE_DOMAIN=.example.com diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index ff3f31ce0..ea605188c 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -38,6 +38,35 @@ export function middleware(request: NextRequest) { request.cookies.has(name), ); + // Safari's ITP (Intelligent Tracking Prevention) limits the lifetime of cookies set + // by direct requests to our API to 7 days. To combat this, we extend cookie lifetimes + // with Next.js to a full year on every request. In oversimplified terms, the frontend + // is on the main domain which is not subject to the same restrictions. + + // This solution is a temporary fix until we redo our client-side API requests to use + // Server Actions, which will route everything through the frontend and bypass the + // restriction. + + // This won't allow authentication for expired sessions, it only ensures that valid + // sessions are not prematurely expired. The source of truth is handled by the backend + // and database. + + existingCookies.forEach((name) => { + const cookieValue = request.cookies.get(name)?.value; + if (cookieValue) { + response.cookies.set({ + name: name, + value: cookieValue, + domain: process.env.COOKIE_DOMAIN, + path: "/", + httpOnly: true, + secure: true, + sameSite: "lax", + maxAge: 60 * 60 * 24 * 365, // 1 year + }); + } + }); + // We defensively try to DELETE the Host-Only (legacy) cookies on every request. // By setting Max-Age=0 and omitting the Domain attribute, we target the Host-Only version. // The valid Domain cookie (.example.com) will remain untouched because the browser From 75d854dbe8afca57721121d2619d513b9d65a4ce Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 4 Aug 2026 18:21:18 -0400 Subject: [PATCH 3/3] Add COOKIE_DOMAIN guard --- frontend/src/middleware.ts | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index ea605188c..a66547a16 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -51,21 +51,23 @@ export function middleware(request: NextRequest) { // sessions are not prematurely expired. The source of truth is handled by the backend // and database. - existingCookies.forEach((name) => { - const cookieValue = request.cookies.get(name)?.value; - if (cookieValue) { - response.cookies.set({ - name: name, - value: cookieValue, - domain: process.env.COOKIE_DOMAIN, - path: "/", - httpOnly: true, - secure: true, - sameSite: "lax", - maxAge: 60 * 60 * 24 * 365, // 1 year - }); - } - }); + if (process.env.COOKIE_DOMAIN) { + existingCookies.forEach((name) => { + const cookieValue = request.cookies.get(name)?.value; + if (cookieValue) { + response.cookies.set({ + name: name, + value: cookieValue, + domain: process.env.COOKIE_DOMAIN, + path: "/", + httpOnly: true, + secure: true, + sameSite: "lax", + maxAge: 60 * 60 * 24 * 365, // 1 year + }); + } + }); + } // We defensively try to DELETE the Host-Only (legacy) cookies on every request. // By setting Max-Age=0 and omitting the Domain attribute, we target the Host-Only version.