
# 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()`](/jwt/jws/verify) and [`decrypt()`](/jwt/jwe/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.

```ts
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

```ts
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`, `nbf` and `iat` must be finite, non-negative numbers when present ([RFC 7519 §2](https://www.rfc-editor.org/rfc/rfc7519#section-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`.

```ts
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.

```ts
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.

```ts
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.

```ts
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`                                                                                                                          |

```ts
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](https://unsecure.s94.dev/safety/sanitize), 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.

```ts
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()](/jwt/jws/verify) and [decrypt()](/jwt/jwe/decrypt): the same options in context.
- [JWS algorithms](/jwt/jws/algorithms) and [JWE algorithms](/jwt/jwe/algorithms).
- [Concepts](/getting-started/concepts): what each registered claim means.
