Claims and durations
validateJwtClaims() checks a claims object against your expectations, Duration spells a span of time, and the inference helpers say which algorithms a key can stand for.
#validateJwtClaims()
Claims are the fields inside a JWT payload: who issued it (iss), who it is about (sub), who it is for (aud), and when it is valid (iat, nbf, exp). validateJwtClaims() checks them and throws a JWTError on the first one that fails. verify() and decrypt() call it for you on a JSON payload, after the signature or decryption has succeeded. Call it yourself when the claims reached you some other way.
import { validateJwtClaims } from "unjwt/utils";
const now = new Date("2026-10-15T12:00:00Z"); // 1792065600
const claims = {
iss: "https://auth.example.com",
sub: "user_42",
aud: ["api", "web"],
iat: 1792065540,
exp: 1792066200,
jti: "t-1",
};
validateJwtClaims(claims, {
issuer: "https://auth.example.com",
audience: "api",
subject: "user_42",
maxTokenAge: "5m",
currentDate: now,
}); // passes, returns undefined#Signature
function validateJwtClaims(claims: JWTClaims, options?: JWTClaimValidationOptions): void;#Options
| Option | Type | Default | What it does |
|---|---|---|---|
issuer | string | string[] | none | iss must equal one of these. Makes iss required. |
audience | string | string[] | none | aud (a string or a list) must contain one of these. Makes aud required. Without this option aud is not checked. |
subject | string | none | sub must equal this. Makes sub required. |
maxTokenAge | Duration | none | iat must be no older than this. Makes iat required. |
clockTolerance | number | Duration | 0 | Seconds of clock skew allowed on exp, nbf and iat. |
currentDate | Date | new Date() | The moment the time claims are compared against. |
requiredClaims | string[] | none | Further claim names that must be present. |
typ | string | none | Ignored here. verify() and decrypt() compare it with the protected header. |
recognizedHeaders | string[] | none | Ignored here. verify() and decrypt() use it for crit. |
Every expectation you set is checked; none is skipped for being empty. An empty string, a non-string, or a list holding one is ERR_INVALID_OPTION. An empty audience or issuer array is accepted and matches no token.
#Behaviour
exp,nbfandiatmust be finite, non-negative numbers when present (RFC 7519 §2). A string ornullis rejected, never skipped.- A token is expired at the second
expnames:exp <= now - clockTolerancefails. - A token is not yet valid while
nbf > now + clockTolerance. - With
maxTokenAge, aniatin the future fails, and so does aniatolder thannow - maxTokenAge - clockTolerance.
validateJwtClaims({ ...claims, exp: 1792065599 }, { currentDate: now });
// JWTError ERR_JWT_EXPIRED: JWT "exp" (Expiration Time) Claim validation failed: Token has expired (exp: 2026-10-15T11:59:59.000Z)
// error.cause: { jti: "t-1", iat: 1792065540, exp: 1792065599 }
validateJwtClaims({ ...claims, exp: 1792065599 }, { currentDate: now, clockTolerance: "1m" }); // passes
validateJwtClaims({ sub: "x" }, { issuer: "a", requiredClaims: ["tenant"] });
// JWTError ERR_JWT_CLAIM_MISSING: Missing required JWT Claims: tenant, iss
validateJwtClaims(claims, { audience: ["admin"], currentDate: now });
// JWTError ERR_JWT_CLAIM_INVALID: Invalid JWT "aud" (Audience) Claim: Expected admin, got api, web
validateJwtClaims({ ...claims, iat: 1792062000 }, { currentDate: now, maxTokenAge: "10m" });
// JWTError ERR_JWT_EXPIRED: JWT "iat" (Issued At) Claim validation failed: Token is too old (maxTokenAge: 10m, iat: 2026-10-15T11:00:00.000Z)
validateJwtClaims({ exp: "1700000000" }, { currentDate: now });
// JWTError ERR_JWT_CLAIM_INVALID: JWT "exp" (Expiration Time) Claim must be a number.#Errors
| Cause | code |
|---|---|
A malformed option, such as issuer: "" or clockTolerance: -1 | ERR_INVALID_OPTION |
A claim implied by an option, or listed in requiredClaims, is absent | ERR_JWT_CLAIM_MISSING |
iss, sub or aud does not match, a time claim is not a NumericDate, or iat is in the future under maxTokenAge | ERR_JWT_CLAIM_INVALID |
nbf is still in the future | ERR_JWT_NBF |
exp has passed, or iat is older than maxTokenAge | ERR_JWT_EXPIRED, with cause: { jti, iat, exp } |
#Duration and computeDurationInSeconds()
Every option that takes a span of time (expiresIn, notBeforeIn, maxTokenAge, clockTolerance, session maxAge) accepts a Duration: an integer of seconds, or an integer followed directly by a unit.
type Duration = number | `${number}` | `${number}${Unit}`;| Unit | Seconds | Spellings |
|---|---|---|
| second | 1 | s, second, seconds (or no unit) |
| minute | 60 | m, minute, minutes |
| hour | 3600 | h, hour, hours |
| day | 86400 | D, day, days |
| week | 604800 | W, week, weeks |
| month | 2592000 (30 days) | M, month, months |
| year | 31536000 (365 days) | Y, year, years |
Units are case-sensitive: m is minutes, M is months, and d is not a unit. No space is allowed between the number and the unit, and fractions are refused.
computeDurationInSeconds(duration) converts a Duration to a positive integer number of seconds.
import { computeDurationInSeconds } from "unjwt/utils";
computeDurationInSeconds(30); // 30
computeDurationInSeconds("10m"); // 600
computeDurationInSeconds("1hour"); // 3600
computeDurationInSeconds("7D"); // 604800
computeDurationInSeconds("3M"); // 7776000
computeDurationInSeconds(0);
// JWTError ERR_INVALID_OPTION: Duration must be a positive integer number of seconds.
computeDurationInSeconds(1.5);
// JWTError ERR_INVALID_OPTION: Duration must be an integer number of seconds.
computeDurationInSeconds("10d");
// JWTError ERR_INVALID_OPTION: Invalid duration format. Must be an integer or a string like '10m', '2h', '7D', etc.#inferJWSAllowedAlgorithms() and inferJWEAllowedAlgorithms()
An algorithm allowlist stops a token from choosing how it is checked. When you omit algorithms, verify() and decrypt() build the allowlist with these two functions. They return the algorithms a key unambiguously stands for, or undefined when the key does not say, in which case you must pass algorithms yourself.
function inferJWSAllowedAlgorithms(
key: unknown,
options?: { allowSymmetric?: boolean },
): JWSAlgorithm[] | undefined;
function inferJWEAllowedAlgorithms(
key: unknown,
options?: { allowSymmetric?: boolean },
): KeyManagementAlgorithm[] | undefined;| Key | JWS result | JWE result |
|---|---|---|
JWK with alg | [alg] for a JWS algorithm, else undefined | [alg] for a key management algorithm, else undefined; an oct key whose alg is a content encryption algorithm gives ["dir"] |
EC JWK without alg | from crv: ["ES256"], ["ES384"] or ["ES512"] | ["ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW"] |
OKP JWK without alg, crv: "Ed25519" | ["EdDSA", "Ed25519"] | undefined |
OKP JWK without alg, crv: "X25519" | undefined | the four ECDH-ES variants |
oct JWK without alg | undefined | undefined |
CryptoKey | from its algorithm and hash, e.g. ["HS384"] | e.g. ["A128KW", "dir"] for an AES-KW key |
CryptoKeyPair | from publicKey | from privateKey |
| JWK Set | union over its entries, oct entries skipped unless allowSymmetric | same |
password string | undefined | the three PBES2-* variants |
Uint8Array, lookup function | undefined | undefined |
import { generateJWK, generateKey } from "unjwt/jwk";
import { inferJWEAllowedAlgorithms, inferJWSAllowedAlgorithms } from "unjwt/utils";
const hs256 = await generateJWK("HS256");
const { publicKey: esPublic } = await generateJWK("ES256");
const { publicKey: edPublic } = await generateJWK("Ed25519");
const { alg, ...esWithoutAlg } = esPublic;
inferJWSAllowedAlgorithms(esWithoutAlg); // ["ES256"]
inferJWSAllowedAlgorithms({ keys: [esPublic, edPublic, hs256] }); // ["ES256", "Ed25519"]
inferJWSAllowedAlgorithms({ keys: [esPublic, edPublic, hs256] }, { allowSymmetric: true });
// ["ES256", "Ed25519", "HS256"]
inferJWSAllowedAlgorithms(new Uint8Array(32)); // undefined
inferJWEAllowedAlgorithms("correct horse battery staple");
// ["PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW"]
inferJWEAllowedAlgorithms(esWithoutAlg);
// ["ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW"]
inferJWEAllowedAlgorithms(await generateJWK("A256GCM")); // ["dir"]
inferJWEAllowedAlgorithms(await generateKey("A256GCM")); // ["A256GCMKW", "dir"]allowSymmetric matches the allowSymmetricJWKSet option on verify() and decrypt(): a set published by someone else must not be able to hand you a shared secret.
#Sanitizers
sanitizeObject, sanitizeObjectCopy and safeJsonParse are the unsecure sanitizers, re-exported. They remove own properties named __proto__, prototype and constructor at every depth. unjwt parses every header and payload with safeJsonParse and copies caller-supplied headers with sanitizeObjectCopy; use them yourself on other untrusted JSON.
import { safeJsonParse, sanitizeObject, sanitizeObjectCopy } from "unjwt/utils";
safeJsonParse('{"a":1,"__proto__":{"admin":true},"n":{"constructor":2}}'); // { a: 1, n: {} }
const parsed = JSON.parse('{"name":"ada","__proto__":{"isAdmin":true}}');
sanitizeObjectCopy(parsed); // { name: "ada" }, parsed is untouched
sanitizeObject(parsed) === parsed; // true, stripped in place#See also
- verify() and decrypt(): the same options in context.
- JWS algorithms and JWE algorithms.
- Concepts: what each registered claim means.