Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
66 changes: 47 additions & 19 deletions frontend/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
Comment thread
jzgom067 marked this conversation as resolved.

// 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;
Expand Down
Loading