Guide

A JSON Web Token (JWT) is three base64url parts joined by dots: a header, a payload of claims, and a signature. The payload might say sub (who), aud (which service it's for) and exp (when it expires). Any service that holds the verification key can check the signature without calling the issuer. That is why JWTs are popular for APIs.

The payload is readable by anyone. Base64url is encoding, not encryption. Never put a secret in a JWT. Only the signature stops tampering.

Walking through it

The validator has to check several things. Skipping any one of them has caused real breaches.

Python
def verify(token, key, audience):
    head, body, sig = token.split(b".")       # sketch: b64url helpers and sign() omitted
    if json.loads(unb64u(head))["alg"] != "HS256":
        raise ValueError("alg not allowed")   # pin the algorithm
    expected = b64u(hmac.new(key, head + b"." + body, hashlib.sha256).digest())
    if not hmac.compare_digest(expected, sig):
        raise ValueError("bad signature")
    claims = json.loads(unb64u(body))
    if claims["aud"] != audience or claims["exp"] < time.time():
        raise ValueError("bad claims")
    return claims

Run against a valid token for orders-api, plus two bad ones, the full version of this code prints:

user_42
rejected: alg not allowed
rejected: bad signature

The first line is the valid token's sub. The none token is rejected by the algorithm pin. The token signed with the wrong key is rejected by the signature check.

The known attacks

  • alg: none. Some early libraries accepted an unsigned token whenever the header said none. The attacker just edits the claims.
  • Algorithm confusion. A service expects RS256, verifying with the public RSA key. The attacker sends an HS256 token and signs it with that public key. The library treats the public key as an HMAC secret, and the forgery passes. Fix it by pinning the expected algorithm in code, never by reading it from the header.
  • Missing aud, iss or exp checks. A token issued for a billing API gets accepted by the orders API. Expired tokens are accepted forever.
  • Weak HMAC secrets. An HS256 secret that is short or guessable can be brute-forced offline, because the attacker has the whole token.

Tip

Use a maintained library and pass the expected algorithm, issuer and audience explicitly. Don't hand-roll verification in production. The sketch above shows what the library should be doing.

When a JWT is the wrong tool

A JWT can't be revoked before it expires. If a user is banned or a laptop is stolen, the token stays valid until exp. Two fixes are common:

  1. Keep access tokens short-lived, around minutes, and use a refresh token that the server can revoke.
  2. Check a revocation list or the session store on every request, which removes most of the benefit of stateless tokens.

For a single web app with one backend, a server-side session is usually simpler and easier to revoke. JWTs earn their place when many independent services must verify identity without a shared session store.

Common mistakes

  • Trusting the alg header, or accepting none.
  • Putting personal data or secrets in the payload.
  • Using long-lived tokens with no revocation path.
  • Skipping aud and iss checks, so tokens from one service work on another.

Check yourself

  • Why does pinning the algorithm stop algorithm confusion? Answer: the verifier decides the algorithm, so an attacker can't turn an RSA public key into an HMAC secret.
  • Why is a JWT payload not private? Answer: it is base64url-encoded, which anyone can decode. Only the signature provides integrity.

Key takeaways

  • A JWT is signed, not encrypted. Its contents are readable by anyone.
  • Pin the algorithm, and check the signature, exp, aud and iss.
  • Access tokens are hard to revoke, so keep them short-lived.
  • Use a session when one backend holds all the state.

Further learning

The OAuth 2.0 Threat Model and Security Best Current Practice (RFC 9700) covers token misuse in detail.

RFC 9700: OAuth 2.0 Security Best Current PracticeThis document describes best current security practice for OAuth 2.0. It updates and extends the threat model and security advice given in RFCs 6749, 6750, and 6819 to incorporate practical experiences gathered since …rfc-editor.org

Next, see how these tokens are issued in OAuth and OpenID Connect.