fix(auth): stop stale session cookies signing users out on refresh

@supabase/ssr 0.1.0 never removed the old unchunked session cookie once a
refreshed session grew past one cookie, and always read that stale copy
first. Every load then retried an already-used refresh token, which the
hosted auth server rejects, signing the user out.

Upgrade @supabase/ssr to 0.12 (and supabase-js to match) and move to the
getAll/setAll cookie API, which clears stale chunks when writing.
This commit is contained in:
Josh Creek
2026-09-14 16:19:20 +01:00
parent f382176804
commit b7d2db4959
5 changed files with 97 additions and 129 deletions
+8 -11
View File
@@ -1,23 +1,20 @@
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
import { createServerClient } from '@supabase/ssr';
import type { Handle } from '@sveltejs/kit';
import type { CookieSerializeOptions } from 'cookie';
export const handle: Handle = async ({ event, resolve }) => {
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: {
get: (key: string) => event.cookies.get(key),
getAll: () => event.cookies.getAll(),
/**
* Note: You have to add the `path` variable to the
* set and remove method due to sveltekit's cookie API
* requiring this to be set, setting the path to an empty string
* will replicate previous/standard behaviour (https://kit.svelte.dev/docs/types#public-types-cookies)
* Note: You have to add the `path` variable to the set method due to sveltekit's cookie
* API requiring this to be set, setting the path to '/' will replicate previous/standard
* behaviour (https://kit.svelte.dev/docs/types#public-types-cookies)
*/
set: (key: string, value: string, options: CookieSerializeOptions) => {
event.cookies.set(key, value, { ...options, path: '/' });
},
remove: (key: string, options: CookieSerializeOptions) => {
event.cookies.delete(key, { ...options, path: '/' });
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
event.cookies.set(name, value, { ...options, path: '/' });
});
}
}
});
+4 -2
View File
@@ -1,10 +1,12 @@
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals: { safeGetSession } }) => {
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
const { session, user } = await safeGetSession();
return {
session,
user
user,
// The universal layout load rebuilds a server-side client from these during SSR.
cookies: cookies.getAll()
};
};
+16 -23
View File
@@ -1,7 +1,6 @@
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public';
import type { LayoutLoad } from './$types';
import { createBrowserClient, isBrowser, parse, serialize } from '@supabase/ssr';
import type { CookieSerializeOptions } from 'cookie';
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr';
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
depends('supabase:auth');
@@ -31,29 +30,23 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
return response;
};
const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch: authFetch
},
cookies: {
get(key: string) {
if (!isBrowser()) {
return JSON.stringify(data.session);
// The browser client manages document.cookie itself, including removing stale session chunks.
const supabase = isBrowser()
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch: authFetch
}
const cookie = parse(document.cookie);
return cookie[key];
},
set(key: string, value: string, options: CookieSerializeOptions) {
if (isBrowser()) document.cookie = serialize(key, value, { ...options, path: '/' });
},
remove(key: string, options: CookieSerializeOptions) {
if (isBrowser()) {
document.cookie = serialize(key, '', { ...options, path: '/', maxAge: 0 });
})
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch
},
cookies: {
getAll() {
return data.cookies;
}
}
}
}
});
});
/**
* It's fine to use `getSession` here, because on the client, `getSession` is