UNPKG

@gaonengwww/jose

Version:

JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes

541 lines (532 loc) 15.9 kB
// src/util/errors.ts var JOSEError = class extends Error { /** * A unique error code for the particular error subclass. * * @ignore */ static code = "ERR_JOSE_GENERIC"; /** A unique error code for {@link JOSEError}. */ code = "ERR_JOSE_GENERIC"; /** @ignore */ constructor(message, options) { super(message, options); this.name = this.constructor.name; Error.captureStackTrace?.(this, this.constructor); } }; var JOSENotSupported = class extends JOSEError { /** @ignore */ static code = "ERR_JOSE_NOT_SUPPORTED"; /** A unique error code for {@link JOSENotSupported}. */ code = "ERR_JOSE_NOT_SUPPORTED"; }; var JWKSInvalid = class extends JOSEError { /** @ignore */ static code = "ERR_JWKS_INVALID"; /** A unique error code for {@link JWKSInvalid}. */ code = "ERR_JWKS_INVALID"; }; var JWKSNoMatchingKey = class extends JOSEError { /** @ignore */ static code = "ERR_JWKS_NO_MATCHING_KEY"; /** A unique error code for {@link JWKSNoMatchingKey}. */ code = "ERR_JWKS_NO_MATCHING_KEY"; /** @ignore */ constructor(message = "no applicable key found in the JSON Web Key Set", options) { super(message, options); } }; var JWKSMultipleMatchingKeys = class extends JOSEError { /** @ignore */ [Symbol.asyncIterator]; /** @ignore */ static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; /** A unique error code for {@link JWKSMultipleMatchingKeys}. */ code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; /** @ignore */ constructor(message = "multiple matching keys found in the JSON Web Key Set", options) { super(message, options); } }; var JWKSTimeout = class extends JOSEError { /** @ignore */ static code = "ERR_JWKS_TIMEOUT"; /** A unique error code for {@link JWKSTimeout}. */ code = "ERR_JWKS_TIMEOUT"; /** @ignore */ constructor(message = "request timed out", options) { super(message, options); } }; // src/lib/buffer_utils.ts var encoder = new TextEncoder(); var decoder = new TextDecoder(); var MAX_INT32 = 2 ** 32; // src/lib/base64.ts function decodeBase64(encoded) { if (Uint8Array.fromBase64) { return Uint8Array.fromBase64(encoded); } const binary = atob(encoded); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; } // src/util/base64url.ts function decode(input) { if (Uint8Array.fromBase64) { return Uint8Array.fromBase64(typeof input === "string" ? input : decoder.decode(input), { alphabet: "base64url" }); } let encoded = input; if (encoded instanceof Uint8Array) { encoded = decoder.decode(encoded); } encoded = encoded.replace(/-/g, "+").replace(/_/g, "/").replace(/\s/g, ""); try { return decodeBase64(encoded); } catch { throw new TypeError("The input to be decoded is not correctly encoded."); } } // src/lib/jwk_to_key.ts function subtleMapping(jwk) { let algorithm; let keyUsages; switch (jwk.kty) { case "RSA": { switch (jwk.alg) { case "PS256": case "PS384": case "PS512": algorithm = { name: "RSA-PSS", hash: `SHA-${jwk.alg.slice(-3)}` }; keyUsages = jwk.d ? ["sign"] : ["verify"]; break; case "RS256": case "RS384": case "RS512": algorithm = { name: "RSASSA-PKCS1-v1_5", hash: `SHA-${jwk.alg.slice(-3)}` }; keyUsages = jwk.d ? ["sign"] : ["verify"]; break; case "RSA-OAEP": case "RSA-OAEP-256": case "RSA-OAEP-384": case "RSA-OAEP-512": algorithm = { name: "RSA-OAEP", hash: `SHA-${parseInt(jwk.alg.slice(-3), 10) || 1}` }; keyUsages = jwk.d ? ["decrypt", "unwrapKey"] : ["encrypt", "wrapKey"]; break; default: throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); } break; } case "EC": { switch (jwk.alg) { case "ES256": algorithm = { name: "ECDSA", namedCurve: "P-256" }; keyUsages = jwk.d ? ["sign"] : ["verify"]; break; case "ES384": algorithm = { name: "ECDSA", namedCurve: "P-384" }; keyUsages = jwk.d ? ["sign"] : ["verify"]; break; case "ES512": algorithm = { name: "ECDSA", namedCurve: "P-521" }; keyUsages = jwk.d ? ["sign"] : ["verify"]; break; case "ECDH-ES": case "ECDH-ES+A128KW": case "ECDH-ES+A192KW": case "ECDH-ES+A256KW": algorithm = { name: "ECDH", namedCurve: jwk.crv }; keyUsages = jwk.d ? ["deriveBits"] : []; break; default: throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); } break; } case "OKP": { switch (jwk.alg) { case "Ed25519": // Fall through case "EdDSA": algorithm = { name: "Ed25519" }; keyUsages = jwk.d ? ["sign"] : ["verify"]; break; case "ECDH-ES": case "ECDH-ES+A128KW": case "ECDH-ES+A192KW": case "ECDH-ES+A256KW": algorithm = { name: jwk.crv }; keyUsages = jwk.d ? ["deriveBits"] : []; break; default: throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); } break; } default: throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value'); } return { algorithm, keyUsages }; } var jwk_to_key_default = async (jwk) => { if (!jwk.alg) { throw new TypeError('"alg" argument is required when "jwk.alg" is not present'); } const { algorithm, keyUsages } = subtleMapping(jwk); const keyData = { ...jwk }; delete keyData.alg; delete keyData.use; return crypto.subtle.importKey( "jwk", keyData, algorithm, jwk.ext ?? (jwk.d ? false : true), jwk.key_ops ?? keyUsages ); }; // src/lib/is_object.ts function isObjectLike(value) { return typeof value === "object" && value !== null; } var is_object_default = (input) => { if (!isObjectLike(input) || Object.prototype.toString.call(input) !== "[object Object]") { return false; } if (Object.getPrototypeOf(input) === null) { return true; } let proto = input; while (Object.getPrototypeOf(proto) !== null) { proto = Object.getPrototypeOf(proto); } return Object.getPrototypeOf(input) === proto; }; // src/key/import.ts async function importJWK(jwk, alg, options) { if (!is_object_default(jwk)) { throw new TypeError("JWK must be an object"); } let ext; alg ??= jwk.alg; ext ??= options?.extractable ?? jwk.ext; switch (jwk.kty) { case "oct": if (typeof jwk.k !== "string" || !jwk.k) { throw new TypeError('missing "k" (Key Value) Parameter value'); } return decode(jwk.k); case "RSA": if ("oth" in jwk && jwk.oth !== void 0) { throw new JOSENotSupported( 'RSA JWK "oth" (Other Primes Info) Parameter value is not supported' ); } case "EC": case "OKP": return jwk_to_key_default({ ...jwk, alg, ext }); default: throw new JOSENotSupported('Unsupported "kty" (Key Type) Parameter value'); } } // src/jwks/local.ts function getKtyFromAlg(alg) { switch (typeof alg === "string" && alg.slice(0, 2)) { case "RS": case "PS": return "RSA"; case "ES": return "EC"; case "Ed": return "OKP"; default: throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set'); } } function isJWKSLike(jwks) { return jwks && typeof jwks === "object" && // @ts-expect-error Array.isArray(jwks.keys) && // @ts-expect-error jwks.keys.every(isJWKLike); } function isJWKLike(key) { return is_object_default(key); } var LocalJWKSet = class { #jwks; #cached = /* @__PURE__ */ new WeakMap(); constructor(jwks) { if (!isJWKSLike(jwks)) { throw new JWKSInvalid("JSON Web Key Set malformed"); } this.#jwks = structuredClone(jwks); } jwks() { return this.#jwks; } async getKey(protectedHeader, token) { const { alg, kid } = { ...protectedHeader, ...token?.header }; const kty = getKtyFromAlg(alg); const candidates = this.#jwks.keys.filter((jwk2) => { let candidate = kty === jwk2.kty; if (candidate && typeof kid === "string") { candidate = kid === jwk2.kid; } if (candidate && typeof jwk2.alg === "string") { candidate = alg === jwk2.alg; } if (candidate && typeof jwk2.use === "string") { candidate = jwk2.use === "sig"; } if (candidate && Array.isArray(jwk2.key_ops)) { candidate = jwk2.key_ops.includes("verify"); } if (candidate) { switch (alg) { case "ES256": candidate = jwk2.crv === "P-256"; break; case "ES384": candidate = jwk2.crv === "P-384"; break; case "ES512": candidate = jwk2.crv === "P-521"; break; case "Ed25519": // Fall through case "EdDSA": candidate = jwk2.crv === "Ed25519"; break; } } return candidate; }); const { 0: jwk, length } = candidates; if (length === 0) { throw new JWKSNoMatchingKey(); } if (length !== 1) { const error = new JWKSMultipleMatchingKeys(); const _cached = this.#cached; error[Symbol.asyncIterator] = async function* () { for (const jwk2 of candidates) { try { yield await importWithAlgCache(_cached, jwk2, alg); } catch { } } }; throw error; } return importWithAlgCache(this.#cached, jwk, alg); } }; async function importWithAlgCache(cache, jwk, alg) { const cached = cache.get(jwk) || cache.set(jwk, {}).get(jwk); if (cached[alg] === void 0) { const key = await importJWK({ ...jwk, ext: true }, alg); if (key instanceof Uint8Array || key.type !== "public") { throw new JWKSInvalid("JSON Web Key Set members must be public keys"); } cached[alg] = key; } return cached[alg]; } function createLocalJWKSet(jwks) { const set = new LocalJWKSet(jwks); const localJWKSet = async (protectedHeader, token) => set.getKey(protectedHeader, token); Object.defineProperties(localJWKSet, { jwks: { value: () => structuredClone(set.jwks()), enumerable: false, configurable: false, writable: false } }); return localJWKSet; } // src/jwks/remote.ts function isCloudflareWorkers() { return ( // @ts-ignore typeof WebSocketPair !== "undefined" || // @ts-ignore typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers" || // @ts-ignore typeof EdgeRuntime !== "undefined" && EdgeRuntime === "vercel" ); } var USER_AGENT; if (typeof navigator === "undefined" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) { const NAME = "jose"; const VERSION = "v6.0.9"; USER_AGENT = `${NAME}/${VERSION}`; } var customFetch = Symbol(); async function fetchJwks(url, headers, signal, fetchImpl = fetch) { const response = await fetchImpl(url, { method: "GET", signal, redirect: "manual", headers }).catch((err) => { if (err.name === "TimeoutError") { throw new JWKSTimeout(); } throw err; }); if (response.status !== 200) { throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response"); } try { return await response.json(); } catch { throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON"); } } var jwksCache = Symbol(); function isFreshJwksCache(input, cacheMaxAge) { if (typeof input !== "object" || input === null) { return false; } if (!("uat" in input) || typeof input.uat !== "number" || Date.now() - input.uat >= cacheMaxAge) { return false; } if (!("jwks" in input) || !is_object_default(input.jwks) || !Array.isArray(input.jwks.keys) || !Array.prototype.every.call(input.jwks.keys, is_object_default)) { return false; } return true; } var RemoteJWKSet = class { #url; #timeoutDuration; #cooldownDuration; #cacheMaxAge; #jwksTimestamp; #pendingFetch; #headers; #customFetch; #local; #cache; constructor(url, options) { if (!(url instanceof URL)) { throw new TypeError("url must be an instance of URL"); } this.#url = new URL(url.href); this.#timeoutDuration = typeof options?.timeoutDuration === "number" ? options?.timeoutDuration : 5e3; this.#cooldownDuration = typeof options?.cooldownDuration === "number" ? options?.cooldownDuration : 3e4; this.#cacheMaxAge = typeof options?.cacheMaxAge === "number" ? options?.cacheMaxAge : 6e5; this.#headers = new Headers(options?.headers); if (USER_AGENT && !this.#headers.has("User-Agent")) { this.#headers.set("User-Agent", USER_AGENT); } if (!this.#headers.has("accept")) { this.#headers.set("accept", "application/json"); this.#headers.append("accept", "application/jwk-set+json"); } this.#customFetch = options?.[customFetch]; if (options?.[jwksCache] !== void 0) { this.#cache = options?.[jwksCache]; if (isFreshJwksCache(options?.[jwksCache], this.#cacheMaxAge)) { this.#jwksTimestamp = this.#cache.uat; this.#local = createLocalJWKSet(this.#cache.jwks); } } } pendingFetch() { return !!this.#pendingFetch; } coolingDown() { return typeof this.#jwksTimestamp === "number" ? Date.now() < this.#jwksTimestamp + this.#cooldownDuration : false; } fresh() { return typeof this.#jwksTimestamp === "number" ? Date.now() < this.#jwksTimestamp + this.#cacheMaxAge : false; } jwks() { return this.#local?.jwks(); } async getKey(protectedHeader, token) { if (!this.#local || !this.fresh()) { await this.reload(); } try { return await this.#local(protectedHeader, token); } catch (err) { if (err instanceof JWKSNoMatchingKey) { if (this.coolingDown() === false) { await this.reload(); return this.#local(protectedHeader, token); } } throw err; } } async reload() { if (this.#pendingFetch && isCloudflareWorkers()) { this.#pendingFetch = void 0; } this.#pendingFetch ||= fetchJwks( this.#url.href, this.#headers, AbortSignal.timeout(this.#timeoutDuration), this.#customFetch ).then((json) => { this.#local = createLocalJWKSet(json); if (this.#cache) { this.#cache.uat = Date.now(); this.#cache.jwks = json; } this.#jwksTimestamp = Date.now(); this.#pendingFetch = void 0; }).catch((err) => { this.#pendingFetch = void 0; throw err; }); await this.#pendingFetch; } }; function createRemoteJWKSet(url, options) { const set = new RemoteJWKSet(url, options); const remoteJWKSet = async (protectedHeader, token) => set.getKey(protectedHeader, token); Object.defineProperties(remoteJWKSet, { coolingDown: { get: () => set.coolingDown(), enumerable: true, configurable: false }, fresh: { get: () => set.fresh(), enumerable: true, configurable: false }, reload: { value: () => set.reload(), enumerable: true, configurable: false, writable: false }, reloading: { get: () => set.pendingFetch(), enumerable: true, configurable: false }, jwks: { value: () => set.jwks(), enumerable: true, configurable: false, writable: false } }); return remoteJWKSet; } export { createRemoteJWKSet, customFetch, jwksCache };