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
JavaScript
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
};