Skip to content

Commit 61dfdd2

Browse files
committed
fix: clear authjs session cookies on logout callback
The logout callback relied solely on the Clear-Site-Data response header to clear Auth.js session cookies, but browsers ignore Clear-Site-Data on 302 redirect responses. This left authjs.session-token (and friends) in the browser after logout, so the user remained signed in. The callback now also emits explicit Set-Cookie deletions for every authjs.* cookie and for logout_state (matching the path it was set on), which browsers do honor on redirects.
1 parent 60e21b8 commit 61dfdd2

File tree

2 files changed

+65
-2
lines changed

2 files changed

+65
-2
lines changed

src/routes/api/auth/logout/callback.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { redirect } from '@solidjs/router';
22
import { APIEvent } from '@solidjs/start/server';
3-
import { getCookie } from 'vinxi/http';
3+
import { deleteCookie, getCookie, parseCookies } from 'vinxi/http';
44

55
// noinspection JSUnusedGlobalSymbols
66
/**
@@ -23,9 +23,23 @@ export async function GET(event: APIEvent) {
2323

2424
if (state && logoutStateCookie && state === logoutStateCookie) {
2525
const successUrl = new URL('/logout/success', event.request.url);
26+
for (const name of Object.keys(parseCookies(event.nativeEvent))) {
27+
if (name.includes('authjs.')) {
28+
deleteCookie(event.nativeEvent, name, { path: '/' });
29+
}
30+
}
31+
deleteCookie(event.nativeEvent, 'logout_state', {
32+
path: '/api/auth/logout/callback',
33+
});
2634
const response = redirect(successUrl.toString());
27-
2835
response.headers.set('Clear-Site-Data', '"cookies"');
36+
const setCookies = event.nativeEvent.node?.res?.getHeader('set-cookie');
37+
if (Array.isArray(setCookies)) {
38+
for (const sc of setCookies)
39+
response.headers.append('set-cookie', String(sc));
40+
} else if (typeof setCookies === 'string') {
41+
response.headers.append('set-cookie', setCookies);
42+
}
2943
return response;
3044
} else {
3145
const errorUrl = new URL('/logout/error', event.request.url);

test/app.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,52 @@ test('app returns 200', async ({ page }) => {
44
const response = await page.goto('/');
55
expect(response?.status()).toBe(200);
66
});
7+
8+
test('GET /api/auth/logout/callback clears authjs.* and logout_state', async ({
9+
request,
10+
baseURL,
11+
}) => {
12+
const res = await request.get(
13+
`${baseURL}/api/auth/logout/callback?state=teststate123`,
14+
{
15+
headers: {
16+
Cookie: [
17+
'logout_state=teststate123',
18+
'authjs.session-token=fakesession',
19+
'authjs.csrf-token=fakecsrf',
20+
'authjs.callback-url=http://example.com',
21+
].join('; '),
22+
},
23+
maxRedirects: 0,
24+
},
25+
);
26+
27+
const status = res.status();
28+
const location = res.headers()['location'];
29+
const setCookies = res
30+
.headersArray()
31+
.filter((h) => h.name.toLowerCase() === 'set-cookie')
32+
.map((h) => h.value) as string[];
33+
34+
expect(status).toBe(302);
35+
expect(location).toMatch(/\/(auth\/)?logout\/success$/);
36+
expect(setCookies).toBeDefined();
37+
expect(Array.isArray(setCookies)).toBe(true);
38+
39+
const wasCleared = (name: string) =>
40+
setCookies.some(
41+
(sc) =>
42+
sc.startsWith(`${name}=`) &&
43+
(sc.includes('Max-Age=0') || /Expires=Thu, 01 Jan 1970/i.test(sc)),
44+
);
45+
46+
expect(wasCleared('authjs.session-token')).toBe(true);
47+
expect(wasCleared('authjs.csrf-token')).toBe(true);
48+
expect(wasCleared('authjs.callback-url')).toBe(true);
49+
expect(wasCleared('logout_state')).toBe(true);
50+
51+
const logoutStateCookie = setCookies.find((sc) =>
52+
sc.startsWith('logout_state='),
53+
);
54+
expect(logoutStateCookie).toMatch(/Path=\/api\/auth\/logout\/callback/);
55+
});

0 commit comments

Comments
 (0)