@clerk/backend
Version:
Clerk Backend SDK - REST Client for Backend API & JWT verification utilities
394 lines (387 loc) • 13 kB
JavaScript
import {
TokenVerificationError,
TokenVerificationErrorAction,
TokenVerificationErrorReason
} from "./chunk-TCIXZLLW.mjs";
// src/runtime.ts
import { webcrypto as crypto } from "#crypto";
var globalFetch = fetch.bind(globalThis);
var runtime = {
crypto,
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/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, allowedTypes = "JWT") => {
if (typeof typ === "undefined") {
return;
}
const allowed = Array.isArray(allowedTypes) ? allowedTypes : [allowedTypes];
if (!allowed.includes(typ)) {
throw new TokenVerificationError({
action: TokenVerificationErrorAction.EnsureClerkJWT,
reason: TokenVerificationErrorReason.TokenInvalid,
message: `Invalid JWT type ${JSON.stringify(typ)}. Expected "${allowed.join(", ")}".`
});
}
};
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/cryptoKeys.ts
import { isomorphicAtob } from "@clerk/shared/isomorphicAtob";
function pemToBuffer(secret) {
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;
}
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/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, headerType } = 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, headerType);
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 };
}
export {
runtime,
base64url,
getCryptoAlgorithm,
assertHeaderType,
assertHeaderAlgorithm,
importKey,
hasValidSignature,
decodeJwt,
verifyJwt
};
//# sourceMappingURL=chunk-7X3P2E3X.mjs.map