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

OptionTypeDefaultWhat it does
issuerstring | string[]noneiss must equal one of these. Makes iss required.
audiencestring | string[]noneaud (a string or a list) must contain one of these. Makes aud required. Without this option aud is not checked.
subjectstringnonesub must equal this. Makes sub required.
maxTokenAgeDurationnoneiat must be no older than this. Makes iat required.
clockTolerancenumber | Duration0Seconds of clock skew allowed on exp, nbf and iat.
currentDateDatenew Date()The moment the time claims are compared against.
requiredClaimsstring[]noneFurther claim names that must be present.
typstringnoneIgnored here. verify() and decrypt() compare it with the protected header.
recognizedHeadersstring[]noneIgnored 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, nbf and iat must be finite, non-negative numbers when present (RFC 7519 §2). A string or null is rejected, never skipped.
  • A token is expired at the second exp names: exp <= now - clockTolerance fails.
  • A token is not yet valid while nbf > now + clockTolerance.
  • With maxTokenAge, an iat in the future fails, and so does an iat older than now - 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

Causecode
A malformed option, such as issuer: "" or clockTolerance: -1ERR_INVALID_OPTION
A claim implied by an option, or listed in requiredClaims, is absentERR_JWT_CLAIM_MISSING
iss, sub or aud does not match, a time claim is not a NumericDate, or iat is in the future under maxTokenAgeERR_JWT_CLAIM_INVALID
nbf is still in the futureERR_JWT_NBF
exp has passed, or iat is older than maxTokenAgeERR_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}`;
UnitSecondsSpellings
second1s, second, seconds (or no unit)
minute60m, minute, minutes
hour3600h, hour, hours
day86400D, day, days
week604800W, week, weeks
month2592000 (30 days)M, month, months
year31536000 (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;
KeyJWS resultJWE 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 algfrom 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"undefinedthe four ECDH-ES variants
oct JWK without algundefinedundefined
CryptoKeyfrom its algorithm and hash, e.g. ["HS384"]e.g. ["A128KW", "dir"] for an AES-KW key
CryptoKeyPairfrom publicKeyfrom privateKey
JWK Setunion over its entries, oct entries skipped unless allowSymmetricsame
password stringundefinedthe three PBES2-* variants
Uint8Array, lookup functionundefinedundefined
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