Files
SnowLuma/.pnpm-store/v11/files/fa/6221827bc07ced0bc8d1a327b1ffa1d198e8f0d3f99743ae8b4a1d39ce41cee1cfc7fc8477c4380693cca19f48b129598f2c053be659f987fa541f8ab3a68c
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

131 lines
5.8 KiB
Plaintext

import { decodeBase64Url, encodeBase64Url } from "../encode.js";
import { AlgorithmTypes } from "./jwa.js";
import { JwtAlgorithmMismatch, JwtAlgorithmNotAllowed, JwtAlgorithmRequired, JwtHeaderInvalid, JwtHeaderRequiresKid, JwtPayloadRequiresAud, JwtSymmetricAlgorithmNotAllowed, JwtTokenAudience, JwtTokenExpired, JwtTokenInvalid, JwtTokenIssuedAt, JwtTokenIssuer, JwtTokenNotBefore, JwtTokenSignatureMismatched } from "./types.js";
import { utf8Decoder, utf8Encoder } from "./utf8.js";
import { signing, verifying } from "./jws.js";
//#region src/utils/jwt/jwt.ts
/**
* @module
* JSON Web Token (JWT)
* https://datatracker.ietf.org/doc/html/rfc7519
*/
const encodeJwtPart = (part) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(part)).buffer).replace(/=/g, "");
const encodeSignaturePart = (buf) => encodeBase64Url(buf).replace(/=/g, "");
const 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;
}
const 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));
return `${partialToken}.${encodeSignaturePart(signaturePart)}`;
};
const verify = async (token, publicKey, algOrOptions) => {
if (!algOrOptions) throw new JwtAlgorithmRequired();
const { alg, iss, nbf = true, exp = true, iat = true, aud } = typeof algOrOptions === "string" ? { alg: algOrOptions } : algOrOptions;
if (!alg) throw new JwtAlgorithmRequired();
const tokenParts = token.split(".");
if (tokenParts.length !== 3) throw new JwtTokenInvalid(token);
const { header, payload } = decode(token);
if (!isTokenHeader(header)) throw new JwtHeaderInvalid(header);
if (header.alg !== alg) throw new JwtAlgorithmMismatch(alg, header.alg);
const now = Math.floor(Date.now() / 1e3);
if (nbf && payload.nbf !== void 0) {
if (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf) || payload.nbf > now) throw new JwtTokenNotBefore(token);
}
if (exp && payload.exp !== void 0) {
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp <= now) throw new JwtTokenExpired(token);
}
if (iat && payload.iat !== void 0) {
if (typeof payload.iat !== "number" || !Number.isFinite(payload.iat) || now < payload.iat) throw new JwtTokenIssuedAt(now, payload.iat);
}
if (iss) {
if (!payload.iss) throw new JwtTokenIssuer(iss, null);
if (typeof iss === "string" && payload.iss !== iss) throw new JwtTokenIssuer(iss, payload.iss);
if (iss instanceof RegExp && !iss.test(payload.iss)) throw new JwtTokenIssuer(iss, payload.iss);
}
if (aud) {
if (!payload.aud) throw new JwtPayloadRequiresAud(payload);
if (!(Array.isArray(payload.aud) ? payload.aud : [payload.aud]).some((payloadAud) => aud instanceof RegExp ? aud.test(payloadAud) : typeof aud === "string" ? payloadAud === aud : Array.isArray(aud) && aud.includes(payloadAud))) throw new JwtTokenAudience(aud, payload.aud);
}
const headerPayload = token.substring(0, token.lastIndexOf("."));
let signature;
try {
signature = decodeBase64Url(tokenParts[2]);
} catch {
throw new JwtTokenInvalid(token);
}
if (!await verifying(publicKey, alg, signature, utf8Encoder.encode(headerPayload))) throw new JwtTokenSignatureMismatched(token);
return payload;
};
const symmetricAlgorithms = [
"HS256",
"HS384",
"HS512"
];
const 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 (symmetricAlgorithms.includes(header.alg)) throw new JwtSymmetricAlgorithmNotAllowed(header.alg);
if (!options.allowedAlgorithms.includes(header.alg)) throw new JwtAlgorithmNotAllowed(header.alg, options.allowedAlgorithms);
let verifyKeys = options.keys ? [...options.keys] : void 0;
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");
verifyKeys ??= [];
verifyKeys.push(...data.keys);
} else if (!verifyKeys) throw new Error("verifyWithJwks requires options for either \"keys\" or \"jwks_uri\" or both");
const matchingKey = verifyKeys.find((key) => key.kid === header.kid);
if (!matchingKey) throw new JwtTokenInvalid(token);
if (matchingKey.alg && matchingKey.alg !== header.alg) throw new JwtAlgorithmMismatch(matchingKey.alg, header.alg);
return await verify(token, matchingKey, {
alg: header.alg,
...verifyOpts
});
};
const decode = (token) => {
const parts = token.split(".");
if (parts.length !== 3) throw new JwtTokenInvalid(token);
try {
return {
header: decodeJwtPart(parts[0]),
payload: decodeJwtPart(parts[1])
};
} catch {
throw new JwtTokenInvalid(token);
}
};
const decodeHeader = (token) => {
const parts = token.split(".");
if (parts.length !== 3) throw new JwtTokenInvalid(token);
try {
return decodeJwtPart(parts[0]);
} catch {
throw new JwtTokenInvalid(token);
}
};
//#endregion
export { decode, decodeHeader, isTokenHeader, sign, verify, verifyWithJwks };