Guide

What a decoded JWT proves, and what it does not

A JSON Web Token is not encrypted. The header and payload are Base64url text that anyone can read, and — more importantly — anyone can write. Reading a token tells you what it claims. It tells you nothing about whether those claims are true.

The three parts, and which one matters

A signed JSON Web Token is three Base64url segments joined by dots: header, payload and signature.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  .eyJzdWIiOiIxMjMiLCJyb2xlIjoidXNlciJ9
  .dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

The first two are JSON documents, encoded but not encrypted. Anyone can read them with nothing more than a Base64 decoder. The header names the signing algorithm; the payload carries the claims — who the subject is, what they may do, when the token expires.

The third segment is the only part that carries any authority. It is a signature over the first two, computed with a key. Change one character of the header or payload and the signature no longer matches.

The header and payload are assertions. The signature is the only thing that makes them evidence, and only to someone who checks it against a key they trust.

Decoding is not verifying

Decoding a token means Base64url-decoding two segments and parsing the JSON. It requires no key, it cannot fail for security reasons, and it tells you exactly one thing: what the token claims.

Verifying means recomputing the signature with a key you trust and comparing it to the one presented, then checking the registered claims. It requires a key, it can fail, and it is the only step that turns a claim into something you may act on.

The gap between the two is where real vulnerabilities live, because a decoded token looks authoritative. A debugger prints "role": "admin" in a neat box, and the instinct is to believe it. Anyone can produce that token. Forging one takes a text editor and a Base64 encoder; the forgery only fails at the point where a server checks the signature.

This is not a theoretical distinction. Libraries in several languages have shipped a decode function that does no verification at all, sitting one autocomplete away from verify. If you take one thing from this guide: search your codebase for the decode-only function and check every call site.

The alg confusion attacks

Two classic attacks come from trusting the header, which is the one part of the token an attacker fully controls.

alg: none

The JWT specification defines an unsecured token: algorithm none, empty signature. It exists for cases where the transport already provides integrity. An attacker sets the header to {"alg":"none"}, writes whatever payload they like, and leaves the signature empty. A library that reads the algorithm from the token and honours it accepts the result as valid, because it did exactly what the token asked.

RS256 downgraded to HS256

The subtler one. A server verifies with RSA: tokens are signed with a private key and verified with the public key, which is public by definition. An attacker rewrites the header to HS256, a symmetric algorithm, and signs the token using the server's public key as the HMAC secret. A library that picks its algorithm from the header now runs HMAC verification, using a key the attacker also has. The signature matches.

Both attacks have the same root cause and the same fix: the verifying side decides the algorithm. Pass the expected algorithm explicitly, reject anything else before touching the signature, and never let the token choose how it is checked.

The checks a server must run

A valid signature is necessary and not sufficient. A token can be perfectly signed and still be the wrong token. In roughly the order they should run:

  1. Algorithm. Compare the header algorithm against your expected value and reject anything else. Never accept none.
  2. Signature. Verify against a key you obtained out of band, not from the token. If the key is selected by a key identifier in the header, that identifier must only ever select from a set you control.
  3. Expiry. Check exp, and allow only a small clock skew — seconds, not minutes.
  4. Not before. Check nbf if present, with the same small tolerance.
  5. Issuer. Check iss against the issuer you expect. A correctly signed token from a different tenant or environment is still not yours.
  6. Audience. Check aud. This is what stops a token minted for one service being replayed against another that shares an issuer.
  7. Subject and scope. Only now read the claims that drive authorisation, and re-check them against what this request is actually asking to do.

Two further points that do not fit the list. Tokens are bearer credentials: anyone holding one can use it, so expiry should be short and transport must be encrypted. And a signed token cannot be withdrawn — if you need revocation, you need server-side state such as a short-lived token paired with a refresh token you can invalidate.

Using a decoder safely while debugging

Reading a token during debugging is entirely legitimate. It answers questions a log line cannot: which issuer minted this, when did it expire, which audience was it for, is that claim spelled the way the code expects.

Two habits make it safe. First, treat every token you paste anywhere as compromised from that moment: it is a live credential, and a decoder is a text box like any other. Use tokens from a test environment, or ones that have already expired, and rotate anything real that you pasted.

Second, be clear about what the tool is telling you. The JWT Decoder on this site decodes the header and payload and shows the claims with their timestamps resolved. It does not verify the signature, and it says so on the page, because verifying would mean asking you for the signing key. No decoder that does not hold your key can tell you a token is genuine — including this one.

What to take away

  • Header and payload are readable by anyone and writable by anyone. Only the signature carries authority.
  • A decode-only function in your codebase is a bug waiting to be found. Audit every call site.
  • The verifier picks the algorithm. Never take it from the token.
  • A valid signature is the start of validation, not the end: check expiry, issuer and audience too.
  • Tokens you paste into any tool should be treated as burned.