UNPKG

jose

Version:

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

148 lines (147 loc) • 7.53 kB
import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "../util/errors.js"; import { encoder, strictDecoder } from "./buffer_utils.js"; import { isObject } from "./validate.js"; const epoch = (date) => Math.floor(date.getTime() / 1e3), multipliers = { s: 1, m: 60, h: 3600, d: 86400, w: 604800, y: 31557600 }, REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i, checkFailed = "check_failed"; function invalidDuration() { throw new TypeError("Invalid time period format"); } function secs(str) { typeof str != "string" && invalidDuration(); const matched = REGEX.exec(str); (!matched || matched[4] && matched[1]) && invalidDuration(); const value = parseFloat(matched[2]), durationSeconds = Math.round(value * multipliers[matched[3][0].toLowerCase()]); return Number.isFinite(durationSeconds) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -durationSeconds : durationSeconds; } function validateInput(label, input) { if (!Number.isFinite(input)) throw new TypeError(`Invalid ${label} input`); return input; } function validateStringClaim(claim, value) { if (typeof value != "string") throw new TypeError(`"${claim}" claim must be a string`); } function validateAudienceClaim(value) { if (typeof value != "string" && (!Array.isArray(value) || Array.from(value).some((member) => typeof member != "string"))) throw new TypeError('"aud" claim must be a string or an array of strings'); } function numericDate(value, label) { return typeof value == "number" ? validateInput(label, value) : value instanceof Date ? validateInput(label, epoch(value)) : epoch(/* @__PURE__ */ new Date()) + secs(value); } const normalizeTyp = (value) => { const normalized = value.toLowerCase(); return value.includes("/") ? normalized : `application/${normalized}`; }, checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : !1; function validateNumericDate(claimsSet, claim, required = !1) { const value = claimsSet[claim]; if (!(value === void 0 && !required)) { if (typeof value != "number") throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, claimsSet, claim, "invalid"); return value; } } function unexpectedClaim(claimsSet, claim) { throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, claimsSet, claim, checkFailed); } function validateClaimsSet(protectedHeader, claimsSetBytes, options = {}) { let claimsSet; try { claimsSet = JSON.parse(strictDecoder.decode(claimsSetBytes)); } catch (cause) { throw new JWTInvalid("JWT Claims Set must be a top-level JSON object", { cause }); } if (!isObject(claimsSet)) throw new JWTInvalid("JWT Claims Set must be a top-level JSON object"); const { typ } = options; if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', claimsSet, "typ", checkFailed); const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims]; maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss"); for (const claim of new Set(presenceCheck.reverse())) if (!Object.hasOwn(claimsSet, claim)) throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, claimsSet, claim, "missing"); issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(claimsSet.iss) && unexpectedClaim(claimsSet, "iss"), subject !== void 0 && claimsSet.sub !== subject && unexpectedClaim(claimsSet, "sub"), audience !== void 0 && !checkAudiencePresence(claimsSet.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(claimsSet, "aud"); const { clockTolerance } = options; let tolerance = 0; if (typeof clockTolerance == "string") tolerance = secs(clockTolerance); else if (clockTolerance !== void 0) { if (typeof clockTolerance != "number") throw new TypeError("Invalid clockTolerance option type"); tolerance = clockTolerance; } validateInput("clockTolerance option", tolerance); const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(claimsSet, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(claimsSet, "nbf"); if (nbf !== void 0 && nbf > now + tolerance) throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', claimsSet, "nbf", checkFailed); const exp = validateNumericDate(claimsSet, "exp"); if (exp !== void 0 && exp <= now - tolerance) throw new JWTExpired('"exp" claim timestamp check failed', claimsSet, "exp", checkFailed); if (maxTokenAge !== void 0) { const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge)); if (age - tolerance > max) throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', claimsSet, "iat", checkFailed); if (age < -tolerance) throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', claimsSet, "iat", checkFailed); } return claimsSet; } let producerClaimsSets; function producerClaimsSet(producer) { return producerClaimsSets.get(producer); } function jwtClaimsSetBytes(producer) { const claimsSet = producerClaimsSet(producer); for (const claim of ["iat", "nbf", "exp"]) { const value = claimsSet[claim]; if (typeof value == "number" && !Number.isFinite(value)) throw new TypeError(`"${claim}" claim must be a finite number`); } return encoder.encode(JSON.stringify(claimsSet)); } function jwtClaim(producer, claim) { return producerClaimsSet(producer)[claim]; } class JWTClaimsBuilder { constructor(claimsSet = {}) { if (!isObject(claimsSet)) throw new TypeError("JWT Claims Set MUST be an object"); (producerClaimsSets ||= /* @__PURE__ */ new WeakMap()).set(this, structuredClone(claimsSet)); } setIssuer(value) { return validateStringClaim("iss", value), producerClaimsSet(this).iss = value, this; } setSubject(value) { return validateStringClaim("sub", value), producerClaimsSet(this).sub = value, this; } setAudience(value) { return validateAudienceClaim(value), producerClaimsSet(this).aud = value, this; } setJti(value) { return validateStringClaim("jti", value), producerClaimsSet(this).jti = value, this; } setNotBefore(value) { return producerClaimsSet(this).nbf = numericDate(value, "setNotBefore"), this; } setExpirationTime(value) { return producerClaimsSet(this).exp = numericDate(value, "setExpirationTime"), this; } setIssuedAt(value) { const claimsSet = producerClaimsSet(this); return value === void 0 ? claimsSet.iat = epoch(/* @__PURE__ */ new Date()) : typeof value == "string" ? claimsSet.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value)) : claimsSet.iat = numericDate(value, "setIssuedAt"), this; } } export { JWTClaimsBuilder, jwtClaim, jwtClaimsSetBytes, secs, validateClaimsSet };