A JSON Web Token is only as trustworthy as the verification you perform on it. Skip a check and you’ve handed an attacker a forged identity. JWTs are defined in RFC 7519, with their signatures specified by RFC 7515 (JWS), and the spec is explicit that the receiver — not the sender — is responsible for validating every claim. The checklist below is the verification work you must do before acting on a token.

1. Verify the signature — never trust the header

A JWT is header.payload.signature. The signature is computed over the base64url-encoded header and payload using the algorithm named in the header — but that header is attacker-controlled. Your library must compute the signature with the key you expect and compare it in constant time. If verification fails, the token is forged; reject it with no further processing.

2. Enforce an algorithm allowlist — reject none

This is the most exploited JWT flaw. The alg field in the header tells the verifier which algorithm was used, but if your library blindly accepts whatever alg says, an attacker can submit:

{ "alg": "none", "typ": "JWT" }

With alg: none, the signature section is empty and the spec says the token is “unsecured” — anyone can forge the payload. Real attacks have gone further: libraries that took the header algorithm and used it to pick a verification key have been tricked into treating the public RSA key as an HMAC secret, signing tokens with the public key the server itself published.

The fix: hard-code the algorithms you accept (e.g. ["RS256", "ES256"]). Reject none. Never let the token’s header decide how it gets verified.

3. Check exp, nbf, and iat with clock skew

Three timestamps govern token lifetime:

  • exp (expiration) — reject tokens past this time.
  • nbf (not before) — reject tokens that aren’t valid yet.
  • iat (issued at) — sanity-check against wild values.

RFC 7519 permits a small clock skew — typically 30 to 60 seconds — to tolerate clock drift between machines. Configure it explicitly. A missing exp should usually be rejected outright for access tokens.

4. Validate iss and aud

  • iss (issuer) — the principal that issued the token. Confirm it equals your trusted issuer URL.
  • aud (audience) — the intended recipient. Confirm it equals your own service identifier.

A token valid for one service is not valid for another. If your auth server issues tokens for both an API and a billing service with different aud values, a token minted for one must never grant access to the other.

5. Keep tokens small

JWTs travel in HTTP headers, and they’re signed — not compressed. A token carrying the user’s entire profile, permissions array, and audit trail can balloon to several kilobytes, inflating every request. Keep the payload to the claims you actually need (subject, scopes, expiry). If you need richer data, look it up server-side by the sub claim.

6. Store tokens securely

The temptation is to drop the token in localStorage for easy access from JavaScript. Don’t — localStorage is readable by any script on the page, including injected XSS. For sensitive access tokens, prefer an httpOnly, Secure, SameSite cookie so the browser manages transport and JavaScript cannot read the value. Refresh tokens belong server-side or behind the same httpOnly boundary.

7. Rotate keys

If your signing key leaks, every token ever signed with it is compromised until expiry. Mitigate by:

  • Using a key identifier (kid) in the header so you can roll keys without downtime.
  • Publishing keys via a JWKS endpoint and rotating them on a schedule.
  • Keeping key lifetimes short — months, not years.
  • Generating keys with a strong password and secret generator rather than a memorable phrase.

Inspect before you trust

When a token misbehaves — wrong claims, weird expiry, unexpected algorithm — the first step is always to look inside it. Paste the token into a JWT decoder to read the header and payload claims in plain JSON. Decoding is read-only and reveals exactly what the server received, which is usually where the bug hides.

A JWT is a signed assertion, not a proof of identity. Trust it only after you’ve verified the signature, enforced the algorithm, checked the timestamps, and validated the issuer and audience. Anything less is a security hole.