How to read a JWT — and why decoding is not verifying
What the three parts of a JSON Web Token mean, how to read claims like exp and iat, and why a decoded token proves nothing until you verify it.
By Felipe YamaokaPublished November 5, 20266 min read
The token in your logs
Sooner or later every developer meets a string like this one, in an Authorization header, a cookie or a log line:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEgTG92ZWxhY2UiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NjcyMjU2MDAsImV4cCI6MTc2NzIyOTIwMH0.fzONOB8GmSymQ8FV5MeQu77nX-m0-7MQ2-eJfTK_EmEIt looks encrypted, but it is not. That is a JSON Web Token (JWT), and most of it is plain JSON wrapped in an encoding that anyone can reverse. Knowing how to read it saves a lot of time when a login fails, an API answers 401 or a user says they were logged out for no reason. Knowing what reading it does not prove keeps you from writing a security bug.
This guide walks through the three parts of a token, the claims you will see most often and the difference between decoding and verifying.
Three parts separated by dots
A JWT in its usual compact form has exactly three segments separated by .:
- Header: a JSON object that describes the token, mainly the signing algorithm.
- Payload: a JSON object with the claims, the statements the token makes about a user or a session.
- Signature: bytes that prove the first two parts were produced by someone who holds the key.
Each segment is encoded with Base64URL, a variant of Base64 that swaps + and / for - and _ and drops the = padding, so the token can travel in URLs and headers without escaping. Base64URL is an encoding, not encryption: there is no key involved in turning it back into text.
Decoding the first segment of the token above gives:
{
"alg": "HS256",
"typ": "JWT"
}And the second segment gives the payload:
{
"sub": "42",
"name": "Ada Lovelace",
"role": "admin",
"iat": 1767225600,
"exp": 1767229200
}The third segment is not JSON. It is the raw signature, also in Base64URL, and it only makes sense to a program that has the key.
The claims you will meet most
The JWT specification, RFC 7519, registers a handful of short claim names. Your identity provider may add many others, but these are the ones worth recognizing on sight:
sub(subject): who the token is about, usually a user id. Treat it as a string, even when it looks like a number.iss(issuer): who created the token, for example the URL of your auth server.aud(audience): who the token is meant for. An API should reject a token issued for a different audience.exp(expiration time): after this moment the token must be refused.nbf(not before): before this moment the token must be refused.iat(issued at): when the token was created.jti(JWT id): a unique id, useful to block a single token.
The time claims are the ones that cause the most confusion, because they are numbers like 1767229200. They are seconds since 1 January 1970 in UTC, the Unix epoch, not milliseconds. In the example, iat is 1 January 2026 at 00:00 UTC and exp is one hour later. If you pass exp straight to new Date() in JavaScript, which expects milliseconds, you get a date in January 1970 and a token that looks expired since forever. Multiply by 1000 first. The Unix Timestamp Converter shows both readings side by side when you are not sure which one you have.
Reading a token safely
When a request fails with 401, decoding the token usually tells you why in seconds:
- Expired: compare
expwith the current time. A token that expired a few seconds ago often points to a client that does not refresh in time. - Clock skew: if
iatornbfis in the future, the clock of the server that issued the token and the clock of the one that checks it disagree. Most libraries accept a small tolerance, such as 30 or 60 seconds, for this reason. - Wrong audience or issuer: a token from your staging environment sent to production fails here.
- Missing claim: the API expects a
roleor ascopethat this kind of token does not carry.
A decoder that runs in the browser is the right place to do this. Pasting a real token into a website that sends it to a server is a bad idea: while it is valid, a token is a password. The JWT Decoder on this site decodes everything locally, accepts the token with or without the Bearer prefix and shows the time claims in your local time, so nothing leaves your device.
Even so, avoid sharing production tokens in tickets, chats or screenshots. If one leaks, revoke the session.
Decoding is not verifying
Here is the part that matters most. Decoding a token tells you what it says. It does not tell you whether that is true.
Anyone can write a token. Take the payload above, change "role": "admin" to anything you like, encode it again and glue it to the original header and signature: the result still decodes perfectly. The only thing that gives the token meaning is the signature, and checking it requires the key:
- With
HS256and the other HMAC algorithms, the same secret signs and verifies. Only the issuer and the APIs that trust it should know it. - With
RS256,ES256and other asymmetric algorithms, a private key signs and a public key verifies. Identity providers usually publish their public keys at a JWKS endpoint.
Verifying means recomputing the signature over header.payload with the key and comparing it with the third segment, then checking the claims: exp, nbf, iss and aud. In practice you never write this by hand; you call a maintained library and give it the expected values. A minimal example in Node.js with the jose package:
import { jwtVerify } from "jose";
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secret, {
issuer: "https://auth.example.com",
audience: "orders-api",
algorithms: ["HS256"],
});Note the algorithms option. A classic attack changes the header to "alg": "none" or swaps an asymmetric algorithm for a symmetric one, hoping the server trusts whatever the header says. Always tell the library which algorithms you accept.
Common mistakes
- Trusting the payload on the client. Reading
rolein the browser to decide what to show is fine for the interface. Deciding what the user is allowed to do must happen on the server, after verification. - Treating the token as secret storage. The payload is readable by anyone who sees the token. Never put passwords, personal documents or internal notes in it.
- Confusing seconds and milliseconds.
expis in seconds.Date.now()is in milliseconds. - Ignoring clock skew. A server with a wrong clock rejects valid tokens or accepts expired ones.
- Very long expirations. A stolen token is valid until
exp. Short-lived access tokens with refresh tokens limit the damage. - Checking only the signature. A valid signature on a token for another audience is still the wrong token.
Summary
A JWT is three Base64URL segments: a header, a payload of claims and a signature. Decoding the first two is safe, instant and the fastest way to debug authentication, especially the exp, iat, iss and aud claims. But a decoded token is only a claim. Before you trust anything in it, verify the signature with the right key, the accepted algorithms and the expected issuer and audience.
Keep reading
Base64 is not encryption — what it is, when to use it and the URL-safe variant
How Base64 turns bytes into text, why it hides nothing, how to handle accents and emoji correctly and when you need the URL-safe alphabet.
November 5, 20265 min readTool: Base64 Encoder/Decoder
The regex that freezes the browser — catastrophic backtracking explained
Why a short regular expression can take seconds or hang a page, how to recognize nested quantifiers and how to rewrite the pattern so it stays fast.
November 5, 20266 min readTool: Regex Tester