Quickstart

This page walks you through a full sign/verify round trip, then a full encrypt/decrypt round trip. You can finish this page in two minutes just by copy-pasting the examples.

Sign and verify a token

The simplest JWT is an HMAC-signed token — same key on both sides.

sign-verify.ts
import { sign, verify } from "unjwt/jws";
import { generateJWK } from "unjwt/jwk";

// 1. Generate a symmetric key (once, at app start)
const key = await generateJWK("HS256");

// 2. Sign a payload (repeat per token)
const token = await sign({ sub: "user_1", role: "admin" }, key, { expiresIn: "1h" });
console.log(token);
// eyJhbGciOiJIUzI1NiIsImtpZCI6IjRj...

// 3. Verify and read the payload
const { payload, protectedHeader } = await verify(token, key);
console.log(payload);
// { sub: "user_1", role: "admin", iat: 1736860800, exp: 1736864400 }
console.log(protectedHeader);
// { alg: "HS256", typ: "JWT", kid: "4c3d..." }

What happened:

  • generateJWK("HS256") returned a symmetric JWK (kty: "oct") with a fresh kid.
  • sign(...) read the algorithm from key.alg, added iat (issued at) and exp (expiration) automatically because you passed expiresIn: "1h", and produced a compact JWS string of the form <header>.<payload>.<signature>.
  • verify(...) decoded the header, checked the signature, and validated the JWT claims (exp, nbf, iat) using the current clock.
expiresIn accepts numbers (seconds) or strings like "30s", "10m", "2h", "7D", "1W", "3M", "1Y". Same format everywhere in unjwt.

Encrypt and decrypt a token

When the payload itself is sensitive (contains a PII, a secret, anything the client shouldn't read), use encryption instead of signing.

encrypt-decrypt.ts
import { encrypt, decrypt } from "unjwt/jwe";

// 1. Encrypt with a password — unjwt uses PBES2 under the hood
const jwe = await encrypt({ creditCard: "4242...", balance: 10_000 }, "my-strong-password");
console.log(jwe);
// eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ci...

// 2. Decrypt with the same password
const { payload } = await decrypt(jwe, "my-strong-password");
console.log(payload);
// { creditCard: "4242...", balance: 10000 }

The payload is now unreadable without the password. A JWE token still looks like dotted base64 segments, but the third segment onwards is ciphertext — no amount of base64 decoding gets you the claims back.

Password-based encryption is the easiest way in, but PBKDF2 is intentionally slow (default 600,000 iterations per OWASP). For high-throughput workloads, use a symmetric or asymmetric key instead.

Where next?

You've now seen the two main workflows. Pick whichever matches what you're building: