From f7c4178cb80d6a6a01c478766a2ad83b42d00e98 Mon Sep 17 00:00:00 2001 From: brkalow Date: Fri, 13 Feb 2026 13:02:49 -0600 Subject: [PATCH 1/5] fix(clerk-js): Set SameSite=None on cookies for .replit.dev origins Cookies on .replit.dev origins need SameSite=None to work correctly. Adds a shared requiresSameSiteNone() helper used by session, clientUat, and devBrowser cookie setters. Co-Authored-By: Claude Opus 4.6 --- .changeset/fix-samesite-replit-dev.md | 5 +++++ .../auth/cookies/__tests__/clientUat.test.ts | 21 +++++++++++++++++++ .../auth/cookies/__tests__/session.test.ts | 16 ++++++++++++++ .../src/core/auth/cookies/clientUat.ts | 7 ++++++- .../src/core/auth/cookies/devBrowser.ts | 3 ++- .../core/auth/cookies/requireSameSiteNone.ts | 11 ++++++++++ .../clerk-js/src/core/auth/cookies/session.ts | 3 ++- 7 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-samesite-replit-dev.md create mode 100644 packages/clerk-js/src/core/auth/cookies/requireSameSiteNone.ts diff --git a/.changeset/fix-samesite-replit-dev.md b/.changeset/fix-samesite-replit-dev.md new file mode 100644 index 00000000000..fb15cdac9a7 --- /dev/null +++ b/.changeset/fix-samesite-replit-dev.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Set `SameSite=None` on cookies for `.replit.dev` origins diff --git a/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts b/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts index 1f48f4e4ee0..c39824d298f 100644 --- a/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts +++ b/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts @@ -6,12 +6,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { getCookieDomain } from '../../getCookieDomain'; import { getSecureAttribute } from '../../getSecureAttribute'; import { createClientUatCookie } from '../clientUat'; +import { requiresSameSiteNone } from '../requireSameSiteNone'; vi.mock('@clerk/shared/cookie'); vi.mock('@clerk/shared/date'); vi.mock('@clerk/shared/internal/clerk-js/runtime'); vi.mock('../../getCookieDomain'); vi.mock('../../getSecureAttribute'); +vi.mock('../requireSameSiteNone'); describe('createClientUatCookie', () => { const mockCookieSuffix = 'test-suffix'; @@ -26,6 +28,7 @@ describe('createClientUatCookie', () => { mockGet.mockReset(); (addYears as ReturnType).mockReturnValue(mockExpires); (inCrossOriginIframe as ReturnType).mockReturnValue(false); + (requiresSameSiteNone as ReturnType).mockReturnValue(false); (getCookieDomain as ReturnType).mockReturnValue(mockDomain); (getSecureAttribute as ReturnType).mockReturnValue(true); (createCookieHandler as ReturnType).mockImplementation(() => ({ @@ -125,4 +128,22 @@ describe('createClientUatCookie', () => { expect(result).toBe(0); }); + + it('should set cookies with None sameSite on .replit.dev origins', () => { + (requiresSameSiteNone as ReturnType).mockReturnValue(true); + const cookieHandler = createClientUatCookie(mockCookieSuffix); + cookieHandler.set({ + id: 'test-client', + updatedAt: new Date('2024-01-01'), + signedInSessions: ['session1'], + }); + + expect(mockSet).toHaveBeenCalledWith('1704067200', { + domain: mockDomain, + expires: mockExpires, + sameSite: 'None', + secure: true, + partitioned: false, + }); + }); }); diff --git a/packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts b/packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts index d51bf732de9..e034c796d05 100644 --- a/packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts +++ b/packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts @@ -4,12 +4,14 @@ import { inCrossOriginIframe } from '@clerk/shared/internal/clerk-js/runtime'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { getSecureAttribute } from '../../getSecureAttribute'; +import { requiresSameSiteNone } from '../requireSameSiteNone'; import { createSessionCookie } from '../session'; vi.mock('@clerk/shared/cookie'); vi.mock('@clerk/shared/date'); vi.mock('@clerk/shared/internal/clerk-js/runtime'); vi.mock('../../getSecureAttribute'); +vi.mock('../requireSameSiteNone'); describe('createSessionCookie', () => { const mockCookieSuffix = 'test-suffix'; @@ -24,6 +26,7 @@ describe('createSessionCookie', () => { mockGet.mockReset(); (addYears as ReturnType).mockReturnValue(mockExpires); (inCrossOriginIframe as ReturnType).mockReturnValue(false); + (requiresSameSiteNone as ReturnType).mockReturnValue(false); (getSecureAttribute as ReturnType).mockReturnValue(true); (createCookieHandler as ReturnType).mockImplementation(() => ({ set: mockSet, @@ -113,4 +116,17 @@ describe('createSessionCookie', () => { expect(result).toBe('non-suffixed-value'); }); + + it('should set cookies with None sameSite on .replit.dev origins', () => { + (requiresSameSiteNone as ReturnType).mockReturnValue(true); + const cookieHandler = createSessionCookie(mockCookieSuffix); + cookieHandler.set(mockToken); + + expect(mockSet).toHaveBeenCalledWith(mockToken, { + expires: mockExpires, + sameSite: 'None', + secure: true, + partitioned: false, + }); + }); }); diff --git a/packages/clerk-js/src/core/auth/cookies/clientUat.ts b/packages/clerk-js/src/core/auth/cookies/clientUat.ts index 20a620938b8..37479f34522 100644 --- a/packages/clerk-js/src/core/auth/cookies/clientUat.ts +++ b/packages/clerk-js/src/core/auth/cookies/clientUat.ts @@ -6,6 +6,7 @@ import type { ClientResource } from '@clerk/shared/types'; import { getCookieDomain } from '../getCookieDomain'; import { getSecureAttribute } from '../getSecureAttribute'; +import { requiresSameSiteNone } from './requireSameSiteNone'; const CLIENT_UAT_COOKIE_NAME = '__client_uat'; @@ -37,7 +38,11 @@ export const createClientUatCookie = (cookieSuffix: string): ClientUatCookieHand * Generally, this is handled by redirectWithAuth() being called and relying on the dev browser ID in the URL, * 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. */ - const sameSite = __BUILD_VARIANT_CHIPS__ ? 'None' : inCrossOriginIframe() ? 'None' : 'Strict'; + const sameSite = __BUILD_VARIANT_CHIPS__ + ? 'None' + : inCrossOriginIframe() || requiresSameSiteNone() + ? 'None' + : 'Strict'; const secure = getSecureAttribute(sameSite); const partitioned = __BUILD_VARIANT_CHIPS__ && secure; const domain = getCookieDomain(); diff --git a/packages/clerk-js/src/core/auth/cookies/devBrowser.ts b/packages/clerk-js/src/core/auth/cookies/devBrowser.ts index ab3f66eb6d4..3bd305e5126 100644 --- a/packages/clerk-js/src/core/auth/cookies/devBrowser.ts +++ b/packages/clerk-js/src/core/auth/cookies/devBrowser.ts @@ -5,6 +5,7 @@ import { inCrossOriginIframe } from '@clerk/shared/internal/clerk-js/runtime'; import { getSuffixedCookieName } from '@clerk/shared/keys'; import { getSecureAttribute } from '../getSecureAttribute'; +import { requiresSameSiteNone } from './requireSameSiteNone'; export type DevBrowserCookieHandler = { set: (jwt: string) => void; @@ -13,7 +14,7 @@ export type DevBrowserCookieHandler = { }; const getCookieAttributes = () => { - const sameSite = inCrossOriginIframe() ? 'None' : 'Lax'; + const sameSite = inCrossOriginIframe() || requiresSameSiteNone() ? 'None' : 'Lax'; const secure = getSecureAttribute(sameSite); return { sameSite, secure } as const; }; diff --git a/packages/clerk-js/src/core/auth/cookies/requireSameSiteNone.ts b/packages/clerk-js/src/core/auth/cookies/requireSameSiteNone.ts new file mode 100644 index 00000000000..3abfe77fca8 --- /dev/null +++ b/packages/clerk-js/src/core/auth/cookies/requireSameSiteNone.ts @@ -0,0 +1,11 @@ +/** + * Certain hosting origins (e.g. .replit.dev) require cookies to be set with + * SameSite=None to work correctly, even outside of cross-origin iframe contexts. + */ +export const requiresSameSiteNone = (): boolean => { + try { + return window.location.hostname.endsWith('.replit.dev'); + } catch { + return false; + } +}; diff --git a/packages/clerk-js/src/core/auth/cookies/session.ts b/packages/clerk-js/src/core/auth/cookies/session.ts index e4362a2522d..754f2262544 100644 --- a/packages/clerk-js/src/core/auth/cookies/session.ts +++ b/packages/clerk-js/src/core/auth/cookies/session.ts @@ -4,6 +4,7 @@ import { inCrossOriginIframe } from '@clerk/shared/internal/clerk-js/runtime'; import { getSuffixedCookieName } from '@clerk/shared/keys'; import { getSecureAttribute } from '../getSecureAttribute'; +import { requiresSameSiteNone } from './requireSameSiteNone'; const SESSION_COOKIE_NAME = '__session'; @@ -14,7 +15,7 @@ export type SessionCookieHandler = { }; const getCookieAttributes = () => { - const sameSite = __BUILD_VARIANT_CHIPS__ ? 'None' : inCrossOriginIframe() ? 'None' : 'Lax'; + const sameSite = __BUILD_VARIANT_CHIPS__ ? 'None' : inCrossOriginIframe() || requiresSameSiteNone() ? 'None' : 'Lax'; const secure = getSecureAttribute(sameSite); const partitioned = __BUILD_VARIANT_CHIPS__ && secure; return { sameSite, secure, partitioned } as const; From 604c835f0d2cf04d30adf28a7a19e29d1593bf3e Mon Sep 17 00:00:00 2001 From: brkalow Date: Fri, 13 Feb 2026 13:10:11 -0600 Subject: [PATCH 2/5] fix(clerk-js): Consolidate third-party domain list in @clerk/shared Extract POPUP_PREFERRED_ORIGINS and requiresSameSiteNone into a shared THIRD_PARTY_COOKIE_DOMAINS list in @clerk/shared. Both originPrefersPopup and cookie SameSite logic now reference the same list, which includes .replit.dev. Co-Authored-By: Claude Opus 4.6 --- .changeset/fix-samesite-replit-dev.md | 4 ++- .../core/auth/cookies/requireSameSiteNone.ts | 12 +------- .../internal/clerk-js/thirdPartyDomains.ts | 30 +++++++++++++++++++ packages/ui/src/utils/originPrefersPopup.ts | 13 ++------ 4 files changed, 36 insertions(+), 23 deletions(-) create mode 100644 packages/shared/src/internal/clerk-js/thirdPartyDomains.ts diff --git a/.changeset/fix-samesite-replit-dev.md b/.changeset/fix-samesite-replit-dev.md index fb15cdac9a7..eed958a398a 100644 --- a/.changeset/fix-samesite-replit-dev.md +++ b/.changeset/fix-samesite-replit-dev.md @@ -1,5 +1,7 @@ --- +'@clerk/shared': patch '@clerk/clerk-js': patch +'@clerk/ui': patch --- -Set `SameSite=None` on cookies for `.replit.dev` origins +Set `SameSite=None` on cookies for `.replit.dev` origins and consolidate third-party domain list diff --git a/packages/clerk-js/src/core/auth/cookies/requireSameSiteNone.ts b/packages/clerk-js/src/core/auth/cookies/requireSameSiteNone.ts index 3abfe77fca8..7797f0b8b51 100644 --- a/packages/clerk-js/src/core/auth/cookies/requireSameSiteNone.ts +++ b/packages/clerk-js/src/core/auth/cookies/requireSameSiteNone.ts @@ -1,11 +1 @@ -/** - * Certain hosting origins (e.g. .replit.dev) require cookies to be set with - * SameSite=None to work correctly, even outside of cross-origin iframe contexts. - */ -export const requiresSameSiteNone = (): boolean => { - try { - return window.location.hostname.endsWith('.replit.dev'); - } catch { - return false; - } -}; +export { isThirdPartyCookieDomain as requiresSameSiteNone } from '@clerk/shared/internal/clerk-js/thirdPartyDomains'; diff --git a/packages/shared/src/internal/clerk-js/thirdPartyDomains.ts b/packages/shared/src/internal/clerk-js/thirdPartyDomains.ts new file mode 100644 index 00000000000..b61aa143641 --- /dev/null +++ b/packages/shared/src/internal/clerk-js/thirdPartyDomains.ts @@ -0,0 +1,30 @@ +/** + * Domains of third-party embedding platforms (e.g. online IDEs, preview environments) + * that require special handling for cookies and OAuth flows. + * + * These domains need: + * - `SameSite=None` on cookies to function correctly + * - Popup-based OAuth flows instead of redirects + */ +export const THIRD_PARTY_COOKIE_DOMAINS = [ + '.lovable.app', + '.lovableproject.com', + '.webcontainer-api.io', + '.vusercontent.net', + '.v0.dev', + '.v0.app', + '.lp.dev', + '.replit.dev', +]; + +/** + * Returns `true` if the current origin belongs to a known third-party + * embedding platform that requires `SameSite=None` on cookies. + */ +export function isThirdPartyCookieDomain(): boolean { + try { + return THIRD_PARTY_COOKIE_DOMAINS.some(domain => window.location.hostname.endsWith(domain)); + } catch { + return false; + } +} diff --git a/packages/ui/src/utils/originPrefersPopup.ts b/packages/ui/src/utils/originPrefersPopup.ts index 26c107cfd9f..ef6b47e6b07 100644 --- a/packages/ui/src/utils/originPrefersPopup.ts +++ b/packages/ui/src/utils/originPrefersPopup.ts @@ -1,14 +1,5 @@ import { inIframe } from '@clerk/shared/internal/clerk-js/runtime'; - -const POPUP_PREFERRED_ORIGINS = [ - '.lovable.app', - '.lovableproject.com', - '.webcontainer-api.io', - '.vusercontent.net', - '.v0.dev', - '.v0.app', - '.lp.dev', -]; +import { THIRD_PARTY_COOKIE_DOMAINS } from '@clerk/shared/internal/clerk-js/thirdPartyDomains'; /** * Returns `true` if the current origin is one that is typically embedded via an iframe, which would benefit from the @@ -16,5 +7,5 @@ const POPUP_PREFERRED_ORIGINS = [ * @returns {boolean} Whether the current origin prefers the popup flow. */ export function originPrefersPopup(): boolean { - return inIframe() || POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin)); + return inIframe() || THIRD_PARTY_COOKIE_DOMAINS.some(domain => window.location.origin.endsWith(domain)); } From 5d17dd8cebe1661d0de0ba500b402b3384f56044 Mon Sep 17 00:00:00 2001 From: brkalow Date: Fri, 13 Feb 2026 13:15:05 -0600 Subject: [PATCH 3/5] fix(clerk-js): Bump bundlewatch limit for clerk.legacy.browser.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 106KB → 107KB to accommodate the new thirdPartyDomains import. Co-Authored-By: Claude Opus 4.6 --- packages/clerk-js/bundlewatch.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index c694488eb56..18b7da9307e 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -3,7 +3,7 @@ { "path": "./dist/clerk.js", "maxSize": "539KB" }, { "path": "./dist/clerk.browser.js", "maxSize": "66KB" }, { "path": "./dist/clerk.chips.browser.js", "maxSize": "66KB" }, - { "path": "./dist/clerk.legacy.browser.js", "maxSize": "106KB" }, + { "path": "./dist/clerk.legacy.browser.js", "maxSize": "107KB" }, { "path": "./dist/clerk.no-rhc.js", "maxSize": "307KB" }, { "path": "./dist/clerk.native.js", "maxSize": "65KB" }, { "path": "./dist/vendors*.js", "maxSize": "7KB" }, From a31f2879266a3a644fe9ece27990be60bcda76f0 Mon Sep 17 00:00:00 2001 From: brkalow Date: Fri, 13 Feb 2026 13:16:40 -0600 Subject: [PATCH 4/5] fix(ui): Use hostname instead of origin for domain matching in originPrefersPopup window.location.origin includes protocol and port, causing domain suffix matching to fail. Switch to window.location.hostname to mirror isThirdPartyCookieDomain() behavior. Co-Authored-By: Claude Opus 4.6 --- .../utils/__tests__/originPrefersPopup.test.ts | 15 +++++++++++---- packages/ui/src/utils/originPrefersPopup.ts | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/utils/__tests__/originPrefersPopup.test.ts b/packages/ui/src/utils/__tests__/originPrefersPopup.test.ts index c80f250a245..3ecd0ddccc2 100644 --- a/packages/ui/src/utils/__tests__/originPrefersPopup.test.ts +++ b/packages/ui/src/utils/__tests__/originPrefersPopup.test.ts @@ -15,11 +15,18 @@ describe('originPrefersPopup', () => { // Store original location to restore after tests const originalLocation = window.location; - // Helper function to mock window.location.origin + // Helper function to mock window.location.hostname const mockLocationOrigin = (origin: string) => { + let hostname: string; + try { + hostname = new URL(origin).hostname; + } catch { + hostname = origin; + } Object.defineProperty(window, 'location', { value: { origin, + hostname, }, writable: true, configurable: true, @@ -176,12 +183,12 @@ describe('originPrefersPopup', () => { expect(originPrefersPopup()).toBe(false); }); - it('should be case sensitive', () => { + it('should be case insensitive (hostnames are normalized to lowercase)', () => { mockLocationOrigin('https://app.LOVABLE.APP'); - expect(originPrefersPopup()).toBe(false); + expect(originPrefersPopup()).toBe(true); mockLocationOrigin('https://APP.V0.DEV'); - expect(originPrefersPopup()).toBe(false); + expect(originPrefersPopup()).toBe(true); }); it('should handle malformed origins gracefully', () => { diff --git a/packages/ui/src/utils/originPrefersPopup.ts b/packages/ui/src/utils/originPrefersPopup.ts index ef6b47e6b07..a54a66432cc 100644 --- a/packages/ui/src/utils/originPrefersPopup.ts +++ b/packages/ui/src/utils/originPrefersPopup.ts @@ -7,5 +7,5 @@ import { THIRD_PARTY_COOKIE_DOMAINS } from '@clerk/shared/internal/clerk-js/thir * @returns {boolean} Whether the current origin prefers the popup flow. */ export function originPrefersPopup(): boolean { - return inIframe() || THIRD_PARTY_COOKIE_DOMAINS.some(domain => window.location.origin.endsWith(domain)); + return inIframe() || THIRD_PARTY_COOKIE_DOMAINS.some(domain => window.location.hostname.endsWith(domain)); } From 80dad189d4e97a8b7c239dccef2a739d435d8dca Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Fri, 13 Feb 2026 13:24:05 -0600 Subject: [PATCH 5/5] Apply suggestion from @brkalow --- .../clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts b/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts index c39824d298f..7e19bbc200e 100644 --- a/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts +++ b/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts @@ -129,7 +129,7 @@ describe('createClientUatCookie', () => { expect(result).toBe(0); }); - it('should set cookies with None sameSite on .replit.dev origins', () => { + it('should set cookies with SameSite=None when the host requires it', () => { (requiresSameSiteNone as ReturnType).mockReturnValue(true); const cookieHandler = createClientUatCookie(mockCookieSuffix); cookieHandler.set({