UNPKG

@authduo/authduo

Version:

Free User-sovereign Authentication for the World

74 lines 3.61 kB
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