A JSON Web Token (JWT) is how most modern APIs and login systems pass identity and permission data between a client and a server without a database lookup on every request. If you've ever opened your browser's dev tools and seen a long string starting with "eyJ" in a request header, that's a JWT.
The three parts
Every JWT is three Base64URL-encoded segments joined by dots: header.payload.signature.
- Header — states the token type and the signing algorithm, commonly HS256 (shared secret) or RS256 (public/private key pair).
- Payload — the actual claims: user ID, roles, expiry, and any custom data the issuer chose to include. This is readable by anyone; it is not encrypted.
- Signature — proves the header and payload haven't been altered since signing. Recomputing it requires the issuer's secret key, which is why only the server can validate a token, not just read it.
A real example, decoded step by step
Here is a sample token created for this article using the made-up secret "demo-secret-key" (it is not a real credential and protects nothing):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEwNDIiLCJuYW1lIjoiQXNoYSBSYW8iLCJyb2xlIjoiZWRpdG9yIiwiaWF0IjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjkyMDB9.mgTE_qJgMxYWao-gT69sFWGALxucPfHTMSyYgcU-ctk
- The first segment, eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9, decodes to {"alg":"HS256","typ":"JWT"} — a token signed with HMAC-SHA-256.
- The second segment decodes to {"sub":"user_1042","name":"Asha Rao","role":"editor","iat":1767225600,"exp":1767229200}.
- iat 1767225600 is 1 January 2026, 00:00:00 UTC. exp 1767229200 is 01:00:00 UTC the same day — exactly 3,600 seconds (one hour) later, so this token is valid for one hour.
- The third segment is 43 characters of Base64URL, which is the 32-byte HMAC-SHA-256 of the text "header.payload" computed with the secret key.
Paste the token into the JWT Decoder and you will see exactly the header and payload above, with the timestamps shown as dates. The decoder cannot check the signature, because that needs the secret.
Base64URL is not quite Base64
JWT segments use the URL-safe variant of Base64: the characters + and / are replaced with - and _, and the trailing = padding is dropped. That is why pasting a segment into a plain Base64 decoder sometimes fails or gives garbage — you have to swap the characters back and restore the padding first. A JWT-aware decoder does this for you.
What happens if someone edits the payload
Suppose someone changes "role":"editor" to "role":"admin" in the example above and re-encodes the payload. The second segment changes, but the signature was calculated over the original header and payload, so it no longer matches. When the server recomputes the HMAC from the received header and payload, the result differs from the signature in the token and the request is rejected. Only someone holding the secret could produce a signature that matches the edited payload.
Decoding isn't a security flaw
This trips up a lot of people the first time they see it: anyone can decode a JWT and read its payload, and that's expected, not a bug. What keeps a JWT trustworthy is the signature — change even one character of the payload and the signature no longer matches, so a server verifying the token will reject it. Decoding shows you the claims; it does not let you forge a valid token, because you'd need the issuer's secret key to produce a matching signature.
Common claims you'll see
- sub — Subject, the user or entity the token identifies.
- iss — Issuer, which server or auth system generated the token.
- aud — Audience, the intended recipient of the token.
- exp — Expiration, a Unix timestamp after which the token is no longer valid.
- iat — Issued At, a Unix timestamp of when the token was created.
Using a decoder to debug auth issues
Most "why is this user logged out" or "why does the API return 401" bugs come down to one of a few things visible directly in the decoded payload: the exp claim has already passed, the aud doesn't match what the API expects, or a role/permission claim is missing entirely. Pasting the token into a decoder turns a guessing game into a five-second check.
Common JWT errors and what they usually mean
- "Token expired" — the current time is later than exp. Request a new token, or check that the issuing and receiving servers have accurate clocks.
- "Invalid signature" — the token was modified, or the server is verifying with a different secret or public key than the one that signed it (for example after a key rotation).
- "Invalid audience" or "invalid issuer" — the aud or iss claim does not match what the API is configured to accept. The decoded payload shows what the token actually says.
- "Token not yet valid" — the token has an nbf (not before) claim in the future. Small clock differences between servers are a common cause, which is why many libraries allow a few seconds of leeway.
- Unexpected 401 on a valid-looking token — check that the algorithm in the header is the one your server expects. A server should pin the algorithms it accepts and never accept "alg":"none".
What not to put in a payload
Because the payload is only encoded, never put passwords, API keys or sensitive personal data in it. Include only what the receiving service needs, and keep the lifetime short: a stolen token works until it expires, so an hour is safer than a month. If a token must be revoked sooner, the server needs its own list of revoked tokens, since the token itself cannot be recalled.
What decoding does not tell you
A decoder cannot verify the signature — that requires the secret or public key, which never leaves the server. If you need to confirm a token is genuinely valid (not just well-formed), that check has to happen server-side, using a JWT library and the actual signing key.