@authduo/authduo
Version:
Free User-sovereign Authentication for the World
74 lines • 3.61 kB
JavaScript
import { Base64url, hexId, Text } from "@benev/slate";
import { CryptoConstants } from "../crypto-constants.js";
import { TokenVerifyError } from "./types.js";
export class Token {
static header = Object.freeze({ typ: "JWT", alg: "ES256" });
static toJsTime = (t) => t * 1000;
static fromJsTime = (t) => t / 1000;
static params = (r) => ({
jti: hexId(),
iat: Date.now(),
exp: Token.fromJsTime(r.expiresAt),
nbf: r.notBefore,
iss: r.issuer,
aud: r.audience,
});
static async sign(privateKey, payload) {
const headerBytes = Text.bytes(JSON.stringify(Token.header));
const headerText = Base64url.string(headerBytes);
const payloadBytes = Text.bytes(JSON.stringify(payload));
const payloadText = Base64url.string(payloadBytes);
const signingText = `${headerText}.${payloadText}`;
const signingBytes = new TextEncoder().encode(signingText);
const signature = Base64url.string(new Uint8Array(await crypto.subtle.sign(CryptoConstants.algos.signing, privateKey, signingBytes)));
return `${signingText}.${signature}`;
}
static decode(token) {
const [headerText, payloadText, signatureText] = token.split(".");
if (!headerText || !payloadText || !signatureText)
throw new Error("invalid jwt structure");
const headerBytes = Base64url.bytes(headerText);
const headerJson = Text.string(headerBytes);
const header = JSON.parse(headerJson);
const payloadBytes = Base64url.bytes(payloadText);
const payloadJson = Text.string(payloadBytes);
const payload = JSON.parse(payloadJson);
const signature = Base64url.bytes(signatureText).buffer;
return { header, payload, signature };
}
static async verify(publicKey, token, options = {}) {
const [headerText, payloadText] = token.split(".");
const { payload, signature } = Token.decode(token);
const signingInput = `${headerText}.${payloadText}`;
const signingInputBytes = new TextEncoder().encode(signingInput);
const isValid = await crypto.subtle.verify(CryptoConstants.algos.signing, publicKey, signature, signingInputBytes);
if (!isValid)
throw new TokenVerifyError("token signature invalid");
if (payload.exp) {
const expiresAt = Token.toJsTime(payload.exp);
if (Date.now() > expiresAt)
throw new TokenVerifyError("token expired");
}
if (payload.nbf) {
const notBefore = Token.toJsTime(payload.nbf);
if (Date.now() < notBefore)
throw new TokenVerifyError("token not ready");
}
if (options.allowedIssuers) {
if (!payload.iss)
throw new TokenVerifyError(`required iss (issuer) is missing`);
if (!options.allowedIssuers.includes(payload.iss))
throw new TokenVerifyError(`invalid iss (issuer) "${payload.iss}"`);
}
if (options.allowedAudiences) {
if (!payload.aud)
throw new TokenVerifyError(`required aud (audience) is missing`);
if (!options.allowedAudiences.includes(payload.aud))
throw new TokenVerifyError(`invalid aud (audience) "${payload.aud}"`);
}
if (payload.aud && !options.allowedAudiences)
throw new TokenVerifyError(`allowedAudiences verification option was not provided, but is required because the token included "aud"`);
return payload;
}
}
//# sourceMappingURL=token.js.map