JWT validation + renewal
This example verifies JWT signatures with HMAC SHA-256, checks expiry, and optionally reissues tokens in a worker pool. It uses built-in Web Crypto APIs without an external JWT package.
How it works
Section titled “How it works”The host builds a batch containing both valid and expired JWTs. Each job verifies the signature, checks the renewal window (renewWindowSec, renewUntil), and either returns the validated claims or issues a fresh token. The result comes back as a stringified JSON response — keeping structured-clone overhead low.
Three files:
jwt_knitting.ts— runs revalidation in host and Knitting modesutils.ts— token verification, renewal logic, task exportsbench_jwt_revalidation.ts— host-vs-worker benchmark withmitata
Example request and response
Section titled “Example request and response”Input to the task is a JSON string like this:
{ "token": "<jwt>", "nowSec": 1767225600, "ttlSec": 180, "renewWindowSec": 30}The task returns a JSON string. A successful non-renewed response looks like:
{ "ok": true, "renewed": false, "token": "<same-jwt>", "sub": "user_42", "sid": "session_42", "exp": 1767225750, "canRenew": true}If the token is near expiry but still renewable, the same shape comes back with renewed: true
and a replacement token.
bun src/jwt_knitting.tsdeno run -A src/jwt_knitting.tsnpx tsx src/jwt_knitting.tsExpected output:
-- host mode -- verified: 900 renewed: 80 rejected: 20
-- knitting mode (2 threads) -- verified: 900 renewed: 80 rejected: 20Optional benchmark
Section titled “Optional benchmark”The benchmark compares verification, optional renewal, and JSON.stringify
through direct imports (host) with the same workload through worker calls (knitting). Batched dispatch keeps the comparison stable.
import { createPool, isMain } from "knitting";import { buildDemoRevalidateRequests, type RenewalSummary, revalidateToken, revalidateTokenHost, summarizeJsonResponses,} from "./utils.ts";
const THREADS = 2;const REQUESTS = 25_000;const INVALID_PERCENT = 10;
async function runHost(rawRequests: string[]): Promise<RenewalSummary> { const outputs = new Array<string>(rawRequests.length); for (let i = 0; i < rawRequests.length; i++) { outputs[i] = await revalidateTokenHost(rawRequests[i]!); } return summarizeJsonResponses(outputs);}
async function runWorkers(rawRequests: string[]): Promise<RenewalSummary> { const pool = createPool({ threads: THREADS })({ revalidateToken }); try { const jobs: Promise<string>[] = []; for (let i = 0; i < rawRequests.length; i++) { jobs.push(pool.call.revalidateToken(rawRequests[i]!)); }
const outputs = await Promise.all(jobs); return summarizeJsonResponses(outputs); } finally { pool.shutdown(); }}
function printSummary(mode: string, totals: RenewalSummary, ms: number): void { const seconds = Math.max(1e-9, ms / 1000); const rps = REQUESTS / seconds;
console.log(mode); console.log("requests :", REQUESTS.toLocaleString()); console.log("invalid rate :", `${INVALID_PERCENT}%`); console.log("accepted :", totals.ok.toLocaleString()); console.log("renewed :", totals.renewed.toLocaleString()); console.log("rejected :", totals.rejected.toLocaleString()); console.log("output bytes :", totals.outputBytes.toLocaleString()); console.log("took :", `${ms.toFixed(2)} ms`); console.log("throughput :", `${rps.toFixed(0)} req/s`);}
async function main() { const rawRequests = await buildDemoRevalidateRequests({ count: REQUESTS, invalidPercent: INVALID_PERCENT, });
const hostStart = performance.now(); const hostTotals = await runHost(rawRequests); const hostMs = performance.now() - hostStart;
const workerStart = performance.now(); const workerTotals = await runWorkers(rawRequests); const workerMs = performance.now() - workerStart;
const uplift = (hostMs / Math.max(1e-9, workerMs) - 1) * 100;
console.log("JWT token revalidation"); console.log(`threads: ${THREADS}`); console.log(""); printSummary("host", hostTotals, hostMs); console.log(""); printSummary("knitting", workerTotals, workerMs); console.log(""); console.log(`uplift: ${uplift.toFixed(1)}%`);}
if (isMain) { main().catch((error) => { console.error(error); process.exitCode = 1; });}import { createPool, isMain } from "knitting";import { bench, boxplot, run, summary } from "mitata";import { buildDemoRevalidateRequests, makeBatches, mergeRenewalSummary, type RenewalSummary, revalidateTokenBatchFast, revalidateTokenBatchFastHost, sameRenewalSummary,} from "./utils.ts";
const THREADS = 2;const REQUESTS = 25_000;const INVALID_PERCENT = 10;const BATCH = 64;
async function runHostBatches(rawBatches: string[][]): Promise<RenewalSummary> { let totals: RenewalSummary = { ok: 0, renewed: 0, rejected: 0, outputBytes: 0, };
for (let i = 0; i < rawBatches.length; i++) { totals = mergeRenewalSummary( totals, await revalidateTokenBatchFastHost(rawBatches[i]!), ); }
return totals;}
async function runWorkerBatches( callBatch: (rawRequests: string[]) => Promise<RenewalSummary>, rawBatches: string[][],): Promise<RenewalSummary> { const jobs: Promise<RenewalSummary>[] = []; for (let i = 0; i < rawBatches.length; i++) { jobs.push(callBatch(rawBatches[i]!)); }
const results = await Promise.all(jobs);
let totals: RenewalSummary = { ok: 0, renewed: 0, rejected: 0, outputBytes: 0, }; for (let i = 0; i < results.length; i++) { totals = mergeRenewalSummary(totals, results[i]!); } return totals;}
async function main() { const rawRequests = await buildDemoRevalidateRequests({ count: REQUESTS, invalidPercent: INVALID_PERCENT, }); const rawBatches = makeBatches(rawRequests, BATCH);
const pool = createPool({ threads: THREADS })({ revalidateTokenBatchFast }); let sink = 0;
try { const hostCheck = await runHostBatches(rawBatches); const workerCheck = await runWorkerBatches( pool.call.revalidateTokenBatchFast, rawBatches, ); if (!sameRenewalSummary(hostCheck, workerCheck)) { throw new Error("Host and worker JWT summaries differ."); }
console.log("JWT revalidation benchmark (mitata)"); console.log( "workload: verify token -> renew when allowed -> JSON.stringify", ); console.log("requests per iteration:", REQUESTS.toLocaleString()); console.log("invalid rate:", `${INVALID_PERCENT}%`); console.log("batch size:", BATCH); console.log("threads:", THREADS);
boxplot(() => { summary(() => { bench( `host (${REQUESTS.toLocaleString()} req, batch ${BATCH})`, async () => { const totals = await runHostBatches(rawBatches); sink = totals.outputBytes; }, );
bench( `knitting (${THREADS} thread${ THREADS === 1 ? "" : "s" }, ${REQUESTS.toLocaleString()} req, batch ${BATCH})`, async () => { const totals = await runWorkerBatches( pool.call.revalidateTokenBatchFast, rawBatches, ); sink = totals.outputBytes; }, ); }); });
await run(); console.log("last output bytes:", sink.toLocaleString()); } finally { pool.shutdown(); }}
if (isMain) { main().catch((error) => { console.error(error); process.exitCode = 1; });}import { task } from "knitting";
export const DEMO_JWT_SECRET = "knitting-docs-demo-hs256-secret";
const DEFAULT_TTL_SEC = 180;const DEFAULT_RENEW_WINDOW_SEC = 30;const MIN_TTL_SEC = 10;const MAX_TTL_SEC = 86_400;const MAX_RENEW_WINDOW_SEC = 3_600;
const encoder = new TextEncoder();const decoder = new TextDecoder();const keyCache = new Map<string, Promise<CryptoKey>>();
type JwtHeader = { alg: "HS256"; typ: "JWT";};
export type JwtClaims = { sub: string; sid: string; scope: string[]; iat: number; exp: number; renewUntil: number;};
export type RevalidateRequest = { token: string; nowSec?: number; ttlSec?: number; renewWindowSec?: number;};
export type RevalidateResponse = | { ok: true; renewed: boolean; token: string; sub: string; sid: string; exp: number; canRenew: boolean; } | { ok: false; renewed: false; reason: string; };
export type RenewalSummary = { ok: number; renewed: number; rejected: number; outputBytes: number;};
export type DemoRequestOptions = { count: number; nowSec?: number; invalidPercent?: number; ttlSec?: number; renewWindowSec?: number;};
export function makeBatches<T>(values: T[], batchSize: number): T[][] { const size = Math.max(1, Math.floor(batchSize)); const batches: T[][] = []; for (let i = 0; i < values.length; i += size) { batches.push(values.slice(i, i + size)); } return batches;}
export function mergeRenewalSummary( a: RenewalSummary, b: RenewalSummary,): RenewalSummary { return { ok: a.ok + b.ok, renewed: a.renewed + b.renewed, rejected: a.rejected + b.rejected, outputBytes: a.outputBytes + b.outputBytes, };}
export function sameRenewalSummary( a: RenewalSummary, b: RenewalSummary,): boolean { return a.ok === b.ok && a.renewed === b.renewed && a.rejected === b.rejected && a.outputBytes === b.outputBytes;}
function clampInt( value: unknown, fallback: number, min: number, max: number,): number { const numberValue = Number(value); if (!Number.isFinite(numberValue)) return fallback; const integer = Math.floor(numberValue); if (integer < min) return min; if (integer > max) return max; return integer;}
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value);}
function bytesToBase64Url(bytes: Uint8Array): string { let binary = ""; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]!); } const base64 = btoa(binary); return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");}
function base64UrlToBytes(base64url: string): Uint8Array { const normalized = base64url.replace(/-/g, "+").replace(/_/g, "/"); const pad = (4 - (normalized.length % 4)) % 4; const padded = normalized + "=".repeat(pad); const binary = atob(padded); const out = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { out[i] = binary.charCodeAt(i); } return out;}
function safeParseJson(raw: string): unknown | null { try { return JSON.parse(raw) as unknown; } catch { return null; }}
function fixedTimeEqual(a: Uint8Array, b: Uint8Array): boolean { if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) { diff |= a[i]! ^ b[i]!; } return diff === 0;}
function parseClaims(value: unknown): JwtClaims | null { if (!isRecord(value)) return null; const { sub, sid, scope, iat, exp, renewUntil } = value;
if (typeof sub !== "string" || sub.length === 0) return null; if (typeof sid !== "string" || sid.length === 0) return null; if (!Array.isArray(scope) || scope.length === 0) return null; if (!scope.every((item) => typeof item === "string" && item.length > 0)) { return null; } if ( !Number.isInteger(iat) || !Number.isInteger(exp) || !Number.isInteger(renewUntil) ) { return null; } if (exp <= iat) return null; if (renewUntil < exp) return null;
return { sub, sid, scope: [...scope], iat, exp, renewUntil };}
async function getHmacKey(secret: string): Promise<CryptoKey> { let keyPromise = keyCache.get(secret); if (!keyPromise) { keyPromise = crypto.subtle.importKey( "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); keyCache.set(secret, keyPromise); } return keyPromise;}
async function signInput( signingInput: string, secret: string,): Promise<string> { const key = await getHmacKey(secret); const signature = await crypto.subtle.sign( "HMAC", key, encoder.encode(signingInput), ); return bytesToBase64Url(new Uint8Array(signature));}
async function verifyToken( token: string, secret: string,): Promise<JwtClaims | null> { try { const parts = token.split("."); if (parts.length !== 3) return null; const [headerPart, payloadPart, signaturePart] = parts; if (!headerPart || !payloadPart || !signaturePart) return null;
const headerRaw = safeParseJson( decoder.decode(base64UrlToBytes(headerPart)), ); if (!isRecord(headerRaw)) return null; if (headerRaw.alg !== "HS256") return null; if (headerRaw.typ !== "JWT") return null;
const expectedSignature = await signInput( `${headerPart}.${payloadPart}`, secret, ); const expectedBytes = base64UrlToBytes(expectedSignature); const providedBytes = base64UrlToBytes(signaturePart); if (!fixedTimeEqual(expectedBytes, providedBytes)) return null;
const payloadRaw = safeParseJson( decoder.decode(base64UrlToBytes(payloadPart)), ); return parseClaims(payloadRaw); } catch { return null; }}
function parseRevalidateRequest(rawRequest: string): RevalidateRequest | null { const parsed = safeParseJson(rawRequest); if (!isRecord(parsed)) return null; if (typeof parsed.token !== "string" || parsed.token.length === 0) { return null; }
return { token: parsed.token, nowSec: parsed.nowSec as number | undefined, ttlSec: parsed.ttlSec as number | undefined, renewWindowSec: parsed.renewWindowSec as number | undefined, };}
function shouldRenewToken( claims: JwtClaims, nowSec: number, renewWindowSec: number,): boolean { if (nowSec > claims.renewUntil) return false; return nowSec >= claims.exp - renewWindowSec;}
function responseError(reason: string): RevalidateResponse { return { ok: false, renewed: false, reason };}
export async function issueTokenHost( claims: JwtClaims, secret = DEMO_JWT_SECRET,): Promise<string> { const header: JwtHeader = { alg: "HS256", typ: "JWT" }; const headerPart = bytesToBase64Url(encoder.encode(JSON.stringify(header))); const payloadPart = bytesToBase64Url(encoder.encode(JSON.stringify(claims))); const signingInput = `${headerPart}.${payloadPart}`; const signaturePart = await signInput(signingInput, secret);
return `${signingInput}.${signaturePart}`;}
export async function revalidateTokenObjectHost( rawRequest: string, secret = DEMO_JWT_SECRET,): Promise<RevalidateResponse> { const request = parseRevalidateRequest(rawRequest); if (!request) { return responseError("payload: expected JSON { token, nowSec? }"); }
const nowSec = clampInt( request.nowSec, Math.floor(Date.now() / 1000), 1, 2_147_483_647, ); const ttlSec = clampInt( request.ttlSec, DEFAULT_TTL_SEC, MIN_TTL_SEC, MAX_TTL_SEC, ); const renewWindowSec = clampInt( request.renewWindowSec, DEFAULT_RENEW_WINDOW_SEC, 0, MAX_RENEW_WINDOW_SEC, );
const claims = await verifyToken(request.token, secret); if (!claims) { return responseError("token: invalid signature, claims, or format"); }
const renewable = shouldRenewToken(claims, nowSec, renewWindowSec); if (renewable) { const renewedExp = Math.min(nowSec + ttlSec, claims.renewUntil); if (renewedExp > nowSec) { const renewedClaims: JwtClaims = { ...claims, iat: nowSec, exp: renewedExp, }; const renewedToken = await issueTokenHost(renewedClaims, secret); return { ok: true, renewed: true, token: renewedToken, sub: claims.sub, sid: claims.sid, exp: renewedExp, canRenew: nowSec < claims.renewUntil, }; } }
if (nowSec > claims.exp) { return responseError("token: expired and outside renewal policy"); }
return { ok: true, renewed: false, token: request.token, sub: claims.sub, sid: claims.sid, exp: claims.exp, canRenew: nowSec <= claims.renewUntil, };}
export async function revalidateTokenHost(rawRequest: string): Promise<string> { const response = await revalidateTokenObjectHost(rawRequest); return JSON.stringify(response);}
export const revalidateToken = task<string, string>({ f: revalidateTokenHost,});
function addSummary( totals: RenewalSummary, response: RevalidateResponse, outputBytes: number,): RenewalSummary { const next: RenewalSummary = { ok: totals.ok, renewed: totals.renewed, rejected: totals.rejected, outputBytes: totals.outputBytes + outputBytes, };
if (!response.ok) { next.rejected += 1; return next; }
next.ok += 1; if (response.renewed) next.renewed += 1; return next;}
export async function revalidateTokenBatchFastHost( rawRequests: string[],): Promise<RenewalSummary> { let totals: RenewalSummary = { ok: 0, renewed: 0, rejected: 0, outputBytes: 0, };
for (let i = 0; i < rawRequests.length; i++) { const response = await revalidateTokenObjectHost(rawRequests[i]!); const responseJson = JSON.stringify(response); totals = addSummary(totals, response, responseJson.length); }
return totals;}
export const revalidateTokenBatchFast = task<string[], RenewalSummary>({ f: revalidateTokenBatchFastHost,});
export function summarizeJsonResponses(rawResponses: string[]): RenewalSummary { const totals: RenewalSummary = { ok: 0, renewed: 0, rejected: 0, outputBytes: 0, };
for (let i = 0; i < rawResponses.length; i++) { const raw = rawResponses[i]!; totals.outputBytes += raw.length;
const parsed = safeParseJson(raw); if (!isRecord(parsed) || parsed.ok !== true) { totals.rejected += 1; continue; }
totals.ok += 1; if (parsed.renewed === true) totals.renewed += 1; }
return totals;}
function tamperToken(token: string): string { const chars = token.split(""); const last = chars.length - 1; chars[last] = chars[last] === "a" ? "b" : "a"; return chars.join("");}
export async function buildDemoRevalidateRequests( options: DemoRequestOptions,): Promise<string[]> { const count = clampInt(options.count, 1, 1, 5_000_000); const nowSec = clampInt( options.nowSec, Math.floor(Date.now() / 1000), 1, 2_147_483_647, ); const ttlSec = clampInt( options.ttlSec, DEFAULT_TTL_SEC, MIN_TTL_SEC, MAX_TTL_SEC, ); const renewWindowSec = clampInt( options.renewWindowSec, DEFAULT_RENEW_WINDOW_SEC, 0, MAX_RENEW_WINDOW_SEC, ); const invalidPercent = clampInt(options.invalidPercent, 10, 0, 95);
const renewable = await issueTokenHost({ sub: "u_demo_renewable", sid: "s_renewable", scope: ["read", "profile"], iat: nowSec - 70, exp: nowSec + Math.max(5, renewWindowSec - 3), renewUntil: nowSec + 900, });
const fresh = await issueTokenHost({ sub: "u_demo_fresh", sid: "s_fresh", scope: ["read"], iat: nowSec - 10, exp: nowSec + 180, renewUntil: nowSec + 900, });
const expiredRenewable = await issueTokenHost({ sub: "u_demo_expired_grace", sid: "s_expired_grace", scope: ["read", "write"], iat: nowSec - 200, exp: nowSec - 4, renewUntil: nowSec + 240, });
const expiredHard = await issueTokenHost({ sub: "u_demo_expired_hard", sid: "s_expired_hard", scope: ["read"], iat: nowSec - 500, exp: nowSec - 30, renewUntil: nowSec - 3, });
const badSignature = tamperToken(fresh); const malformed = "not-a-jwt";
const requests = new Array<string>(count); for (let i = 0; i < count; i++) { const withinInvalid = i % 100 < invalidPercent; let token: string;
if (withinInvalid) { token = i % 2 === 0 ? badSignature : malformed; } else { switch (i % 4) { case 0: token = renewable; break; case 1: token = fresh; break; case 2: token = expiredRenewable; break; default: token = expiredHard; } }
requests[i] = JSON.stringify({ token, nowSec, ttlSec, renewWindowSec, }); }
return requests;}Why offload JWT work
Section titled “Why offload JWT work”HMAC verification and key derivation are CPU-bound. On a busy API server handling hundreds of authenticated requests per second, that crypto work competes with route handling on the main thread. Moving it to a pool keeps your event loop responsive — especially under mixed traffic where some routes are cheap and others do substantial authentication work.