Skip to content

Commit

Permalink
feat(backend): Add new method for signing jwt tokens
Browse files Browse the repository at this point in the history
This method is marked as unstable since it will be only used for
internal purposes for now.
  • Loading branch information
Nikpolik committed Oct 6, 2023
1 parent 6b5277d commit 51b07ba
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-bags-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': patch
---

Added \_\_unstable\_\_signJwt
1 change: 1 addition & 0 deletions packages/backend/src/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default (QUnit: QUnit) => {
'Token',
'User',
'Verification',
'__unstable__signJwt',
'buildRequestUrl',
'constants',
'createAuthenticateRequest',
Expand Down
45 changes: 45 additions & 0 deletions packages/backend/src/tokens/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,35 @@ export const mockPEMJwtKey =
'cQIDAQAB\n' +
'-----END PUBLIC KEY-----';

export const pemEncodedSignKey = `-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCpjLcxjx86d4TL
M0O72WnqIZcqQ1QX6791SeuWRp1ljIHfl5/MoUkSv19+2Za/k9SPW5EdNDduHpfV
xx45iwiPLTTx0dZkmwqEY7GB1ON4r3WuNqSXG3u3IVcSIocg6vUtKArOikKU58Ii
PEr+g9Q5/fylWHtad6RxIFCZTl+oD4TMoyLqT1XC9vOoqVkzhdCyIXKfbx31W5sl
aNNTyc2i3SfU0T72TpPEzyeCUzhHCQEfg2LgHQuEoo45X4Aby0E2JlWKXjXl2kGV
2Yn+PTCTsB3hUWL16fdnXugIqv4r7O5Pu8owpJvXnjx2TS+eaLS+PZAZdKj7rXAz
nJBamTqxAgMBAAECggEAB/SNR+sCORkQhwRBwleiK5Ul5ZrBIFo0Yol0X1my2ufr
1BTmL5DFv/ZwwZ/t/dEu4QcX2PnxO959m087cNHANg+V8164I4JOzQVsd74Iako5
SFJSCLEGbgJHdpdeJcJAfLzrPOOp2hjBuB+CGU0QMSRkrVFogEcq1RACGB9gR59X
Kft9GC+iZowLUwwlUWpUpPK94ZIfxFflJdBSl9DPSjUq9lNPWhy2/qwjkDluKIG1
9p4gmRRNT1vSwBmwfq74jrB+rSYL6+IpmSw0PX41pSkuuNPQ0LgrtM7+9dr9tNVP
Wxc1HVZYj8r0FF3Yr5JFlHy9nxf/XMzQxNhZpaNRXQKBgQDirp04vgu3TB08UoKo
jovEzMT/jBwGkP1lV48MsjM9iiNZZ2mz01T4mvE70GwuMBwOwtjUuNPogoJT+i6I
dnaPCinr3JwMW1UUFSa/4b15nDsPxFZit1nficJXKMc0c5VxFn2Xpbcq6aeif/ny
a6bI1vh5N/CYIroZXqays4XbuwKBgQC/enY3H/clEVRGGytnqz/5JBncskCU0F/A
RsbYBPUg3tPZoBURTcLRPsCDWZKXCl2MLzP8h0ia1hMQiz88tsZl8PS5IYB4eEfy
iEpwuU7q4pNJDgiZzMIs7h7KlKJOGv56HCQfWW/9HUpyZA634IIN+TnCD5YCoNLo
IoqYoz++gwKBgFHZmwuSE8jrwuK1KFiUoAM/rSJZBQWZ9OVS6GQ9NCNUbc8qeBBm
jpf12oUujOFgncD2ujSVSG78MPMBsyuzGrwrf1ebIP2VPPMzb/p5GGGA+BKJYmfi
rKD6rSGrp8JYue1Loa3QOINWOyGB9E6EcIS0mqOqf0VvxKLEeoysJflhAoGAMPYp
gFMGKU5TFFIiOTIK+7QFgO97oBHgShRPCDHMVIll9oH+oRwXMtYu9+dRmpml7hCr
5GjbYexXl6VjmCzMcoi4qxYr+aIYE6ZSEpzv1xP0wXt7K4i2JjMFYJu9HOe+Jo9H
lVSTVE/HF5UKRm58EwKliD/gBfAFviIG+pzT0e0CgYBjVfmflnceTDiWGUqZB/6K
VemEqCD+L3Pf6FIGI0h2RfFcLowvyOC3qwINTrYXdNUHtLI1BDUkMYBv6YJdI4E/
EJa5L6umCqdlL4+iL3FKgnsVSkb7io8+n1XLF+qrbRjWpEDuSn8ICC+k/fea/mvj
5gTPwKCFpNesz5MP8D2kRg==
-----END PRIVATE KEY-----`;

export const pemEncodedPublicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqYy3MY8fOneEyzNDu9lp
6iGXKkNUF+u/dUnrlkadZYyB35efzKFJEr9fftmWv5PUj1uRHTQ3bh6X1cceOYsI
Expand All @@ -80,6 +109,22 @@ PSNjILVJLKvb6MhKoQMyaP5k0c1rEkVJr9jQk5Z/6WPklCNK3oT5+gh2lgi7ZxBd
oQIDAQAB
-----END PUBLIC KEY-----`;

// These should be identical to the pem encoded keys above
export const signingJwks = {
key_ops: ['sign'],
ext: true,
kty: 'RSA',
n: 'qYy3MY8fOneEyzNDu9lp6iGXKkNUF-u_dUnrlkadZYyB35efzKFJEr9fftmWv5PUj1uRHTQ3bh6X1cceOYsIjy008dHWZJsKhGOxgdTjeK91rjaklxt7tyFXEiKHIOr1LSgKzopClOfCIjxK_oPUOf38pVh7WnekcSBQmU5fqA-EzKMi6k9VwvbzqKlZM4XQsiFyn28d9VubJWjTU8nNot0n1NE-9k6TxM8nglM4RwkBH4Ni4B0LhKKOOV-AG8tBNiZVil415dpBldmJ_j0wk7Ad4VFi9en3Z17oCKr-K-zuT7vKMKSb1548dk0vnmi0vj2QGXSo-61wM5yQWpk6sQ',
e: 'AQAB',
d: 'B_SNR-sCORkQhwRBwleiK5Ul5ZrBIFo0Yol0X1my2ufr1BTmL5DFv_ZwwZ_t_dEu4QcX2PnxO959m087cNHANg-V8164I4JOzQVsd74Iako5SFJSCLEGbgJHdpdeJcJAfLzrPOOp2hjBuB-CGU0QMSRkrVFogEcq1RACGB9gR59XKft9GC-iZowLUwwlUWpUpPK94ZIfxFflJdBSl9DPSjUq9lNPWhy2_qwjkDluKIG19p4gmRRNT1vSwBmwfq74jrB-rSYL6-IpmSw0PX41pSkuuNPQ0LgrtM7-9dr9tNVPWxc1HVZYj8r0FF3Yr5JFlHy9nxf_XMzQxNhZpaNRXQ',
p: '4q6dOL4Lt0wdPFKCqI6LxMzE_4wcBpD9ZVePDLIzPYojWWdps9NU-JrxO9BsLjAcDsLY1LjT6IKCU_ouiHZ2jwop69ycDFtVFBUmv-G9eZw7D8RWYrdZ34nCVyjHNHOVcRZ9l6W3Kumnon_58mumyNb4eTfwmCK6GV6msrOF27s',
q: 'v3p2Nx_3JRFURhsrZ6s_-SQZ3LJAlNBfwEbG2AT1IN7T2aAVEU3C0T7Ag1mSlwpdjC8z_IdImtYTEIs_PLbGZfD0uSGAeHhH8ohKcLlO6uKTSQ4ImczCLO4eypSiThr-ehwkH1lv_R1KcmQOt-CCDfk5wg-WAqDS6CKKmKM_voM',
dp: 'UdmbC5ITyOvC4rUoWJSgAz-tIlkFBZn05VLoZD00I1Rtzyp4EGaOl_XahS6M4WCdwPa6NJVIbvww8wGzK7MavCt_V5sg_ZU88zNv-nkYYYD4EoliZ-KsoPqtIaunwli57UuhrdA4g1Y7IYH0ToRwhLSao6p_RW_EosR6jKwl-WE',
dq: 'MPYpgFMGKU5TFFIiOTIK-7QFgO97oBHgShRPCDHMVIll9oH-oRwXMtYu9-dRmpml7hCr5GjbYexXl6VjmCzMcoi4qxYr-aIYE6ZSEpzv1xP0wXt7K4i2JjMFYJu9HOe-Jo9HlVSTVE_HF5UKRm58EwKliD_gBfAFviIG-pzT0e0',
qi: 'Y1X5n5Z3Hkw4lhlKmQf-ilXphKgg_i9z3-hSBiNIdkXxXC6ML8jgt6sCDU62F3TVB7SyNQQ1JDGAb-mCXSOBPxCWuS-rpgqnZS-Poi9xSoJ7FUpG-4qPPp9Vyxfqq20Y1qRA7kp_CAgvpP33mv5r4-YEz8CghaTXrM-TD_A9pEY',
alg: 'RS256',
};

export const publicJwks = {
key_ops: ['verify'],
ext: true,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/tokens/jwt/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { hasValidSignature, decodeJwt, verifyJwt } from './verifyJwt';
export { __unstable__signJwt } from './signJwt';

export type { VerifyJwtOptions } from './verifyJwt';
export type { SignJwtOptions } from './signJwt';
36 changes: 36 additions & 0 deletions packages/backend/src/tokens/jwt/signJwt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type QUnit from 'qunit';

import {
mockJwtHeader,
mockJwtPayload,
pemEncodedPublicKey,
pemEncodedSignKey,
publicJwks,
signingJwks,
} from '../fixtures';
import { __unstable__signJwt } from './signJwt';
import { decodeJwt, hasValidSignature } from './verifyJwt';

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

module('signJwt(payload, options)', () => {
test('signs a JWT with a JWK formatted secret', async assert => {
const jwt = await __unstable__signJwt(mockJwtPayload, signingJwks, {
algorithm: mockJwtHeader.alg,
header: mockJwtHeader,
});

assert.true(await hasValidSignature(decodeJwt(jwt), publicJwks));
});

test('signs a JWT with a pkcs8 formatted secret', async assert => {
const jwt = await __unstable__signJwt(mockJwtPayload, pemEncodedSignKey, {
algorithm: mockJwtHeader.alg,
header: mockJwtHeader,
});

assert.true(await hasValidSignature(decodeJwt(jwt), pemEncodedPublicKey));
});
});
};
45 changes: 45 additions & 0 deletions packages/backend/src/tokens/jwt/signJwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import runtime from '../../runtime';
import { base64url } from '../../util/rfc4648';
import { getCryptoAlgorithm, importKey } from './algorithms';

export interface SignJwtOptions {
algorithm?: string;
header?: Record<string, unknown>;
}

function encodeJwtData(value: unknown): string {
const stringified = JSON.stringify(value);
const encoder = new TextEncoder();
const encoded = encoder.encode(stringified);
return base64url.stringify(encoded, { pad: false });
}

export async function __unstable__signJwt(
payload: Record<string, unknown>,
secret: string | JsonWebKey,
options: SignJwtOptions,
): Promise<string> {
if (!options.algorithm) {
throw new Error('No algorithm specified');
}
const encoder = new TextEncoder();

const algorithm = getCryptoAlgorithm(options.algorithm);
if (!algorithm) {
throw new Error(`Unsupported algorithm ${options.algorithm}`);
}

const cryptoKey = await importKey(secret, algorithm, 'sign');
const header = options.header || { typ: 'JWT' };

header.alg = options.algorithm;
payload.iat = Math.floor(Date.now() / 1000);

const encodedHeader = encodeJwtData(header);
const encodedPayload = encodeJwtData(payload);
const firstPart = `${encodedHeader}.${encodedPayload}`;

const signature = await runtime.crypto.subtle.sign(algorithm, cryptoKey, encoder.encode(firstPart));

return `${firstPart}.${base64url.stringify(new Uint8Array(signature), { pad: false })}`;
}
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 exportsTest from './dist/exports.test.js';
import redirectTest from './dist/redirections.test.js';
import interstitialRequestTest from './dist/tokens/interstitial.test.js';
import jwtAssertionsTest from './dist/tokens/jwt/assertions.test.js';
import signJwtTest from './dist/tokens/jwt/signJwt.test.js';
import verifyJwtTest from './dist/tokens/jwt/verifyJwt.test.js';
import keysTest from './dist/tokens/keys.test.js';
import requestTest from './dist/tokens/request.test.js';
Expand All @@ -25,6 +26,7 @@ const suites = [
verifyTest,
pathTest,
verifyJwtTest,
signJwtTest,
factoryTest,
redirectTest,
utilsTest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ exports[`/api public exports should not include a breaking change 1`] = `
"Token",
"User",
"Verification",
"__unstable__signJwt",
"allowlistIdentifiers",
"buildRequestUrl",
"clerkClient",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ exports[`/edge-middleware public exports should not include a breaking change 1`
"Token",
"User",
"Verification",
"__unstable__signJwt",
"allowlistIdentifiers",
"buildRequestUrl",
"clerkApi",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ exports[`/server public exports should not include a breaking change 1`] = `
"Token",
"User",
"Verification",
"__unstable__signJwt",
"auth",
"authMiddleware",
"buildClerkProps",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ exports[`module exports should not change unless explicitly set 1`] = `
"Token",
"User",
"Verification",
"__unstable__signJwt",
"allowlistIdentifiers",
"buildRequestUrl",
"clerkClient",
Expand Down

0 comments on commit 51b07ba

Please sign in to comment.