Skip to content

Commit ed6f3ab

Browse files
brkalowwobsoriano
authored andcommitted
fix(clerk-js): keep dev browser token in memory to prevent stale partitioned cookie reads (#8161)
1 parent a75061a commit ed6f3ab

3 files changed

Lines changed: 103 additions & 2 deletions

File tree

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 token being read from a stale non-partitioned cookie when `partitionedCookies` is enabled. The token is now kept in memory so FAPI requests always use the authoritative value.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { expect, test } from '@playwright/test';
2+
import { parsePublishableKey } from '@clerk/shared/keys';
3+
4+
import { appConfigs } from '../presets';
5+
import type { FakeUser } from '../testUtils';
6+
import { createTestUtils, testAgainstRunningApps } from '../testUtils';
7+
8+
testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
9+
'dev browser partitioned cookies @generic',
10+
({ app }) => {
11+
test.describe.configure({ mode: 'serial' });
12+
13+
let fakeUser: FakeUser;
14+
15+
test.beforeAll(async () => {
16+
const u = createTestUtils({ app });
17+
fakeUser = u.services.users.createFakeUser();
18+
await u.services.users.createBapiUser(fakeUser);
19+
});
20+
21+
test.afterAll(async () => {
22+
await fakeUser.deleteIfExists();
23+
await app.teardown();
24+
});
25+
26+
test('URL query param dev browser token takes precedence over existing partitioned cookie on initial load', async ({
27+
page,
28+
context,
29+
}) => {
30+
const pk = app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY');
31+
const { frontendApi } = parsePublishableKey(pk)!;
32+
const fapiOrigin = `https://${frontendApi}`;
33+
34+
// Obtain a valid dev browser token directly from FAPI before any page load
35+
const devBrowserRes = await page.request.post(`${fapiOrigin}/v1/dev_browser`);
36+
expect(devBrowserRes.ok()).toBe(true);
37+
const { id: freshToken } = await devBrowserRes.json();
38+
expect(freshToken).toBeTruthy();
39+
40+
// Pre-set a stale __clerk_db_jwt cookie before the page ever loads.
41+
// This simulates the partitioned cookie that already exists in the browser
42+
// from a previous session.
43+
const appUrl = new URL(app.serverUrl);
44+
await context.addCookies([
45+
{
46+
name: '__clerk_db_jwt',
47+
value: 'stale_partitioned_value',
48+
domain: appUrl.hostname,
49+
path: '/',
50+
},
51+
]);
52+
53+
// Collect every dev browser token attached to FAPI requests
54+
const fapiTokens: string[] = [];
55+
page.on('request', req => {
56+
if (req.url().includes('__clerk_db_jwt') && req.url().includes('/v1/')) {
57+
const url = new URL(req.url());
58+
const token = url.searchParams.get('__clerk_db_jwt');
59+
if (token) {
60+
fapiTokens.push(token);
61+
}
62+
}
63+
});
64+
65+
// Initial page load with the fresh token in the URL query param,
66+
// simulating a redirect back from Clerk's Account Portal.
67+
const signInUrl = new URL(app.serverUrl + '/sign-in');
68+
signInUrl.searchParams.set('__clerk_db_jwt', freshToken);
69+
70+
await page.goto(signInUrl.toString());
71+
await page.waitForLoadState('networkidle');
72+
73+
// Every FAPI request during initial load must use the URL token,
74+
// not the stale partitioned cookie.
75+
expect(fapiTokens.length).toBeGreaterThan(0);
76+
for (const token of fapiTokens) {
77+
expect(token).toBe(freshToken);
78+
expect(token).not.toBe('stale_partitioned_value');
79+
}
80+
81+
// Verify clerk-js is functional: sign in should succeed
82+
const u = createTestUtils({ app, page, context });
83+
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
84+
await u.po.expect.toBeSignedIn();
85+
});
86+
},
87+
);

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,22 @@ export function createDevBrowser({
3737
}: CreateDevBrowserOptions): DevBrowser {
3838
const devBrowserCookie = createDevBrowserCookie(cookieSuffix, cookieOptions);
3939

40+
// Hold the dev browser token in memory so it's always available to FAPI
41+
// interceptors, even before Environment resolves and cookies can be written
42+
// with the correct Partitioned attribute.
43+
let devBrowserInMemory: string | undefined;
44+
4045
function getDevBrowser() {
41-
return devBrowserCookie.get();
46+
return devBrowserInMemory || devBrowserCookie.get();
4247
}
4348

4449
function setDevBrowser(devBrowser: string) {
50+
devBrowserInMemory = devBrowser;
4551
devBrowserCookie.set(devBrowser);
4652
}
4753

4854
function removeDevBrowser() {
55+
devBrowserInMemory = undefined;
4956
devBrowserCookie.remove();
5057
}
5158

@@ -81,7 +88,9 @@ export function createDevBrowser({
8188
}
8289

8390
// 2. If no dev browser is found in the first step, check if one is already available in the __clerk_db_jwt JS cookie
84-
if (devBrowserCookie.get()) {
91+
const existingDevBrowser = devBrowserCookie.get();
92+
if (existingDevBrowser) {
93+
devBrowserInMemory = existingDevBrowser;
8594
return;
8695
}
8796

0 commit comments

Comments
 (0)