Data the client can't fake
A JWT is header.payload.signature, each part base64url-encoded. The signature is an HMAC of the first two parts with a server secret: anyone can *read* the payload, but changing a single character breaks the signature. Add an expiry (exp) and you have a stateless session token.
Sign and verify
const b64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const fromB64url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
const jsonPart = (value) => b64url(new TextEncoder().encode(JSON.stringify(value)));
async function hmac(secret, data) {
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
return new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data)));
}
async function signToken(payload, secret) {
const head = jsonPart({ alg: "HS256", typ: "JWT" });
const body = jsonPart(payload);
return `${head}.${body}.${b64url(await hmac(secret, `${head}.${body}`))}`;
}
async function verifyToken(token, secret, now = Date.now() / 1000) {
const parts = String(token).split(".");
if (parts.length !== 3) throw new Error("Malformed token");
const expected = b64url(await hmac(secret, `${parts[0]}.${parts[1]}`));
if (!safeEqual(expected, parts[2])) throw new Error("Bad signature");
const payload = JSON.parse(new TextDecoder().decode(fromB64url(parts[1])));
if (payload.exp !== undefined && now >= payload.exp) throw new Error("Expired");
return payload;
}