hono
Version:
Web framework built on Web Standards
157 lines (156 loc) • 4.89 kB
JavaScript
// src/utils/jwt/jwt.ts
import { decodeBase64Url, encodeBase64Url } from "../../utils/encode.js";
import { AlgorithmTypes } from "./jwa.js";
import { signing, verifying } from "./jws.js";
import {
JwtHeaderInvalid,
JwtHeaderRequiresKid,
JwtTokenExpired,
JwtTokenInvalid,
JwtTokenIssuedAt,
JwtTokenIssuer,
JwtTokenNotBefore,
JwtTokenSignatureMismatched
} from "./types.js";
import { utf8Decoder, utf8Encoder } from "./utf8.js";
var encodeJwtPart = (part) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(part)).buffer).replace(/=/g, "");
var encodeSignaturePart = (buf) => encodeBase64Url(buf).replace(/=/g, "");
var decodeJwtPart = (part) => JSON.parse(utf8Decoder.decode(decodeBase64Url(part)));
function isTokenHeader(obj) {
if (typeof obj === "object" && obj !== null) {
const objWithAlg = obj;
return "alg" in objWithAlg && Object.values(AlgorithmTypes).includes(objWithAlg.alg) && (!("typ" in objWithAlg) || objWithAlg.typ === "JWT");
}
return false;
}
var sign = async (payload, privateKey, alg = "HS256") => {
const encodedPayload = encodeJwtPart(payload);
let encodedHeader;
if (typeof privateKey === "object" && "alg" in privateKey) {
alg = privateKey.alg;
encodedHeader = encodeJwtPart({ alg, typ: "JWT", kid: privateKey.kid });
} else {
encodedHeader = encodeJwtPart({ alg, typ: "JWT" });
}
const partialToken = `${encodedHeader}.${encodedPayload}`;
const signaturePart = await signing(privateKey, alg, utf8Encoder.encode(partialToken));
const signature = encodeSignaturePart(signaturePart);
return `${partialToken}.${signature}`;
};
var verify = async (token, publicKey, algOrOptions) => {
const optsIn = typeof algOrOptions === "string" ? { alg: algOrOptions } : algOrOptions || {};
const opts = {
alg: optsIn.alg ?? "HS256",
iss: optsIn.iss,
nbf: optsIn.nbf ?? true,
exp: optsIn.exp ?? true,
iat: optsIn.iat ?? true
};
const tokenParts = token.split(".");
if (tokenParts.length !== 3) {
throw new JwtTokenInvalid(token);
}
const { header, payload } = decode(token);
if (!isTokenHeader(header)) {
throw new JwtHeaderInvalid(header);
}
const now = Date.now() / 1e3 | 0;
if (opts.nbf && payload.nbf && payload.nbf > now) {
throw new JwtTokenNotBefore(token);
}
if (opts.exp && payload.exp && payload.exp <= now) {
throw new JwtTokenExpired(token);
}
if (opts.iat && payload.iat && now < payload.iat) {
throw new JwtTokenIssuedAt(now, payload.iat);
}
if (opts.iss) {
if (!payload.iss) {
throw new JwtTokenIssuer(opts.iss, null);
}
if (typeof opts.iss === "string" && payload.iss !== opts.iss) {
throw new JwtTokenIssuer(opts.iss, payload.iss);
}
if (opts.iss instanceof RegExp && !opts.iss.test(payload.iss)) {
throw new JwtTokenIssuer(opts.iss, payload.iss);
}
}
const headerPayload = token.substring(0, token.lastIndexOf("."));
const verified = await verifying(
publicKey,
opts.alg,
decodeBase64Url(tokenParts[2]),
utf8Encoder.encode(headerPayload)
);
if (!verified) {
throw new JwtTokenSignatureMismatched(token);
}
return payload;
};
var verifyWithJwks = async (token, options, init) => {
const verifyOpts = options.verification || {};
const header = decodeHeader(token);
if (!isTokenHeader(header)) {
throw new JwtHeaderInvalid(header);
}
if (!header.kid) {
throw new JwtHeaderRequiresKid(header);
}
if (options.jwks_uri) {
const response = await fetch(options.jwks_uri, init);
if (!response.ok) {
throw new Error(`failed to fetch JWKS from ${options.jwks_uri}`);
}
const data = await response.json();
if (!data.keys) {
throw new Error('invalid JWKS response. "keys" field is missing');
}
if (!Array.isArray(data.keys)) {
throw new Error('invalid JWKS response. "keys" field is not an array');
}
if (options.keys) {
options.keys.push(...data.keys);
} else {
options.keys = data.keys;
}
} else if (!options.keys) {
throw new Error('verifyWithJwks requires options for either "keys" or "jwks_uri" or both');
}
const matchingKey = options.keys.find((key) => key.kid === header.kid);
if (!matchingKey) {
throw new JwtTokenInvalid(token);
}
return await verify(token, matchingKey, {
alg: matchingKey.alg || header.alg,
...verifyOpts
});
};
var decode = (token) => {
try {
const [h, p] = token.split(".");
const header = decodeJwtPart(h);
const payload = decodeJwtPart(p);
return {
header,
payload
};
} catch {
throw new JwtTokenInvalid(token);
}
};
var decodeHeader = (token) => {
try {
const [h] = token.split(".");
return decodeJwtPart(h);
} catch {
throw new JwtTokenInvalid(token);
}
};
export {
decode,
decodeHeader,
isTokenHeader,
sign,
verify,
verifyWithJwks
};