JWS
JWS — signed tokens
A JSON Web Signature (RFC 7515) is a JWT whose payload is signed: the data travels in the clear (base64url-encoded), but anyone who tampers with it invalidates the signature.
Import:
import { sign, verify } from "unjwt/jws";
// or from the flat barrel:
import { sign, verify } from "unjwt";
Basic usage
import { sign, verify } from "unjwt/jws";
import { generateJWK } from "unjwt/jwk";
const key = await generateJWK("HS256");
const token = await sign({ sub: "user_1" }, key, { expiresIn: "1h" });
const { payload } = await verify(token, key);
console.log(payload);
// { sub: "user_1", iat: 1736860800, exp: 1736864400 }
The algorithm (HS256) was read from key.alg. The iat and exp claims were added because you passed expiresIn. Claim validation ran automatically on verify because the payload is a JSON object.
Signed-in-the-clear, not encrypted
A JWS is not private. Decoding the second segment of the token with any base64url decoder reveals the payload:
import { base64UrlDecode } from "unjwt/utils";
const [, payloadSegment] = token.split(".");
console.log(base64UrlDecode(payloadSegment));
// {"sub":"user_1","iat":1736860800,"exp":1736864400}
What a JWS gives you is integrity and authenticity: flipping a bit in the payload changes its base64url string, which invalidates the signature, which makes verify() throw. If you need confidentiality instead (or as well), use JWE.
Symmetric vs. asymmetric
Two families of signing keys, chosen by the algorithm:
- Symmetric (
HS256,HS384,HS512) — one secret key signs and verifies. Use when signer and verifier are the same party (e.g. your API signs tokens and your API verifies them). - Asymmetric (
RS*,PS*,ES*,Ed25519/EdDSA) — a private key signs, a public key verifies. Use when anyone should be able to verify without holding signing power (e.g. you publish a JWKS endpoint; third parties verify).
Asymmetric round trip:
const { privateKey, publicKey } = await generateJWK("ES256");
const token = await sign({ sub: "user_1" }, privateKey, { expiresIn: "1h" });
const { payload } = await verify(token, publicKey);
Full algorithm table: JWS algorithms →.
Standard claims, automatic
When the payload is an object, verify() runs claim validation automatically (no opt-in needed):
exp— rejected if expired (past current time minusclockTolerance).nbf— rejected if used before its "not before" timestamp.iat— must be a valid number if present.aud,iss,sub,typ,maxTokenAge,requiredClaims— checked when you pass the matching option.
const { payload } = await verify(token, key, {
issuer: "https://auth.example.com",
audience: "my-api",
maxTokenAge: "24h",
});
See validateJwtClaims for the full option list.
typ header, because typ is attacker-controlled. A token whose payload happens to look like JWT claims gets validated even if typ is absent or exotic. Pass validateClaims: false only when you've genuinely chosen JWS-for-arbitrary-bytes semantics.Going further
- Signing in depth → — every option on
sign(), includingb64: false(unencoded payload) andkidfallback. - Verifying in depth → — JWKSet retry, dynamic key lookup, allowlist inference, advanced claim options.
- Multi-signature → — more than one signer on a single payload (JSON Serialization).
- Algorithms → — picking between HMAC, RSA-PSS, ECDSA, EdDSA.