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.
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
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.
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). An entry is a candidate when:
- it is a JWK and not
oct, unlessallowSymmetricJWKSetis set; - its
kidequals the headerkid, when the header has one; - its
alg, when it declares one, equals the headeralg; - its
useis absent or"sig", and itskey_opsis 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.
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.
await verify(token, async (header) => keysByKid.get(header.kid), { algorithms: ["ES256"] });Strings are refused as keys with ERR_JWK_INVALID. See 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:
"none" and any alg outside the supported list are ERR_JWS_ALG_NOT_ALLOWED, before the key is touched.algorithms, the header alg must be in it.alg accepts that one only.algorithms is required.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:
exp,nbfandiatmust be non-negative numbers when present. Anexpat or before now isERR_JWT_EXPIRED, annbfin the future isERR_JWT_NBF, each widened byclockTolerance.issuer,audience,subjectandmaxTokenAgeeach require their claim (iss,aud,sub,iat) and then compare it.maxTokenAgerejects an oldiatwithERR_JWT_EXPIREDand a futureiatwithERR_JWT_CLAIM_INVALID.
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), 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).
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.
await verify(critToken, publicKey);
// JWTError ERR_JWS_INVALID: Unprocessed critical header parameters: urn:example:ext
await verify(critToken, publicKey, { recognizedHeaders: ["urn:example:ext"] }); // okb64. A b64: false token (RFC 7797) 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()for the producing side.verifyMulti()for JSON serializations.- Errors for
isJWTErrorand the full code list.