# Verifying

> 

```ts
verify(token, key, options?)
```

Checks a compact JWS, validates its claims (when the payload is an object), and returns `{ payload, protectedHeader }`.

## Parameters

<table>
<thead>
  <tr>
    <th>
      Name
    </th>
    
    <th>
      Type
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        token
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
      
       — the compact JWS
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        key
      </code>
    </td>
    
    <td>
      <code>
        CryptoKey | JWKSet | JWSVerifyJWK | Uint8Array | JWKLookupFunction
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        options.algorithms
      </code>
    </td>
    
    <td>
      <code>
        JWSAlgorithm[]
      </code>
      
       — allowlist
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        options.validateClaims
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
      
       — force-skip claim validation
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        options.forceUint8Array
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
      
       — always return payload as bytes
    </td>
  </tr>
  
  <tr>
    <td>
      Claim options
    </td>
    
    <td>
      <code>
        audience
      </code>
      
      , <code>
        issuer
      </code>
      
      , <code>
        subject
      </code>
      
      , <code>
        maxTokenAge
      </code>
      
      , <code>
        clockTolerance
      </code>
      
      , <code>
        typ
      </code>
      
      , <code>
        currentDate
      </code>
      
      , <code>
        requiredClaims
      </code>
      
      , <code>
        recognizedHeaders
      </code>
    </td>
  </tr>
</tbody>
</table>

`JWSVerifyJWK` is the public counterpart of [`JWSSignJWK`](/jwt/jws/signing) — `JWK_oct<JWK_HMAC>` or a public asymmetric JWK with a signing `alg`. `JWKSet` stays fully permissive (`JWK[]`); wire JWKS are heterogeneous and the runtime filters candidates per header.

Returns `Promise<{ payload: T, protectedHeader: JWSProtectedHeader }>`.

## The simple case

```ts
const { payload, protectedHeader } = await verify(token, key);
```

If `key` is a JWK with an `alg` field (as every key produced by `generateJWK()` is), unjwt:

<steps level="4">

#### decodes the protected header,

#### checks the token's `alg` is in the allowlist inferred from the key,

#### verifies the signature,

#### parses the payload,

#### runs claim validation if the payload is a JSON object.

</steps>

## Dynamic key resolution — `JWKLookupFunction`

For OIDC/OAuth providers or anywhere the verifier picks a key based on the token's `kid`, pass a lookup function:

```ts [lookup.ts]
const { payload } = await verify(
  token,
  async (header, _rawToken) => {
    // fetch the JWK for the given kid — typically from a cache or a JWKS endpoint
    return await fetchKeyByKid(header.kid!);
  },
  { algorithms: ["RS256"] }, // required — the function returns unknowable shapes
);
```

The lookup function receives:

- `header` — the protected header (`kid`, `alg`, `typ`, `crit`, and any custom fields).
- `rawToken` — the original token string (useful for structured logging).

It can return any of: `JWK`, `JWKSet`, `CryptoKey`, `Uint8Array`, or a `string`. A `string` return is meaningful for JWE (PBES2 password); for JWS it's UTF-8 encoded and used as a raw symmetric key. Async is allowed.

<tip>

Always pass `algorithms` explicitly when using a lookup function — the library can't infer an allowlist from a function's return type. Leaving it out means no default guard against `alg` confusion attacks.

</tip>

## JWKSet — automatic key rotation

A **JWKSet** is any object with a `keys: JWK[]` array. When you pass a set — directly, or returned from a lookup function — unjwt selects candidate keys like this:

<steps level="4">

#### **Token has a kid** — only keys in the set with that exact `kid` are candidates. Typically one key, no retry.

#### **Token has no kid** — every key whose `alg` field matches the token is a candidate, tried in order until one succeeds.

#### **No candidates at all** — throws `JWTError("ERR_JWK_KEY_NOT_FOUND")` **before** any crypto attempt.

</steps>

```ts [jwks-endpoint.ts]
const jwks = await fetch("https://auth.example.com/.well-known/jwks.json").then((r) => r.json());

// With kid: O(1) selection
const { payload } = await verify(tokenFromProvider, jwks);

// Rotating set, no kid yet on old tokens — all compatible keys are tried
const rotatingSet = { keys: [newKey, legacyKey] };
const { payload: p } = await verify(oldToken, rotatingSet);
```

This is how transparent key rotation works without any retry code in userland. See the [JWKS endpoint example](/examples/jwks-endpoint) for a full walkthrough.

