@gaonengwww/jose
Version:
JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes
499 lines (490 loc) • 14.3 kB
JavaScript
// src/lib/buffer_utils.ts
var encoder = new TextEncoder();
var decoder = new TextDecoder();
var MAX_INT32 = 2 ** 32;
// src/lib/base64.ts
function encodeBase64(input) {
if (Uint8Array.prototype.toBase64) {
return input.toBase64();
}
const CHUNK_SIZE = 32768;
const arr = [];
for (let i = 0; i < input.length; i += CHUNK_SIZE) {
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
}
return btoa(arr.join(""));
}
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.");
}
}
function encode(input) {
let unencoded = input;
if (typeof unencoded === "string") {
unencoded = encoder.encode(unencoded);
}
if (Uint8Array.prototype.toBase64) {
return unencoded.toBase64({ alphabet: "base64url", omitPadding: true });
}
return encodeBase64(unencoded).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
// 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 JWTClaimValidationFailed = class extends JOSEError {
/** @ignore */
static code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
/** A unique error code for {@link JWTClaimValidationFailed}. */
code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
/** The Claim for which the validation failed. */
claim;
/** Reason code for the validation failure. */
reason;
/**
* The parsed JWT Claims Set (aka payload). Other JWT claims may or may not have been verified at
* this point. The JSON Web Signature (JWS) or a JSON Web Encryption (JWE) structures' integrity
* has however been verified. Claims Set verification happens after the JWS Signature or JWE
* Decryption processes.
*/
payload;
/** @ignore */
constructor(message, payload, claim = "unspecified", reason = "unspecified") {
super(message, { cause: { claim, reason, payload } });
this.claim = claim;
this.reason = reason;
this.payload = payload;
}
};
var JWTExpired = class extends JOSEError {
/** @ignore */
static code = "ERR_JWT_EXPIRED";
/** A unique error code for {@link JWTExpired}. */
code = "ERR_JWT_EXPIRED";
/** The Claim for which the validation failed. */
claim;
/** Reason code for the validation failure. */
reason;
/**
* The parsed JWT Claims Set (aka payload). Other JWT claims may or may not have been verified at
* this point. The JSON Web Signature (JWS) or a JSON Web Encryption (JWE) structures' integrity
* has however been verified. Claims Set verification happens after the JWS Signature or JWE
* Decryption processes.
*/
payload;
/** @ignore */
constructor(message, payload, claim = "unspecified", reason = "unspecified") {
super(message, { cause: { claim, reason, payload } });
this.claim = claim;
this.reason = reason;
this.payload = payload;
}
};
var JWTInvalid = class extends JOSEError {
/** @ignore */
static code = "ERR_JWT_INVALID";
/** A unique error code for {@link JWTInvalid}. */
code = "ERR_JWT_INVALID";
};
// src/lib/epoch.ts
var epoch_default = (date) => Math.floor(date.getTime() / 1e3);
// src/lib/secs.ts
var minute = 60;
var hour = minute * 60;
var day = hour * 24;
var week = day * 7;
var year = day * 365.25;
var REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i;
var secs_default = (str) => {
const matched = REGEX.exec(str);
if (!matched || matched[4] && matched[1]) {
throw new TypeError("Invalid time period format");
}
const value = parseFloat(matched[2]);
const unit = matched[3].toLowerCase();
let numericDate;
switch (unit) {
case "sec":
case "secs":
case "second":
case "seconds":
case "s":
numericDate = Math.round(value);
break;
case "minute":
case "minutes":
case "min":
case "mins":
case "m":
numericDate = Math.round(value * minute);
break;
case "hour":
case "hours":
case "hr":
case "hrs":
case "h":
numericDate = Math.round(value * hour);
break;
case "day":
case "days":
case "d":
numericDate = Math.round(value * day);
break;
case "week":
case "weeks":
case "w":
numericDate = Math.round(value * week);
break;
// years matched
default:
numericDate = Math.round(value * year);
break;
}
if (matched[1] === "-" || matched[4] === "ago") {
return -numericDate;
}
return numericDate;
};
// 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/lib/jwt_claims_set.ts
function validateInput(label, input) {
if (!Number.isFinite(input)) {
throw new TypeError(`Invalid ${label} input`);
}
return input;
}
var normalizeTyp = (value) => value.toLowerCase().replace(/^application\//, "");
var checkAudiencePresence = (audPayload, audOption) => {
if (typeof audPayload === "string") {
return audOption.includes(audPayload);
}
if (Array.isArray(audPayload)) {
return audOption.some(Set.prototype.has.bind(new Set(audPayload)));
}
return false;
};
function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
let payload;
try {
payload = JSON.parse(decoder.decode(encodedPayload));
} catch {
}
if (!is_object_default(payload)) {
throw new JWTInvalid("JWT Claims Set must be a top-level JSON object");
}
const { typ } = options;
if (typ && (typeof protectedHeader.typ !== "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) {
throw new JWTClaimValidationFailed(
'unexpected "typ" JWT header value',
payload,
"typ",
"check_failed"
);
}
const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options;
const presenceCheck = [...requiredClaims];
if (maxTokenAge !== void 0) presenceCheck.push("iat");
if (audience !== void 0) presenceCheck.push("aud");
if (subject !== void 0) presenceCheck.push("sub");
if (issuer !== void 0) presenceCheck.push("iss");
for (const claim of new Set(presenceCheck.reverse())) {
if (!(claim in payload)) {
throw new JWTClaimValidationFailed(
`missing required "${claim}" claim`,
payload,
claim,
"missing"
);
}
}
if (issuer && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) {
throw new JWTClaimValidationFailed(
'unexpected "iss" claim value',
payload,
"iss",
"check_failed"
);
}
if (subject && payload.sub !== subject) {
throw new JWTClaimValidationFailed(
'unexpected "sub" claim value',
payload,
"sub",
"check_failed"
);
}
if (audience && !checkAudiencePresence(payload.aud, typeof audience === "string" ? [audience] : audience)) {
throw new JWTClaimValidationFailed(
'unexpected "aud" claim value',
payload,
"aud",
"check_failed"
);
}
let tolerance;
switch (typeof options.clockTolerance) {
case "string":
tolerance = secs_default(options.clockTolerance);
break;
case "number":
tolerance = options.clockTolerance;
break;
case "undefined":
tolerance = 0;
break;
default:
throw new TypeError("Invalid clockTolerance option type");
}
const { currentDate } = options;
const now = epoch_default(currentDate || /* @__PURE__ */ new Date());
if ((payload.iat !== void 0 || maxTokenAge) && typeof payload.iat !== "number") {
throw new JWTClaimValidationFailed('"iat" claim must be a number', payload, "iat", "invalid");
}
if (payload.nbf !== void 0) {
if (typeof payload.nbf !== "number") {
throw new JWTClaimValidationFailed('"nbf" claim must be a number', payload, "nbf", "invalid");
}
if (payload.nbf > now + tolerance) {
throw new JWTClaimValidationFailed(
'"nbf" claim timestamp check failed',
payload,
"nbf",
"check_failed"
);
}
}
if (payload.exp !== void 0) {
if (typeof payload.exp !== "number") {
throw new JWTClaimValidationFailed('"exp" claim must be a number', payload, "exp", "invalid");
}
if (payload.exp <= now - tolerance) {
throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", "check_failed");
}
}
if (maxTokenAge) {
const age = now - payload.iat;
const max = typeof maxTokenAge === "number" ? maxTokenAge : secs_default(maxTokenAge);
if (age - tolerance > max) {
throw new JWTExpired(
'"iat" claim timestamp check failed (too far in the past)',
payload,
"iat",
"check_failed"
);
}
if (age < 0 - tolerance) {
throw new JWTClaimValidationFailed(
'"iat" claim timestamp check failed (it should be in the past)',
payload,
"iat",
"check_failed"
);
}
}
return payload;
}
var JWTClaimsBuilder = class {
#payload;
constructor(payload) {
if (!is_object_default(payload)) {
throw new TypeError("JWT Claims Set MUST be an object");
}
this.#payload = structuredClone(payload);
}
data() {
return encoder.encode(JSON.stringify(this.#payload));
}
get iss() {
return this.#payload.iss;
}
set iss(value) {
this.#payload.iss = value;
}
get sub() {
return this.#payload.sub;
}
set sub(value) {
this.#payload.sub = value;
}
get aud() {
return this.#payload.aud;
}
set aud(value) {
this.#payload.aud = value;
}
get jti() {
return this.#payload.jti;
}
set jti(value) {
this.#payload.jti = value;
}
get nbf() {
return this.#payload.nbf;
}
set nbf(value) {
if (typeof value === "number") {
this.#payload.nbf = validateInput("setNotBefore", value);
} else if (value instanceof Date) {
this.#payload.nbf = validateInput("setNotBefore", epoch_default(value));
} else {
this.#payload.nbf = epoch_default(/* @__PURE__ */ new Date()) + secs_default(value);
}
}
get exp() {
return this.#payload.exp;
}
set exp(value) {
if (typeof value === "number") {
this.#payload.exp = validateInput("setExpirationTime", value);
} else if (value instanceof Date) {
this.#payload.exp = validateInput("setExpirationTime", epoch_default(value));
} else {
this.#payload.exp = epoch_default(/* @__PURE__ */ new Date()) + secs_default(value);
}
}
get iat() {
return this.#payload.iat;
}
set iat(value) {
if (typeof value === "undefined") {
this.#payload.iat = epoch_default(/* @__PURE__ */ new Date());
} else if (value instanceof Date) {
this.#payload.iat = validateInput("setIssuedAt", epoch_default(value));
} else if (typeof value === "string") {
this.#payload.iat = validateInput("setIssuedAt", epoch_default(/* @__PURE__ */ new Date()) + secs_default(value));
} else {
this.#payload.iat = validateInput("setIssuedAt", value);
}
}
};
// src/jwt/unsecured.ts
var UnsecuredJWT = class {
#jwt;
/**
* {@link UnsecuredJWT} constructor
*
* @param payload The JWT Claims Set object. Defaults to an empty object.
*/
constructor(payload = {}) {
this.#jwt = new JWTClaimsBuilder(payload);
}
/** Encodes the Unsecured JWT. */
encode() {
const header = encode(JSON.stringify({ alg: "none" }));
const payload = encode(this.#jwt.data());
return `${header}.${payload}.`;
}
setIssuer(issuer) {
this.#jwt.iss = issuer;
return this;
}
setSubject(subject) {
this.#jwt.sub = subject;
return this;
}
setAudience(audience) {
this.#jwt.aud = audience;
return this;
}
setJti(jwtId) {
this.#jwt.jti = jwtId;
return this;
}
setNotBefore(input) {
this.#jwt.nbf = input;
return this;
}
setExpirationTime(input) {
this.#jwt.exp = input;
return this;
}
setIssuedAt(input) {
this.#jwt.iat = input;
return this;
}
/**
* Decodes an unsecured JWT.
*
* @param jwt Unsecured JWT to decode the payload of.
* @param options JWT Claims Set validation options.
*/
static decode(jwt, options) {
if (typeof jwt !== "string") {
throw new JWTInvalid("Unsecured JWT must be a string");
}
const { 0: encodedHeader, 1: encodedPayload, 2: signature, length } = jwt.split(".");
if (length !== 3 || signature !== "") {
throw new JWTInvalid("Invalid Unsecured JWT");
}
let header;
try {
header = JSON.parse(decoder.decode(decode(encodedHeader)));
if (header.alg !== "none") throw new Error();
} catch {
throw new JWTInvalid("Invalid Unsecured JWT");
}
const payload = validateClaimsSet(
header,
decode(encodedPayload),
options
);
return { payload, header };
}
};
export {
UnsecuredJWT
};