Skip to content

Commit a492b1b

Browse files
authored
fix(clerk-js): dev browser partitioned cookie cleanup (#8538)
1 parent ed76cd7 commit a492b1b

3 files changed

Lines changed: 172 additions & 18 deletions

File tree

.changeset/clean-dev-browsers.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Fix dev browser recovery by clearing stale partitioned and non-partitioned dev browser cookie variants before minting a new dev browser.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { createDevBrowserCookie } from '../devBrowser';
4+
5+
const { cookieStore, removeCalls, setCalls } = vi.hoisted(() => ({
6+
cookieStore: new Map<string, string>(),
7+
removeCalls: [] as Array<{ name: string; attributes?: object }>,
8+
setCalls: [] as Array<{ name: string; value: string; attributes?: object }>,
9+
}));
10+
11+
vi.mock('@clerk/shared/cookie', () => ({
12+
createCookieHandler: (name: string) => ({
13+
get: () => cookieStore.get(name),
14+
remove: (attributes?: object) => {
15+
removeCalls.push({ name, attributes });
16+
cookieStore.delete(name);
17+
},
18+
set: (value: string, attributes?: object) => {
19+
setCalls.push({ name, value, attributes });
20+
cookieStore.set(name, value);
21+
},
22+
}),
23+
}));
24+
25+
describe('createDevBrowserCookie', () => {
26+
const cookieSuffix = 'test-suffix';
27+
const suffixedCookieName = '__clerk_db_jwt_test-suffix';
28+
const unsuffixedCookieName = '__clerk_db_jwt';
29+
const devBrowser = 'test-dev-browser';
30+
const now = new Date('2024-01-01T00:00:00.000Z');
31+
const expires = new Date('2025-01-01T00:00:00.000Z');
32+
const defaultOptions = { usePartitionedCookies: () => false };
33+
34+
beforeEach(() => {
35+
vi.useFakeTimers();
36+
vi.setSystemTime(now);
37+
cookieStore.clear();
38+
removeCalls.length = 0;
39+
setCalls.length = 0;
40+
});
41+
42+
afterEach(() => {
43+
vi.useRealTimers();
44+
vi.unstubAllGlobals();
45+
});
46+
47+
it('removes current, non-partitioned, and partitioned cookie variants for both dev browser cookie names', () => {
48+
const cookieHandler = createDevBrowserCookie(cookieSuffix, defaultOptions);
49+
50+
cookieHandler.remove();
51+
52+
expect(removeCalls).toEqual([
53+
{
54+
name: suffixedCookieName,
55+
attributes: {
56+
sameSite: 'Lax',
57+
secure: false,
58+
partitioned: false,
59+
},
60+
},
61+
{ name: suffixedCookieName, attributes: undefined },
62+
{
63+
name: suffixedCookieName,
64+
attributes: {
65+
sameSite: 'None',
66+
secure: true,
67+
partitioned: true,
68+
},
69+
},
70+
{
71+
name: unsuffixedCookieName,
72+
attributes: {
73+
sameSite: 'Lax',
74+
secure: false,
75+
partitioned: false,
76+
},
77+
},
78+
{ name: unsuffixedCookieName, attributes: undefined },
79+
{
80+
name: unsuffixedCookieName,
81+
attributes: {
82+
sameSite: 'None',
83+
secure: true,
84+
partitioned: true,
85+
},
86+
},
87+
]);
88+
});
89+
90+
it('clears stale partitioned cookie variants before writing a new dev browser', () => {
91+
const cookieHandler = createDevBrowserCookie(cookieSuffix, defaultOptions);
92+
93+
cookieHandler.set(devBrowser);
94+
95+
expect(removeCalls).toContainEqual({
96+
name: suffixedCookieName,
97+
attributes: {
98+
sameSite: 'None',
99+
secure: true,
100+
partitioned: true,
101+
},
102+
});
103+
expect(removeCalls).toContainEqual({
104+
name: unsuffixedCookieName,
105+
attributes: {
106+
sameSite: 'None',
107+
secure: true,
108+
partitioned: true,
109+
},
110+
});
111+
expect(setCalls).toEqual([
112+
{
113+
name: suffixedCookieName,
114+
value: devBrowser,
115+
attributes: {
116+
expires,
117+
sameSite: 'Lax',
118+
secure: false,
119+
partitioned: false,
120+
},
121+
},
122+
{
123+
name: unsuffixedCookieName,
124+
value: devBrowser,
125+
attributes: {
126+
expires,
127+
sameSite: 'Lax',
128+
secure: false,
129+
partitioned: false,
130+
},
131+
},
132+
]);
133+
});
134+
135+
it('reads the suffixed cookie before falling back to the unsuffixed cookie', () => {
136+
const cookieHandler = createDevBrowserCookie(cookieSuffix, defaultOptions);
137+
138+
cookieStore.set(unsuffixedCookieName, 'unsuffixed-value');
139+
cookieStore.set(suffixedCookieName, 'suffixed-value');
140+
141+
expect(cookieHandler.get()).toBe('suffixed-value');
142+
143+
cookieStore.delete(suffixedCookieName);
144+
145+
expect(cookieHandler.get()).toBe('unsuffixed-value');
146+
});
147+
});

packages/clerk-js/src/core/auth/cookies/devBrowser.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ const getCookieAttributes = (options: DevBrowserCookieOptions) => {
2525
return { sameSite, secure, partitioned } as const;
2626
};
2727

28+
const partitionedCookieAttributes = {
29+
sameSite: 'None',
30+
secure: true,
31+
partitioned: true,
32+
} as const;
33+
2834
/**
2935
* Create a long-lived JS cookie to store the dev browser token
3036
* ONLY for development instances.
@@ -40,33 +46,29 @@ export const createDevBrowserCookie = (
4046

4147
const get = () => suffixedDevBrowserCookie.get() || devBrowserCookie.get();
4248

49+
const removeAll = () => {
50+
const attributes = getCookieAttributes(options);
51+
52+
for (const cookie of [suffixedDevBrowserCookie, devBrowserCookie]) {
53+
cookie.remove(attributes);
54+
cookie.remove();
55+
cookie.remove(partitionedCookieAttributes);
56+
}
57+
};
58+
4359
const set = (devBrowser: string) => {
4460
const expires = addYears(Date.now(), 1);
4561
const { sameSite, secure, partitioned } = getCookieAttributes(options);
4662

47-
// Remove old non-partitioned cookies — the browser treats partitioned and
48-
// non-partitioned cookies with the same name as distinct cookies.
49-
if (partitioned) {
50-
suffixedDevBrowserCookie.remove();
51-
devBrowserCookie.remove();
52-
}
63+
// Remove stale variants before writing. The environment may not be loaded
64+
// yet, so the current partitioned-cookies setting cannot be trusted.
65+
removeAll();
5366

5467
suffixedDevBrowserCookie.set(devBrowser, { expires, sameSite, secure, partitioned });
5568
devBrowserCookie.set(devBrowser, { expires, sameSite, secure, partitioned });
5669
};
5770

58-
const remove = () => {
59-
const attributes = getCookieAttributes(options);
60-
suffixedDevBrowserCookie.remove(attributes);
61-
devBrowserCookie.remove(attributes);
62-
63-
// Also remove non-partitioned variants — the browser treats partitioned and
64-
// non-partitioned cookies with the same name as distinct cookies.
65-
if (attributes.partitioned) {
66-
suffixedDevBrowserCookie.remove();
67-
devBrowserCookie.remove();
68-
}
69-
};
71+
const remove = () => removeAll();
7072

7173
return {
7274
get,

0 commit comments

Comments
 (0)