JWTs show up in virtually every modern web app — auth headers, refresh flows, API access control. They’re also one of the most consistently misused standards in the wild. If you’re building with them, understanding what’s inside the token, what decoding versus verifying actually means, and which shortcuts quietly break your security is non-negotiable.
The Anatomy of a JWT
A JWT is three base64url-encoded strings joined by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MTI3NjQ4MDAsImV4cCI6MTcxMjg1MTIwMH0.4Xr8mNkZQWpH2TvL9uY3sKdJwFqBzEcAoMnRiVePxlU
- Header — algorithm and token type (
alg,typ) - Payload — the claims (data your app actually needs)
- Signature — HMAC or RSA signature over the first two parts
Decoded, that payload looks like this:
{
"sub": "user_123",
"email": "alice@example.com",
"iat": 1712764800,
"exp": 1712851200
}
Nothing here is encrypted. Anyone holding the token can read these values. The signature only proves the token wasn’t tampered with after it was issued — it doesn’t hide the contents.
Decoding Is Not Verifying
This distinction trips up more developers than it should.
Decoding splits the token at the dots and base64url-decodes each section. No secret needed — any tool or one-liner can do it. If you want to jwt decode online without installing anything, paste the token into IO Tools JWT Decoder and get the header, payload, and expiry breakdown instantly.
Verifying checks that the signature is valid using the secret (or public key for asymmetric algorithms). It also confirms the token hasn’t expired and that the claims match what your application expects. Skipping verification and trusting a decoded token is how authentication bypasses happen.
Here’s a Node.js snippet that verifies a JWT and handles the common edge cases:
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET;
function verifyToken(token) {
try {
const payload = jwt.verify(token, SECRET, {
algorithms: ['HS256'], // whitelist — never allow 'none'
audience: 'myapp', // validate aud claim
});
// jwt.verify throws if exp is in the past, but be explicit:
if (payload.exp < Math.floor(Date.now() / 1000)) {
throw new Error('Token expired');
}
return payload;
} catch (err) {
// Never silently swallow verification failures
throw new Error(`Invalid token: ${err.message}`);
}
}
Claims Worth Validating
The JWT spec defines standard claims your server should actively check, not just read:
| Claim | Meaning | Validate it? |
|---|---|---|
exp | Expiration timestamp | Always — stale tokens are a real attack surface |
iat | Issued-at timestamp | Optional, useful for max-age checks |
sub | Subject (usually user ID) | Yes — confirm it matches the expected user |
aud | Intended audience | Yes — prevents tokens for service A being used on service B |
Most JWT libraries validate exp automatically — but only if you configure them to. Read your library’s docs. Don’t assume it’s on by default.
Three Mistakes That Get Developers Burned
1. The alg: none Attack
The JWT spec allows an algorithm value of none, meaning no signature. Some libraries — particularly older ones — accept this and skip signature verification entirely. An attacker strips the signature, sets "alg": "none" in the header, and forges arbitrary claims. The server trusts it.
Fix: explicitly whitelist algorithms when verifying. Never accept none. The snippet above demonstrates this with algorithms: ['HS256'].
2. Not Validating Expiry
Decoding a token and trusting the payload without checking exp means a token issued months ago is still accepted. If a user’s session was revoked or an attacker stole an old token, your application would never know.
Fix: treat expiry as mandatory, not optional. To check when a specific token runs out, IO Tools JWT Expiry Checker decodes the exp claim and tells you exactly how much time is left — useful for debugging refresh flows without writing code.
3. Storing JWTs in localStorage
localStorage is readable by any JavaScript on the page. A single XSS vulnerability means an attacker can exfiltrate your user’s auth token silently. Here’s how the storage options compare:
| Storage | XSS risk | CSRF risk | Accessible from JS | Notes |
|---|---|---|---|---|
| localStorage | High | None | Yes | Avoid for auth tokens |
| sessionStorage | High | None | Yes | Same risks as localStorage |
| httpOnly cookie | None | Medium | No | Best for auth; pair with SameSite + CSRF token |
| In-memory (JS var) | Low | None | Yes (same context) | Lost on refresh; fine for short-lived tokens |
httpOnly cookies can’t be read by JavaScript at all, which eliminates the XSS theft vector entirely. The trade-off is CSRF exposure, which you handle with SameSite=Strict or a CSRF token.
JWTs vs Sessions: The Honest Trade-off
JWTs are stateless — the server validates them without querying a database. That’s useful in distributed systems where you don’t want every service hitting a shared session store.
But stateless has a real cost: you can’t revoke a JWT before it expires. If a user logs out or is compromised, the token remains valid until exp. Workarounds (token blocklists, short expiry + refresh tokens) exist, but they add complexity and often recreate what a session store already does.
Use JWTs when: you have multiple services authenticating requests independently, you want to embed roles/permissions directly in the token, or your tokens are short-lived and revocation latency is acceptable.
Use sessions when: you need immediate revocation (logout should actually work), you’re building a server-rendered app, or simplicity outweighs stateless scalability.
Inspect a Token in Seconds
Need to see what’s inside a JWT right now? Terminal:
echo "YOUR.JWT.HERE" | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
Or skip the terminal — paste the token into IO Tools JWT Decoder to get a formatted breakdown of the header and payload. Neither approach verifies the signature; they decode. For a quick expiry read without writing code, the JWT Expiry Checker gives you the exact timestamp and time remaining.
Install Our Extensions
Add IO tools to your favorite browser for instant access and faster searching
恵 Scoreboard Has Arrived!
Scoreboard is a fun way to keep track of your games, all data is stored in your browser. More features are coming soon!
Must-Try Tools
View All New Arrivals
View AllUpdate: Our latest tool was added on Apr 16, 2026
