diff --git a/.changeset/happy-points-relax.md b/.changeset/happy-points-relax.md new file mode 100644 index 0000000000..52e0486e72 --- /dev/null +++ b/.changeset/happy-points-relax.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +--- + +Update the debBrowser handling logic to remove hash-based devBrowser JWTs from the URL. Even if v5 does not use the hash-based JWT at all, we still need to remove it from the URL in case clerk-js is initialised on a page after a redirect from an older clerk-js version, such as an AccountPortal using the v4 components diff --git a/packages/clerk-js/src/core/devBrowser.ts b/packages/clerk-js/src/core/devBrowser.ts index 4bcfd77dc6..41c5d42dc9 100644 --- a/packages/clerk-js/src/core/devBrowser.ts +++ b/packages/clerk-js/src/core/devBrowser.ts @@ -1,4 +1,4 @@ -import { DEV_BROWSER_JWT_HEADER, getDevBrowserJWTFromURL, setDevBrowserJWTInURL } from '@clerk/shared/devBrowser'; +import { DEV_BROWSER_JWT_HEADER, extractDevBrowserJWTFromURL, setDevBrowserJWTInURL } from '@clerk/shared/devBrowser'; import { parseErrors } from '@clerk/shared/error'; import type { ClerkAPIErrorJSON } from '@clerk/types'; @@ -61,8 +61,8 @@ export function createDevBrowser({ frontendApi, fapiClient }: CreateDevBrowserOp } }); - // 1. Get the JWT from hash or search parameters when the redirection comes from AP - const devBrowserToken = getDevBrowserJWTFromURL(new URL(window.location.href)); + // 1. Get the JWT from search parameters when the redirection comes from AP + const devBrowserToken = extractDevBrowserJWTFromURL(new URL(window.location.href)); if (devBrowserToken) { setDevBrowserJWT(devBrowserToken); return; diff --git a/packages/shared/src/__tests__/devbrowser.test.ts b/packages/shared/src/__tests__/devbrowser.test.ts index 0a656eb872..2a1822f8d4 100644 --- a/packages/shared/src/__tests__/devbrowser.test.ts +++ b/packages/shared/src/__tests__/devbrowser.test.ts @@ -1,4 +1,4 @@ -import { getDevBrowserJWTFromURL, setDevBrowserJWTInURL } from '../devBrowser'; +import { extractDevBrowserJWTFromURL, setDevBrowserJWTInURL } from '../devBrowser'; const DUMMY_URL_BASE = 'http://clerk-dummy'; @@ -50,34 +50,29 @@ describe('getDevBrowserJWTFromURL(url)', () => { }); it('does not replaceState if the url does not contain a dev browser JWT', () => { - expect(getDevBrowserJWTFromURL(new URL('/foo', DUMMY_URL_BASE))).toEqual(''); + expect(extractDevBrowserJWTFromURL(new URL('/foo', DUMMY_URL_BASE))).toEqual(''); expect(replaceStateMock).not.toHaveBeenCalled(); }); - const testCases: Array<[string, string, null | string]> = [ - ['', '', null], - ['foo', '', null], - ['?__clerk_db_jwt=deadbeef', 'deadbeef', ''], - ['foo?__clerk_db_jwt=deadbeef', 'deadbeef', 'foo'], - ['/foo?__clerk_db_jwt=deadbeef', 'deadbeef', '/foo'], - ['?__clerk_db_jwt=deadbeef#foo', 'deadbeef', '#foo'], - [ - '/foo?bar=42&__clerk_db_jwt=deadbeef#qux__clerk_db_jwt[deadbeef2]', - 'deadbeef', - '/foo?bar=42#qux__clerk_db_jwt[deadbeef2]', - ], + it('does call replaceState if the url contains a dev browser JWT', () => { + expect(extractDevBrowserJWTFromURL(new URL('/foo?__clerk_db_jwt=token', DUMMY_URL_BASE))).toEqual('token'); + expect(replaceStateMock).toHaveBeenCalled(); + }); + + const testCases: Array<[string, string]> = [ + ['', ''], + ['foo', ''], + ['?__clerk_db_jwt=token', 'token'], + ['foo?__clerk_db_jwt=token', 'token'], + ['/foo?__clerk_db_jwt=token', 'token'], + ['?__clerk_db_jwt=token#foo', 'token'], + ['/foo?bar=42&__clerk_db_jwt=token#qux__clerk_db_jwt[token2]', 'token'], ]; test.each(testCases)( - 'returns the dev browser JWT from a url. Params: url=(%s), jwt=(%s)', - (input, jwt, calledWith) => { - expect(getDevBrowserJWTFromURL(new URL(input, DUMMY_URL_BASE))).toEqual(jwt); - - if (calledWith === null) { - expect(replaceStateMock).not.toHaveBeenCalled(); - } else { - expect(replaceStateMock).toHaveBeenCalledWith(null, '', new URL(calledWith, DUMMY_URL_BASE).href); - } + 'returns the dev browser JWT from a url and cleans all dev . Params: url=(%s), jwt=(%s)', + (input, jwt) => { + expect(extractDevBrowserJWTFromURL(new URL(input, DUMMY_URL_BASE))).toEqual(jwt); }, ); }); diff --git a/packages/shared/src/devBrowser.ts b/packages/shared/src/devBrowser.ts index cec2382a29..7e11c08fb0 100644 --- a/packages/shared/src/devBrowser.ts +++ b/packages/shared/src/devBrowser.ts @@ -19,20 +19,49 @@ export function setDevBrowserJWTInURL(url: URL, jwt: string): URL { return resultURL; } -// Gets the dev_browser JWT from either the hash or the search -// Side effect: -// Removes dev_browser JWT from the URL as a side effect and updates the browser history -export function getDevBrowserJWTFromURL(url: URL): string { - const resultURL = new URL(url); - - // extract & strip existing jwt from search - const jwt = resultURL.searchParams.get(DEV_BROWSER_JWT_KEY) || ''; - resultURL.searchParams.delete(DEV_BROWSER_JWT_KEY); - - // eslint-disable-next-line valid-typeof - if (jwt && typeof globalThis.history !== undefined) { - globalThis.history.replaceState(null, '', resultURL.href); +/** + * Gets the __clerk_db_jwt JWT from either the hash or the search + * Side effect: + * Removes __clerk_db_jwt JWT from the URL (hash and searchParams) and updates the browser history + */ +export function extractDevBrowserJWTFromURL(url: URL): string { + const jwt = readDevBrowserJwtFromSearchParams(url); + if (jwt && typeof globalThis.history !== 'undefined') { + globalThis.history.replaceState(null, '', removeDevBrowserJwt(url)); } - return jwt; } + +const readDevBrowserJwtFromSearchParams = (url: URL) => { + return url.searchParams.get(DEV_BROWSER_JWT_KEY) || ''; +}; + +const removeDevBrowserJwt = (url: URL) => { + return removeDevBrowserJwtFromURLSearchParams(removeLegacyDevBrowserJwtFromURLHash(new URL(url))); +}; + +const removeDevBrowserJwtFromURLSearchParams = (_url: URL) => { + const url = new URL(_url); + url.searchParams.delete(DEV_BROWSER_JWT_KEY); + return url; +}; + +/** + * Removes the __clerk_db_jwt JWT from the URL hash. + * We no longer need to use this value, however, we should remove it from the URL + * Existing v4 apps will write the JWT to the hash and the search params in order to ensure + * backwards compatibility with older v4 apps. + * The only use case where this is needed now is when a user upgrades to clerk@5 locally + * without changing the component's version on their dashboard. + * In this scenario, the AP@4 -> localhost@5 redirect will still have the JWT in the hash, + * in which case we need to remove it. + */ +const removeLegacyDevBrowserJwtFromURLHash = (_url: URL) => { + const DEV_BROWSER_JWT_MARKER_REGEXP = /__clerk_db_jwt\[(.*)\]/; + const url = new URL(_url); + url.hash = url.hash.replace(DEV_BROWSER_JWT_MARKER_REGEXP, ''); + if (url.href.endsWith('#')) { + url.hash = ''; + } + return url; +}; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 0a148aa790..c2d51b320c 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -31,4 +31,4 @@ export * from './underscore'; export * from './url'; export * from './object'; export { createWorkerTimers } from './workerTimers'; -export { DEV_BROWSER_JWT_KEY, getDevBrowserJWTFromURL, setDevBrowserJWTInURL } from './devBrowser'; +export { DEV_BROWSER_JWT_KEY, extractDevBrowserJWTFromURL, setDevBrowserJWTInURL } from './devBrowser';