# Validate a JWT correctly

Source: https://startwithidentity.com/recipes/validate-a-jwt/
Last updated: 2026-06-19
License: content by Start with Identity. Cite the source URL.

---

A JWT (JSON Web Token, RFC 7519) is only as trustworthy as your validation of it. Decoding a JWT is not validating it: the payload is base64url, readable by anyone. Validation means verifying the signature and checking the claims. Skipping either step is how token forgery happens. To inspect a token interactively without trusting it, use the [JWT decoder tool](https://startwithidentity.com/tools/jwt-decoder/).

## What a JWT looks like

Three base64url segments separated by dots: `header.payload.signature`.

```
eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJ...fQ.SflKxw...
```

The header names the signing algorithm (`alg`) and key id (`kid`). The payload carries the claims. The signature covers `header.payload`.

## 1. Fetch the signing keys (JWKS)

Providers publish their public keys at a JWKS endpoint, listed as `jwks_uri` in the discovery document. A good library fetches and caches these, selecting the key whose `kid` matches the token header.

```ts
import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL('https://id.example.com/.well-known/jwks.json'),
);
```

## 2. Verify the signature and claims together

Do not verify the signature and then separately parse claims with a different code path. Pass the expected `issuer`, `audience`, and `algorithms` into the verify call so a single function enforces all of them.

```ts
const { payload } = await jwtVerify(token, JWKS, {
  issuer: 'https://id.example.com',        // must equal the iss claim
  audience: 'your-client-id-or-api',       // must equal/contain the aud claim
  algorithms: ['RS256'],                   // pin the algorithm; never trust the header alone
  clockTolerance: 5,                       // seconds of leeway for clock skew
});

// payload is now trusted: { sub, iss, aud, exp, iat, ... }
```

`jwtVerify` checks the signature against the JWKS and validates `exp` (expiry) and `nbf` (not-before) automatically. By passing `issuer` and `audience`, you also enforce those.

## 3. Check the remaining claims yourself

The library validates the standard time and identity claims, but application-specific ones are on you.

```ts
if (payload.nonce && payload.nonce !== expectedNonce) {
  throw new Error('nonce mismatch'); // bind ID token to your auth request
}
if (typeof payload.sub !== 'string') {
  throw new Error('missing subject');
}
// For access tokens, check scope/roles before authorizing:
const scopes = String(payload.scope ?? '').split(' ');
if (!scopes.includes('read:reports')) throw new Error('insufficient_scope');
```

## The pitfalls that cause breaches

- **`alg: none`.** Some libraries historically accepted unsigned tokens when the header said `alg: none`. Always pin an allow-list of algorithms; never read the algorithm from the token to decide how to verify it.
- **Algorithm confusion (RS256 to HS256).** If your code passes the RSA public key as the secret to an HMAC verifier, an attacker can sign a token with that public key as the HMAC secret. Pinning `algorithms: ['RS256']` prevents this.
- **Skipping `aud`.** A token minted for another service should not be accepted by yours. Always check the audience.
- **Trusting `exp` but ignoring clock skew, or vice versa.** Use a small `clockTolerance`, not a large one.
- **Decoding without verifying.** Reading claims from an unverified token and acting on them is equivalent to having no authentication at all.

## Do not validate by calling the issuer on every request

Signature verification is local and fast once the JWKS is cached. Refresh the JWKS only when you see an unknown `kid` (key rotation). For opaque (non-JWT) access tokens, you instead use token introspection (RFC 7662) against the provider.

References: [OpenID Connect](https://startwithidentity.com/standards/openid-connect/) defines ID token validation; [OAuth 2.0](https://startwithidentity.com/standards/oauth-2-0/) covers access tokens.
