UNPKG

@clerk/backend

Version:

Clerk Backend SDK - REST Client for Backend API & JWT verification utilities

394 lines (387 loc) • 13 kB
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