Skip to content
Merged
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
23 changes: 23 additions & 0 deletions packages/web/src/lib/auth/session-cookie.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { SESSION_MAX_AGE_SECONDS, sessionCookieMaxAge } from './session-cookie';

describe('SESSION_MAX_AGE_SECONDS', () => {
it('keeps a user signed in for 7 days without a session refresh', () => {
expect(SESSION_MAX_AGE_SECONDS).toBe(7 * 24 * 60 * 60);
});
});

describe('sessionCookieMaxAge', () => {
it('applies the session window to a cookie supabase-ssr is setting', () => {
// supabase-ssr 0.6 sets session cookies with its own 400-day default.
expect(sessionCookieMaxAge(400 * 24 * 60 * 60)).toBe(SESSION_MAX_AGE_SECONDS);
});

it('applies the session window when no max-age was requested', () => {
expect(sessionCookieMaxAge(undefined)).toBe(SESSION_MAX_AGE_SECONDS);
});

it('passes a deletion (maxAge 0) through so sign-out clears the cookie', () => {
expect(sessionCookieMaxAge(0)).toBe(0);
});
});
32 changes: 32 additions & 0 deletions packages/web/src/lib/auth/session-cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Lifetime of the Supabase auth cookies the middleware writes.
*
* Dependency-free on purpose: the edge middleware imports it, and the edge
* bundle must not pull in Node-only modules.
*/

/**
* 7 days. This is how long a browser can go without a server-side session
* refresh before the auth cookies expire and the user is signed out. Every
* refresh re-issues the cookies with a fresh max-age, so the window slides with
* use: someone who opens the dashboard at least once a week stays signed in.
*
* It must stay longer than the access token's `jwt_expiry` (1 h on the hosted
* project, 24 h in `supabase/config.toml`), so the refresh token is still in
* the cookie when the access token runs out.
*/
export const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 7;

/**
* The max-age to write for one auth cookie, given the max-age supabase-ssr
* asked for.
*
* supabase-ssr deletes a cookie by writing it empty with `maxAge: 0` (on sign
* out, and when a session cookie is re-chunked). That deletion has to pass
* through unchanged; overriding it would leave empty auth cookies in the
* browser for the whole session window.
*/
export function sessionCookieMaxAge(requestedMaxAge: number | undefined): number {
if (requestedMaxAge === 0) return 0;
return SESSION_MAX_AGE_SECONDS;
}
10 changes: 4 additions & 6 deletions packages/web/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import { NextResponse, type NextRequest } from 'next/server';
import { safeNextPath, boundedReturnTo } from '@/lib/auth-redirect';
import { supabaseAnonKey, supabaseUrl } from '@/lib/supabase/config';
import { ensureFlagAnonIdCookie } from '@/lib/feature-flags/anon-id';

/** 24 hours — matches the Supabase project jwt_expiry so the cookie
* outlives the access token and the refresh token can be used. */
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24;
import { sessionCookieMaxAge } from '@/lib/auth/session-cookie';

export async function middleware(request: NextRequest) {
// Short-circuit OPTIONS preflights before hitting any auth logic.
Expand Down Expand Up @@ -90,8 +87,9 @@ export async function middleware(request: NextRequest) {
response.cookies.set(name, value, {
...options,
// Persist the session cookie across browser restarts so the
// refresh token survives and the user stays logged in for the day.
maxAge: SESSION_MAX_AGE_SECONDS,
// refresh token survives and the user stays signed in for the
// session window (7 days, sliding with each refresh).
maxAge: sessionCookieMaxAge(options?.maxAge),
}),
);
},
Expand Down
Loading