
# verify()

> Check a compact JWS against a key, a JWK Set or a key lookup, then validate its header and claims and return the payload.

```ts
import { verify } from "unjwt/jws";

const { payload, protectedHeader } = await verify(token, publicKey, {
  issuer: "https://auth.example.com",
  audience: "api",
});
// payload:         { sub: "user_1", iss: "https://auth.example.com", aud: "api", iat: 1792068494, exp: 1792072094 }
// protectedHeader: { kid: "ySN6Jmo0…", alg: "ES256", typ: "JWT" }
```

## Signature

```ts
function verify<T extends object = JWTClaims>(
  jws: string,
  key: JWSVerifyJWK | JWKSet | CryptoKey | Uint8Array | JWSKeyLookup,
  options?: JWSVerifyOptions,
): Promise<{ payload: T | string | Uint8Array; protectedHeader: JWSProtectedHeader }>;

type JWSKeyLookup = (
  header: JWSHeaderParameters, // the decoded protected header, its `alg` already checked
  token: string,
) => key | JWKSet | Promise<key | JWKSet>;
```

## Options

| Option                 | Type                            | Default               | What it does                                                          |
| ---------------------- | ------------------------------- | --------------------- | --------------------------------------------------------------------- |
| `algorithms`           | `JWSAlgorithm[]`                | inferred from the key | The `alg` values to accept. Required for raw bytes and alg-less keys. |
| `payload`              | `"json" \| "string" \| "bytes"` | `"json"`              | How to decode the payload. Sets the return type.                      |
| `validateClaims`       | `boolean`                       | `true` under `"json"` | `false` skips the claim checks below.                                 |
| `audience`             | `string \| string[]`            | none                  | Expected `aud`. Without it `aud` is not checked.                      |
| `issuer`               | `string \| string[]`            | none                  | Expected `iss`.                                                       |
| `subject`              | `string`                        | none                  | Expected `sub`.                                                       |
| `maxTokenAge`          | `Duration`                      | none                  | Rejects a token whose `iat` is older than this.                       |
| `clockTolerance`       | `number \| Duration`            | `0`                   | Seconds of skew allowed on `exp`, `nbf` and `iat`.                    |
| `requiredClaims`       | `string[]`                      | `[]`                  | Claim names that must be present.                                     |
| `currentDate`          | `Date`                          | `new Date()`          | The moment the time claims are checked against.                       |
| `typ`                  | `string`                        | none                  | Expected header `typ`, compared as a media type.                      |
| `recognizedHeaders`    | `string[]`                      | `[]`                  | `crit` extensions your code understands.                              |
| `allowSymmetricJWKSet` | `boolean`                       | `false`               | Admit `oct` entries of a JWK Set.                                     |
| `limits.keyCandidates` | `number`                        | `8`                   | Most JWK Set entries one token may be tried against.                  |

Every option is checked before the token is read. An empty `audience`, `issuer`, `subject` or `typ`, a `maxTokenAge` of `0`, or a list that is not an array of strings is `ERR_INVALID_OPTION`. An empty list such as `algorithms: []` accepts no token.

## Return shape

`protectedHeader` is the decoded header as signed. `payload` follows the `payload` option:

| `payload`           | `payload` type                       | Claims validated |
| ------------------- | ------------------------------------ | ---------------- |
| omitted or `"json"` | `T`, `JWTClaims` unless you pass one | yes              |
| `"string"`          | `string` (strict UTF-8)              | no               |
| `"bytes"`           | `Uint8Array`                         | no               |

The signer never chooses the shape: `typ` and `cty` do not change how the payload is decoded. Under `"json"` a payload that is not a JSON object is `ERR_JWS_INVALID`.

