Skip to content

Commit 78dc225

Browse files
brkalowclaude
andauthored
fix(clerk-js): Prevent duplicate __client_uat cookies in iframe contexts (#7875)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d987411 commit 78dc225

4 files changed

Lines changed: 43 additions & 8 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 `__client_uat` cookie being set on two different domain scopes when app is loaded in both iframe and non-iframe contexts. `getCookieDomain()` now falls back to `hostname` instead of `undefined` when the eTLD+1 probe fails, and the eTLD+1 probe uses the same `SameSite`/`Secure` attributes as the actual cookie to ensure consistent behavior across contexts.

packages/clerk-js/src/core/auth/__tests__/getCookieDomain.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ describe('getCookieDomain', () => {
3535
expect(result).toBe(hostname);
3636
});
3737

38+
it('passes cookie attributes to the probe', () => {
39+
const hostname = 'app.example.com';
40+
const handler: CookieHandler = {
41+
get: vi.fn().mockReturnValueOnce(undefined).mockReturnValueOnce('1'),
42+
set: vi.fn().mockReturnValue(undefined),
43+
remove: vi.fn().mockReturnValue(undefined),
44+
};
45+
const attrs = { sameSite: 'None', secure: true };
46+
getCookieDomain(hostname, handler, attrs);
47+
expect(handler.set).toHaveBeenCalledWith('1', { sameSite: 'None', secure: true, domain: 'example.com' });
48+
expect(handler.set).toHaveBeenCalledWith('1', { sameSite: 'None', secure: true, domain: 'app.example.com' });
49+
});
50+
3851
it('handles localhost', () => {
3952
const hostname = 'localhost';
4053
const result = getCookieDomain(hostname);
@@ -53,15 +66,15 @@ describe('getCookieDomain', () => {
5366
expect(getCookieDomain('bryce-local')).toBe('bryce-local');
5467
});
5568

56-
it('returns undefined if the domain could not be determined', () => {
69+
it('falls back to hostname if the domain could not be determined', () => {
5770
const handler: CookieHandler = {
5871
get: vi.fn().mockReturnValue(undefined),
5972
set: vi.fn().mockReturnValue(undefined),
6073
remove: vi.fn().mockReturnValue(undefined),
6174
};
6275
const hostname = 'app.hello.co.uk';
6376
const result = getCookieDomain(hostname, handler);
64-
expect(result).toBeUndefined();
77+
expect(result).toBe(hostname);
6578
});
6679

6780
it('uses cached value if there is one', () => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const createClientUatCookie = (cookieSuffix: string): ClientUatCookieHand
4545
: 'Strict';
4646
const secure = getSecureAttribute(sameSite);
4747
const partitioned = __BUILD_VARIANT_CHIPS__ && secure;
48-
const domain = getCookieDomain();
48+
const domain = getCookieDomain(undefined, undefined, { sameSite, secure });
4949

5050
// '0' indicates the user is signed out
5151
let val = '0';

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

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,19 @@ import { createCookieHandler } from '@clerk/shared/cookie';
88
let cachedETLDPlusOne: string;
99
const eTLDCookie = createCookieHandler('__clerk_test_etld');
1010

11-
export function getCookieDomain(hostname = window.location.hostname, cookieHandler = eTLDCookie) {
11+
/**
12+
* @param hostname - The hostname to determine the eTLD+1 for.
13+
* @param cookieHandler - The cookie handler to use for the eTLD+1 probe.
14+
* @param cookieAttributes - Optional cookie attributes (sameSite, secure) to use
15+
* during the eTLD+1 probe. These should match the attributes that will be used
16+
* when setting the actual cookie, so the probe accurately reflects whether a
17+
* domain-scoped cookie can be set in the current context.
18+
*/
19+
export function getCookieDomain(
20+
hostname = window.location.hostname,
21+
cookieHandler = eTLDCookie,
22+
cookieAttributes?: { sameSite?: string; secure?: boolean },
23+
) {
1224
// only compute it once per session to avoid unnecessary cookie ops
1325
if (cachedETLDPlusOne) {
1426
return cachedETLDPlusOne;
@@ -28,17 +40,22 @@ export function getCookieDomain(hostname = window.location.hostname, cookieHandl
2840
// we know for sure that the first entry is definitely a TLD, skip it
2941
for (let i = hostnameParts.length - 2; i >= 0; i--) {
3042
const eTLD = hostnameParts.slice(i).join('.');
31-
cookieHandler.set('1', { domain: eTLD });
43+
cookieHandler.set('1', { ...cookieAttributes, domain: eTLD });
3244

3345
const res = cookieHandler.get();
3446
if (res === '1') {
35-
cookieHandler.remove({ domain: eTLD });
47+
cookieHandler.remove({ ...cookieAttributes, domain: eTLD });
3648
cachedETLDPlusOne = eTLD;
3749
return eTLD;
3850
}
3951

40-
cookieHandler.remove({ domain: eTLD });
52+
cookieHandler.remove({ ...cookieAttributes, domain: eTLD });
4153
}
4254

43-
return;
55+
// Fallback to hostname to ensure domain-scoped cookie rather than host-only.
56+
// In restricted contexts (e.g. cross-origin iframes), the set() will silently
57+
// fail — which is preferable to creating a host-only cookie that conflicts
58+
// with domain-scoped cookies set by non-iframe contexts.
59+
cachedETLDPlusOne = hostname;
60+
return hostname;
4461
}

0 commit comments

Comments
 (0)