Skip to content

Commit

Permalink
refactor(backend): Refactor hasValidSignature to also work with PEM keys
Browse files Browse the repository at this point in the history
Also extracted some functionality to scr/tokens/jwt/algorithms so it can
be reused in the future and added some spec.
  • Loading branch information
Nikpolik committed Sep 26, 2023
1 parent 490e0e3 commit 09c4f1e
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 34 deletions.
2 changes: 1 addition & 1 deletion packages/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ import { decodeJwt } from '@clerk/backend';
decodeJwt(token);
```

#### hasValidSignature(jwt: Jwt, jwk: JsonWebKey)
#### hasValidSignature(jwt: Jwt, key: JsonWebKey | string)

Verifies that the JWT has a valid signature. The key needs to be provided.

Expand Down
33 changes: 33 additions & 0 deletions packages/backend/src/tokens/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,36 @@ export const mockPEMJwtKey =
'qpj5Cb+sxfFI+Vhf1GB1bNeOLPR10nkSMJ74HB0heHi/SsM83JiGekv0CpZPCC8j\n' +
'cQIDAQAB\n' +
'-----END PUBLIC KEY-----';

export const pemEncodedPublicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqYy3MY8fOneEyzNDu9lp
6iGXKkNUF+u/dUnrlkadZYyB35efzKFJEr9fftmWv5PUj1uRHTQ3bh6X1cceOYsI
jy008dHWZJsKhGOxgdTjeK91rjaklxt7tyFXEiKHIOr1LSgKzopClOfCIjxK/oPU
Of38pVh7WnekcSBQmU5fqA+EzKMi6k9VwvbzqKlZM4XQsiFyn28d9VubJWjTU8nN
ot0n1NE+9k6TxM8nglM4RwkBH4Ni4B0LhKKOOV+AG8tBNiZVil415dpBldmJ/j0w
k7Ad4VFi9en3Z17oCKr+K+zuT7vKMKSb1548dk0vnmi0vj2QGXSo+61wM5yQWpk6
sQIDAQAB
-----END PUBLIC KEY-----`;

