Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
615 changes: 615 additions & 0 deletions handshake.test.ts

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions handshakeTestConfigs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// @ts-ignore ignore types
import * as jwt from 'jsonwebtoken';
// @ts-ignore ignore types
import * as uuid from 'uuid';

const rsaPairs = {
a: {
private: `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAlRVgJiQJ0nfuctIVSLnFJlAC76YPKly8Y5xrY36ADo472G1w
FpeiykQRyDdGOwrkJBEVmLpAybV4yTgQFpQ0A4YzeDlKKkOxBhCmuANZXluAm2MW
3ehNAm0svievMfKtG6UjjYz6v67U9Om/oMt1ehsOmR8MrDYvs3Wy+dxYpZaxyn6w
ajL7GkICHxc8cGsI/MBZr9jKtKyzFY++r8TQKAJwn9TcSQljRivomz1wQvjtdLnq
ZSLP3BFQB7e7DuM6SBsodIHhkVEVK2EaGVLOY+ifAITt7MqEcvast14AP0rICBSq
vbQQjZuwLgIrlJgqvJ4YBRfIaIx/qQzs0+eFtwIDAQABAoIBAD1H4xTqfWsZR1fF
SWBylDqSaxKNRPCZ3ApqEq58IjFZf/oPyiJPRGg2IMUXC3RbnrnAmAsGjHkdcj/s
HpjZZKQKNv/1NKo41vxyPcWoAsVJgYzd51liEr2rmNe1QkuawFN7xyh5Sd0fBYSC
zPVQjMKbep2waKolP+hZui8AxyORLtu6aUQawaCdWFyiyHtqEnlcb/YSTtGl/3W/
/LqYyv60dG0QdcAO37MAE42vp3R4GGcJelsFo/lxSKg+KiLn7NdsNr7bCJrqbVXz
93Fu5jgHQD9+BeVyvHJ/R2yg+utYEvIMiFvwX7z4MLh9PsWJbf9vbDNlw9ErWpf5
r1xUiqECgYEA/lLJP+qla0vd+ocYNe3ufOG4kaUFsrqRoChiS1JxwQr/WGTmV8sT
ZyTPwyxnsHtbzn4lwuI6CpAeyvd9O6G3FfTzUqsyPaknsGlymf8LEwL4AVo0BVY0
YGodRnDISBBU/yPQ2kvq6c72ouq5cQxWF45f8Z/Z+fFDjuHG6Q44hYcCgYEAlhD6
sm8wTWVklMAxOnhQJoseWQ1vcl6VCxRVv4QBiX9CQm/4ANVOWun4KRC5qqTdoNHc
RyuiWpZVgGblqUu4sWSQgi3CZyyLbHOJ9wTPTeo0oDVaFa9MMwS8rq35HXjpgREz
JtTRi6c9WVsjBygYiE5IYO0FGbEjI9qIiD5CClECgYA+wtVRRamu0dkk0yPhYycg
gF+Y6Z1/XtVDLdQb/GuAFSOwf63sanwOTyJKavHntnmQesb80fE63BgNRIgOKDlT
XNCTTRYn60+VFGCoqizkcy4av1TpID3qsSUqVfjG9+jR0dffly6Qpnds+vnqcP3p
8EOzEByttqFSaFs69jxyjwKBgFCQbQa+isACXy08wTESxnTq2zAT9nEANiPsltxq
kiivGXNxiUNpQNeuJHxnbkYenJ1qDUhoNJFNhDmbBFEPReh2hN5ekq+xSmi+3qKv
AlxiED6yZdqecdoyANoGrGcWMsYH5d5DAvxmnJkMRJHjBMiovlLK7KIOZz8oY4RB
aFMBAoGBAJ8UoGHwz7AdOLAldGY3c0HzaH1NaGrZoocSH19XI3ZYr8Qvl2XUww9G
UC1OG4e2PtZ8PINQGIYPacUaab5Yg1tOmxBoAx4gUkpgyjtSm2ZPd4EUVOdylU3E
aFa08+0FF7mqqJTgz5XlvHMrCcUTsJ9u+e05rr1G1PHsATuuMD9m
-----END RSA PRIVATE KEY-----`,
public: {
kty: 'RSA',
n: 'lRVgJiQJ0nfuctIVSLnFJlAC76YPKly8Y5xrY36ADo472G1wFpeiykQRyDdGOwrkJBEVmLpAybV4yTgQFpQ0A4YzeDlKKkOxBhCmuANZXluAm2MW3ehNAm0svievMfKtG6UjjYz6v67U9Om_oMt1ehsOmR8MrDYvs3Wy-dxYpZaxyn6wajL7GkICHxc8cGsI_MBZr9jKtKyzFY--r8TQKAJwn9TcSQljRivomz1wQvjtdLnqZSLP3BFQB7e7DuM6SBsodIHhkVEVK2EaGVLOY-ifAITt7MqEcvast14AP0rICBSqvbQQjZuwLgIrlJgqvJ4YBRfIaIx_qQzs0-eFtw',
e: 'AQAB',
},
},
b: {
private: `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAt9zSYl1hFhFKXvv8uJcT2X15iOqi1mTtxqVxNDnzPQSj1RSa
Jryjhkzpyd16c+PDo+FFtMgZTUv6Z2hr5QYMuAjlsM+apHmfE8MRMQRQHXNF0+sE
Bd1241W0mL7fId2ZChaGgufFOGFl2Obby56FH4Z86lCFi7Z4Ow7TBSpVSN598OKH
oVKwbYOVPKtmWBar0JeCPVpng4Ntx7kvuHGdFSoJ8z8+Uy5ybLlk1qSlQ5lsymfW
hxs0C9j7/x/h24n9jUbq51pzx2URcsEi0Wbuv26Ba0Q4v1ySl0I6IM5Xemwrzjo1
H6kz6IYldqPtTwkzhJSnJvJFzKJZn3hH9N+rGwIDAQABAoIBAGLGdx/xGp9IWrP8
nCBuyXMmPYyYwTJ8tmDpsI9mMo6tV3a5wrbc0NztpQuVuJtZ2VjJRTGB7lXgY336
UzyOq3aTERKT9Xg2/ocXXL0AnCm2K+VVdKvR9nTbLlKA+E6xRe5te4YIDaPkb1q/
a4VQfCQblDAtYhFUzfKsXCGCRJ8IPlhZxiA9RHfQTmQUSoBW+12IovyMdMxVLoPT
qjdnwL1TS3iARim+eV+buHW+8Drn8eldeSFoTJd0B9eRf7pMpRH/X8G7X0YYdDjF
ADWI770CQj45QQeuVsZYIuONIPmzai4nGiNQ85v+Yy0L47lYUp5XsDvwYO5tMCQK
v1og8cECgYEA7zIfBWM1AIY763FGJ6F5ym41A4i7WSRaWusfQx6fOdGTyHJ3hXu9
+1kQS97IKElJ1V7oK69dJGxq+aupsd/AaRJb4oVZCBSby4Fo6VeJoKyJSdNSCks6
tonT3hGUsJO1ER2ItWcgiCxgGY+vrK0rkacX/VgNZKGIjlGv8pQUpaUCgYEAxMeL
2jyx+5odGM51e/1G4Vn6t+ffNNC/03NbwZeJ93+0NPgdagIo1kErZQrFPEfoExOr
KMkwnAsnR/xfn4X21voK1pUc7VhzzoODb8l9LA9kB7efWtRZA79gcsbOH5wNkp9Z
i76AtaVU/p1grFKNcnes1lbFfcRUnO880g5dsb8CgYBacuuEEAWk0x2pZEYRCmCR
iacGVRfzF2oLY0mJCfVP2c42SAKmOSqX9w/QgMfTZBNFWgQVMNTZxx2Ul7Mtjdym
XsjcGWyXP6PCCodvZSin11Z60iv9tIDZMbkqCh/dvZ0EgdSGNB77HzyfrdPSShFl
nHfX1woJeYO3vW/5HMHJ+QKBgQCNema7pq3Ulq5a2n2vgp9GgJn5RXW+lGOG1Mbg
vmJMlv1qpAUJ5bmUqdBYWlEKkSxzIs4JifUwC/jXEcVyfS/GyommVBkzMEg672U9
pyEe34Xs4oFpHYlOX3cprnQeV+WOSJFqHrKNZuxgD6ik3MmjxhV3GXXugYzQNFWH
NRr6IwKBgH9aN5mY4fcVL76mMEVZ5BIHE+JpPMZ6OOamOHAiA5jrWRX4aRMICq3t
cKVfcj/M4dyBuRV5EW1y1m2QhRECFPSKpScykpD9nyCb+XqbMSLH+f+j1BGfLKWl
t5o8u/dlwJ1fGGday48gs/hA4V/F9zDjecNkYWUB/wUwVStqZljn
-----END RSA PRIVATE KEY-----`,
public: {
kty: 'RSA',
n: 't9zSYl1hFhFKXvv8uJcT2X15iOqi1mTtxqVxNDnzPQSj1RSaJryjhkzpyd16c-PDo-FFtMgZTUv6Z2hr5QYMuAjlsM-apHmfE8MRMQRQHXNF0-sEBd1241W0mL7fId2ZChaGgufFOGFl2Obby56FH4Z86lCFi7Z4Ow7TBSpVSN598OKHoVKwbYOVPKtmWBar0JeCPVpng4Ntx7kvuHGdFSoJ8z8-Uy5ybLlk1qSlQ5lsymfWhxs0C9j7_x_h24n9jUbq51pzx2URcsEi0Wbuv26Ba0Q4v1ySl0I6IM5Xemwrzjo1H6kz6IYldqPtTwkzhJSnJvJFzKJZn3hH9N-rGw',
e: 'AQAB',
},
},
};

