@clerk/backend
Version:
Clerk Backend SDK - REST Client for Backend API & JWT verification utilities
515 lines (504 loc) • 17.3 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/jwt/index.ts
var jwt_exports = {};
__export(jwt_exports, {
decodeJwt: () => decodeJwt2,
hasValidSignature: () => hasValidSignature2,
signJwt: () => signJwt2,
verifyJwt: () => verifyJwt2
});
module.exports = __toCommonJS(jwt_exports);
// src/jwt/legacyReturn.ts
function withLegacyReturn(cb) {
return async (...args) => {
const { data, errors } = await cb(...args);
if (errors) {
throw errors[0];
}
return data;
};
}
function withLegacySyncReturn(cb) {
return (...args) => {
const { data, errors } = cb(...args);
if (errors) {
throw errors[0];
}
return data;
};
}
// src/errors.ts
var TokenVerificationErrorReason = {
TokenExpired: "token-expired",
TokenInvalid: "token-invalid",
TokenInvalidAlgorithm: "token-invalid-algorithm",
TokenInvalidAuthorizedParties: "token-invalid-authorized-parties",
TokenInvalidSignature: "token-invalid-signature",
TokenNotActiveYet: "token-not-active-yet",
TokenIatInTheFuture: "token-iat-in-the-future",
TokenVerificationFailed: "token-verification-failed",
InvalidSecretKey: "secret-key-invalid",
LocalJWKMissing: "jwk-local-missing",
RemoteJWKFailedToLoad: "jwk-remote-failed-to-load",
RemoteJWKInvalid: "jwk-remote-invalid",
RemoteJWKMissing: "jwk-remote-missing",
JWKFailedToResolve: "jwk-failed-to-resolve",
JWKKidMismatch: "jwk-kid-mismatch"
};
var TokenVerificationErrorAction = {
ContactSupport: "Contact support@clerk.com",
EnsureClerkJWT: "Make sure that this is a valid Clerk generate JWT.",
SetClerkJWTKey: "Set the CLERK_JWT_KEY environment variable.",
SetClerkSecretKey: "Set the CLERK_SECRET_KEY environment variable.",
EnsureClockSync: "Make sure your system clock is in sync (e.g. turn off and on automatic time synchronization)."
};
var TokenVerificationError = class _TokenVerificationError extends Error {
constructor({
action,
message,
reason
}) {
super(message);
Object.setPrototypeOf(this, _TokenVerificationError.prototype);
this.reason = reason;
this.message = message;
this.action = action;
}
getFullMessage() {
return `${[this.message, this.action].filter((m) => m).join(" ")} (reason=${this.reason}, token-carrier=${this.tokenCarrier})`;
}
};
var SignJWTError = class extends Error {
};
// src/runtime.ts
var import_crypto = require("#crypto");
var globalFetch = fetch.bind(globalThis);
var runtime = {
crypto: import_crypto.webcrypto,
get fetch() {
return process.env.NODE_ENV === "test" ? fetch : globalFetch;
},
AbortController: globalThis.AbortController,
Blob: globalThis.Blob,
FormData: globalThis.FormData,
Headers: globalThis.Headers,
Request: globalThis.Request,
Response: globalThis.Response
};
// src/util/rfc4648.ts
var base64url = {
parse(string, opts) {
return parse(string, base64UrlEncoding, opts);
},
stringify(data, opts) {
return stringify(data, base64UrlEncoding, opts);
}
};
var base64UrlEncoding = {
chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
bits: 6
};
function parse(string, encoding, opts = {}) {
if (!encoding.codes) {
encoding.codes = {};
for (let i = 0; i < encoding.chars.length; ++i) {
encoding.codes[encoding.chars[i]] = i;
}
}
if (!opts.loose && string.length * encoding.bits & 7) {
throw new SyntaxError("Invalid padding");
}
let end = string.length;
while (string[end - 1] === "=") {
--end;
if (!opts.loose && !((string.length - end) * encoding.bits & 7)) {
throw new SyntaxError("Invalid padding");
}
}
const out = new (opts.out ?? Uint8Array)(end * encoding.bits / 8 | 0);
let bits = 0;
let buffer = 0;
let written = 0;
for (let i = 0; i < end; ++i) {
const value = encoding.codes[string[i]];
if (value === void 0) {
throw new SyntaxError("Invalid character " + string[i]);
}
buffer = buffer << encoding.bits | value;
bits += encoding.bits;
if (bits >= 8) {
bits -= 8;
out[written++] = 255 & buffer >> bits;
}
}
if (bits >= encoding.bits || 255 & buffer << 8 - bits) {
throw new SyntaxError("Unexpected end of data");
}
return out;
}
function stringify(data, encoding, opts = {}) {
const { pad = true } = opts;
const mask = (1 << encoding.bits) - 1;
let out = "";
let bits = 0;
let buffer = 0;
for (let i = 0; i < data.length; ++i) {
buffer = buffer << 8 | 255 & data[i];
bits += 8;
while (bits > encoding.bits) {
bits -= encoding.bits;
out += encoding.chars[mask & buffer >> bits];
}
}
if (bits) {
out += encoding.chars[mask & buffer << encoding.bits - bits];
}
if (pad) {
while (out.length * encoding.bits & 7) {
out += "=";
}
}
return out;
}
// src/jwt/algorithms.ts
var algToHash = {
RS256: "SHA-256",
RS384: "SHA-384",
RS512: "SHA-512"
};
var RSA_ALGORITHM_NAME = "RSASSA-PKCS1-v1_5";
var jwksAlgToCryptoAlg = {
RS256: RSA_ALGORITHM_NAME,
RS384: RSA_ALGORITHM_NAME,
RS512: RSA_ALGORITHM_NAME
};
var algs = Object.keys(algToHash);
function getCryptoAlgorithm(algorithmName) {
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]
};
}
// src/jwt/cryptoKeys.ts
var import_isomorphicAtob = require("@clerk/shared/isomorphicAtob");
function pemToBuffer(secret) {
const trimmed = secret.replace(/-----BEGIN.*?-----/g, "").replace(/-----END.*?-----/g, "").replace(/\s/g, "");
const decoded = (0, import_isomorphicAtob.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;
}
function importKey(key, algorithm, keyUsage) {
if (typeof key === "object") {
return runtime.crypto.subtle.importKey("jwk", key, algorithm, false, [keyUsage]);
}
const keyData = pemToBuffer(key);
const format = keyUsage === "sign" ? "pkcs8" : "spki";
return runtime.crypto.subtle.importKey(format, keyData, algorithm, false, [keyUsage]);
}
// src/jwt/signJwt.ts
function encodeJwtData(value) {
const stringified = JSON.stringify(value);
const encoder = new TextEncoder();
const encoded = encoder.encode(stringified);
return base64url.stringify(encoded, { pad: false });
}
async function signJwt(payload, key, options) {
if (!options.algorithm) {
throw new Error("No algorithm specified");
}
const encoder = new TextEncoder();
const algorithm = getCryptoAlgorithm(options.algorithm);
if (!algorithm) {
return {
errors: [new SignJWTError(`Unsupported algorithm ${options.algorithm}`)]
};
}
const cryptoKey = await importKey(key, algorithm, "sign");
const header = options.header || { typ: "JWT" };
header.alg = options.algorithm;
payload.iat = Math.floor(Date.now() / 1e3);
const encodedHeader = encodeJwtData(header);
const encodedPayload = encodeJwtData(payload);
const firstPart = `${encodedHeader}.${encodedPayload}`;
try {
const signature = await runtime.crypto.subtle.sign(algorithm, cryptoKey, encoder.encode(firstPart));
const encodedSignature = `${firstPart}.${base64url.stringify(new Uint8Array(signature), { pad: false })}`;
return { data: encodedSignature };
} catch (error) {
return { errors: [new SignJWTError(error?.message)] };
}
}
// src/jwt/assertions.ts
var isArrayString = (s) => {
return Array.isArray(s) && s.length > 0 && s.every((a) => typeof a === "string");
};
var assertAudienceClaim = (aud, audience) => {
const audienceList = [audience].flat().filter((a) => !!a);
const audList = [aud].flat().filter((a) => !!a);
const shouldVerifyAudience = audienceList.length > 0 && audList.length > 0;
if (!shouldVerifyAudience) {
return;
}
if (typeof aud === "string") {
if (!audienceList.includes(aud)) {
throw new TokenVerificationError({
action: TokenVerificationErrorAction.EnsureClerkJWT,
reason: TokenVerificationErrorReason.TokenVerificationFailed,
message: `Invalid JWT audience claim (aud) ${JSON.stringify(aud)}. Is not included in "${JSON.stringify(
audienceList
)}".`
});
}
} else if (isArrayString(aud)) {
if (!aud.some((a) => audienceList.includes(a))) {
throw new TokenVerificationError({
action: TokenVerificationErrorAction.EnsureClerkJWT,
reason: TokenVerificationErrorReason.TokenVerificationFailed,
message: `Invalid JWT audience claim array (aud) ${JSON.stringify(aud)}. Is not included in "${JSON.stringify(
audienceList
)}".`
});
}
}
};
var assertHeaderType = (typ) => {
if (typeof typ === "undefined") {
return;
}
if (typ !== "JWT") {
throw new TokenVerificationError({
action: TokenVerificationErrorAction.EnsureClerkJWT,
reason: TokenVerificationErrorReason.TokenInvalid,
message: `Invalid JWT type ${JSON.stringify(typ)}. Expected "JWT".`
});
}
};
var assertHeaderAlgorithm = (alg) => {
if (!algs.includes(alg)) {
throw new TokenVerificationError({
action: TokenVerificationErrorAction.EnsureClerkJWT,
reason: TokenVerificationErrorReason.TokenInvalidAlgorithm,
message: `Invalid JWT algorithm ${JSON.stringify(alg)}. Supported: ${algs}.`
});
}
};
var assertSubClaim = (sub) => {
if (typeof sub !== "string") {
throw new TokenVerificationError({
action: TokenVerificationErrorAction.EnsureClerkJWT,
reason: TokenVerificationErrorReason.TokenVerificationFailed,
message: `Subject claim (sub) is required and must be a string. Received ${JSON.stringify(sub)}.`
});
}
};
var assertAuthorizedPartiesClaim = (azp, authorizedParties) => {
if (!azp || !authorizedParties || authorizedParties.length === 0) {
return;
}
if (!authorizedParties.includes(azp)) {
throw new TokenVerificationError({
reason: TokenVerificationErrorReason.TokenInvalidAuthorizedParties,
message: `Invalid JWT Authorized party claim (azp) ${JSON.stringify(azp)}. Expected "${authorizedParties}".`
});
}
};
var assertExpirationClaim = (exp, clockSkewInMs) => {
if (typeof exp !== "number") {
throw new TokenVerificationError({
action: TokenVerificationErrorAction.EnsureClerkJWT,
reason: TokenVerificationErrorReason.TokenVerificationFailed,
message: `Invalid JWT expiry date claim (exp) ${JSON.stringify(exp)}. Expected number.`
});
}
const currentDate = new Date(Date.now());
const expiryDate = /* @__PURE__ */ new Date(0);
expiryDate.setUTCSeconds(exp);
const expired = expiryDate.getTime() <= currentDate.getTime() - clockSkewInMs;
if (expired) {
throw new TokenVerificationError({
reason: TokenVerificationErrorReason.TokenExpired,
message: `JWT is expired. Expiry date: ${expiryDate.toUTCString()}, Current date: ${currentDate.toUTCString()}.`
});
}
};
var assertActivationClaim = (nbf, clockSkewInMs) => {
if (typeof nbf === "undefined") {
return;
}
if (typeof nbf !== "number") {
throw new TokenVerificationError({
action: TokenVerificationErrorAction.EnsureClerkJWT,
reason: TokenVerificationErrorReason.TokenVerificationFailed,
message: `Invalid JWT not before date claim (nbf) ${JSON.stringify(nbf)}. Expected number.`
});
}
const currentDate = new Date(Date.now());
const notBeforeDate = /* @__PURE__ */ new Date(0);
notBeforeDate.setUTCSeconds(nbf);
const early = notBeforeDate.getTime() > currentDate.getTime() + clockSkewInMs;
if (early) {
throw new TokenVerificationError({
reason: TokenVerificationErrorReason.TokenNotActiveYet,
message: `JWT cannot be used prior to not before date claim (nbf). Not before date: ${notBeforeDate.toUTCString()}; Current date: ${currentDate.toUTCString()};`
});
}
};
var assertIssuedAtClaim = (iat, clockSkewInMs) => {
if (typeof iat === "undefined") {
return;
}
if (typeof iat !== "number") {
throw new TokenVerificationError({
action: TokenVerificationErrorAction.EnsureClerkJWT,
reason: TokenVerificationErrorReason.TokenVerificationFailed,
message: `Invalid JWT issued at date claim (iat) ${JSON.stringify(iat)}. Expected number.`
});
}
const currentDate = new Date(Date.now());
const issuedAtDate = /* @__PURE__ */ new Date(0);
issuedAtDate.setUTCSeconds(iat);
const postIssued = issuedAtDate.getTime() > currentDate.getTime() + clockSkewInMs;
if (postIssued) {
throw new TokenVerificationError({
reason: TokenVerificationErrorReason.TokenIatInTheFuture,
message: `JWT issued at date claim (iat) is in the future. Issued at date: ${issuedAtDate.toUTCString()}; Current date: ${currentDate.toUTCString()};`
});
}
};
// src/jwt/verifyJwt.ts
var DEFAULT_CLOCK_SKEW_IN_MS = 5 * 1e3;
async function hasValidSignature(jwt, key) {
const { header, signature, raw } = jwt;
const encoder = new TextEncoder();
const data = encoder.encode([raw.header, raw.payload].join("."));
const algorithm = getCryptoAlgorithm(header.alg);
try {
const cryptoKey = await importKey(key, algorithm, "verify");
const verified = await runtime.crypto.subtle.verify(algorithm.name, cryptoKey, signature, data);
return { data: verified };
} catch (error) {
return {
errors: [
new TokenVerificationError({
reason: TokenVerificationErrorReason.TokenInvalidSignature,
message: error?.message
})
]
};
}
}
function decodeJwt(token) {
const tokenParts = (token || "").toString().split(".");
if (tokenParts.length !== 3) {
return {
errors: [
new TokenVerificationError({
reason: TokenVerificationErrorReason.TokenInvalid,
message: `Invalid JWT form. A JWT consists of three parts separated by dots.`
})
]
};
}
const [rawHeader, rawPayload, rawSignature] = tokenParts;
const decoder = new TextDecoder();
const header = JSON.parse(decoder.decode(base64url.parse(rawHeader, { loose: true })));
const payload = JSON.parse(decoder.decode(base64url.parse(rawPayload, { loose: true })));
const signature = base64url.parse(rawSignature, { loose: true });
const data = {
header,
payload,
signature,
raw: {
header: rawHeader,
payload: rawPayload,
signature: rawSignature,
text: token
}
};
return { data };
}
async function verifyJwt(token, options) {
const { audience, authorizedParties, clockSkewInMs, key } = options;
const clockSkew = clockSkewInMs || DEFAULT_CLOCK_SKEW_IN_MS;
const { data: decoded, errors } = decodeJwt(token);
if (errors) {
return { errors };
}
const { header, payload } = decoded;
try {
const { typ, alg } = header;
assertHeaderType(typ);
assertHeaderAlgorithm(alg);
const { azp, sub, aud, iat, exp, nbf } = payload;
assertSubClaim(sub);
assertAudienceClaim([aud], [audience]);
assertAuthorizedPartiesClaim(azp, authorizedParties);
assertExpirationClaim(exp, clockSkew);
assertActivationClaim(nbf, clockSkew);
assertIssuedAtClaim(iat, clockSkew);
} catch (err) {
return { errors: [err] };
}
const { data: signatureValid, errors: signatureErrors } = await hasValidSignature(decoded, key);
if (signatureErrors) {
return {
errors: [
new TokenVerificationError({
action: TokenVerificationErrorAction.EnsureClerkJWT,
reason: TokenVerificationErrorReason.TokenVerificationFailed,
message: `Error verifying JWT signature. ${signatureErrors[0]}`
})
]
};
}
if (!signatureValid) {
return {
errors: [
new TokenVerificationError({
reason: TokenVerificationErrorReason.TokenInvalidSignature,
message: "JWT signature is invalid."
})
]
};
}
return { data: payload };
}
// src/jwt/index.ts
var verifyJwt2 = withLegacyReturn(verifyJwt);
var decodeJwt2 = withLegacySyncReturn(decodeJwt);
var signJwt2 = withLegacyReturn(signJwt);
var hasValidSignature2 = withLegacyReturn(hasValidSignature);
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
decodeJwt,
hasValidSignature,
signJwt,
verifyJwt
});
//# sourceMappingURL=index.js.map