diff --git a/.changeset/afraid-countries-smash.md b/.changeset/afraid-countries-smash.md new file mode 100644 index 0000000000..649af7084a --- /dev/null +++ b/.changeset/afraid-countries-smash.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Pass dev_browser to AP via query param, fix AP origin detection util diff --git a/packages/clerk-js/src/core/clerk.test.ts b/packages/clerk-js/src/core/clerk.test.ts index ce4ef203cd..bdeeece80c 100644 --- a/packages/clerk-js/src/core/clerk.test.ts +++ b/packages/clerk-js/src/core/clerk.test.ts @@ -1494,5 +1494,41 @@ describe('Clerk singleton', () => { const url = sut.buildUrlWithAuth('foo'); expect(url).toBe('foo'); }); + + it('uses the hash to propagate the dev_browser JWT by default on dev', async () => { + mockUsesUrlBasedSessionSync.mockReturnValue(true); + const sut = new Clerk(devFrontendApi); + await sut.load(); + + const url = sut.buildUrlWithAuth('https://example.com/some-path'); + expect(url).toBe('https://example.com/some-path#__clerk_db_jwt[deadbeef]'); + }); + + it('uses the query param to propagate the dev_browser JWT if specified by option on dev', async () => { + mockUsesUrlBasedSessionSync.mockReturnValue(true); + const sut = new Clerk(devFrontendApi); + await sut.load(); + + const url = sut.buildUrlWithAuth('https://example.com/some-path', { useQueryParam: true }); + expect(url).toBe('https://example.com/some-path?__dev_session=deadbeef'); + }); + + it('uses the query param to propagate the dev_browser JWT to Account Portal pages on dev - non-kima', async () => { + mockUsesUrlBasedSessionSync.mockReturnValue(true); + const sut = new Clerk(devFrontendApi); + await sut.load(); + + const url = sut.buildUrlWithAuth('https://accounts.abcef.12345.dev.lclclerk.com'); + expect(url).toBe('https://accounts.abcef.12345.dev.lclclerk.com/?__dev_session=deadbeef'); + }); + + it('uses the query param to propagate the dev_browser JWT to Account Portal pages on dev - kima', async () => { + mockUsesUrlBasedSessionSync.mockReturnValue(true); + const sut = new Clerk(devFrontendApi); + await sut.load(); + + const url = sut.buildUrlWithAuth('https://rested-anemone-14.accounts.dev'); + expect(url).toBe('https://rested-anemone-14.accounts.dev/?__dev_session=deadbeef'); + }); }); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index ed0f6e621d..d43b936e1d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -65,7 +65,7 @@ import { ignoreEventValue, inActiveBrowserTab, inBrowser, - isAccountsHostedPages, + isDevAccountPortalOrigin, isDevOrStagingUrl, isError, isRedirectForFAPIInitiatedFlow, @@ -668,7 +668,8 @@ export default class Clerk implements ClerkInterface { return clerkMissingDevBrowserJwt(); } - const asQueryParam = !!options?.useQueryParam; + // Use query param for Account Portal pages so that SSR can access the dev_browser JWT + const asQueryParam = !!options?.useQueryParam || isDevAccountPortalOrigin(toURL.hostname); return setDevBrowserJWTInURL(toURL.href, devBrowserJwt, asQueryParam); } @@ -1231,7 +1232,7 @@ export default class Clerk implements ClerkInterface { this.#authService = new SessionCookieService(this); this.#pageLifecycle = createPageLifecycle(); - const isInAccountsHostedPages = isAccountsHostedPages(window?.location.hostname); + const isInAccountsHostedPages = isDevAccountPortalOrigin(window?.location.hostname); this.#setupListeners(); diff --git a/packages/clerk-js/src/utils/__tests__/url.test.ts b/packages/clerk-js/src/utils/__tests__/url.test.ts index e864617163..76888279d8 100644 --- a/packages/clerk-js/src/utils/__tests__/url.test.ts +++ b/packages/clerk-js/src/utils/__tests__/url.test.ts @@ -8,9 +8,9 @@ import { getSearchParameterFromHash, hasBannedProtocol, hasExternalAccountSignUpError, - isAccountsHostedPages, isAllowedRedirectOrigin, isDataUri, + isDevAccountPortalOrigin, isRedirectForFAPIInitiatedFlow, isValidUrl, mergeFragmentIntoUrl, @@ -18,22 +18,22 @@ import { trimTrailingSlash, } from '../url'; -describe('isAccountsHostedPages(url)', () => { +describe('isDevAccountPortalOrigin(url)', () => { const goodUrls: Array<[string | URL, boolean]> = [ ['clerk.dev.lclclerk.com', false], ['clerk.prod.lclclerk.com', false], ['clerk.abc.efg.lclstage.dev', false], ['clerk.abc.efg.stgstage.dev', false], ['accounts.abc.efg.dev.lclclerk.com', true], - ['https://accounts.abc.efg.stg.lclclerk.com', true], - [new URL('https://clerk.abc.efg.lcl.dev'), false], - [new URL('https://accounts.abc.efg.lcl.dev'), true], - [new URL('https://accounts.abc.efg.stg.dev'), true], + ['rested-anemone-14.accounts.dev', true], + ['rested-anemone-14.accounts.dev.accountstage.dev', true], + ['rested-anemone-14.accounts.dev.accounts.lclclerk.com', true], + ['rested-anemone-14.clerk.accounts.dev', false], ]; - test.each(goodUrls)('.isAccountsHostedPages(%s)', (a, expected) => { + test.each(goodUrls)('.isDevAccountPortalOrigin(%s)', (a, expected) => { // @ts-ignore - expect(isAccountsHostedPages(a)).toBe(expected); + expect(isDevAccountPortalOrigin(a)).toBe(expected); }); }); diff --git a/packages/clerk-js/src/utils/url.ts b/packages/clerk-js/src/utils/url.ts index 21c97bb738..26c6c0561b 100644 --- a/packages/clerk-js/src/utils/url.ts +++ b/packages/clerk-js/src/utils/url.ts @@ -27,26 +27,53 @@ export const DEV_OR_STAGING_SUFFIXES = [ 'accounts.dev', ]; +export const LEGACY_DEV_SUFFIXES = ['.lcl.dev', '.lclstage.dev', '.lclclerk.com']; + +export const KIMA_DEV_SUFFIXES = ['.accounts.dev', '.accountstage.dev', '.accounts.lclclerk.com']; + const BANNED_URI_PROTOCOLS = ['javascript:'] as const; const { isDevOrStagingUrl } = createDevOrStagingUrlCache(); export { isDevOrStagingUrl }; -const accountsCache = new Map(); +const accountPortalCache = new Map(); -export function isAccountsHostedPages(url: string | URL = window.location.hostname): boolean { - if (!url) { +export function isDevAccountPortalOrigin(hostname: string = window.location.hostname): boolean { + if (!hostname) { return false; } - const hostname = typeof url === 'string' ? url : url.hostname; - let res = accountsCache.get(hostname); + let res = accountPortalCache.get(hostname); + if (res === undefined) { - res = DEV_OR_STAGING_SUFFIXES.some(s => /^(https?:\/\/)?accounts\./.test(hostname) && hostname.endsWith(s)); - accountsCache.set(hostname, res); + res = isLegacyDevAccountPortalOrigin(hostname) || isKimaDevAccountPortalOrigin(hostname); + accountPortalCache.set(hostname, res); } + return res; } +// Returns true for hosts such as: +// * accounts.foo.bar-13.lcl.dev +// * accounts.foo.bar-13.lclstage.dev +// * accounts.foo.bar-13.dev.lclclerk.com +function isLegacyDevAccountPortalOrigin(host: string): boolean { + return LEGACY_DEV_SUFFIXES.some(legacyDevSuffix => { + return host.startsWith('accounts.') && host.endsWith(legacyDevSuffix); + }); +} + +// Returns true for hosts such as: +// * foo-bar-13.accounts.dev +// * foo-bar-13.accountstage.dev +// * foo-bar-13.accounts.lclclerk.com +// But false for: +// * foo-bar-13.clerk.accounts.lclclerk.com +function isKimaDevAccountPortalOrigin(host: string): boolean { + return KIMA_DEV_SUFFIXES.some(kimaDevSuffix => { + return host.endsWith(kimaDevSuffix) && !host.endsWith('.clerk' + kimaDevSuffix); + }); +} + export function getETLDPlusOneFromFrontendApi(frontendApi: string): string { return frontendApi.replace('clerk.', ''); }