Skip to content

Commit

Permalink
chore(clerk-js,shared,nextjs): Support __clerk_db_jwt and `__dev_se…
Browse files Browse the repository at this point in the history
…ssion` 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
  • Loading branch information
dimkl committed Dec 21, 2023
1 parent a8feab7 commit df40705
Show file tree
Hide file tree
Showing 13 changed files with 104 additions and 124 deletions.
7 changes: 7 additions & 0 deletions .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
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/clerk.ts
Expand Up @@ -12,6 +12,7 @@ import {
noop,
parsePublishableKey,
proxyUrlToAbsoluteURL,
setDevBrowserJWTInURL,
stripScheme,
} from '@clerk/shared';
import type {
Expand Down Expand Up @@ -84,7 +85,6 @@ import {
removeClerkQueryParam,
requiresUserInput,
sessionExistsAndSingleSessionModeEnabled,
setDevBrowserJWTInURL,
stripOrigin,
stripSameOrigin,
toURL,
Expand Down
26 changes: 1 addition & 25 deletions 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,)', () => {
Expand Down
3 changes: 1 addition & 2 deletions 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);
53 changes: 4 additions & 49 deletions packages/clerk-js/src/utils/devBrowser.ts
@@ -1,64 +1,19 @@
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:
// 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 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] : '';
}
2 changes: 1 addition & 1 deletion 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';
Expand All @@ -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,
Expand Down
45 changes: 0 additions & 45 deletions packages/nextjs/src/server/devBrowser.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/shared/.gitignore
Expand Up @@ -4,6 +4,7 @@ color
cookie
date
deprecated
devBrowser
error
file
globs
Expand Down
1 change: 1 addition & 0 deletions packages/shared/package.json
Expand Up @@ -51,6 +51,7 @@
"cookie",
"date",
"deprecated",
"devBrowser",
"error",
"file",
"globs",
Expand Down
28 changes: 28 additions & 0 deletions 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,
);
},
);
});
56 changes: 56 additions & 0 deletions 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 || '';
}
3 changes: 2 additions & 1 deletion packages/shared/src/index.ts
Expand Up @@ -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';
Expand All @@ -27,3 +27,4 @@ export * from './poller';
export * from './proxy';
export * from './underscore';
export * from './url';
export { createWorkerTimers } from './workerTimers';
1 change: 1 addition & 0 deletions packages/shared/subpaths.mjs
Expand Up @@ -8,6 +8,7 @@ export const subpathNames = [
'cookie',
'date',
'deprecated',
'devBrowser',
'error',
'file',
'globs',
Expand Down

0 comments on commit df40705

Please sign in to comment.