Skip to content

Commit b17e4bb

Browse files
brkalowclaude
andauthored
fix(clerk-js): Set SameSite=None on cookies for .replit.dev origins (core 2) (#7864)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6503c1d commit b17e4bb

11 files changed

Lines changed: 98 additions & 19 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/shared': patch
3+
'@clerk/clerk-js': patch
4+
---
5+
6+
Set `SameSite=None` on cookies for `.replit.dev` origins and consolidate third-party domain list

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{ "path": "./dist/clerk.js", "maxSize": "928KB" },
44
{ "path": "./dist/clerk.browser.js", "maxSize": "87KB" },
55
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "129KB" },
6-
{ "path": "./dist/clerk.headless*.js", "maxSize": "66KB" },
6+
{ "path": "./dist/clerk.headless*.js", "maxSize": "67KB" },
77
{ "path": "./dist/ui-common*.js", "maxSize": "123KB" },
88
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "126KB" },
99
{ "path": "./dist/vendors*.js", "maxSize": "50KB" },

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { inCrossOriginIframe } from '../../../../utils';
66
import { getCookieDomain } from '../../getCookieDomain';
77
import { getSecureAttribute } from '../../getSecureAttribute';
88
import { createClientUatCookie } from '../clientUat';
9+
import { requiresSameSiteNone } from '../requireSameSiteNone';
910

1011
vi.mock('@clerk/shared/cookie');
1112
vi.mock('@clerk/shared/date');
1213
vi.mock('../../../../utils');
1314
vi.mock('../../getCookieDomain');
1415
vi.mock('../../getSecureAttribute');
16+
vi.mock('../requireSameSiteNone');
1517