const allConfigs: any = [];

export function generateConfig({ mode, matchedKeys = true }: { mode: 'test' | 'live'; matchedKeys?: boolean }) {
const ins_id = uuid.v4();
const pkHost = `clerk.${uuid.v4()}.com`;
const pk = `pk_${mode}_${btoa(`${pkHost}$`)}`;
const sk = `sk_${mode}_${uuid.v4()}`;
const rsa = matchedKeys
? rsaPairs.a
: {
private: rsaPairs.a.private,
public: rsaPairs.b.public,
};
const jwks = {
keys: [
{
...rsa.public,
kid: ins_id,
use: 'sig',
alg: 'RS256',
},
],
};

type Claims = {
sub: string;
iat: number;
exp: number;
nbf: number;
};
const generateToken = ({ state }: { state: 'active' | 'expired' | 'early' }) => {
let claims = { sub: 'user_12345' } as Claims;

const now = Math.floor(Date.now() / 1000);
if (state === 'active') {
claims.iat = now;
claims.nbf = now - 10;
claims.exp = now + 60;
} else if (state === 'expired') {
claims.iat = now - 600;
claims.nbf = now - 10 - 600;
claims.exp = now + 60 - 600;
} else if (state === 'early') {
claims.iat = now + 600;
claims.nbf = now - 10 + 600;
claims.exp = now + 60 + 600;
}
return {
token: jwt.sign(claims, rsa.private, {
algorithm: 'RS256',
header: { kid: ins_id },
}),
claims,
};
};
const config = Object.freeze({
pk,
sk,
generateToken,
jwks,
pkHost,
});
allConfigs.push(config);
return config;
}