export const someOtherPublicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5wdNEDm/HUAO6xarLs6e
cS0/J8GencMs5I6rYS825knb8jsNbfoukYfBiK81vQy1/eK5gdWrEprpXQrmIcwG
akdeUhybYlK68UhHNA5+TAmZ+ReLTJ2QDk5YU4I1NlRRq/bqtEhWsBDOCCkpVsC4
OLnUpsZKGUwpCrE/8stMSJ6Xx+TzBlDe21cV1j0gn5CWswrrXo7m8OIZ9xkRnNn4
fTNypMSCbx6BS7fgmer6Efx9HOu9UIKgXD/29q3pEpFXiHRdQRbVoAc9vEZl0QIw
PSNjILVJLKvb6MhKoQMyaP5k0c1rEkVJr9jQk5Z/6WPklCNK3oT5+gh2lgi7ZxBd
oQIDAQAB
-----END PUBLIC KEY-----`;

export const publicJwks = {
key_ops: ['verify'],
ext: true,
kty: 'RSA',
n: 'qYy3MY8fOneEyzNDu9lp6iGXKkNUF-u_dUnrlkadZYyB35efzKFJEr9fftmWv5PUj1uRHTQ3bh6X1cceOYsIjy008dHWZJsKhGOxgdTjeK91rjaklxt7tyFXEiKHIOr1LSgKzopClOfCIjxK_oPUOf38pVh7WnekcSBQmU5fqA-EzKMi6k9VwvbzqKlZM4XQsiFyn28d9VubJWjTU8nNot0n1NE-9k6TxM8nglM4RwkBH4Ni4B0LhKKOOV-AG8tBNiZVil415dpBldmJ_j0wk7Ad4VFi9en3Z17oCKr-K-zuT7vKMKSb1548dk0vnmi0vj2QGXSo-61wM5yQWpk6sQ',
e: 'AQAB',
alg: 'RS256',
};

// this jwt has be signe with the keys above. The payload is mockJwtPayload and the header is mockJwtHeader
export const signedJwt =
'eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL2FjY291bnRzLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsImV4cCI6MTY2NjY0ODMxMCwiaWF0IjoxNjY2NjQ4MjUwLCJpc3MiOiJodHRwczovL2NsZXJrLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsIm5iZiI6MTY2NjY0ODI0MCwic2lkIjoic2Vzc18yR2JEQjRlbk5kQ2E1dlMxenBDM1h6Zzl0SzkiLCJzdWIiOiJ1c2VyXzJHSXBYT0VwVnlKdzUxcmtabjlLbW5jNlN4ciJ9.j3rB92k32WqbQDkFB093H4GoQsBVLH4HLGF6ObcwUaVGiHC8SEu6T31FuPf257SL8A5sSGtWWM1fqhQpdLohgZb_hbJswGBuYI-Clxl9BtpIRHbWFZkLBIj8yS9W9aVtD3fWBbF6PHx7BY1udio-rbGWg1YAOZNtVcxF02p-MvX-8XIK92Vwu3Un5zyfCoVIg__qo3Xntzw3tznsZ4XDe212c6kVz1R_L1d5DKjeWXpjUPAS_zFeZSIJEQLf4JNr4JCY38tfdnc3ajfDA3p36saf1XwmTdWXQKCXi75c2TJAXROs3Pgqr5Kw_5clygoFuxN5OEMhFWFSnvIBdi3M6w';
65 changes: 65 additions & 0 deletions packages/backend/src/tokens/jwt/algorithms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import runtime from '../../runtime';
import isomorphicAtob from '../../shared/isomorphicAtob';

const algToHash: Record<string, string> = {
RS256: 'SHA-256',
RS384: 'SHA-384',
RS512: 'SHA-512',
};
const RSA_ALGORITHM_NAME = 'RSASSA-PKCS1-v1_5';

const jwksAlgToCryptoAlg: Record<string, string> = {
RS256: RSA_ALGORITHM_NAME,
RS384: RSA_ALGORITHM_NAME,
RS512: RSA_ALGORITHM_NAME,
};

const algs = Object.keys(algToHash);

// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#pkcs_8_import
function pemToBuffer(secret: string): ArrayBuffer {
const trimmed = secret
.replace(/-----BEGIN.*?-----/g, '')
.replace(/-----END.*?-----/g, '')
.replace(/\s/g, '');

const decoded = isomorphicAtob(trimmed);

const buffer = new ArrayBuffer(decoded.length);
const bufView = new Uint8Array(buffer);

for (let i = 0, strLen = decoded.length; i < strLen; i++) {
bufView[i] = decoded.charCodeAt(i);
}

return bufView;
}

export function getCryptoAlgorithm(algorithmName: string): RsaHashedImportParams {
const hash = algToHash[algorithmName];
const name = jwksAlgToCryptoAlg[algorithmName];

if (!hash || !name) {
throw new Error(`Unsupported algorithm ${algorithmName}, expected one of ${algs.join(', ')}`);
}

return {
hash: { name: algToHash[algorithmName] },
name: jwksAlgToCryptoAlg[algorithmName],
};
}

export function importKey(
secret: JsonWebKey | string,
algorithm: RsaHashedImportParams,
keyUsage: 'verify' | 'sign',
): Promise<CryptoKey> {
if (typeof secret === 'object') {
return runtime.crypto.subtle.importKey('jwk', secret, algorithm, false, [keyUsage]);
}

const keyData = pemToBuffer(secret);
const format = keyUsage === 'sign' ? 'pkcs8' : 'spki';

return runtime.crypto.subtle.importKey(format, keyData, algorithm, false, [keyUsage]);
}
23 changes: 23 additions & 0 deletions packages/backend/src/tokens/jwt/hasValidSignature.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type QUnit from 'qunit';

import { pemEncodedPublicKey, publicJwks, signedJwt, someOtherPublicKey } from '../fixtures';
import { hasValidSignature } from '../jwt/hasValidSignature';
import { decodeJwt } from './verifyJwt';

export default (QUnit: QUnit) => {
const { module, test } = QUnit;

module('hasValidSignature(jwt, key)', () => {
test('verifies the signature with a JWK formatted key', async assert => {
assert.true(await hasValidSignature(decodeJwt(signedJwt), publicJwks));
});

test('verifies the signature with a PEM formatted key', async assert => {
assert.true(await hasValidSignature(decodeJwt(signedJwt), pemEncodedPublicKey));
});

test('it returns false if the key is not correct', async assert => {
assert.false(await hasValidSignature(decodeJwt(signedJwt), someOtherPublicKey));
});
});
};
38 changes: 5 additions & 33 deletions packages/backend/src/tokens/jwt/hasValidSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,15 @@ import type { Jwt } from '@clerk/types';
// DO NOT CHANGE: Runtime needs to be imported as a default export so that we can stub its dependencies with Sinon.js
// For more information refer to https://sinonjs.org/how-to/stub-dependency/
import runtime from '../../runtime';
import { getCryptoAlgorithm, importKey } from './algorithms';

const algToHash: Record<string, string> = {
RS256: 'SHA-256',
RS384: 'SHA-384',
RS512: 'SHA-512',
ES256: 'SHA-256',
ES384: 'SHA-384',
ES512: 'SHA-512',
};

const RSA_ALGORITHM_NAME = 'RSASSA-PKCS1-v1_5';
const EC_ALGORITHM_NAME = 'ECDSA';

const jwksAlgToCryptoAlg: Record<string, string> = {
RS256: RSA_ALGORITHM_NAME,
RS384: RSA_ALGORITHM_NAME,
RS512: RSA_ALGORITHM_NAME,
ES256: EC_ALGORITHM_NAME,
ES384: EC_ALGORITHM_NAME,
ES512: EC_ALGORITHM_NAME,
};

export async function hasValidSignature(jwt: Jwt, jwk: JsonWebKey) {
export async function hasValidSignature(jwt: Jwt, key: JsonWebKey | string) {
const { header, signature, raw } = jwt;
const encoder = new TextEncoder();
const data = encoder.encode([raw.header, raw.payload].join('.'));
const algorithm = getCryptoAlgorithm(header.alg);

const cryptoKey = await runtime.crypto.subtle.importKey(
'jwk',
jwk,
{
name: jwksAlgToCryptoAlg[header.alg],
hash: algToHash[header.alg],
},
false,
['verify'],
);
const cryptoKey = await importKey(key, algorithm, 'verify');

return runtime.crypto.subtle.verify('RSASSA-PKCS1-v1_5', cryptoKey, signature, data);
return runtime.crypto.subtle.verify(algorithm.name, cryptoKey, signature, data);
}
2 changes: 2 additions & 0 deletions packages/backend/tests/suites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import keysTest from './dist/tokens/keys.test.js';
import pathTest from './dist/util/path.test.js';
import verifyTest from './dist/tokens/verify.test.js';
import verifyJwtTest from './dist/tokens/jwt/verifyJwt.test.js';
import hasValidSignatureTest from './dist/tokens/jwt/hasValidSignature.test.js';
import interstitialRequestTest from './dist/tokens/interstitial.test.js';

import jwtAssertionsTest from './dist/tokens/jwt/assertions.test.js';
Expand All @@ -29,6 +30,7 @@ const suites = [
verifyTest,
pathTest,
verifyJwtTest,
hasValidSignatureTest,
factoryTest,
redirectTest,
utilsTest,
Expand Down

0 comments on commit 09c4f1e

Please sign in to comment.