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
5 changes: 5 additions & 0 deletions .changeset/fix-dev-browser-partitioned-cookie.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

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.
87 changes: 87 additions & 0 deletions integration/tests/dev-browser-partitioned-cookies.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
import { expect, test } from '@playwright/test';
import { parsePublishableKey } from '@clerk/shared/keys';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
'dev browser partitioned cookies @generic',
({ app }) => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test('URL query param dev browser token takes precedence over existing partitioned cookie on initial load', async ({
page,
context,
}) => {
const pk = app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY');
const { frontendApi } = parsePublishableKey(pk)!;
const fapiOrigin = `https://${frontendApi}`;

// Obtain a valid dev browser token directly from FAPI before any page load
const devBrowserRes = await page.request.post(`${fapiOrigin}/v1/dev_browser`);
expect(devBrowserRes.ok()).toBe(true);
const { id: freshToken } = await devBrowserRes.json();
expect(freshToken).toBeTruthy();

// Pre-set a stale __clerk_db_jwt cookie before the page ever loads.
// This simulates the partitioned cookie that already exists in the browser
// from a previous session.
const appUrl = new URL(app.serverUrl);
await context.addCookies([
{
name: '__clerk_db_jwt',
value: 'stale_partitioned_value',
domain: appUrl.hostname,
path: '/',
},
]);

// Collect every dev browser token attached to FAPI requests
const fapiTokens: string[] = [];
page.on('request', req => {
if (req.url().includes('__clerk_db_jwt') && req.url().includes('/v1/')) {
const url = new URL(req.url());
const token = url.searchParams.get('__clerk_db_jwt');
if (token) {
fapiTokens.push(token);
}
}
});

// Initial page load with the fresh token in the URL query param,
// simulating a redirect back from Clerk's Account Portal.
const signInUrl = new URL(app.serverUrl + '/sign-in');
signInUrl.searchParams.set('__clerk_db_jwt', freshToken);

await page.goto(signInUrl.toString());
await page.waitForLoadState('networkidle');

// Every FAPI request during initial load must use the URL token,
// not the stale partitioned cookie.
expect(fapiTokens.length).toBeGreaterThan(0);
for (const token of fapiTokens) {
expect(token).toBe(freshToken);
expect(token).not.toBe('stale_partitioned_value');
}

// Verify clerk-js is functional: sign in should succeed
const u = createTestUtils({ app, page, context });
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();
});
},
);
13 changes: 11 additions & 2 deletions packages/clerk-js/src/core/auth/devBrowser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,15 +37,22 @@ export function createDevBrowser({
}: CreateDevBrowserOptions): DevBrowser {
const devBrowserCookie = createDevBrowserCookie(cookieSuffix, cookieOptions);

// Hold the dev browser token in memory so it's always available to FAPI
// interceptors, even before Environment resolves and cookies can be written
// with the correct Partitioned attribute.
let devBrowserInMemory: string | undefined;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a reason why this wouldn't be safe. Potentially if there are already multiple dev browser cookies and a different value would have been read from cookies, but that would be a separate issue for us.

  • Dev browser should be unique per device per instance
  • Using this in-memory value should actually be more efficient and avoid unnecessary cookie reads
  • Any time the dev browser cooke is mutated on the client this value is updated accordingly


function getDevBrowser() {
return devBrowserCookie.get();
return devBrowserInMemory || devBrowserCookie.get();
}

function setDevBrowser(devBrowser: string) {
devBrowserInMemory = devBrowser;
devBrowserCookie.set(devBrowser);
}

function removeDevBrowser() {
devBrowserInMemory = undefined;
devBrowserCookie.remove();
}

Expand DownExpand Up@@ -81,7 +88,9 @@ export function createDevBrowser({
}

// 2. If no dev browser is found in the first step, check if one is already available in the __clerk_db_jwt JS cookie
if (devBrowserCookie.get()) {
const existingDevBrowser = devBrowserCookie.get();
if (existingDevBrowser) {
devBrowserInMemory = existingDevBrowser;
return;
Comment thread
jacekradko marked this conversation as resolved.
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(clerk-js): keep dev browser token in memory to prevent stale partitioned cookie reads by brkalow · Pull Request #8161 · clerk/javascript · GitHub
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
5 changes: 5 additions & 0 deletions .changeset/fix-dev-browser-partitioned-cookie.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

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.
87 changes: 87 additions & 0 deletions integration/tests/dev-browser-partitioned-cookies.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
import { expect, test } from '@playwright/test';
import { parsePublishableKey } from '@clerk/shared/keys';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
'dev browser partitioned cookies @generic',
({ app }) => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test('URL query param dev browser token takes precedence over existing partitioned cookie on initial load', async ({
page,
context,
}) => {
const pk = app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY');
const { frontendApi } = parsePublishableKey(pk)!;
const fapiOrigin = `https://${frontendApi}`;

// Obtain a valid dev browser token directly from FAPI before any page load
const devBrowserRes = await page.request.post(`${fapiOrigin}/v1/dev_browser`);
expect(devBrowserRes.ok()).toBe(true);
const { id: freshToken } = await devBrowserRes.json();
expect(freshToken).toBeTruthy();

// Pre-set a stale __clerk_db_jwt cookie before the page ever loads.
// This simulates the partitioned cookie that already exists in the browser
// from a previous session.
const appUrl = new URL(app.serverUrl);
await context.addCookies([
{
name: '__clerk_db_jwt',
value: 'stale_partitioned_value',
domain: appUrl.hostname,
path: '/',
},
]);

// Collect every dev browser token attached to FAPI requests
const fapiTokens: string[] = [];
page.on('request', req => {
if (req.url().includes('__clerk_db_jwt') && req.url().includes('/v1/')) {
const url = new URL(req.url());
const token = url.searchParams.get('__clerk_db_jwt');
if (token) {
fapiTokens.push(token);
}
}
});

// Initial page load with the fresh token in the URL query param,
// simulating a redirect back from Clerk's Account Portal.
const signInUrl = new URL(app.serverUrl + '/sign-in');
signInUrl.searchParams.set('__clerk_db_jwt', freshToken);

await page.goto(signInUrl.toString());
await page.waitForLoadState('networkidle');

// Every FAPI request during initial load must use the URL token,
// not the stale partitioned cookie.
expect(fapiTokens.length).toBeGreaterThan(0);
for (const token of fapiTokens) {
expect(token).toBe(freshToken);
expect(token).not.toBe('stale_partitioned_value');
}

// Verify clerk-js is functional: sign in should succeed
const u = createTestUtils({ app, page, context });
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();
});
},
);
13 changes: 11 additions & 2 deletions packages/clerk-js/src/core/auth/devBrowser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,15 +37,22 @@ export function createDevBrowser({
}: CreateDevBrowserOptions): DevBrowser {
const devBrowserCookie = createDevBrowserCookie(cookieSuffix, cookieOptions);

// Hold the dev browser token in memory so it's always available to FAPI
// interceptors, even before Environment resolves and cookies can be written
// with the correct Partitioned attribute.
let devBrowserInMemory: string | undefined;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a reason why this wouldn't be safe. Potentially if there are already multiple dev browser cookies and a different value would have been read from cookies, but that would be a separate issue for us.

  • Dev browser should be unique per device per instance
  • Using this in-memory value should actually be more efficient and avoid unnecessary cookie reads
  • Any time the dev browser cooke is mutated on the client this value is updated accordingly


function getDevBrowser() {
return devBrowserCookie.get();
return devBrowserInMemory || devBrowserCookie.get();
}

function setDevBrowser(devBrowser: string) {
devBrowserInMemory = devBrowser;
devBrowserCookie.set(devBrowser);
}

function removeDevBrowser() {
devBrowserInMemory = undefined;
devBrowserCookie.remove();
}

Expand DownExpand Up@@ -81,7 +88,9 @@ export function createDevBrowser({
}

// 2. If no dev browser is found in the first step, check if one is already available in the __clerk_db_jwt JS cookie
if (devBrowserCookie.get()) {
const existingDevBrowser = devBrowserCookie.get();
if (existingDevBrowser) {
devBrowserInMemory = existingDevBrowser;
return;
Comment thread
jacekradko marked this conversation as resolved.
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(clerk-js): keep dev browser token in memory to prevent stale partitioned cookie reads by brkalow · Pull Request #8161 · clerk/javascript · GitHub
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
5 changes: 5 additions & 0 deletions .changeset/fix-dev-browser-partitioned-cookie.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

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.
87 changes: 87 additions & 0 deletions integration/tests/dev-browser-partitioned-cookies.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
import { expect, test } from '@playwright/test';
import { parsePublishableKey } from '@clerk/shared/keys';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
'dev browser partitioned cookies @generic',
({ app }) => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test('URL query param dev browser token takes precedence over existing partitioned cookie on initial load', async ({
page,
context,
}) => {
const pk = app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY');
const { frontendApi } = parsePublishableKey(pk)!;
const fapiOrigin = `https://${frontendApi}`;

// Obtain a valid dev browser token directly from FAPI before any page load
const devBrowserRes = await page.request.post(`${fapiOrigin}/v1/dev_browser`);
expect(devBrowserRes.ok()).toBe(true);
const { id: freshToken } = await devBrowserRes.json();
expect(freshToken).toBeTruthy();

// Pre-set a stale __clerk_db_jwt cookie before the page ever loads.
// This simulates the partitioned cookie that already exists in the browser
// from a previous session.
const appUrl = new URL(app.serverUrl);
await context.addCookies([
{
name: '__clerk_db_jwt',
value: 'stale_partitioned_value',
domain: appUrl.hostname,
path: '/',
},
]);

// Collect every dev browser token attached to FAPI requests
const fapiTokens: string[] = [];
page.on('request', req => {
if (req.url().includes('__clerk_db_jwt') && req.url().includes('/v1/')) {
const url = new URL(req.url());
const token = url.searchParams.get('__clerk_db_jwt');
if (token) {
fapiTokens.push(token);
}
}
});

// Initial page load with the fresh token in the URL query param,
// simulating a redirect back from Clerk's Account Portal.
const signInUrl = new URL(app.serverUrl + '/sign-in');
signInUrl.searchParams.set('__clerk_db_jwt', freshToken);

await page.goto(signInUrl.toString());
await page.waitForLoadState('networkidle');

// Every FAPI request during initial load must use the URL token,
// not the stale partitioned cookie.
expect(fapiTokens.length).toBeGreaterThan(0);
for (const token of fapiTokens) {
expect(token).toBe(freshToken);
expect(token).not.toBe('stale_partitioned_value');
}

// Verify clerk-js is functional: sign in should succeed
const u = createTestUtils({ app, page, context });
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();
});
},
);
13 changes: 11 additions & 2 deletions packages/clerk-js/src/core/auth/devBrowser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,15 +37,22 @@ export function createDevBrowser({
}: CreateDevBrowserOptions): DevBrowser {
const devBrowserCookie = createDevBrowserCookie(cookieSuffix, cookieOptions);

// Hold the dev browser token in memory so it's always available to FAPI
// interceptors, even before Environment resolves and cookies can be written
// with the correct Partitioned attribute.
let devBrowserInMemory: string | undefined;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a reason why this wouldn't be safe. Potentially if there are already multiple dev browser cookies and a different value would have been read from cookies, but that would be a separate issue for us.

  • Dev browser should be unique per device per instance
  • Using this in-memory value should actually be more efficient and avoid unnecessary cookie reads
  • Any time the dev browser cooke is mutated on the client this value is updated accordingly


function getDevBrowser() {
return devBrowserCookie.get();
return devBrowserInMemory || devBrowserCookie.get();
}

function setDevBrowser(devBrowser: string) {
devBrowserInMemory = devBrowser;
devBrowserCookie.set(devBrowser);
}

function removeDevBrowser() {
devBrowserInMemory = undefined;
devBrowserCookie.remove();
}

Expand DownExpand Up@@ -81,7 +88,9 @@ export function createDevBrowser({
}

// 2. If no dev browser is found in the first step, check if one is already available in the __clerk_db_jwt JS cookie
if (devBrowserCookie.get()) {
const existingDevBrowser = devBrowserCookie.get();
if (existingDevBrowser) {
devBrowserInMemory = existingDevBrowser;
return;
Comment thread
jacekradko marked this conversation as resolved.
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(clerk-js): keep dev browser token in memory to prevent stale partitioned cookie reads by brkalow · Pull Request #8161 · clerk/javascript · GitHub
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
5 changes: 5 additions & 0 deletions .changeset/fix-dev-browser-partitioned-cookie.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

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.
87 changes: 87 additions & 0 deletions integration/tests/dev-browser-partitioned-cookies.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
import { expect, test } from '@playwright/test';
import { parsePublishableKey } from '@clerk/shared/keys';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
'dev browser partitioned cookies @generic',
({ app }) => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test('URL query param dev browser token takes precedence over existing partitioned cookie on initial load', async ({
page,
context,
}) => {
const pk = app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY');
const { frontendApi } = parsePublishableKey(pk)!;
const fapiOrigin = `https://${frontendApi}`;

// Obtain a valid dev browser token directly from FAPI before any page load
const devBrowserRes = await page.request.post(`${fapiOrigin}/v1/dev_browser`);
expect(devBrowserRes.ok()).toBe(true);
const { id: freshToken } = await devBrowserRes.json();
expect(freshToken).toBeTruthy();

// Pre-set a stale __clerk_db_jwt cookie before the page ever loads.
// This simulates the partitioned cookie that already exists in the browser
// from a previous session.
const appUrl = new URL(app.serverUrl);
await context.addCookies([
{
name: '__clerk_db_jwt',
value: 'stale_partitioned_value',
domain: appUrl.hostname,
path: '/',
},
]);

// Collect every dev browser token attached to FAPI requests
const fapiTokens: string[] = [];
page.on('request', req => {
if (req.url().includes('__clerk_db_jwt') && req.url().includes('/v1/')) {
const url = new URL(req.url());
const token = url.searchParams.get('__clerk_db_jwt');
if (token) {
fapiTokens.push(token);
}
}
});

// Initial page load with the fresh token in the URL query param,
// simulating a redirect back from Clerk's Account Portal.
const signInUrl = new URL(app.serverUrl + '/sign-in');
signInUrl.searchParams.set('__clerk_db_jwt', freshToken);

await page.goto(signInUrl.toString());
await page.waitForLoadState('networkidle');

// Every FAPI request during initial load must use the URL token,
// not the stale partitioned cookie.
expect(fapiTokens.length).toBeGreaterThan(0);
for (const token of fapiTokens) {
expect(token).toBe(freshToken);
expect(token).not.toBe('stale_partitioned_value');
}

// Verify clerk-js is functional: sign in should succeed
const u = createTestUtils({ app, page, context });
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();
});
},
);
13 changes: 11 additions & 2 deletions packages/clerk-js/src/core/auth/devBrowser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,15 +37,22 @@ export function createDevBrowser({
}: CreateDevBrowserOptions): DevBrowser {
const devBrowserCookie = createDevBrowserCookie(cookieSuffix, cookieOptions);

// Hold the dev browser token in memory so it's always available to FAPI
// interceptors, even before Environment resolves and cookies can be written
// with the correct Partitioned attribute.
let devBrowserInMemory: string | undefined;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a reason why this wouldn't be safe. Potentially if there are already multiple dev browser cookies and a different value would have been read from cookies, but that would be a separate issue for us.

  • Dev browser should be unique per device per instance
  • Using this in-memory value should actually be more efficient and avoid unnecessary cookie reads
  • Any time the dev browser cooke is mutated on the client this value is updated accordingly


function getDevBrowser() {
return devBrowserCookie.get();
return devBrowserInMemory || devBrowserCookie.get();
}

function setDevBrowser(devBrowser: string) {
devBrowserInMemory = devBrowser;
devBrowserCookie.set(devBrowser);
}

function removeDevBrowser() {
devBrowserInMemory = undefined;
devBrowserCookie.remove();
}

Expand DownExpand Up@@ -81,7 +88,9 @@ export function createDevBrowser({
}

// 2. If no dev browser is found in the first step, check if one is already available in the __clerk_db_jwt JS cookie
if (devBrowserCookie.get()) {
const existingDevBrowser = devBrowserCookie.get();
if (existingDevBrowser) {
devBrowserInMemory = existingDevBrowser;
return;
Comment thread
jacekradko marked this conversation as resolved.
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(clerk-js): keep dev browser token in memory to prevent stale partitioned cookie reads by brkalow · Pull Request #8161 · clerk/javascript · GitHub
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
5 changes: 5 additions & 0 deletions .changeset/fix-dev-browser-partitioned-cookie.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

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.
87 changes: 87 additions & 0 deletions integration/tests/dev-browser-partitioned-cookies.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
import { expect, test } from '@playwright/test';
import { parsePublishableKey } from '@clerk/shared/keys';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
'dev browser partitioned cookies @generic',
({ app }) => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test('URL query param dev browser token takes precedence over existing partitioned cookie on initial load', async ({
page,
context,
}) => {
const pk = app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY');
const { frontendApi } = parsePublishableKey(pk)!;
const fapiOrigin = `https://${frontendApi}`;

// Obtain a valid dev browser token directly from FAPI before any page load
const devBrowserRes = await page.request.post(`${fapiOrigin}/v1/dev_browser`);
expect(devBrowserRes.ok()).toBe(true);
const { id: freshToken } = await devBrowserRes.json();
expect(freshToken).toBeTruthy();

// Pre-set a stale __clerk_db_jwt cookie before the page ever loads.
// This simulates the partitioned cookie that already exists in the browser
// from a previous session.
const appUrl = new URL(app.serverUrl);
await context.addCookies([
{
name: '__clerk_db_jwt',
value: 'stale_partitioned_value',
domain: appUrl.hostname,
path: '/',
},
]);

// Collect every dev browser token attached to FAPI requests
const fapiTokens: string[] = [];
page.on('request', req => {
if (req.url().includes('__clerk_db_jwt') && req.url().includes('/v1/')) {
const url = new URL(req.url());
const token = url.searchParams.get('__clerk_db_jwt');
if (token) {
fapiTokens.push(token);
}
}
});

// Initial page load with the fresh token in the URL query param,
// simulating a redirect back from Clerk's Account Portal.
const signInUrl = new URL(app.serverUrl + '/sign-in');
signInUrl.searchParams.set('__clerk_db_jwt', freshToken);

await page.goto(signInUrl.toString());
await page.waitForLoadState('networkidle');

// Every FAPI request during initial load must use the URL token,
// not the stale partitioned cookie.
expect(fapiTokens.length).toBeGreaterThan(0);
for (const token of fapiTokens) {
expect(token).toBe(freshToken);
expect(token).not.toBe('stale_partitioned_value');
}

// Verify clerk-js is functional: sign in should succeed
const u = createTestUtils({ app, page, context });
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();
});
},
);
13 changes: 11 additions & 2 deletions packages/clerk-js/src/core/auth/devBrowser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,15 +37,22 @@ export function createDevBrowser({
}: CreateDevBrowserOptions): DevBrowser {
const devBrowserCookie = createDevBrowserCookie(cookieSuffix, cookieOptions);

// Hold the dev browser token in memory so it's always available to FAPI
// interceptors, even before Environment resolves and cookies can be written
// with the correct Partitioned attribute.
let devBrowserInMemory: string | undefined;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a reason why this wouldn't be safe. Potentially if there are already multiple dev browser cookies and a different value would have been read from cookies, but that would be a separate issue for us.

  • Dev browser should be unique per device per instance
  • Using this in-memory value should actually be more efficient and avoid unnecessary cookie reads
  • Any time the dev browser cooke is mutated on the client this value is updated accordingly


function getDevBrowser() {
return devBrowserCookie.get();
return devBrowserInMemory || devBrowserCookie.get();
}

function setDevBrowser(devBrowser: string) {
devBrowserInMemory = devBrowser;
devBrowserCookie.set(devBrowser);
}

function removeDevBrowser() {
devBrowserInMemory = undefined;
devBrowserCookie.remove();
}

Expand DownExpand Up@@ -81,7 +88,9 @@ export function createDevBrowser({
}

// 2. If no dev browser is found in the first step, check if one is already available in the __clerk_db_jwt JS cookie
if (devBrowserCookie.get()) {
const existingDevBrowser = devBrowserCookie.get();
if (existingDevBrowser) {
devBrowserInMemory = existingDevBrowser;
return;
Comment thread
jacekradko marked this conversation as resolved.
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(clerk-js): keep dev browser token in memory to prevent stale partitioned cookie reads by brkalow · Pull Request #8161 · clerk/javascript · GitHub
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
5 changes: 5 additions & 0 deletions .changeset/fix-dev-browser-partitioned-cookie.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

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.
87 changes: 87 additions & 0 deletions integration/tests/dev-browser-partitioned-cookies.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
import { expect, test } from '@playwright/test';
import { parsePublishableKey } from '@clerk/shared/keys';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
'dev browser partitioned cookies @generic',
({ app }) => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test('URL query param dev browser token takes precedence over existing partitioned cookie on initial load', async ({
page,
context,
}) => {
const pk = app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY');
const { frontendApi } = parsePublishableKey(pk)!;
const fapiOrigin = `https://${frontendApi}`;

// Obtain a valid dev browser token directly from FAPI before any page load
const devBrowserRes = await page.request.post(`${fapiOrigin}/v1/dev_browser`);
expect(devBrowserRes.ok()).toBe(true);
const { id: freshToken } = await devBrowserRes.json();
expect(freshToken).toBeTruthy();

// Pre-set a stale __clerk_db_jwt cookie before the page ever loads.
// This simulates the partitioned cookie that already exists in the browser
// from a previous session.
const appUrl = new URL(app.serverUrl);
await context.addCookies([
{
name: '__clerk_db_jwt',
value: 'stale_partitioned_value',
domain: appUrl.hostname,
path: '/',
},
]);

// Collect every dev browser token attached to FAPI requests
const fapiTokens: string[] = [];
page.on('request', req => {
if (req.url().includes('__clerk_db_jwt') && req.url().includes('/v1/')) {
const url = new URL(req.url());
const token = url.searchParams.get('__clerk_db_jwt');
if (token) {
fapiTokens.push(token);
}
}
});

// Initial page load with the fresh token in the URL query param,
// simulating a redirect back from Clerk's Account Portal.
const signInUrl = new URL(app.serverUrl + '/sign-in');
signInUrl.searchParams.set('__clerk_db_jwt', freshToken);

await page.goto(signInUrl.toString());
await page.waitForLoadState('networkidle');

// Every FAPI request during initial load must use the URL token,
// not the stale partitioned cookie.
expect(fapiTokens.length).toBeGreaterThan(0);
for (const token of fapiTokens) {
expect(token).toBe(freshToken);
expect(token).not.toBe('stale_partitioned_value');
}

// Verify clerk-js is functional: sign in should succeed
const u = createTestUtils({ app, page, context });
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();
});
},
);
13 changes: 11 additions & 2 deletions packages/clerk-js/src/core/auth/devBrowser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,15 +37,22 @@ export function createDevBrowser({
}: CreateDevBrowserOptions): DevBrowser {
const devBrowserCookie = createDevBrowserCookie(cookieSuffix, cookieOptions);

// Hold the dev browser token in memory so it's always available to FAPI
// interceptors, even before Environment resolves and cookies can be written
// with the correct Partitioned attribute.
let devBrowserInMemory: string | undefined;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a reason why this wouldn't be safe. Potentially if there are already multiple dev browser cookies and a different value would have been read from cookies, but that would be a separate issue for us.

  • Dev browser should be unique per device per instance
  • Using this in-memory value should actually be more efficient and avoid unnecessary cookie reads
  • Any time the dev browser cooke is mutated on the client this value is updated accordingly


function getDevBrowser() {
return devBrowserCookie.get();
return devBrowserInMemory || devBrowserCookie.get();
}

function setDevBrowser(devBrowser: string) {
devBrowserInMemory = devBrowser;
devBrowserCookie.set(devBrowser);
}

function removeDevBrowser() {
devBrowserInMemory = undefined;
devBrowserCookie.remove();
}

Expand DownExpand Up@@ -81,7 +88,9 @@ export function createDevBrowser({
}

// 2. If no dev browser is found in the first step, check if one is already available in the __clerk_db_jwt JS cookie
if (devBrowserCookie.get()) {
const existingDevBrowser = devBrowserCookie.get();
if (existingDevBrowser) {
devBrowserInMemory = existingDevBrowser;
return;
Comment thread
jacekradko marked this conversation as resolved.
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(clerk-js): keep dev browser token in memory to prevent stale partitioned cookie reads by brkalow · Pull Request #8161 · clerk/javascript · GitHub
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
5 changes: 5 additions & 0 deletions .changeset/fix-dev-browser-partitioned-cookie.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

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.
87 changes: 87 additions & 0 deletions integration/tests/dev-browser-partitioned-cookies.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
import { expect, test } from '@playwright/test';
import { parsePublishableKey } from '@clerk/shared/keys';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
'dev browser partitioned cookies @generic',
({ app }) => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test('URL query param dev browser token takes precedence over existing partitioned cookie on initial load', async ({
page,
context,
}) => {
const pk = app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY');
const { frontendApi } = parsePublishableKey(pk)!;
const fapiOrigin = `https://${frontendApi}`;

// Obtain a valid dev browser token directly from FAPI before any page load
const devBrowserRes = await page.request.post(`${fapiOrigin}/v1/dev_browser`);
expect(devBrowserRes.ok()).toBe(true);
const { id: freshToken } = await devBrowserRes.json();
expect(freshToken).toBeTruthy();

// Pre-set a stale __clerk_db_jwt cookie before the page ever loads.
// This simulates the partitioned cookie that already exists in the browser
// from a previous session.
const appUrl = new URL(app.serverUrl);
await context.addCookies([
{
name: '__clerk_db_jwt',
value: 'stale_partitioned_value',
domain: appUrl.hostname,
path: '/',
},
]);

// Collect every dev browser token attached to FAPI requests
const fapiTokens: string[] = [];
page.on('request', req => {
if (req.url().includes('__clerk_db_jwt') && req.url().includes('/v1/')) {
const url = new URL(req.url());
const token = url.searchParams.get('__clerk_db_jwt');
if (token) {
fapiTokens.push(token);
}
}
});

// Initial page load with the fresh token in the URL query param,
// simulating a redirect back from Clerk's Account Portal.
const signInUrl = new URL(app.serverUrl + '/sign-in');
signInUrl.searchParams.set('__clerk_db_jwt', freshToken);

await page.goto(signInUrl.toString());
await page.waitForLoadState('networkidle');

// Every FAPI request during initial load must use the URL token,
// not the stale partitioned cookie.
expect(fapiTokens.length).toBeGreaterThan(0);
for (const token of fapiTokens) {
expect(token).toBe(freshToken);
expect(token).not.toBe('stale_partitioned_value');
}

// Verify clerk-js is functional: sign in should succeed
const u = createTestUtils({ app, page, context });
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();
});
},
);
13 changes: 11 additions & 2 deletions packages/clerk-js/src/core/auth/devBrowser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,15 +37,22 @@ export function createDevBrowser({
}: CreateDevBrowserOptions): DevBrowser {
const devBrowserCookie = createDevBrowserCookie(cookieSuffix, cookieOptions);

// Hold the dev browser token in memory so it's always available to FAPI
// interceptors, even before Environment resolves and cookies can be written
// with the correct Partitioned attribute.
let devBrowserInMemory: string | undefined;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a reason why this wouldn't be safe. Potentially if there are already multiple dev browser cookies and a different value would have been read from cookies, but that would be a separate issue for us.

  • Dev browser should be unique per device per instance
  • Using this in-memory value should actually be more efficient and avoid unnecessary cookie reads
  • Any time the dev browser cooke is mutated on the client this value is updated accordingly


function getDevBrowser() {
return devBrowserCookie.get();
return devBrowserInMemory || devBrowserCookie.get();
}

function setDevBrowser(devBrowser: string) {
devBrowserInMemory = devBrowser;
devBrowserCookie.set(devBrowser);
}

function removeDevBrowser() {
devBrowserInMemory = undefined;
devBrowserCookie.remove();
}

Expand DownExpand Up@@ -81,7 +88,9 @@ export function createDevBrowser({
}

// 2. If no dev browser is found in the first step, check if one is already available in the __clerk_db_jwt JS cookie
if (devBrowserCookie.get()) {
const existingDevBrowser = devBrowserCookie.get();
if (existingDevBrowser) {
devBrowserInMemory = existingDevBrowser;
return;
Comment thread
jacekradko marked this conversation as resolved.
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(clerk-js): keep dev browser token in memory to prevent stale partitioned cookie reads by brkalow · Pull Request #8161 · clerk/javascript · GitHub
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
5 changes: 5 additions & 0 deletions .changeset/fix-dev-browser-partitioned-cookie.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

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.
87 changes: 87 additions & 0 deletions integration/tests/dev-browser-partitioned-cookies.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
import { expect, test } from '@playwright/test';
import { parsePublishableKey } from '@clerk/shared/keys';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
'dev browser partitioned cookies @generic',
({ app }) => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test('URL query param dev browser token takes precedence over existing partitioned cookie on initial load', async ({
page,
context,
}) => {
const pk = app.env.publicVariables.get('CLERK_PUBLISHABLE_KEY');
const { frontendApi } = parsePublishableKey(pk)!;
const fapiOrigin = `https://${frontendApi}`;

// Obtain a valid dev browser token directly from FAPI before any page load
const devBrowserRes = await page.request.post(`${fapiOrigin}/v1/dev_browser`);
expect(devBrowserRes.ok()).toBe(true);
const { id: freshToken } = await devBrowserRes.json();
expect(freshToken).toBeTruthy();

// Pre-set a stale __clerk_db_jwt cookie before the page ever loads.
// This simulates the partitioned cookie that already exists in the browser
// from a previous session.
const appUrl = new URL(app.serverUrl);
await context.addCookies([
{
name: '__clerk_db_jwt',
value: 'stale_partitioned_value',
domain: appUrl.hostname,
path: '/',
},
]);

// Collect every dev browser token attached to FAPI requests
const fapiTokens: string[] = [];
page.on('request', req => {
if (req.url().includes('__clerk_db_jwt') && req.url().includes('/v1/')) {
const url = new URL(req.url());
const token = url.searchParams.get('__clerk_db_jwt');
if (token) {
fapiTokens.push(token);
}
}
});

// Initial page load with the fresh token in the URL query param,
// simulating a redirect back from Clerk's Account Portal.
const signInUrl = new URL(app.serverUrl + '/sign-in');
signInUrl.searchParams.set('__clerk_db_jwt', freshToken);

await page.goto(signInUrl.toString());
await page.waitForLoadState('networkidle');

// Every FAPI request during initial load must use the URL token,
// not the stale partitioned cookie.
expect(fapiTokens.length).toBeGreaterThan(0);
for (const token of fapiTokens) {
expect(token).toBe(freshToken);
expect(token).not.toBe('stale_partitioned_value');
}

// Verify clerk-js is functional: sign in should succeed
const u = createTestUtils({ app, page, context });
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();
});
},
);
13 changes: 11 additions & 2 deletions packages/clerk-js/src/core/auth/devBrowser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,15 +37,22 @@ export function createDevBrowser({
}: CreateDevBrowserOptions): DevBrowser {
const devBrowserCookie = createDevBrowserCookie(cookieSuffix, cookieOptions);

// Hold the dev browser token in memory so it's always available to FAPI
// interceptors, even before Environment resolves and cookies can be written
// with the correct Partitioned attribute.
let devBrowserInMemory: string | undefined;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a reason why this wouldn't be safe. Potentially if there are already multiple dev browser cookies and a different value would have been read from cookies, but that would be a separate issue for us.

  • Dev browser should be unique per device per instance
  • Using this in-memory value should actually be more efficient and avoid unnecessary cookie reads
  • Any time the dev browser cooke is mutated on the client this value is updated accordingly


function getDevBrowser() {
return devBrowserCookie.get();
return devBrowserInMemory || devBrowserCookie.get();
}

function setDevBrowser(devBrowser: string) {
devBrowserInMemory = devBrowser;
devBrowserCookie.set(devBrowser);
}

function removeDevBrowser() {
devBrowserInMemory = undefined;
devBrowserCookie.remove();
}

Expand DownExpand Up@@ -81,7 +88,9 @@ export function createDevBrowser({
}

// 2. If no dev browser is found in the first step, check if one is already available in the __clerk_db_jwt JS cookie
if (devBrowserCookie.get()) {
const existingDevBrowser = devBrowserCookie.get();
if (existingDevBrowser) {
devBrowserInMemory = existingDevBrowser;
return;
Comment thread
jacekradko marked this conversation as resolved.
}

Expand Down
Loading