Verifying

verify(token, key, options?)

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

Parameters

NameType
tokenstring — the compact JWS
keyCryptoKey | JWKSet | JWSVerifyJWK | Uint8Array | JWKLookupFunction
options.algorithmsJWSAlgorithm[] — allowlist
options.validateClaimsboolean — force-skip claim validation
options.forceUint8Arrayboolean — always return payload as bytes
Claim optionsaudience, issuer, subject, maxTokenAge, clockTolerance, typ, currentDate, requiredClaims, recognizedHeaders

JWSVerifyJWK is the public counterpart of JWSSignJWKJWK_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

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

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

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.

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:

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.

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.

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:

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.

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 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 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:

const { payload } = await verify(token, lookupFn, {
  algorithms: ["RS256", "PS256"], // only these will be considered
});
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.

Claim validation options

These all come from the shared JWTClaimValidationOptions interface:

OptionEffect
audienceMust match (or be included in) the token's aud.
issuerMust match (or be one of) the token's iss.
subjectMust match the token's sub.
maxTokenAgeiat must be within this duration of currentDate.
clockToleranceSeconds of slack for exp/nbf/iat comparisons. Defaults to 0.
typMust match the token's typ header.
requiredClaimsArray of claim names that must be present.
currentDateOverride "now" for comparisons.
recognizedHeadersCritical-header allowlist (for RFC 7515 §4.1.11 crit).

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:

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:

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. unjwt recognizes b64 natively; anything else in crit must be listed in options.recognizedHeaders or verification fails:

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

Full signature

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

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

See also