From df40705d3fbb22b8b4d6fd8ee0a52b100146d88a Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Thu, 21 Dec 2023 11:16:20 +0200 Subject: [PATCH] chore(clerk-js,shared,nextjs): Support `__clerk_db_jwt` and `__dev_session` query params (#2428) * chore(clerk-js,shared,nextjs): Move `setDevBrowserJWTInURL` to shared * chore(shared): Support reading dev browser jwt from `__dev_session` and `__clerk_db_jwt` * chore(repo): Add changeset --- .changeset/chatty-insects-run.md | 7 +++ packages/clerk-js/src/core/clerk.ts | 2 +- .../src/utils/__tests__/devbrowser.test.ts | 26 +-------- .../clerk-js/src/utils/cookies/devBrowser.ts | 3 +- packages/clerk-js/src/utils/devBrowser.ts | 53 ++---------------- packages/nextjs/src/server/authMiddleware.ts | 2 +- packages/nextjs/src/server/devBrowser.ts | 45 --------------- packages/shared/.gitignore | 1 + packages/shared/package.json | 1 + .../shared/src/__tests__/devbrowser.test.ts | 28 ++++++++++ packages/shared/src/devBrowser.ts | 56 +++++++++++++++++++ packages/shared/src/index.ts | 3 +- packages/shared/subpaths.mjs | 1 + 13 files changed, 104 insertions(+), 124 deletions(-) create mode 100644 .changeset/chatty-insects-run.md delete mode 100644 packages/nextjs/src/server/devBrowser.ts create mode 100644 packages/shared/src/__tests__/devbrowser.test.ts create mode 100644 packages/shared/src/devBrowser.ts diff --git a/.changeset/chatty-insects-run.md b/.changeset/chatty-insects-run.md new file mode 100644 index 0000000000..d8260e7b6c --- /dev/null +++ b/.changeset/chatty-insects-run.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/nextjs': minor +'@clerk/shared': minor +--- + +Support reading from `__clerk_db_jwt` and `__dev_session` the dev browser jwt in development diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d37c267e25..a3f7f6f76f 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -12,6 +12,7 @@ import { noop, parsePublishableKey, proxyUrlToAbsoluteURL, + setDevBrowserJWTInURL, stripScheme, } from '@clerk/shared'; import type { @@ -84,7 +85,6 @@ import { removeClerkQueryParam, requiresUserInput, sessionExistsAndSingleSessionModeEnabled, - setDevBrowserJWTInURL, stripOrigin, stripSameOrigin, toURL, diff --git a/packages/clerk-js/src/utils/__tests__/devbrowser.test.ts b/packages/clerk-js/src/utils/__tests__/devbrowser.test.ts index 13a24e86be..f5acba8e5d 100644 --- a/packages/clerk-js/src/utils/__tests__/devbrowser.test.ts +++ b/packages/clerk-js/src/utils/__tests__/devbrowser.test.ts @@ -1,30 +1,6 @@ -import { getDevBrowserJWTFromURL, setDevBrowserJWTInURL } from '../devBrowser'; +import { getDevBrowserJWTFromURL } from '../devBrowser'; const DUMMY_URL_BASE = 'http://clerk-dummy'; - -describe('setDevBrowserJWTInURL(url, jwt)', () => { - const testCases: Array<[string, string, boolean, string]> = [ - ['', 'deadbeef', false, '#__clerk_db_jwt[deadbeef]'], - ['foo', 'deadbeef', false, 'foo#__clerk_db_jwt[deadbeef]'], - ['/foo', 'deadbeef', false, '/foo#__clerk_db_jwt[deadbeef]'], - ['#foo', 'deadbeef', false, '#foo__clerk_db_jwt[deadbeef]'], - ['/foo?bar=42#qux', 'deadbeef', false, '/foo?bar=42#qux__clerk_db_jwt[deadbeef]'], - ['/foo#__clerk_db_jwt[deadbeef]', 'deadbeef', false, '/foo#__clerk_db_jwt[deadbeef]'], - ['/foo?bar=42#qux__clerk_db_jwt[deadbeef]', 'deadbeef', false, '/foo?bar=42#qux__clerk_db_jwt[deadbeef]'], - ['/foo', 'deadbeef', true, '/foo?__dev_session=deadbeef'], - ['/foo?bar=42', 'deadbeef', true, '/foo?bar=42&__dev_session=deadbeef'], - ]; - - test.each(testCases)( - 'sets the dev browser JWT at the end of the provided url. Params: url=(%s), jwt=(%s), expected url=(%s)', - (input, paramName, asQueryParam, expected) => { - expect(setDevBrowserJWTInURL(new URL(input, DUMMY_URL_BASE), paramName, asQueryParam).href).toEqual( - new URL(expected, DUMMY_URL_BASE).href, - ); - }, - ); -}); - const oldHistory = globalThis.history; describe('getDevBrowserJWTFromURL(url,)', () => { diff --git a/packages/clerk-js/src/utils/cookies/devBrowser.ts b/packages/clerk-js/src/utils/cookies/devBrowser.ts index 849d2aef37..9153451f82 100644 --- a/packages/clerk-js/src/utils/cookies/devBrowser.ts +++ b/packages/clerk-js/src/utils/cookies/devBrowser.ts @@ -1,5 +1,4 @@ +import { DEV_BROWSER_JWT_MARKER } from '@clerk/shared'; import { createCookieHandler } from '@clerk/shared/cookie'; -import { DEV_BROWSER_JWT_MARKER } from '../devBrowser'; - export const devBrowserCookie = createCookieHandler(DEV_BROWSER_JWT_MARKER); diff --git a/packages/clerk-js/src/utils/devBrowser.ts b/packages/clerk-js/src/utils/devBrowser.ts index 8352c858b5..04fa38aedb 100644 --- a/packages/clerk-js/src/utils/devBrowser.ts +++ b/packages/clerk-js/src/utils/devBrowser.ts @@ -1,36 +1,4 @@ -import { DEV_BROWSER_SSO_JWT_PARAMETER } from '../core/constants'; - -export const DEV_BROWSER_JWT_MARKER = '__clerk_db_jwt'; -const DEV_BROWSER_JWT_MARKER_REGEXP = /__clerk_db_jwt\[(.*)\]/; - -// Sets the dev_browser JWT in the hash or the search -export function setDevBrowserJWTInURL(url: URL, jwt: string, asQueryParam: boolean): URL { - const resultURL = new URL(url); - - // extract & strip existing jwt from hash - const jwtFromHash = extractDevBrowserJWTFromHash(resultURL.hash); - resultURL.hash = resultURL.hash.replace(DEV_BROWSER_JWT_MARKER_REGEXP, ''); - if (resultURL.href.endsWith('#')) { - resultURL.hash = ''; - } - - // extract & strip existing jwt from search - const jwtFromSearch = resultURL.searchParams.get(DEV_BROWSER_SSO_JWT_PARAMETER); - resultURL.searchParams.delete(DEV_BROWSER_SSO_JWT_PARAMETER); - - // Existing jwt takes precedence - const jwtToSet = jwtFromHash || jwtFromSearch || jwt; - - if (jwtToSet) { - if (asQueryParam) { - resultURL.searchParams.append(DEV_BROWSER_SSO_JWT_PARAMETER, jwtToSet); - } else { - resultURL.hash = resultURL.hash + `${DEV_BROWSER_JWT_MARKER}[${jwtToSet}]`; - } - } - - return resultURL; -} +import { extractDevBrowserJWTFromURLHash, extractDevBrowserJWTFromURLSearchParams } from '@clerk/shared'; // Gets the dev_browser JWT from either the hash or the search // Side effect: @@ -38,27 +6,14 @@ export function setDevBrowserJWTInURL(url: URL, jwt: string, asQueryParam: boole export function getDevBrowserJWTFromURL(url: URL): string { const resultURL = new URL(url); - // extract & strip existing jwt from hash - const jwtFromHash = extractDevBrowserJWTFromHash(resultURL.hash); - resultURL.hash = resultURL.hash.replace(DEV_BROWSER_JWT_MARKER_REGEXP, ''); - if (resultURL.href.endsWith('#')) { - resultURL.hash = ''; - } - - // extract & strip existing jwt from search - const jwtFromSearch = resultURL.searchParams.get(DEV_BROWSER_SSO_JWT_PARAMETER) || ''; - resultURL.searchParams.delete(DEV_BROWSER_SSO_JWT_PARAMETER); + const jwtFromHash = extractDevBrowserJWTFromURLHash(resultURL); + const jwtFromSearch = extractDevBrowserJWTFromURLSearchParams(resultURL); const jwt = jwtFromHash || jwtFromSearch; - if (jwt && typeof globalThis.history !== undefined) { + if (jwt && typeof globalThis.history !== 'undefined') { globalThis.history.replaceState(null, '', resultURL.href); } return jwt; } - -function extractDevBrowserJWTFromHash(hash: string): string { - const matches = hash.match(DEV_BROWSER_JWT_MARKER_REGEXP); - return matches ? matches[1] : ''; -} diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index f823fa45fa..22b62b352c 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -1,6 +1,7 @@ /* eslint-disable turbo/no-undeclared-env-vars */ import type { AuthObject, RequestState } from '@clerk/backend'; import { buildRequestUrl, constants } from '@clerk/backend'; +import { DEV_BROWSER_JWT_MARKER, setDevBrowserJWTInURL } from '@clerk/shared/devBrowser'; import type Link from 'next/link'; import type { NextFetchEvent, NextMiddleware, NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; @@ -9,7 +10,6 @@ import { isRedirect, mergeResponses, paths, setHeader, stringifyHeaders } from ' import { withLogger } from '../utils/debugLogger'; import { authenticateRequest, handleInterstitialState, handleUnknownState } from './authenticateRequest'; import { SECRET_KEY } from './clerkClient'; -import { DEV_BROWSER_JWT_MARKER, setDevBrowserJWTInURL } from './devBrowser'; import { clockSkewDetected, infiniteRedirectLoopDetected, diff --git a/packages/nextjs/src/server/devBrowser.ts b/packages/nextjs/src/server/devBrowser.ts deleted file mode 100644 index 5a7074f8f7..0000000000 --- a/packages/nextjs/src/server/devBrowser.ts +++ /dev/null @@ -1,45 +0,0 @@ -// TODO: This is a partial duplicate of part of packages/clerk-js/src/utils/devBrowser.ts -// TODO: To be removed when we can extract this utility to @clerk/shared - -export const DEV_BROWSER_SSO_JWT_PARAMETER = '__dev_session'; - -// -// Below this line should be identical to clerk-js version -// - -export const DEV_BROWSER_JWT_MARKER = '__clerk_db_jwt'; -const DEV_BROWSER_JWT_MARKER_REGEXP = /__clerk_db_jwt\[(.*)\]/; - -// Sets the dev_browser JWT in the hash or the search -export function setDevBrowserJWTInURL(url: URL, jwt: string, asQueryParam: boolean): URL { - const resultURL = new URL(url); - - // extract & strip existing jwt from hash - const jwtFromHash = extractDevBrowserJWTFromHash(resultURL.hash); - resultURL.hash = resultURL.hash.replace(DEV_BROWSER_JWT_MARKER_REGEXP, ''); - if (resultURL.href.endsWith('#')) { - resultURL.hash = ''; - } - - // extract & strip existing jwt from search - const jwtFromSearch = resultURL.searchParams.get(DEV_BROWSER_SSO_JWT_PARAMETER); - resultURL.searchParams.delete(DEV_BROWSER_SSO_JWT_PARAMETER); - - // Existing jwt takes precedence - const jwtToSet = jwtFromHash || jwtFromSearch || jwt; - - if (jwtToSet) { - if (asQueryParam) { - resultURL.searchParams.append(DEV_BROWSER_SSO_JWT_PARAMETER, jwtToSet); - } else { - resultURL.hash = resultURL.hash + `${DEV_BROWSER_JWT_MARKER}[${jwtToSet}]`; - } - } - - return resultURL; -} - -function extractDevBrowserJWTFromHash(hash: string): string { - const matches = hash.match(DEV_BROWSER_JWT_MARKER_REGEXP); - return matches ? matches[1] : ''; -} diff --git a/packages/shared/.gitignore b/packages/shared/.gitignore index 45af1b0ba8..b31add20cf 100644 --- a/packages/shared/.gitignore +++ b/packages/shared/.gitignore @@ -4,6 +4,7 @@ color cookie date deprecated +devBrowser error file globs diff --git a/packages/shared/package.json b/packages/shared/package.json index 6671cd6a32..24588db789 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -51,6 +51,7 @@ "cookie", "date", "deprecated", + "devBrowser", "error", "file", "globs", diff --git a/packages/shared/src/__tests__/devbrowser.test.ts b/packages/shared/src/__tests__/devbrowser.test.ts new file mode 100644 index 0000000000..edf4995dbb --- /dev/null +++ b/packages/shared/src/__tests__/devbrowser.test.ts @@ -0,0 +1,28 @@ +import { setDevBrowserJWTInURL } from '../devBrowser'; + +const DUMMY_URL_BASE = 'http://clerk-dummy'; + +describe('setDevBrowserJWTInURL(url, jwt)', () => { + const testCases: Array<[string, string, boolean, string]> = [ + ['', 'deadbeef', false, '#__clerk_db_jwt[deadbeef]'], + ['foo', 'deadbeef', false, 'foo#__clerk_db_jwt[deadbeef]'], + ['/foo', 'deadbeef', false, '/foo#__clerk_db_jwt[deadbeef]'], + ['#foo', 'deadbeef', false, '#foo__clerk_db_jwt[deadbeef]'], + ['/foo?bar=42#qux', 'deadbeef', false, '/foo?bar=42#qux__clerk_db_jwt[deadbeef]'], + ['/foo#__clerk_db_jwt[deadbeef]', 'deadbeef', false, '/foo#__clerk_db_jwt[deadbeef]'], + ['/foo?bar=42#qux__clerk_db_jwt[deadbeef]', 'deadbeef', false, '/foo?bar=42#qux__clerk_db_jwt[deadbeef]'], + ['/foo', 'deadbeef', true, '/foo?__dev_session=deadbeef'], + ['/foo?bar=42', 'deadbeef', true, '/foo?bar=42&__dev_session=deadbeef'], + ['/foo?bar=42&__clerk_db_jwt=deadbeef', 'deadbeef', true, '/foo?bar=42&__dev_session=deadbeef'], + ['/foo?bar=42&__dev_session=deadbeef', 'deadbeef', true, '/foo?bar=42&__dev_session=deadbeef'], + ]; + + test.each(testCases)( + 'sets the dev browser JWT at the end of the provided url. Params: url=(%s), jwt=(%s), expected url=(%s)', + (input, paramName, asQueryParam, expected) => { + expect(setDevBrowserJWTInURL(new URL(input, DUMMY_URL_BASE), paramName, asQueryParam).href).toEqual( + new URL(expected, DUMMY_URL_BASE).href, + ); + }, + ); +}); diff --git a/packages/shared/src/devBrowser.ts b/packages/shared/src/devBrowser.ts new file mode 100644 index 0000000000..c3033fbf34 --- /dev/null +++ b/packages/shared/src/devBrowser.ts @@ -0,0 +1,56 @@ +export const DEV_BROWSER_SSO_JWT_PARAMETER = '__dev_session'; +export const DEV_BROWSER_JWT_MARKER = '__clerk_db_jwt'; +const DEV_BROWSER_JWT_MARKER_REGEXP = /__clerk_db_jwt\[(.*)\]/; + +// Sets the dev_browser JWT in the hash or the search +export function setDevBrowserJWTInURL(url: URL, jwt: string, asQueryParam: boolean): URL { + const resultURL = new URL(url); + + const jwtFromHash = extractDevBrowserJWTFromURLHash(resultURL); + const jwtFromSearch = extractDevBrowserJWTFromURLSearchParams(resultURL); + // Existing jwt takes precedence + const jwtToSet = jwtFromHash || jwtFromSearch || jwt; + + if (jwtToSet) { + if (asQueryParam) { + resultURL.searchParams.append(DEV_BROWSER_SSO_JWT_PARAMETER, jwtToSet); + } else { + resultURL.hash = resultURL.hash + `${DEV_BROWSER_JWT_MARKER}[${jwtToSet}]`; + } + } + + return resultURL; +} + +function extractDevBrowserJWTFromHash(hash: string): string { + const matches = hash.match(DEV_BROWSER_JWT_MARKER_REGEXP); + return matches ? matches[1] : ''; +} + +/** + * Extract & strip existing jwt from hash + * Side effect: Removes dev browser from the url hash + **/ +export function extractDevBrowserJWTFromURLHash(url: URL) { + const jwt = extractDevBrowserJWTFromHash(url.hash); + url.hash = url.hash.replace(DEV_BROWSER_JWT_MARKER_REGEXP, ''); + if (url.href.endsWith('#')) { + url.hash = ''; + } + + return jwt; +} + +/** + * Extract & strip existing jwt from search params + * Side effect: Removes dev browser from the search params + **/ +export function extractDevBrowserJWTFromURLSearchParams(url: URL) { + const jwtFromDevSession = url.searchParams.get(DEV_BROWSER_SSO_JWT_PARAMETER); + url.searchParams.delete(DEV_BROWSER_SSO_JWT_PARAMETER); + + const jwtFromClerkDbJwt = url.searchParams.get(DEV_BROWSER_JWT_MARKER); + url.searchParams.delete(DEV_BROWSER_JWT_MARKER); + + return jwtFromDevSession || jwtFromClerkDbJwt || ''; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index b5d8d07b76..ec0bd305c6 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -10,12 +10,12 @@ export * from './utils'; -export { createWorkerTimers } from './workerTimers'; export * from './browser'; export { callWithRetry } from './callWithRetry'; export * from './color'; export * from './date'; export * from './deprecated'; +export * from './devBrowser'; export * from './error'; export * from './file'; export { handleValueOrFn } from './handleValueOrFn'; @@ -27,3 +27,4 @@ export * from './poller'; export * from './proxy'; export * from './underscore'; export * from './url'; +export { createWorkerTimers } from './workerTimers'; diff --git a/packages/shared/subpaths.mjs b/packages/shared/subpaths.mjs index 6a1c22d8ec..8ae38c20c1 100644 --- a/packages/shared/subpaths.mjs +++ b/packages/shared/subpaths.mjs @@ -8,6 +8,7 @@ export const subpathNames = [ 'cookie', 'date', 'deprecated', + 'devBrowser', 'error', 'file', 'globs',