```ts
type Session = { sub: string; role: "admin" | "user" };
const { payload } = await verify<Session>(token, publicKey);
payload.role; // "admin"

const text = await sign("hello", privateKey);
await verify(text, publicKey);
// JWTError ERR_JWS_INVALID: Invalid JWS: Payload is not a JSON object.
(await verify(text, publicKey, { payload: "string" })).payload; // "hello"
(await verify(text, publicKey, { payload: "bytes" })).payload; // Uint8Array(5) [104, 101, 108, 108, 111]
```

The claims type can be a `type` alias or an `interface`. A claims type together with `payload: "string"` or `"bytes"` is a type error, and so is `validateClaims: true` with either (`ERR_INVALID_OPTION` at runtime).

## Keys

**A single key.** A public JWK, a `CryptoKey` or HMAC bytes. An asymmetric JWK still carrying `d` is `ERR_JWK_INVALID`, so a private key cannot slip into a verifier.

**A JWK Set** (`{ keys: [...] }`). The header's `kid` is a hint to pick the key ([RFC 7515 §4.1.4](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.4)). An entry is a candidate when:

- it is a JWK and not `oct`, unless `allowSymmetricJWKSet` is set;
- its `kid` equals the header `kid`, when the header has one;
- its `alg`, when it declares one, equals the header `alg`;
- its `use` is absent or `"sig"`, and its `key_ops` is absent or includes `"verify"`.

No candidate is `ERR_JWK_KEY_NOT_FOUND` before any crypto runs. Candidates are tried in order and the first that verifies wins. When none verifies, `ERR_JWS_SIGNATURE_INVALID` carries one `JWTError` per attempt as its `cause`. More than `limits.keyCandidates` candidates is `ERR_JWK_KEY_NOT_FOUND`, which bounds the work a token without a `kid` can cause.

```ts
await verify(token, { keys: [other.publicKey, publicKey] }); // picks publicKey by kid
await verify(token, { keys: [other.publicKey] });
// JWTError ERR_JWK_KEY_NOT_FOUND: No key found in JWK Set with kid "ySN6Jmo0…".
```

A JWK Set is treated as public material. Keep `allowSymmetricJWKSet` off for any set fetched from a `jwks_uri`, or anyone who can add an `oct` entry could forge tokens.

**A lookup function** receives the decoded header and the raw token and returns any of the above. Throwing, or returning `undefined` or `null`, is `ERR_JWK_KEY_NOT_FOUND` with the thrown value as `cause`.

```ts
await verify(token, async (header) => keysByKid.get(header.kid), { algorithms: ["ES256"] });
```

Strings are refused as keys with `ERR_JWK_INVALID`. See [JWK Sets](/jwk/sets) for building and rotating sets.

## Algorithm allow-list

The header names its own `alg`, and the attacker writes the header. `verify` decides what to accept before trusting it:

1. `"none"` and any `alg` outside the [supported list](/jwt/jws/algorithms) are `ERR_JWS_ALG_NOT_ALLOWED`, before the key is touched.
2. With `algorithms`, the header `alg` must be in it.
3. Without it, a JWK that declares `alg` accepts that one only.
4. Otherwise the list is inferred from the key shape (curve, hash, key type; for a JWK Set, the union over its entries). Raw bytes infer nothing, so `algorithms` is required.

```ts
await verify(hsToken, secretBytes);
// JWTError ERR_JWS_ALG_NOT_ALLOWED: Cannot infer allowed algorithms from this key; pass "options.algorithms" explicitly.
await verify(hsToken, secretBytes, { algorithms: ["HS256"] }); // ok
await verify(token, publicKey, { algorithms: ["RS256"] });
// JWTError ERR_JWS_ALG_NOT_ALLOWED: Algorithm not allowed: ES256
```

## Claims

Under `payload: "json"`, after the signature checks out, `verify` runs [`validateJwtClaims`](/utils/claims):

