Reading a JWT without trusting it

A JWT is signed, not encrypted — anyone holding one can read it. Here is what the three segments actually contain, why decoding is not verifying, and the four checks a verifier has to make.

· 7 min read

A JSON Web Token is three chunks of Base64url separated by dots. The first two are plain JSON once decoded, and that is the single most misunderstood fact about them: a signed JWT is signed, not encrypted. Anyone who has the token can read every claim in it.

The three segments

The header says how the token is signed and, usually, which key signed it:

{ "alg": "RS256", "typ": "JWT", "kid": "k1" }

The payload is a bag of claims. Some are registered by the specification — iss, sub, aud, exp, nbf, iat, jti — and the rest are whatever the issuer decided to include.

The signature covers the first two segments joined by a dot. It guarantees that nobody changed them. It guarantees nothing about confidentiality.

Decoding is not verifying

Decoding needs no key, so it tells you what a token claims and nothing about whether those claims are true. Verifying needs the issuer's key and answers a different question: did this issuer really mint this token, and is it still valid?

This is why the JWT Decoder here deliberately does not offer signature verification. A tool that asks you to paste a signing secret into a web page is teaching a habit that leaks secrets. Verification belongs in your application, against a key fetched from the issuer's JWKS endpoint.

The four checks a verifier must make

  1. The signature, against a key you fetched from the issuer — not one embedded in the token. A jku or x5u header pointing at a URL is an attacker-controlled input, not a key source.
  2. The algorithm, pinned to what you expect. Accepting whatever alg says is how the alg: none and RS256-to-HS256 confusion attacks work: in the second, an attacker signs a token with your public key as an HMAC secret, and a library that trusts the header verifies it happily.
  3. The expiryexp, and nbf if present. Allow a small clock skew, seconds not minutes.
  4. The audience and issueraud must name your API and iss must be the issuer you trust. A token minted for a different service by the same identity provider decodes perfectly and must still be rejected.

Practical consequences

Because the payload is readable, never put anything sensitive in a claim. If you must, use JWE, which actually encrypts.

Because a bearer token is a credential, treat any token you have pasted into a tool you do not control as disclosed, and rotate it. That is also why this site's decoder runs entirely in your browser and is excluded from the recently-used list — you can confirm both in your network tab and your local storage.

Because tokens cannot be revoked once issued, keep lifetimes short. A token with a year of validity is a password you cannot change. If you need revocation, you need a token introspection endpoint or a short-lived access token paired with a refresh token.

When a valid-looking token is rejected

In rough order of likelihood: exp has passed (check against the server's clock, not yours); aud names a different service; iss is a different tenant of the same provider; the kid refers to a key that has been rotated out of the JWKS; or the token was truncated in transit — an Authorization header that has passed through a proxy with a header size limit is a classic.

Tools mentioned in this guide

Esc

Loading the catalog…