A JWT, or JSON Web Token, is three Base64URL strings joined by dots: a header, a payload and a signature. To read one, decode the first two parts, which are plain JSON, and turn the time claims exp, iat and nbf into dates. That tells you what the token says and whether it is still within its lifetime. It does not tell you that the token is genuine: the contents are only encoded, not protected, and only a signature check proves that someone who holds the key produced them. This article walks through a real token piece by piece, explains the time claims, compares HS256 with RS256, and gives honest advice on pasting tokens into any online tool.
The three parts
Here is a token made for this article, with the signing secret demo-secret. It is split into three lines for reading; the real token is one line with dots between the parts.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkxpbmggTmd1eWVuIiwiaWF0IjoxNzAwMDAwMDAwLCJuYmYiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0
.U6xZB-kxVEScuawWsaJimzYTOzDy1ZDS_N15xeZKxhoHeader. The first part decodes to {"alg":"HS256","typ":"JWT"}. It says how the token is signed (alg) and what it is (typ). It may also carry a kid, the identifier of the key to use.
Payload. The second part decodes to:
{
"sub": "1234567890",
"name": "Linh Nguyen",
"iat": 1700000000,
"nbf": 1700000000,
"exp": 1700003600
}These are the claims. Some are registered names with a fixed meaning, and some are whatever the issuer chose, such as name.
Signature. The third part is not JSON. It is the raw bytes of a signature over the first two parts exactly as written, header.payload, written in Base64URL. For HS256 it is 32 bytes, which is 43 characters in Base64URL without padding.
The encoding is Base64URL, the URL-safe variant with - and _ and no = padding. The article on Base64 explains why, and why this is encoding and not encryption.
The claims you will meet most
| Claim | Meaning |
|---|---|
iss | Issuer: who created the token |
sub | Subject: who or what it is about, often a user ID |
aud | Audience: who the token is meant for |
exp | Expiration time: on or after this moment the token must be refused |
nbf | Not before: the token must not be accepted before this moment |
iat | Issued at: when the token was created |
jti | A unique ID for this token |
These names come from RFC 7519, the JWT standard. Everything else is up to the application.
Reading exp, iat and nbf
The three time claims hold a number of seconds since 1 January 1970 00:00:00 UTC, the Unix epoch. For the token above:
| Claim | Value | Date (UTC) |
|---|---|---|
iat | 1700000000 | 14 November 2023, 22:13:20 |
nbf | 1700000000 | 14 November 2023, 22:13:20 |
exp | 1700003600 | 14 November 2023, 23:13:20 |
So the token lived for exactly 3600 seconds, one hour. Today it is long past exp, so a decoder reports it as expired. A token is "not valid yet" if the current time is before nbf, and it expires at the moment exp is reached, not one second later.
Three things go wrong with these dates.
- Milliseconds written as seconds. JavaScript's
Date.now()returns milliseconds. A token whoseexpwas filled with that value, such as1700003600000, lies tens of thousands of years in the future, and the token never expires. A good decoder says that the value is not a sensible number of seconds instead of showing a nonsense year. The article on seconds versus milliseconds shows how to tell them apart. - Clock differences. Two servers rarely agree to the second. Many systems allow a small leeway, often a minute or two, when they check
expandnbf. If a token "expires before it was issued", suspect a clock. - Time zones. The values are always UTC. A decoder shows them in your local time too, and the timestamp converter does the same for any epoch number.
Decoding is not verification
This is the most important rule about JWTs. Anyone who has a token can read it, and anyone can write a token with any contents they like. The header {"alg":"none","typ":"JWT"} is the text eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0, and a token that uses it has no signature at all. Seeing "admin": true in a decoded payload proves nothing by itself.
What proves a token is genuine is checking its signature with the right key, on the server, on every request. Two rules from the best-practice document, RFC 8725, are worth remembering:
- The server decides which algorithms it accepts. It must never trust the
algfield of the token to choose the check, which is how unsigned tokens and algorithm-confusion attacks get through. - A token is only as trustworthy as its checks of
exp,audandiss, not just its signature.
Do not put secrets in the payload. It is readable by anyone who sees the token, including a browser's local storage, a log file and a screenshot.
HS256 versus RS256
The alg value names the signing method. Two are by far the most common.
- HS256 is HMAC with SHA-256. One shared secret both signs and verifies. That is simple, but everyone who can verify can also forge: every service that holds the secret can create tokens. The HS384 and HS512 variants use longer hashes with the same idea. The signature is 32 bytes for HS256, so 43 characters.
- RS256 is RSA with SHA-256. A private key signs, and a public key verifies. The issuer keeps the private key, and any service can check tokens with the public key, which is often published as a set of keys. A 2048-bit RSA signature is 256 bytes, which is 342 characters. ES256 uses elliptic curves for the same private-and-public split, with shorter signatures.
A signature of about 43 characters therefore suggests HS256 and one of about 342 suggests a 2048-bit RSA key, though only the alg header names it for certain.
Reading a token with the decoder
- Open the JWT decoder and paste the token. A leading
Bearer, quotes and line breaks are removed for you. "Example" loads a sample. - Read the Header and Payload as formatted JSON. The Dates section shows
exp,nbfandiatin your time zone, as ISO 8601 in UTC and as a relative time, and a state: expired, not valid yet, not expired, or no expiration date. - To check an HS256, HS384 or HS512 signature, type the secret under Secret for HS256, HS384, HS512. With
demo-secretthe token above shows "Signature valid"; with any other secret it shows that the signature does not match. Tick "The secret is Base64URL" if your system stores it that way. - For RS256, ES256 and other private-key algorithms, the tool says it cannot check them. Use your language's JWT library with the public key.
If the text has five parts instead of three, it is an encrypted token (JWE) and cannot be read without the key. An error such as "A JWT has three parts" usually means a piece was cut off while copying.
Be careful with real tokens
The decoder runs in your browser. The token and the secret are processed on your device and are not uploaded. Even so, a token is a credential: anyone who holds a valid one can use it until it expires. So:
- Prefer a test token, or one that has already expired, when you only need to understand a structure.
- Never paste a live token in a chat, a ticket or a screenshot. If one has been exposed, treat it as leaked: revoke it or let it expire, and consider shortening token lifetimes.
- Do not paste your production signing secret into any web page, ours included. With an HS256 secret, anyone who learns it can forge tokens. Check production signatures in your own code or on your own machine instead.
To read the payload without any website, use your terminal. This Node.js command prints the payload as an object:
node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1],'base64url')))" "$TOKEN"A short checklist
Split on the dots, decode the first two parts, and read exp, nbf and iat as seconds in UTC. Remember that decoding proves nothing: the signature must be verified on the server with a fixed list of accepted algorithms, and aud and iss checked too. Know whether the token is HS256 with a shared secret or RS256 with a key pair. Never paste live tokens or production secrets anywhere you do not need to.