export function getJwksFromSecretKey(sk: any) {
return allConfigs.find((x: any) => x.sk === sk)?.jwks;
}
5 changes: 5 additions & 0 deletions integration/templates/next-app-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"start": "next start"
},
"dependencies": {
"@clerk/backend": "file:.yalc/@clerk/backend",
"@clerk/clerk-react": "file:.yalc/@clerk/clerk-react",
"@clerk/nextjs": "file:.yalc/@clerk/nextjs",
"@clerk/shared": "file:.yalc/@clerk/shared",
"@clerk/types": "file:.yalc/@clerk/types",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this change will not work for tests running in our CICD and will break the integration tests. As far as i know we don't depend on yalc in the integration tests.
cc: @nikosdouvlis

"@types/node": "^18.17.0",
"@types/react": "18.2.14",
"@types/react-dom": "18.2.6",
Expand Down
19 changes: 19 additions & 0 deletions jest.config.handshake.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
extensionsToTreatAsEsm: ['.ts'],
testRegex: ['handshake.test.tsx?$'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
// '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
'^.+\\.tsx?$': [
'ts-jest',
{
diagnostics: false,
useESM: true,
},
],
},
};
12 changes: 1 addition & 11 deletions packages/backend/src/tokens/interstitialRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,7 @@ export async function runInterstitialRules<T extends InterstitialRuleOptions>(
}

async function verifyRequestState(options: InterstitialRuleOptions, token: string) {
const { isSatellite, proxyUrl } = options;
let issuer;
if (isSatellite) {
issuer = null;
} else if (proxyUrl) {
issuer = proxyUrl;
} else {
issuer = (iss: string) => iss.startsWith('https://clerk.') || iss.includes('.clerk.accounts');
}

return verifyToken(token, { ...options, issuer });
return verifyToken(token, { ...options });
}