## Algorithm allowlist

`options.algorithms` constrains which `alg` values are acceptable. Omitting it is **safe when the key has metadata** — unjwt calls [`inferJWSAllowedAlgorithms`](/utilities#inferjwsallowedalgorithms) to derive a narrow allowlist from the key shape (a key with `alg: "HS256"` yields `["HS256"]`, an RSA public key yields both `RS*` and `PS*` variants).

When inference is impossible, unjwt throws `ERR_JWS_ALG_NOT_ALLOWED` ("Cannot infer allowed algorithms from this key; pass `options.algorithms` explicitly.") before attempting verification. Inference returns `undefined` for:

- Raw `Uint8Array` keys.
- JWKs without an `alg` field (and whose `kty`/`crv` doesn't unambiguously pin the signing alg — e.g. an RSA JWK without `alg`).
- Lookup functions that resolve to such shapes.

For these cases, pass `algorithms` explicitly:

```ts
const { payload } = await verify(token, lookupFn, {
  algorithms: ["RS256", "PS256"], // only these will be considered
});
```

<warning>

**The "alg: none" attack** — a classic JWT pitfall. unjwt rejects `alg: "none"` outright, but an overly permissive allowlist (e.g. accepting both `HS256` and `RS256` against a key that could be interpreted either way) opens the door to other confusion attacks. Keep the allowlist as narrow as the key allows.

</warning>

## Claim validation options

These all come from the shared [`JWTClaimValidationOptions`](/utilities#validatejwtclaims) interface:

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Effect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        audience
      </code>
    </td>
    
    <td>
      Must match (or be included in) the token's <code>
        aud
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        issuer
      </code>
    </td>
    
    <td>
      Must match (or be one of) the token's <code>
        iss
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        subject
      </code>
    </td>
    
    <td>
      Must match the token's <code>
        sub
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        maxTokenAge
      </code>
    </td>
    
    <td>
      <code>
        iat
      </code>
      
       must be within this duration of <code>
        currentDate
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        clockTolerance
      </code>
    </td>
    
    <td>
      Seconds of slack for <code>
        exp
      </code>
      
      /<code>
        nbf
      </code>
      
      /<code>
        iat
      </code>
      
       comparisons. Defaults to <code>
        0
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        typ
      </code>
    </td>
    
    <td>
      Must match the token's <code>
        typ
      </code>
      
       header.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        requiredClaims
      </code>
    </td>
    
    <td>
      Array of claim names that must be present.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        currentDate
      </code>
    </td>
    
    <td>
      Override "now" for comparisons.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        recognizedHeaders
      </code>
    </td>
    
    <td>
      Critical-header allowlist (for RFC 7515 §4.1.11 <code>
        crit
      </code>
      
      ).
    </td>
  </tr>
</tbody>
</table>

`validateClaims` has three states:

- **undefined** (default) — validate whenever the decoded payload is a plain JSON object (independent of the `typ` header).
- **true** — always validate claims. Same practical effect as `undefined` here, since non-object payloads (`Uint8Array`, forced-bytes) still skip validation.
- **false** — skip validation entirely (useful when signing arbitrary bytes).

## Payload typing

Pass a generic to get a typed payload:

```ts [typed.ts]
interface MyClaims {
  sub: string;
  role: "admin" | "user";
  org: string;
}

const { payload } = await verify<MyClaims>(token, key);
payload.role; // "admin" | "user"
```

Or return bytes directly:

```ts
const { payload } = await verify(token, key, { forceUint8Array: true });
payload instanceof Uint8Array; // true
```

## Critical header handling

Headers listed in the token's `crit` field must be understood by the verifier, per [RFC 7515 §4.1.11](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.11). unjwt recognizes `b64` natively; anything else in `crit` must be listed in `options.recognizedHeaders` or verification fails:

```ts
await verify(token, key, {
  recognizedHeaders: ["my-custom-crit-header"],
});
```

## Full signature

```ts
interface JWSVerifyOptions extends JWTClaimValidationOptions {
  algorithms?: JWSAlgorithm[];
  forceUint8Array?: boolean;
  validateClaims?: boolean;
}

interface JWSVerifyResult<T> {
  payload: T;
  protectedHeader: JWSProtectedHeader;
}
```

## See also

- [Signing →](/jwt/jws/signing) — the producer side.
- [Multi-signature →](/jwt/jws/multi-signature) — verifying multiple signatures at once.
- [JWK Sets →](/jwk/jwk-sets) — managing multiple keys.
