How JWTs Work: Decoding Headers, Payloads and Claims
A JSON Web Token is three Base64URL strings and a lot of misunderstanding. Learn what each part holds, which claims matter, and why decoding is not verifying.
JSON Web Tokens are everywhere: OAuth access tokens, OpenID Connect ID tokens, session cookies in single-page apps, service-to-service auth. They are also widely misunderstood. The two misconceptions I run into most are that the payload is encrypted, and that decoding a token tells you it is valid.
This guide walks through what is inside a JWT, which claims matter, and what proper verification involves. Paste any token into the JWT Decoder to follow along.
Anatomy of a token
A signed JWT, defined in RFC 7519 with signatures specified by JWS (RFC 7515), is three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSIsImlhdCI6MTUxNjIzOTAyMn0.<signature>
Each of the first two parts is Base64URL-encoded JSON. The third is a Base64URL-encoded signature.
Header
{ "alg": "HS256", "typ": "JWT" }
alg says how the token was signed. typ is usually JWT. You may also see kid, a key ID that tells the verifier which key to use from a set.
Payload
{ "sub": "1234567890", "name": "Ada", "iat": 1516239022 }
The payload holds claims: statements about the subject and the token itself.
Signature
For HS256, the signature is an HMAC-SHA256 of base64url(header) + "." + base64url(payload) using a shared secret. For RS256 or ES256, it is an RSA or ECDSA signature made with the issuer's private key and checked with the public key.
That eyJ prefix you see on every token is simply what {" looks like in Base64. Once you know it, you will spot JWTs in logs instantly.
Registered claims worth knowing
| Claim | Name | What to check |
|---|---|---|
iss | Issuer | Is it the identity provider you trust? |
sub | Subject | The user or client the token is about |
aud | Audience | Is this token meant for your API? |
exp | Expiration | Unix time in seconds; reject if past |
nbf | Not before | Reject if the current time is earlier |
iat | Issued at | When it was created |
jti | JWT ID | Unique ID, useful for revocation lists |
The times are Unix timestamps in seconds, not milliseconds. If you want to read one, paste it into the Epoch Converter. A very common bug is comparing exp with JavaScript's Date.now(), which returns milliseconds, so every token looks like it expired in 1970.
Identity providers add their own claims too: scope or scp for OAuth scopes, roles, email, tenant IDs and so on.
Decoding is not verifying
Anyone can decode a JWT. There is no key involved; it is Base64. Decoding tells you what the token claims. Only verification tells you whether those claims were issued by someone you trust and have not been changed.
Proper verification, done by your server or API gateway, means:
- Parse the header, but do not let it decide the algorithm. Use an allow-list your server configures, for example only
RS256. - Fetch the right key: a shared secret for HMAC, or the issuer's public key, often from a JWKS endpoint like
/.well-known/jwks.json. - Verify the signature over the header and payload.
- Check
expandnbf, allowing a small clock skew. - Check
issandaudmatch what you expect.
Use a maintained library for this, such as jose in JavaScript, PyJWT in Python or golang-jwt in Go, rather than hand-rolling it.
The classic vulnerabilities
Most JWT security incidents come from step 1 being skipped:
alg: none. The JWT spec allows unsigned tokens. Early libraries accepted a token with"alg": "none"and no signature, letting anyone forge one. Always rejectnone.- Algorithm confusion. If a server uses RS256 but lets the token header choose, an attacker can switch the header to HS256 and sign with the server's public key as the HMAC secret. Libraries that trusted the header would verify it. Pin the algorithm.
- Weak HMAC secrets. HS256 tokens signed with a short secret can be brute-forced offline from a single token. Use at least 256 bits of random secret, or switch to asymmetric keys.
- Not checking
aud. A token issued for one service is accepted by another that trusts the same identity provider.
The payload is public
Because signing does not hide the payload, treat every claim as readable by the user and anyone who intercepts the token. Never put passwords, API keys or sensitive personal data in a JWT. If the content really must be confidential, JWE (RFC 7516) defines encrypted tokens, but in most systems the simpler answer is to keep the data on the server and put only an ID in the token.
Size matters as well. A JWT usually rides in an Authorization header or cookie on every request. Stuffing a hundred permissions into it adds bytes to every call, and some proxies reject very large headers.
Debugging with a decoder
When an API returns 401 Unauthorized, these are the checks I run on the token, in order:
- Is it a JWT at all? Three parts, starting with
eyJ. Some providers issue opaque tokens that cannot be decoded. - Expired? Compare
expwith the current Unix time. - Right audience? Does
audmatch the API's configured audience? - Right issuer? Staging tokens sent to production are a frequent culprit.
- Right scopes or roles? Is the permission the endpoint needs actually present?
- Algorithm? Does
algmatch what the API expects?
The RedBison decoder shows the header and payload as formatted JSON. It runs in your browser and sends nothing anywhere. Even so, a live token is a credential until it expires, so prefer expired or test tokens when you can.
Sessions or JWTs?
JWTs shine for stateless verification across services: any service with the public key can check a token without calling the identity provider. The trade-off is revocation. A signed token stays valid until exp, so logging a user out or banning them immediately requires short lifetimes plus refresh tokens, or a deny-list keyed by jti. For a single web application, a classic server-side session cookie is often simpler and easier to revoke.
Related tools
- Epoch Converter to read
exp,iatandnbf - Base64 encoder and decoder to see how Base64 works
- JSON Formatter to query large payloads