A JSON Web Token (JWT) looks like a long random string, but it isn't encrypted or random at all. It's three pieces of readable data joined by dots, and anyone who has the token can decode it in seconds. What makes it trustworthy is the signature at the end, and only a server holding the right key can check that.
This guide takes one real token apart by hand, explains each claim inside it, and covers the checks a server has to make before it believes a word of it.
What a JWT is
A JWT is a compact way to pass a set of claims, such as "this is user 8412 and they may read orders," between two parties. It's defined in RFC 7519. Most JWTs you'll meet are signed tokens in the JWS compact format from RFC 7515, and they have three parts:
header.payload.signature
- Header: JSON that names the signing algorithm and the token type.
- Payload: JSON with the claims.
- Signature: proof that the header and payload haven't been changed since the issuer signed them.
Each part is encoded with Base64URL, the URL-safe variant of Base64 that uses - and _ instead of + and / and drops the = padding. That's what lets a JWT travel in a URL or an HTTP header without escaping. If Base64 itself is new to you, What Is Base64 Encoding? explains how it works.
Decoding a JWT by hand
Here's a token signed with HS256 using a demo secret (demo-secret-for-this-article-only, which you should never use for anything real):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg0MTIiLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJhcGkuZXhhbXBsZS5jb20iLCJpYXQiOjE3OTIzMTA0MDAsImV4cCI6MTc5MjMxNDAwMCwic2NvcGUiOiJvcmRlcnM6cmVhZCJ9.lvvdHJYS-MI7GWXSpSMkeWPJa3ZkHSnot7icGMkjnVI
Split it at the two dots. Then paste each of the first two parts, on its own, into the Base64 Decoder. It accepts the URL-safe alphabet and missing padding, but not the dots, so decode the parts one at a time.
The header
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
→ {"alg":"HS256","typ":"JWT"}
alg says the signature is HMAC with SHA-256. typ says this is a JWT.
The payload
{"sub":"user_8412","iss":"https://auth.example.com","aud":"api.example.com","iat":1792310400,"exp":1792314000,"scope":"orders:read"}
That's the whole secret: there isn't one. Every claim in a signed JWT is readable by anyone who has the token. The OWASP JSON Web Token Cheat Sheet has a section titled simply "Signed JWTs are not confidential."
The signature
The third part is 32 bytes of HMAC-SHA256 output, Base64URL-encoded. Decoding it gives binary, not text. What it means comes next.
The claims, explained
RFC 7519 section 4.1 registers a short set of claim names. None is required, but most tokens use several:
| Claim | Name | In the example |
|---|---|---|
iss | Issuer: who created the token | https://auth.example.com |
sub | Subject: who it's about | user_8412 |
aud | Audience: who it's meant for | api.example.com |
iat | Issued at | 1792310400 |
exp | Expiration time | 1792314000 |
nbf | Not before | (not used) |
jti | JWT ID, a unique token ID | (not used) |
Anything else, like scope here, is a custom claim that the issuer and the API agree on.
Reading exp and iat
Time claims are a "NumericDate": seconds since January 1, 1970 UTC, the same Unix time used almost everywhere. Put the values into the Timestamp Converter:
iat1792310400 = 2026-10-18T08:00:00Z (4:00 AM EDT)exp1792314000 = 2026-10-18T09:00:00Z (5:00 AM EDT)
So this token is valid for exactly one hour. Two details trip people up. First, the values are seconds, not milliseconds; JavaScript's Date.now() returns milliseconds, so compare against Date.now() / 1000. Second, RFC 7519 lets verifiers allow "some small leeway, usually no more than a few minutes, to account for clock skew" between servers. If you're curious why these numbers will need more than 32 bits soon, see The Year 2038 Problem.
What the signature proves
For HS256, the issuer computes HMAC-SHA256 over the first two parts exactly as they appear in the token, dot included:
HMAC-SHA256(secret, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg0MTIi…")
You can reproduce the example's signature yourself. Paste the header and payload with the dot between them into the SHA Generator, enter demo-secret-for-this-article-only as the HMAC key, and pick Base64 output. The SHA-256 line shows:
lvvdHJYS+MI7GWXSpSMkeWPJa3ZkHSnot7icGMkjnVI=
Swap + for - and drop the =, and you have the token's third part. Change one character of the payload, say orders:read to orders:write, and the HMAC comes out completely different. Without the secret, nobody can produce a matching signature.
So the signature proves two things: the token was created by someone with the key, and nothing in the header or payload has changed since. It doesn't hide anything. For the difference between signing, hashing and encrypting, see Hashing vs Encryption vs Encoding.
HS256 uses one shared secret for signing and checking. Algorithms like RS256 and ES256 use a private key to sign and a public key to verify, so services that only need to check tokens never hold the signing key.
Decoding is not verifying
Decoding tells you what a token says. Verifying tells you whether to believe it. Code that decodes a JWT and acts on the claims without checking the signature accepts any token anyone cares to write.
RFC 8725, the JWT best-practices document, describes real attacks that came from trusting the token's own header:
- "alg": "none". "The algorithm can be changed to 'none' by an attacker, and some libraries would trust this value and 'validate' the JWT without checking any signature."
- RS256 to HS256 confusion. An attacker changes the header to HS256, and a library that trusts it checks an HMAC using the server's RSA public key as the secret. Since that key is public, the attacker can forge a valid-looking token.
The fix, in RFC 8725 section 3.1, is that the server decides which algorithm it accepts, not the token: libraries "MUST NOT use any other algorithms," and each key must be used with exactly one algorithm.
The checks a server has to make
- Verify the signature with the expected algorithm and the right key. Reject
noneand anything else you didn't choose. - Check
exp(andnbfif present) against the current time, with at most a small leeway. - Check
issis the issuer you trust. RFC 8725 section 3.8 covers this. - Check
audincludes your service, so a token issued for another API can't be replayed against yours (section 3.9). - Check the token type when one issuer creates several kinds of JWTs, so an ID token can't be passed off as an access token (section 3.11).
Use a well-maintained JWT library for this rather than hand-written code. Hand decoding is for debugging, never for authorization.
Where JWTs go wrong in practice
- Secrets in the payload. Never put passwords, card numbers or anything private in a signed JWT. If the contents must be hidden, that's what encrypted JWTs (JWE) are for.
- Weak HMAC keys. RFC 8725 warns that HS256 tokens signed with a human-memorable password can be brute-forced offline by anyone holding one token, and says such passwords "MUST NOT be directly used as the key." Use a long random key.
- No way to log out. A JWT stays valid until it expires. OWASP notes that if you use JWTs for sessions, "you will need a solution for managing session invalidation," such as a deny list, which gives up some of the stateless appeal. Short expiry times help.
- Tokens are bearer credentials. Whoever holds an unexpired access token can use it. Treat tokens like passwords in logs, URLs and screenshots.
Is it safe to paste a JWT into a decoder?
A token from a test environment, or one that has already expired, is harmless. A live production token is a credential until its exp passes, so don't paste one into a site that sends it to a server. Our Base64 Decoder runs entirely in your browser, but the safest habit is still to use test tokens or tokens that have already expired when you debug.
FAQ
Can anyone read a JWT?
Yes, if it's a signed JWT (the usual kind). The header and payload are only Base64URL-encoded. Only encrypted JWTs (JWE) hide their contents.
Can I change the payload and keep using the token?
You can change it, but the signature won't match anymore, and a server that verifies tokens properly will reject it.
Why does my JWT have five parts instead of three?
That's an encrypted JWT in JWE compact format, which has five Base64URL parts. The payload is encrypted, so decoding the parts won't show readable claims.
Are JWT exp values in seconds or milliseconds?
Seconds. A value around 1.79 billion is a date in 2026. A 13-digit value is almost certainly milliseconds by mistake.
What's the difference between HS256 and RS256?
HS256 signs and verifies with one shared secret. RS256 signs with a private RSA key and verifies with the matching public key, so verifiers can't create tokens.