- `exp`, `nbf` and `iat` must be non-negative numbers when present. An `exp` at or before now is `ERR_JWT_EXPIRED`, an `nbf` in the future is `ERR_JWT_NBF`, each widened by `clockTolerance`.
- `issuer`, `audience`, `subject` and `maxTokenAge` each require their claim (`iss`, `aud`, `sub`, `iat`) and then compare it.
- `maxTokenAge` rejects an old `iat` with `ERR_JWT_EXPIRED` and a future `iat` with `ERR_JWT_CLAIM_INVALID`.

```ts
await verify(expired, publicKey);
// JWTError ERR_JWT_EXPIRED: JWT "exp" (Expiration Time) Claim validation failed: Token has expired (exp: 2026-10-15T11:49:14.000Z)
// error.cause: { jti: undefined, iat: 1792064894, exp: 1792064954 }
await verify(token, publicKey, { audience: "billing" });
// JWTError ERR_JWT_CLAIM_INVALID: Invalid JWT "aud" (Audience) Claim: Expected billing, got api
await verify(token, publicKey, { requiredClaims: ["jti"] });
// JWTError ERR_JWT_CLAIM_MISSING: Missing required JWT Claims: jti
```

## Header checks

**`typ`.** With `typ` set, the header must carry a matching `typ`. The comparison is case-insensitive and ignores an `application/` prefix on either side ([RFC 7515 §4.1.9](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9)), so `"application/AT+JWT"` matches `"at+jwt"`. Pin `typ` when one key signs several kinds of token, so one kind cannot pass as another ([RFC 8725 §3.11](https://www.rfc-editor.org/rfc/rfc8725#section-3.11)).

```ts
await verify(token, publicKey, { typ: "at+jwt" });
// JWTError ERR_JWS_INVALID: Invalid JWS: "typ" (Type) Header Parameter mismatch. Expected "at+jwt", got "JWT".
```

**`crit`.** Each listed name must be present in the header, must not be a registered name, and must be understood: `b64` always is, anything else only when listed in `recognizedHeaders`.

```ts
await verify(critToken, publicKey);
// JWTError ERR_JWS_INVALID: Unprocessed critical header parameters: urn:example:ext
await verify(critToken, publicKey, { recognizedHeaders: ["urn:example:ext"] }); // ok
```

**`b64`.** A `b64: false` token ([RFC 7797](https://www.rfc-editor.org/rfc/rfc7797)) verifies only when `crit` lists `"b64"`. The unencoded segment then goes through the same `payload` modes.

## Errors

| Code                                                                               | When                                                                                                                                                                                                 |
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ERR_INVALID_OPTION`                                                               | A malformed or empty option, or `validateClaims: true` with a non-JSON mode.                                                                                                                         |
| `ERR_JWS_INVALID`                                                                  | Not a string, not three segments, a header that is not a JSON object or lacks `alg`, a non-canonical segment, a `typ` mismatch, a `b64` or `crit` violation, a payload that does not match the mode. |
| `ERR_JWS_ALG_NOT_ALLOWED`                                                          | `"none"`, an unsupported `alg`, one outside the allow-list, or a key nothing can be inferred from.                                                                                                   |
| `ERR_JWK_KEY_NOT_FOUND`                                                            | No JWK Set candidate, too many candidates, or a lookup that threw or returned nothing.                                                                                                               |
| `ERR_JWK_INVALID`                                                                  | A string key, a private JWK, or a key that cannot serve the `alg` (Web Crypto error as `cause`).                                                                                                     |
| `ERR_JWS_SIGNATURE_INVALID`                                                        | The signature does not match. For a JWK Set, `cause` lists each attempt.                                                                                                                             |
| `ERR_JWT_EXPIRED`, `ERR_JWT_NBF`, `ERR_JWT_CLAIM_INVALID`, `ERR_JWT_CLAIM_MISSING` | A claim check failed.                                                                                                                                                                                |

## See also

- [`sign()`](/jwt/jws/sign) for the producing side.
- [`verifyMulti()`](/jwt/jws/multi) for JSON serializations.
- [Errors](/errors) for `isJWTError` and the full code list.
