UNPKG

@clerk/backend

Version:

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

515 lines (504 loc) • 17.3 kB
"use strict"; 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