1618
describe('createClientUatCookie', () => {
1719
const mockCookieSuffix = 'test-suffix';
@@ -26,6 +28,7 @@ describe('createClientUatCookie', () => {
2628
mockGet.mockReset();
2729
(addYears as ReturnType<typeof vi.fn>).mockReturnValue(mockExpires);
2830
(inCrossOriginIframe as ReturnType<typeof vi.fn>).mockReturnValue(false);
31+
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
2932
(getCookieDomain as ReturnType<typeof vi.fn>).mockReturnValue(mockDomain);
3033
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
3134
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
@@ -125,4 +128,22 @@ describe('createClientUatCookie', () => {
125128

126129
expect(result).toBe(0);
127130
});
131+
132+
it('should set cookies with SameSite=None when the host requires it', () => {
133+
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(true);
134+
const cookieHandler = createClientUatCookie(mockCookieSuffix);
135+
cookieHandler.set({
136+
id: 'test-client',
137+
updatedAt: new Date('2024-01-01'),
138+
signedInSessions: ['session1'],
139+
});
140+
141+
expect(mockSet).toHaveBeenCalledWith('1704067200', {
142+
domain: mockDomain,
143+
expires: mockExpires,
144+
sameSite: 'None',
145+
secure: true,
146+
partitioned: false,
147+
});
148+
});
128149
});

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
44

55
import { inCrossOriginIframe } from '../../../../utils';
66
import { getSecureAttribute } from '../../getSecureAttribute';
7+
import { requiresSameSiteNone } from '../requireSameSiteNone';
78
import { createSessionCookie } from '../session';
89

910
vi.mock('@clerk/shared/cookie');
1011
vi.mock('@clerk/shared/date');
1112
vi.mock('../../../../utils');
1213
vi.mock('../../getSecureAttribute');
14+
vi.mock('../requireSameSiteNone');
1315

1416
describe('createSessionCookie', () => {
1517
const mockCookieSuffix = 'test-suffix';
@@ -24,6 +26,7 @@ describe('createSessionCookie', () => {
2426
mockGet.mockReset();
2527
(addYears as ReturnType<typeof vi.fn>).mockReturnValue(mockExpires);
2628
(inCrossOriginIframe as ReturnType<typeof vi.fn>).mockReturnValue(false);
29+
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(false);
2730
(getSecureAttribute as ReturnType<typeof vi.fn>).mockReturnValue(true);
2831
(createCookieHandler as ReturnType<typeof vi.fn>).mockImplementation(() => ({
2932
set: mockSet,
@@ -113,4 +116,17 @@ describe('createSessionCookie', () => {
113116

114117
expect(result).toBe('non-suffixed-value');
115118
});
119+
120+
it('should set cookies with None sameSite on .replit.dev origins', () => {
121+
(requiresSameSiteNone as ReturnType<typeof vi.fn>).mockReturnValue(true);
122+
const cookieHandler = createSessionCookie(mockCookieSuffix);
123+
cookieHandler.set(mockToken);
124+
125+
expect(mockSet).toHaveBeenCalledWith(mockToken, {
126+
expires: mockExpires,
127+
sameSite: 'None',
128+
secure: true,
129+
partitioned: false,
130+
});
131+
});
116132
});

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ClientResource } from '@clerk/shared/types';
66
import { inCrossOriginIframe } from '../../../utils';
77
import { getCookieDomain } from '../getCookieDomain';
88
import { getSecureAttribute } from '../getSecureAttribute';
9+
import { requiresSameSiteNone } from './requireSameSiteNone';
910

1011
const CLIENT_UAT_COOKIE_NAME = '__client_uat';
1112

@@ -37,7 +38,11 @@ export const createClientUatCookie = (cookieSuffix: string): ClientUatCookieHand
3738
* Generally, this is handled by redirectWithAuth() being called and relying on the dev browser ID in the URL,
3839
* but if that isn't used we rely on this. In production, nothing is cross-domain and Lax is used when client_uat is set from FAPI.
3940
*/
40-
const sameSite = __BUILD_VARIANT_CHIPS__ ? 'None' : inCrossOriginIframe() ? 'None' : 'Strict';
41+
const sameSite = __BUILD_VARIANT_CHIPS__
42+
? 'None'
43+
: inCrossOriginIframe() || requiresSameSiteNone()
44+
? 'None'
45+
: 'Strict';
4146
const secure = getSecureAttribute(sameSite);
4247
const partitioned = __BUILD_VARIANT_CHIPS__ && secure;
4348
const domain = getCookieDomain();

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getSuffixedCookieName } from '@clerk/shared/keys';
55

66
import { inCrossOriginIframe } from '../../../utils';
77
import { getSecureAttribute } from '../getSecureAttribute';
8+
import { requiresSameSiteNone } from './requireSameSiteNone';
89

910
export type DevBrowserCookieHandler = {
1011
set: (jwt: string) => void;
@@ -13,7 +14,7 @@ export type DevBrowserCookieHandler = {
1314
};
1415

1516
const getCookieAttributes = (): { sameSite: string; secure: boolean } => {
16-
const sameSite = inCrossOriginIframe() ? 'None' : 'Lax';
17+
const sameSite = inCrossOriginIframe() || requiresSameSiteNone() ? 'None' : 'Lax';
1718
const secure = getSecureAttribute(sameSite);
1819
return { sameSite, secure };
1920
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { isThirdPartyCookieDomain as requiresSameSiteNone } from '../../../utils/thirdPartyDomains';

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getSuffixedCookieName } from '@clerk/shared/keys';
44

55
import { inCrossOriginIframe } from '../../../utils';
66
import { getSecureAttribute } from '../getSecureAttribute';
7+
import { requiresSameSiteNone } from './requireSameSiteNone';
78

89
const SESSION_COOKIE_NAME = '__session';
910

@@ -14,7 +15,7 @@ export type SessionCookieHandler = {
1415
};
1516

1617
const getCookieAttributes = (): { sameSite: string; secure: boolean; partitioned: boolean } => {
17-
const sameSite = __BUILD_VARIANT_CHIPS__ ? 'None' : inCrossOriginIframe() ? 'None' : 'Lax';
18+
const sameSite = __BUILD_VARIANT_CHIPS__ ? 'None' : inCrossOriginIframe() || requiresSameSiteNone() ? 'None' : 'Lax';
1819
const secure = getSecureAttribute(sameSite);
1920
const partitioned = __BUILD_VARIANT_CHIPS__ && secure;
2021
return { sameSite, secure, partitioned };

packages/clerk-js/src/ui/utils/__tests__/originPrefersPopup.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,18 @@ describe('originPrefersPopup', () => {
1515
// Store original location to restore after tests
1616
const originalLocation = window.location;
1717

18-
// Helper function to mock window.location.origin
18+
// Helper function to mock window.location.hostname
1919
const mockLocationOrigin = (origin: string) => {
20+
let hostname: string;
21+
try {
22+
hostname = new URL(origin).hostname;
23+
} catch {
24+
hostname = origin;
25+
}
2026
Object.defineProperty(window, 'location', {
2127
value: {
2228
origin,
29+
hostname,
2330
},
2431
writable: true,
2532
configurable: true,
@@ -176,12 +183,12 @@ describe('originPrefersPopup', () => {
176183
expect(originPrefersPopup()).toBe(false);
177184
});
178185

179-
it('should be case sensitive', () => {
186+
it('should be case insensitive (hostnames are normalized to lowercase)', () => {
180187
mockLocationOrigin('https://app.LOVABLE.APP');
181-
expect(originPrefersPopup()).toBe(false);
188+
expect(originPrefersPopup()).toBe(true);
182189

183190
mockLocationOrigin('https://APP.V0.DEV');
184-
expect(originPrefersPopup()).toBe(false);
191+
expect(originPrefersPopup()).toBe(true);
185192
});
186193

187194
it('should handle malformed origins gracefully', () => {
Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
import { inIframe } from '@/utils';
2-
3-
const POPUP_PREFERRED_ORIGINS = [
4-
'.lovable.app',
5-
'.lovableproject.com',
6-
'.webcontainer-api.io',
7-
'.vusercontent.net',
8-
'.v0.dev',
9-
'.v0.app',
10-
'.lp.dev',
11-
];
2+
import { THIRD_PARTY_COOKIE_DOMAINS } from '@/utils/thirdPartyDomains';
123

134
/**
145
* Returns `true` if the current origin is one that is typically embedded via an iframe, which would benefit from the
156
* popup flow.
167
* @returns {boolean} Whether the current origin prefers the popup flow.
178
*/
189
export function originPrefersPopup(): boolean {
19-
return inIframe() || POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin));
10+
return inIframe() || THIRD_PARTY_COOKIE_DOMAINS.some(domain => window.location.hostname.endsWith(domain));
2011
}

0 commit comments

Comments
 (0)