← All modules
Protocols·20 min

JSON Web Tokens & API authentication

JWTs put a signed claim in every request header. They're everywhere in modern APIs — and a few well-known implementation mistakes keep recurring.

Developer / EngineerSecurity Architect

Structure

A JSON Web Token (JWT) is three base64url-encoded segments separated by dots: a header (naming the signing algorithm), a payload (the claims — arbitrary data such as user ID and expiry), and a signature over the first two segments. The signature can be produced with a symmetric HMAC (HS256) or an asymmetric algorithm like RSA or ECDSA (RS256, ES256).

Anatomy of a JWT

Header (base64url)

{"alg": "HS256", "typ": "JWT"} — names the signing algorithm.

Payload (base64url)

{"sub": "user123", "exp": 1735689600, ...} — the claims. Encoded, not encrypted.

Signature

HMAC or RSA/ECDSA signature over "header.payload", proving neither was altered.

signature=HMAC-SHA256(base64url(header)"."base64url(payload), secret)\text{signature} = \mathrm{HMAC\text{-}SHA256}(\text{base64url(header)} \,\|\, \texttt{"."} \,\|\, \text{base64url(payload)},\ \text{secret})

A real token, decoded

Here's an actual HS256-signed JWT encoding the claims {"sub": "1234567890", "name": "Alice", "iat": 1516239022}, signed with a symmetric secret. Each of the three segments below decodes with ordinary base64url — paste the first two into any base64 decoder and you'll get readable JSON back; only the third segment (the signature) is opaque, because it's the output of an HMAC, not an encoding of anything.

A real HS256 token, segment by segment

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Decodes to {"alg":"HS256","typ":"JWT"}

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ

Decodes to {"sub":"1234567890","name":"Alice","iat":1516239022}

gJh7jdj0jULbD61KFeWRh9Ux05MSQMND99uj_GbqN_k

HMAC-SHA256 over the first two segments, using the server's secret key

What the signature does and doesn't guarantee

Verifying a JWT's signature confirms the claims haven't been altered since signing and that they were signed by a holder of the corresponding key — it says nothing about whether the token has since been revoked or is still meant to be valid, which is why expiry (exp) claims and short lifetimes matter. The payload is only encoded, not encrypted: anyone can base64-decode it and read the claims, so secrets never belong there.

JWTs vs. opaque session tokens

The alternative to a JWT is an opaque session token: a random string that means nothing on its own, looked up in a server-side database on every request. The trade-off between them is really a trade-off about where state lives.

Opaque session token

  • Server looks up session state in a database on every request
  • Revoking access is instant — just delete the server-side record
  • Doesn't scale as easily across independent services without a shared session store

JWT

  • Self-contained — any service holding the public key (or shared secret) can verify it with no database lookup
  • Scales well across microservices with no shared state
  • Can't be revoked before its expiry without extra infrastructure (a blocklist, short lifetimes, etc.)

Well-known implementation pitfalls

The "alg: none" vulnerability let an attacker submit a token whose header claims no signature algorithm was used, and some early libraries would accept it as valid — effectively an unsigned token treated as trusted.

Algorithm confusion: turning a public key into an HMAC secret

A more subtle attack targets servers that support both RS256 (asymmetric) and HS256 (symmetric) verification. If the verifier trusts the algorithm named in the token's own header rather than pinning the algorithm it expects, an attacker can take a server's public RSA key (which is, by design, public) and use it as the secret for an HS256-signed forged token. A verifier that blindly follows the header's "alg": "HS256" will compute the HMAC using that public key as the secret — and the attacker, who also has that public key, can compute the exact same HMAC.

Algorithm confusion attack

  1. 1

    Attacker obtains the server's public key

    RSA/ECDSA public keys used for RS256/ES256 verification are, by design, not secret.

  2. 2

    Attacker crafts a forged token

    They write whatever claims they want, set the header to "alg": "HS256", and sign it with HMAC using the public key as the HMAC secret.

  3. 3

    Vulnerable server verifies

    If the server reads "alg" from the token and uses HS256 verification with the same public key value, the forged signature checks out.

  4. 4

    The fix

    A correctly implemented verifier pins the expected algorithm itself and rejects any token that doesn't match — never trusting the header's own claim.

Knowledge check

Test what you just learned →

3 quick questions, with an explanation for every answer.

Up next

SSH: key exchange, host keys, and authentication

The protocol behind every remote login and git push combines the same primitives as TLS, arranged slightly differently.