djwt
Version:
A JWT Library with blockchain key based signing for JWS.
611 lines (587 loc) • 22.8 kB
JavaScript
import { Buffer } from 'buffer';
import { z } from 'zod';
import ms from 'ms';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
function toString(obj) {
if (typeof obj === "string")
return obj;
if (typeof obj === "number" || Buffer.isBuffer(obj))
return obj.toString();
return JSON.stringify(obj);
}
function signPayload(payload, signer) {
return __awaiter(this, void 0, void 0, function* () {
return signer(payload);
});
}
class BaseDjwtError extends Error {
constructor(msg, innerError = new Error("dJWT Error")) {
super(msg);
this.innerError = innerError;
this.name = this.constructor.name;
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(new.target.constructor);
}
else {
this.stack = new Error().stack;
}
}
static convertToString(value) {
if (value === null || value === undefined)
return 'undefined';
const result = JSON.stringify(value);
return result;
}
toJSON() {
return {
name: this.name,
code: this.code,
message: this.message,
innerError: this.innerError,
};
}
}
class TokenExpiredError extends BaseDjwtError {
constructor(message, expiredAt) {
super(message);
this.expiredAt = expiredAt;
}
}
class NotBeforeError extends BaseDjwtError {
constructor(message, date) {
super(message);
this.date = date;
}
}
class TimespanDecodingError extends BaseDjwtError {
constructor(message, date) {
super(message);
this.time = date;
}
}
class VerificationError extends BaseDjwtError {
constructor(message) {
super(message);
}
}
class OptionsVerificationError extends BaseDjwtError {
constructor(message, incorrectValue, expectedValue) {
super(message + ` ${incorrectValue} is not equal to expected: ${expectedValue}`);
this.incorrectValue = incorrectValue;
this.expectedValue = expectedValue;
}
}
class InvalidPayloadError extends BaseDjwtError {
constructor(message) {
super(message);
}
}
class InvalidSignOptionsError extends BaseDjwtError {
constructor(message, payloadVal, optionsVal) {
super(message);
this.payloadValue = payloadVal;
this.optionsValue = optionsVal;
}
}
class JwsDecodingError extends BaseDjwtError {
constructor(message, token) {
super(message);
this.jwt = token;
}
}
class JwsEncodingError extends BaseDjwtError {
constructor(message, token) {
super(message + ` ${token}`);
this.jwt = token;
}
}
class JwsVerifyError extends BaseDjwtError {
constructor(message) {
super(message);
}
}
class JwaVerifyError extends BaseDjwtError {
constructor(message, arg) {
super(message);
this.argument = arg;
}
}
class JwaAddressIncorrectError extends BaseDjwtError {
constructor(expectedAddress, returnedAddress) {
super(`Expected address : ${expectedAddress}, does not match with Returned Address: ${returnedAddress}`);
this.expectedAddress = expectedAddress;
this.returnedAddress = returnedAddress;
}
}
function jwaVerify(verifier, payload, signature, address) {
return __awaiter(this, void 0, void 0, function* () {
const result = yield verifier(payload, signature, address);
switch (typeof result) {
case "boolean": {
return result;
}
case "string": {
if (result === address)
return true;
else
throw new JwaAddressIncorrectError(address, result);
}
default:
throw new JwaVerifyError("verifier() does not return boolean or string", result);
}
});
}
function isHexString(str) {
if (str.slice(0, 2) === '0x') {
str = str.split('0x')[1];
}
const regExp = /^[0-9a-fA-F]+$/;
if (regExp.test(str))
return { str, isHex: true };
return { str, isHex: false };
}
function base64url(string, encoding) {
if (encoding === 'hex') {
let { str, isHex } = isHexString(string);
if (!isHex)
throw new JwsEncodingError('Non-Hexstring provided with hex encoding', string);
string = str;
}
return Buffer.from(string, encoding)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function jwsSecuredInput(header, payload) {
let encodedHeader = base64url(toString(header), 'utf-8');
let encodedPayload = base64url(toString(payload), 'utf-8');
return `${encodedHeader}.${encodedPayload}`;
}
function signJws(header, payload, signer, sigEncoding) {
return __awaiter(this, void 0, void 0, function* () {
let securedInput = jwsSecuredInput(header, payload);
let signature = yield signPayload(securedInput, signer);
return `${securedInput}.${base64url(signature, sigEncoding)}`;
});
}
const JWS_REGEX = /^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/;
function isObject(thing) {
return Object.prototype.toString.call(thing) === "[object Object]";
}
function safeJsonParse(thing) {
if (isObject(thing))
return thing;
try {
return JSON.parse(thing);
}
catch (e) {
return undefined;
}
}
function headerFromJWS(jwsSig) {
let encodedHeader = jwsSig.split(".", 1)[0];
if (encodedHeader)
return safeJsonParse(Buffer.from(encodedHeader, "base64").toString("binary"));
else
throw new JwsDecodingError("Error decoding jws from this jwt", jwsSig);
}
function securedInputFromJWS(jwsSig) {
return jwsSig.split(".", 2).join(".");
}
function signatureFromJWS(jwsSig, sigEncoding = 'hex') {
const sig = jwsSig.split(".")[2];
if (sig) {
const signature = Buffer.from(sig, "base64").toString(sigEncoding);
const returnValue = sigEncoding === 'hex' ? '0x' + signature : signature;
return returnValue;
}
throw new JwsDecodingError("Signature not present in token", jwsSig);
}
function payloadFromJWS(jwsSig) {
let payload = jwsSig.split(".")[1];
if (payload)
return Buffer.from(payload, "base64").toString('utf-8');
else
throw new JwsDecodingError("Error decoding jws", jwsSig);
}
function isValidJws(string) {
return JWS_REGEX.test(string) && !!headerFromJWS(string);
}
function jwsVerify(verifier, jwsSig, address, sigEncoding) {
return __awaiter(this, void 0, void 0, function* () {
let signature = signatureFromJWS(jwsSig, sigEncoding);
let securedInput = securedInputFromJWS(jwsSig);
return jwaVerify(verifier, securedInput, signature, address);
});
}
function decodeJws(jwsSig, sigEncoding) {
if (!isValidJws(jwsSig))
throw new JwsDecodingError("JWT doesn't pass regex", jwsSig);
let header = headerFromJWS(jwsSig);
if (!header)
throw new JwsDecodingError("JWT doesn't contain header", jwsSig);
let payload = JSON.parse(payloadFromJWS(jwsSig));
return {
header: header,
payload: payload,
signature: signatureFromJWS(jwsSig, sigEncoding),
};
}
const payloadSchema = z.object({
iss: z
.string({ invalid_type_error: "Issuer (iss) claim has to be a string" })
.min(1, "Issuer (iss) claim has to be non-empty."),
nonce: z
.number({ invalid_type_error: "nonce has to be a number" })
.positive("A nonce > 0 has to be provided."),
exp: z
.number({
invalid_type_error: "Expiration Time (exp) claim has to be a number",
})
.positive("A positive number as Expiration Time (exp) claim has to be provided."),
iat: z
.number()
.positive("Issued At (iat) claim, if provided, has to be a positive number.")
.optional(),
nbf: z
.number()
.positive("Not Before (nbf) claim, if provided, has to be a positive number.")
.optional(),
sub: z
.string()
.min(1, "Subject (sub) claim, if provided, has to be a non-empty string.")
.optional(),
jti: z
.string()
.min(1, "JWT ID (jti) claim, if provided, has to be a non-empty string.")
.optional(),
aud: z
.union([
z.string().min(1),
z
.array(z.string())
.min(1, "Audience (aud) claim, if provided, has to be a non-empty string or an array of string."),
])
.optional(),
}).strict();
const headerSchema = z.object({
alg: z.string().min(1, "header.alg has to be provided.")
}).strict();
const signOptionsSchema = z.object({
algorithm: z
.string({ invalid_type_error: "Algorithm has to be a string" })
.min(1, "options.algorithm has to be provided."),
header: headerSchema,
sigEncoding: z
.string()
.min(1, "sigEncoding, if provided, has to be a non-empty string."),
noTimestamp: z.boolean(),
expiresIn: z
.union([
z.number().positive(),
z
.string()
.min(1, 'options.expiresIn, if provided, has to be a number of seconds or string representing a timespan eg: "1d", "20h", 60'),
]),
notBefore: z
.union([
z.number().positive(),
z
.string()
.min(1, 'options.notBefore, if provided, has to be a number of seconds or string representing a timespan eg: "1d", "20h", 60'),
]),
}).partial().strict();
const verifyOptionsSchema = z
.object({
audience: z.union([
z.string().min(1),
z
.array(z.string())
.min(1, "options.audience, if provided, has to be a non-empty string or an array of string."),
]),
issuer: z.union([
z.string().min(1),
z
.array(z.string())
.min(1, "options.issuer, if provided, has to be a non-empty string or an array of string."),
]),
subject: z
.string()
.min(1, "options.subject, if provided, has to be a non-empty string."),
jwtid: z
.string()
.min(1, "options.jwtid, if provided, has to be a non-empty string."),
clockTimestamp: z
.number()
.positive("options.clockTimestamp, if provided, has to be a positive number."),
nonce: z
.number({
invalid_type_error: "options.nonce, if provided, has to be a number",
})
.positive("options.nonce, if provided, has to be a positive integer"),
ignoreNotBefore: z.boolean({
invalid_type_error: "options.ignoreNotBefore, if provided, has to be a boolean",
}),
clockTolerance: z
.number()
.positive("options.clockTolerance, if provided, has to be a positive number."),
ignoreExpiration: z.boolean({
invalid_type_error: "options.ignoreExpiration, if provided, has to be a boolean.",
}),
maxAge: z
.number()
.positive("options.maxAge, if provided, has to be a positive number."),
complete: z.boolean({
invalid_type_error: "options.complete, if provided, has to be a boolean"
}),
sigEncoding: z
.string()
.min(1, "sigEncoding, if provided, has to be a non-empty string."),
algorithm: z
.string()
.min(1, "options.algorithm, if provided, has to be a non-empty string."),
})
.partial()
.strict();
const decodeOptionsSchema = z
.object({
complete: z.boolean({
invalid_type_error: "options.complete, if provided, has to be a boolean"
}),
sigEncoding: z
.string()
.min(1, "sigEncoding, if provided, has to be a non-empty string.")
})
.partial()
.strict();
function decodeJWT(jwtString, options) {
options = Object.assign({}, options);
const optionsParseResult = decodeOptionsSchema.safeParse(options);
if (!optionsParseResult.success) {
throw new VerificationError(JSON.parse(optionsParseResult.error.message)[0].message);
}
const decoded = decodeJws(jwtString, options.sigEncoding);
const payload = decoded.payload;
if (options.complete)
return {
header: decoded.header,
payload: payload,
signature: decoded.signature,
};
else
return payload;
}
function timespan(time, timestamp) {
if (typeof time === "string") {
let milliseconds = ms(time);
if (typeof milliseconds === "undefined") {
throw new TimespanDecodingError("Error while decoding time", time);
}
return Math.floor(timestamp + milliseconds / 1000);
}
else if (typeof time === "number") {
return timestamp + time;
}
else {
throw new TimespanDecodingError("Time is not of the tpye number or string", time);
}
}
function verifyJWT(jwtString, verifier, options) {
return __awaiter(this, void 0, void 0, function* () {
//clone this object since we are going to mutate it.
options = Object.assign({}, options);
const optionsParseResult = verifyOptionsSchema.safeParse(options);
if (!optionsParseResult.success) {
throw new VerificationError(JSON.parse(optionsParseResult.error.message)[0].message);
}
jwtString = z
.string({
invalid_type_error: "jwtString must be provided",
})
.min(1, "jwtString must be non-empty")
.parse(jwtString);
if (jwtString.split(".").length !== 3) {
throw new VerificationError("jwt malformed");
}
const clockTimestamp = options.clockTimestamp || Math.floor(Date.now() / 1000);
const decodedToken = decodeJWT(jwtString, {
complete: true,
sigEncoding: options.sigEncoding
});
if (!decodedToken) {
throw new VerificationError("Invalid token");
}
if (!decodedToken.header) {
throw new VerificationError("Invalid token decoding, header not present in decoded token");
}
if (!decodedToken.payload) {
throw new VerificationError("Invalid token decoding, payload not present in decoded token");
}
if (!decodedToken.signature) {
throw new VerificationError("Invalid token decoding, signature not present in decoded token");
}
const header = decodedToken.header;
const headerParseResult = headerSchema.safeParse(header);
if (!headerParseResult.success) {
throw new VerificationError(JSON.parse(headerParseResult.error.message)[0].message);
}
const payload = decodedToken.payload;
const payloadParseResult = payloadSchema.safeParse(payload);
if (!payloadParseResult.success) {
throw new InvalidPayloadError(JSON.parse(payloadParseResult.error.message)[0].message);
}
if (options.algorithm) {
if (options.algorithm !== header.alg)
throw new OptionsVerificationError("Header.alg is incorrect.", header.alg, options.algorithm);
}
if (payload.nbf !== undefined && !options.ignoreNotBefore) {
if (payload.nbf > clockTimestamp + (options.clockTolerance || 0)) {
throw new NotBeforeError("jwt not active", payload.nbf);
}
}
if (payload.exp !== undefined && !options.ignoreExpiration) {
if (clockTimestamp >= payload.exp + (options.clockTolerance || 0)) {
throw new TokenExpiredError("jwt expired", payload.exp);
}
}
if (options.audience) {
if (payload.aud === undefined)
throw new VerificationError("options.audience is present but payload.aud is not");
const audiences = Array.isArray(options.audience)
? options.audience
: [options.audience];
const target = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
const match = target.some(function (targetAudience) {
return audiences.some(function (audience) {
return audience instanceof RegExp
? audience.test(targetAudience)
: audience === targetAudience;
});
});
if (!match) {
throw new OptionsVerificationError("JWT audience is incorrect.", target.join(" or "), audiences.join(" or "));
}
}
if (options.issuer) {
const invalid_issuer = payload.iss !== options.issuer ||
(Array.isArray(options.issuer) &&
options.issuer.indexOf(payload.iss) === -1);
if (invalid_issuer) {
throw new OptionsVerificationError("JWT issuer invalid.", payload.iss, Array.isArray(options.issuer)
? options.issuer.join(" or ")
: options.issuer);
}
}
if (options.subject) {
if (payload.sub !== options.subject) {
throw new OptionsVerificationError("JWT subject invalid.", payload.sub ? payload.sub : "undefined", options.subject);
}
}
if (options.jwtid) {
if (payload.jti !== options.jwtid) {
throw new OptionsVerificationError("JWT ID invalid.", payload.jti ? payload.jti : "undefined", options.jwtid);
}
}
if (options.nonce) {
if (payload.nonce !== options.nonce) {
throw new OptionsVerificationError("Signature nonce invalid.", payload.nonce, options.nonce);
}
}
if (options.maxAge) {
if (typeof payload.iat !== "number") {
throw new VerificationError("iat required when maxAge is specified");
}
const maxAgeTimestamp = timespan(options.maxAge, payload.iat);
if (maxAgeTimestamp === undefined) {
throw new VerificationError('"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60');
}
if (clockTimestamp >= maxAgeTimestamp + (options.clockTolerance || 0)) {
throw new TokenExpiredError("maxAge exceeded", maxAgeTimestamp);
}
}
const valid = yield jwsVerify(verifier, jwtString, decodedToken.payload.iss, options.sigEncoding);
if (!valid) {
throw new VerificationError("Invalid signature");
}
const signature = decodedToken.signature;
if (options.complete)
return {
header: header,
payload: payload,
signature: signature,
};
else
return payload;
});
}
function signJWT(payload, signer, options) {
return __awaiter(this, void 0, void 0, function* () {
const payloadParseResult = payloadSchema.safeParse(payload);
if (!payloadParseResult.success) {
throw new InvalidPayloadError(JSON.parse(payloadParseResult.error.message)[0].message);
}
const optionsParseResult = signOptionsSchema.safeParse(options);
if (!optionsParseResult.success) {
throw new InvalidSignOptionsError(JSON.parse(optionsParseResult.error.message)[0].message);
}
// Default encoding values
const sigEncoding = options.sigEncoding || "hex";
let header = options.header;
if (!header) {
if (!options.algorithm)
throw new InvalidSignOptionsError("Either header or algorithm is required in options");
header = {
alg: options.algorithm,
};
}
const timestamp = payload.iat || Math.floor(Date.now() / 1000);
if (options.noTimestamp) {
delete payload.iat;
}
else {
payload.iat = timestamp;
}
if (options.expiresIn !== undefined) {
if (payload.exp !== undefined)
throw new InvalidSignOptionsError('Bad "options.expiresIn" option the payload already has an "exp" property.');
else
payload.exp = timespan(options.expiresIn, timestamp);
}
if (options.notBefore !== undefined) {
if (payload.nbf !== undefined) {
throw new InvalidSignOptionsError('Bad "options.notBefore" option the payload already has an "nbf" property.');
}
else
payload.nbf = timespan(options.notBefore, timestamp);
}
return signJws(header, payload, signer, sigEncoding);
});
}
export { BaseDjwtError, InvalidPayloadError, InvalidSignOptionsError, JwaAddressIncorrectError, JwaVerifyError, JwsDecodingError, JwsEncodingError, JwsVerifyError, NotBeforeError, OptionsVerificationError, TimespanDecodingError, TokenExpiredError, VerificationError, decodeJWT, signJWT, verifyJWT };
//# sourceMappingURL=index.esm.js.map