diff --git a/frontend/.env.example b/frontend/.env.example index 26ef4402..dbdc794c 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 c42febd3..a66547a1 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -34,30 +34,58 @@ 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. - - // 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`, - ); + // 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. + + 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. + // 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 + + 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;