/**
Expand Down
6 changes: 1 addition & 5 deletions packages/backend/src/tokens/jwt/verifyJwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import runtime from '../../runtime';
import { base64url } from '../../util/rfc4648';
import { TokenVerificationError, TokenVerificationErrorAction, TokenVerificationErrorReason } from '../errors';
import { getCryptoAlgorithm } from './algorithms';
import type { IssuerResolver } from './assertions';
import {
assertActivationClaim,
assertAudienceClaim,
Expand All @@ -15,7 +14,6 @@ import {
assertHeaderAlgorithm,
assertHeaderType,
assertIssuedAtClaim,
assertIssuerClaim,
assertSubClaim,
} from './assertions';
import { importKey } from './cryptoKeys';
Expand Down Expand Up @@ -82,13 +80,12 @@ export type VerifyJwtOptions = {
audience?: string | string[];
authorizedParties?: string[];
clockSkewInMs?: number;
issuer: IssuerResolver | string | null;
key: JsonWebKey | string;
};

export async function verifyJwt(
token: string,
{ audience, authorizedParties, clockSkewInMs, issuer, key }: VerifyJwtOptions,
{ audience, authorizedParties, clockSkewInMs, key }: VerifyJwtOptions,
): Promise<JwtPayload> {
const clockSkew = clockSkewInMs || DEFAULT_CLOCK_SKEW_IN_SECONDS;

Expand All @@ -108,7 +105,6 @@ export async function verifyJwt(
assertSubClaim(sub);
assertAudienceClaim([aud], [audience]);
assertAuthorizedPartiesClaim(azp, authorizedParties);
assertIssuerClaim(iss, issuer);
assertExpirationClaim(exp, clockSkew);
assertActivationClaim(nbf, clockSkew);
assertIssuedAtClaim(iat, clockSkew);
Expand Down
19 changes: 5 additions & 14 deletions packages/backend/src/tokens/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ export type LoadClerkJWKFromRemoteOptions = {
secretKey?: string;
apiUrl?: string;
apiVersion?: string;
issuer?: string;
};

/**
Expand All @@ -108,7 +107,6 @@ export type LoadClerkJWKFromRemoteOptions = {
* The cache lasts 1 hour by default.
*
* @param {Object} options
* @param {string} options.issuer - The issuer origin of the JWT
* @param {string} options.kid - The id of the key that the JWT was signed with
* @param {string} options.alg - The algorithm of the JWT
* @param {number} options.jwksCacheTtlInMs - The TTL of the jwks cache (defaults to 1 hour)
Expand All @@ -118,27 +116,20 @@ export async function loadClerkJWKFromRemote({
secretKey,
apiUrl = API_URL,
apiVersion = API_VERSION,
issuer,
kid,
jwksCacheTtlInMs = JWKS_CACHE_TTL_MS,
skipJwksCache,
}: LoadClerkJWKFromRemoteOptions): Promise<JsonWebKey> {
const shouldRefreshCache = !getFromCache(kid) && reachedMaxCacheUpdatedAt();
if (skipJwksCache || shouldRefreshCache) {
let fetcher;

if (secretKey) {
fetcher = () => fetchJWKSFromBAPI(apiUrl, secretKey, apiVersion);
} else if (issuer) {
fetcher = () => fetchJWKSFromFAPI(issuer);
} else {
const needsFetch = !getFromCache(kid) || cacheHasExpired();
if (skipJwksCache || needsFetch) {
if (!secretKey) {
throw new TokenVerificationError({
action: TokenVerificationErrorAction.ContactSupport,
message: 'Failed to load JWKS from Clerk Backend or Frontend API.',
reason: TokenVerificationErrorReason.RemoteJWKFailedToLoad,
});
}

const fetcher = () => fetchJWKSFromBAPI(apiUrl, secretKey, apiVersion);
const { keys } = await callWithRetry<{ keys: JsonWebKeyWithKid[] }>(fetcher);

if (!keys || !keys.length) {
Expand Down Expand Up @@ -231,6 +222,6 @@ async function fetchJWKSFromBAPI(apiUrl: string, key: string, apiVersion: string
return response.json();
}

function reachedMaxCacheUpdatedAt() {
function cacheHasExpired() {
return Date.now() - lastUpdatedAt >= MAX_CACHE_LAST_UPDATED_AT_SECONDS * 1000;
}
23 changes: 11 additions & 12 deletions packages/backend/src/tokens/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ export async function authenticateRequest(options: AuthenticateRequestOptions):
const pkFapi = pk?.frontendApi || '';
// determine proper FAPI url, taking into account multi-domain setups
const frontendApi = proxyUrl || (!isDevOrStagingUrl(pkFapi) ? addClerkPrefix(domain) : '') || pkFapi;
const frontendApiNoProtocol = frontendApi.replace(/http(s)?:\/\//, '');

const url = new URL(`https://${frontendApi}/v1/client/handshake`);
const url = new URL(`https://${frontendApiNoProtocol}/v1/client/handshake`);
url.searchParams.append('redirect_url', redirectUrl);

if (pk?.instanceType === 'development' && devBrowserToken) {
Expand All @@ -106,17 +107,7 @@ export async function authenticateRequest(options: AuthenticateRequestOptions):
}

async function verifyRequestState(options: InterstitialRuleOptions, token: string) {
const { isSatellite, proxyUrl } = options;
let issuer;
if (isSatellite) {
issuer = null;
} else if (proxyUrl) {
issuer = proxyUrl;
} else {
issuer = (iss: string) => iss.startsWith('https://clerk.') || iss.includes('.clerk.accounts');
}

return verifyToken(token, { ...options, issuer });
return verifyToken(token, options);
}

const clientUat = parseInt(ruleOptions.clientUat || '', 10) || 0;
Expand Down Expand Up @@ -196,6 +187,8 @@ export async function authenticateRequest(options: AuthenticateRequestOptions):
buildRedirectToHandshake({
publishableKey: ruleOptions.publishableKey!,
devBrowserToken: ruleOptions.devBrowserToken!,
proxyUrl: ruleOptions.proxyUrl,
domain: ruleOptions.domain,
redirectUrl: ruleOptions.request.url.toString(),
}),
);
Expand All @@ -211,6 +204,8 @@ export async function authenticateRequest(options: AuthenticateRequestOptions):
buildRedirectToHandshake({
publishableKey: ruleOptions.publishableKey!,
devBrowserToken: ruleOptions.devBrowserToken!,
proxyUrl: ruleOptions.proxyUrl,
domain: ruleOptions.domain,
redirectUrl: ruleOptions.request.url.toString(),
}),
);
Expand All @@ -228,6 +223,8 @@ export async function authenticateRequest(options: AuthenticateRequestOptions):
buildRedirectToHandshake({
publishableKey: ruleOptions.publishableKey!,
devBrowserToken: ruleOptions.devBrowserToken!,
proxyUrl: ruleOptions.proxyUrl,
domain: ruleOptions.domain,
redirectUrl: ruleOptions.request.url.toString(),
}),
);
Expand Down Expand Up @@ -257,6 +254,8 @@ export async function authenticateRequest(options: AuthenticateRequestOptions):
buildRedirectToHandshake({
publishableKey: ruleOptions.publishableKey!,
devBrowserToken: ruleOptions.devBrowserToken!,
proxyUrl: ruleOptions.proxyUrl,
domain: ruleOptions.domain,
redirectUrl: ruleOptions.request.url.toString(),
}),
);
Expand Down
15 changes: 3 additions & 12 deletions packages/backend/src/tokens/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,9 @@ import { loadClerkJWKFromLocal, loadClerkJWKFromRemote } from './keys';
/**
*
*/
export type VerifyTokenOptions = Pick<
VerifyJwtOptions,
'authorizedParties' | 'audience' | 'issuer' | 'clockSkewInMs'
> & { jwtKey?: string; proxyUrl?: string } & Pick<
LoadClerkJWKFromRemoteOptions,
'secretKey' | 'apiUrl' | 'apiVersion' | 'jwksCacheTtlInMs' | 'skipJwksCache'
>;
export type VerifyTokenOptions = Pick<VerifyJwtOptions, 'authorizedParties' | 'audience' | 'clockSkewInMs'> & {
jwtKey?: string;
} & Pick<LoadClerkJWKFromRemoteOptions, 'secretKey' | 'apiUrl' | 'apiVersion' | 'jwksCacheTtlInMs' | 'skipJwksCache'>;

export async function verifyToken(token: string, options: VerifyTokenOptions): Promise<JwtPayload> {
const {
Expand All @@ -25,7 +21,6 @@ export async function verifyToken(token: string, options: VerifyTokenOptions): P
audience,
authorizedParties,
clockSkewInMs,
issuer,
jwksCacheTtlInMs,
jwtKey,
skipJwksCache,
Expand All @@ -38,9 +33,6 @@ export async function verifyToken(token: string, options: VerifyTokenOptions): P

if (jwtKey) {
key = loadClerkJWKFromLocal(jwtKey);
} else if (typeof issuer === 'string') {
// Fetch JWKS from Frontend API if an issuer of type string has been provided
key = await loadClerkJWKFromRemote({ issuer, kid, jwksCacheTtlInMs, skipJwksCache });
} else if (secretKey) {
// Fetch JWKS from Backend API using the key
key = await loadClerkJWKFromRemote({ secretKey, apiUrl, apiVersion, kid, jwksCacheTtlInMs, skipJwksCache });
Expand All @@ -57,6 +49,5 @@ export async function verifyToken(token: string, options: VerifyTokenOptions): P
authorizedParties,
clockSkewInMs,
key,
issuer,
});
}
Loading