If you’ve built or debugged a login system, you’ve met the JWT — a compact token that travels between client and server to prove who someone is. Here’s how it works and how to read one safely.
Decode one yourself, no key required. Pasting the standard sample token from the JWT docs into our JWT decoder gives back its two readable halves — header
{ "alg": "HS256", "typ": "JWT" }and payload{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }— with no secret supplied. That is the single most important thing to understand about JWTs: the claims travel in the clear. The signature proves the token was issued by someone holding the key and has not been altered since, which is a completely different guarantee from confidentiality. Our decoder runs in your browser, so you can safely inspect a real token without it leaving your machine.
The three parts
A JWT is three Base64URL strings joined by dots: header.payload.signature.
- Header — metadata, mainly the signing algorithm, e.g.
{"alg":"HS256","typ":"JWT"}. - Payload — the claims, a JSON object of data about the user and token.
- Signature — a cryptographic signature over the header and payload, created with a secret (or private key), used to detect tampering.
You can split and read the first two parts with a JWT decoder.
Common claims
The payload holds claims — some standardized:
sub— subject (usually the user ID).iss— issuer (who created the token).iat— issued-at time (a Unix timestamp).exp— expiry time (a Unix timestamp). After this, the token should be rejected.aud— intended audience.
Plus any custom claims your app adds (roles, email, etc.).
How JWTs are used for auth
In a typical stateless flow: you log in, the server returns a signed JWT, and your client sends it on every request (usually in an Authorization: Bearer … header). The server verifies the signature and checks exp — if valid, it trusts the claims without a database lookup. That statelessness is the main appeal.
Signed, not (usually) encrypted
Most JWTs are signed (JWS), not encrypted. Signing proves integrity and authenticity, but the payload is only Base64URL-encoded — anyone can read it. So:
- Never put secrets in the payload (passwords, API keys, private data).
- Encryption is a separate thing (JWE) and much less common.
This is the point at which most JWT confusion resolves, and it resolves as output. We pasted the token from the JWT specification into our own JWT decoder and supplied no key of any kind. It returned the header
{
"alg": "HS256",
"typ": "JWT"
}
and the payload
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Nothing was decrypted, because nothing was encrypted. The two segments are Base64url text, and the signature protects them from modification, not from reading. Anyone holding the token can read every claim in it, which is why a JWT must never carry anything you would not put in a URL.
Security rules that matter
- Always verify the signature server-side with your secret/public key. Decoding (like our tool does) is not verification.
- Check
exp. An expired token must be rejected. - HS256 vs. RS256: HS256 uses one shared secret; RS256 uses a private key to sign and a public key to verify — better when multiple services need to verify but not issue.
- Never paste a real secret key into an online tool. Decoding a token to inspect claims is fine; verifying requires the key and belongs in your own code.
Reading a token
To debug an auth issue — is the token expired? does it have the right role? — decode it and inspect the payload. The free JWT decoder shows the header and payload and converts exp to a readable time, all locally in your browser so the token is never uploaded.
That also settles the practical question of what to log. { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } is safe to print — it is already readable to anyone who has the token. The third segment is not: it is the only part that proves the first two were not edited, and pasting a full token into a bug report hands over a working credential until it expires.
A decoded example
Take this widely used sample token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Decoding the first two parts gives:
// header
{ "alg": "HS256", "typ": "JWT" }
// payload
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
The third part is the signature — you can’t “read” it; it only proves the first two weren’t changed.
HS256 vs. RS256 at a glance
| HS256 | RS256 | |
|---|---|---|
| Family | HMAC-SHA256 (symmetric) | RSA-SHA256 (asymmetric) |
| Keys | one shared secret | private key signs, public key verifies |
| Who can mint a valid token | anyone holding the secret | only the private-key holder |
| Best when | one service issues and verifies | many services verify, one issues |
Working with exp and iat
exp and iat are Unix timestamps — seconds since 1970-01-01 UTC, not milliseconds. The 1516239022 above is 2018-01-18. Convert them with the timestamp converter, or read the Unix timestamp guide if the numbers look unfamiliar.
Two more security gotchas
- Reject
alg: noneand always pin the algorithm you expect — otherwise an attacker can strip the signature or downgrade RS256 to HS256. - JWTs are hard to revoke before
exp. Keep lifetimes short and use refresh tokens for anything sensitive.
Remember: our decoder only decodes — it never verifies. Treat a decoded-but-unverified payload as untrusted until your server checks the signature.