jose
Version:
JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes
102 lines (101 loc) • 4.28 kB
JavaScript
import { JOSEError, JWKSNoMatchingKey, JWKSTimeout } from "../util/errors.js";
import { createLocalJWKSet } from "./local.js";
import { isJwkSet } from "../lib/validate.js";
function isCloudflareWorkers() {
return typeof WebSocketPair < "u" || typeof navigator < "u" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime < "u" && EdgeRuntime === "vercel";
}
let USER_AGENT;
(typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.13");
const customFetch = /* @__PURE__ */ Symbol();
async function fetchJwks(url, headers, signal, fetchImpl = fetch) {
const response = await fetchImpl(url, {
method: "GET",
signal,
redirect: "manual",
headers
}).catch((err) => {
throw err.name === "TimeoutError" ? new JWKSTimeout(void 0, { cause: err }) : 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 (cause) {
throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON", { cause });
}
}
const jwksCache = /* @__PURE__ */ Symbol();
function isFreshFor(timestamp, duration) {
return Number.isFinite(timestamp) && Date.now() < timestamp + duration;
}
function validateDuration(value, fallback, option) {
if (Number.isNaN(value))
throw new TypeError(`"${option}" option must not be NaN`);
return typeof value == "number" ? value : fallback;
}
function createRemoteJWKSet(url, options) {
if (!(url instanceof URL))
throw new TypeError("url must be an instance of URL");
const href = new URL(url.href).href, opts = options ?? {}, timeoutOption = opts.timeoutDuration;
if (typeof timeoutOption == "number" && (!Number.isInteger(timeoutOption) || timeoutOption < 0))
throw new TypeError('"timeoutDuration" option must be a non-negative integer');
const timeoutDuration = typeof timeoutOption == "number" ? timeoutOption : 5e3, cooldownDuration = validateDuration(opts.cooldownDuration, 3e4, "cooldownDuration"), cacheMaxAge = validateDuration(opts.cacheMaxAge, 6e5, "cacheMaxAge"), headers = new Headers(opts.headers);
USER_AGENT && !headers.has("User-Agent") && headers.set("User-Agent", USER_AGENT), headers.has("accept") || headers.set("accept", "application/json, application/jwk-set+json");
const fetchImpl = opts[customFetch], cache = opts[jwksCache];
let jwksTimestamp, pendingFetch, reloadSequence = 0, appliedSequence = 0, local;
if (cache && typeof cache == "object") {
const { uat, jwks } = cache;
isFreshFor(uat, cacheMaxAge) && isJwkSet(jwks) && (jwksTimestamp = uat, local = createLocalJWKSet(jwks));
}
const reload = async () => {
if (pendingFetch && isCloudflareWorkers() && (pendingFetch = void 0), !pendingFetch) {
const sequence = ++reloadSequence, current = pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl).then((json) => {
const next = createLocalJWKSet(json);
if (sequence <= appliedSequence)
return;
local = next;
const updatedAt = Date.now();
cache && (cache.uat = updatedAt, cache.jwks = json), jwksTimestamp = updatedAt, appliedSequence = sequence;
}).finally(() => {
pendingFetch === current && (pendingFetch = void 0);
});
}
await pendingFetch;
};
return Object.defineProperties(async (protectedHeader, token) => {
(!local || !isFreshFor(jwksTimestamp, cacheMaxAge)) && await reload();
try {
return await local(protectedHeader, token);
} catch (err) {
if (err instanceof JWKSNoMatchingKey && !isFreshFor(jwksTimestamp, cooldownDuration))
return await reload(), local(protectedHeader, token);
throw err;
}
}, {
coolingDown: {
get: () => isFreshFor(jwksTimestamp, cooldownDuration),
enumerable: !0
},
fresh: {
get: () => isFreshFor(jwksTimestamp, cacheMaxAge),
enumerable: !0
},
reload: {
value: reload,
enumerable: !0
},
reloading: {
get: () => !!pendingFetch,
enumerable: !0
},
jwks: {
value: () => local?.jwks(),
enumerable: !0
}
});
}
export {
createRemoteJWKSet,
customFetch,
jwksCache
};