What is a JWT token?
A JSON Web Token (JWT) is a compact, URL-safe way to represent claims (information) between two parties. JWTs are widely used for authentication — when you log in to a web application, the server often returns a JWT that your browser stores and sends with every request to prove you're authenticated.
JWT structure
A JWT consists of 3 parts separated by dots:
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMTIzIn0.signature
^HEADER^ ^PAYLOAD^ ^SIGNATURE^
Each part is Base64URL encoded.
Header
Contains the algorithm and token type:
{
"alg": "RS256",
"typ": "JWT"
}
Payload
Contains the claims (data):
{
"sub": "user123",
"email": "user@example.com",
"iat": 1712000000,
"exp": 1712086400
}
Signature
Verifies the token hasn't been tampered with. Cannot be verified without the secret key.
Common JWT claims explained
| Claim | Full name | Meaning |
|---|---|---|
sub |
Subject | User ID or identifier |
iat |
Issued At | When the token was created (Unix timestamp) |
exp |
Expiration | When the token expires (Unix timestamp) |
nbf |
Not Before | Token not valid before this time |
iss |
Issuer | Who created the token |
aud |
Audience | Who the token is intended for |
JWT security warnings
- Never paste production JWTs into any online tool — JWT tokens grant access to accounts
- JWTs are encoded, not encrypted — anyone can decode the payload
- The signature cannot be verified without the secret key
- Expired tokens should always be rejected by the server
Why Base64URL, not plain Base64?
JWTs travel in URLs, query strings, and HTTP headers, where standard Base64 breaks. Base64URL swaps the two problem characters and drops the padding:
| Standard Base64 | Base64URL | Why |
|---|---|---|
+ |
- |
+ means a space in query strings |
/ |
_ |
/ is a path separator |
= padding |
removed | = separates key/value pairs |
This is why a JWT decodes correctly in a JWT tool but produces garbage in a generic Base64 decoder that does not handle the URL-safe alphabet.
Decoding by hand
Each segment is just Base64URL. In a terminal:
# payload is the 2nd dot-separated segment.
# Base64 requires a length divisible by 4, and JWTs strip the padding — so
# re-add it, or `base64 -d` fails silently and prints nothing.
p='eyJzdWIiOiJ1c2VyMTIzIn0'
printf '%s' "$p$(printf '=%.0s' $(seq $(( (4 - ${#p} % 4) % 4 ))))" | base64 -d
That padding arithmetic is exactly the fiddly step a decoder handles for you. More importantly, note what is not in the command: no secret, no key, no verification. Anyone holding the token can read the payload.
The alg: none attack
Early JWT libraries honored a header of {"alg": "none"} and skipped signature verification entirely. An attacker could take a valid token, change "role": "user" to "role": "admin", set alg to none, strip the signature, and be trusted.
Modern libraries reject this by default, but the lesson generalizes: the server must decide which algorithm to accept, never the token. A server that reads alg from the header and trusts it is vulnerable to this and to RS256→HS256 confusion, where an attacker signs a token using the public key as an HMAC secret.
Reading exp and iat
Both are Unix timestamps — seconds since 1970-01-01 UTC, not milliseconds. A frequent bug is comparing them against JavaScript's Date.now(), which returns milliseconds, making every token look ~50,000 years expired:
// wrong
if (payload.exp < Date.now()) { /* always true */ }
// right
if (payload.exp < Math.floor(Date.now() / 1000)) { /* expired */ }
Frequently asked questions
Can I decode a JWT without the secret key?
Yes, completely. The header and payload are only encoded, not encrypted. The secret is required to verify the signature — to prove the token was not altered — but never to read it.
Is it safe to paste a JWT into an online decoder?
Only if the decoding happens in your browser. A JWT is a live credential; pasting one into a server-side tool hands over account access for as long as the token is valid. This decoder runs entirely client-side and never transmits the token. For a production token, the safest option is still to decode it locally.
Can I edit a JWT payload?
You can change it, but the signature will no longer match and any correctly configured server will reject it. That is precisely what the signature exists to prevent.
What is the difference between a JWT and a session cookie?
A session cookie is an opaque ID; the server looks up the real data in its own store. A JWT carries the data inside itself, so the server needs no lookup — which makes it fast to scale but hard to revoke. A stolen JWT stays valid until it expires.
Why is my token so large?
Every claim is stored inside the token, so payloads grow quickly. Since the token is sent on every request, large JWTs add measurable overhead. Keep claims minimal and rely on short expiry plus a refresh token.
What does "token expired" mean if the time looks correct?
Check for clock skew between the issuing and validating servers. A few seconds of drift can reject freshly issued tokens. Most libraries expose a leeway or clockTolerance setting for this.
How to decode JWT for free
- Go to JWT Decoder
- Paste your JWT token
- View decoded header and payload instantly
- Check expiry time in human-readable format
- Your token is never sent to any server