JSON Web Tokens (JWTs) are the backbone of modern authentication — they carry user identity and permissions across APIs and services. But they are also opaque by design: a raw JWT looks like random noise. This guide explains what is inside a JWT, how to decode it, and the security traps developers fall into.
What Is a JWT?
A JWT is a compact, URL-safe string that encodes a set of claims (key-value pairs) and optionally carries a cryptographic signature. It is used to pass identity and authorization data between parties.
A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Three Base64URL-encoded parts separated by dots (.):
- Header — algorithm and token type
- Payload — the claims (user data)
- Signature — cryptographic proof of authenticity
Decoding a JWT
Try the ZeroTool JWT Decoder →
Paste any JWT and instantly see the decoded header, payload, and signature details. The tool works entirely in your browser — your token never leaves your machine.
What Decoding Reveals
Decoding the example token above:
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"sub": "1234567890",
"name": "Alice",
"iat": 1516239022
}
Decoding is trivial — Base64URL decoding with no secret needed. This means the payload is not encrypted and not secret. Never put sensitive data (passwords, payment info, private API keys) in a JWT payload.
Standard Claims
The JWT specification defines several registered claims:
| Claim | Meaning |
|---|---|
sub | Subject — who the token is about (typically a user ID) |
iss | Issuer — who created the token |
aud | Audience — who the token is intended for |
exp | Expiration time (Unix timestamp) |
iat | Issued at (Unix timestamp) |
nbf | Not before — token not valid before this time |
jti | JWT ID — unique identifier for the token |
exp is critical: always check it. A token is only valid between nbf (or iat) and exp.
Signature Algorithms
HMAC (HS256, HS384, HS512)
A shared secret key is used for both signing and verification. Simple, but requires that both the issuer and verifier share the same secret.
Signature = HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)
If the secret is weak (e.g., "secret" or "123456"), the token can be brute-forced.
RSA / ECDSA (RS256, RS384, ES256, ES384)
Asymmetric algorithms: the issuer signs with a private key, and verifiers check the signature with a public key. The public key can be distributed freely — only the holder of the private key can create valid tokens.
This is the right choice for multi-service architectures where multiple services need to verify tokens but only one service should issue them.
Verifying a JWT in Code
Node.js (jsonwebtoken)
const jwt = require('jsonwebtoken');
// Verify with a symmetric secret
try {
const decoded = jwt.verify(token, 'your-secret-key');
console.log(decoded.sub); // user ID
} catch (err) {
// TokenExpiredError, JsonWebTokenError, etc.
console.error('Invalid token:', err.message);
}
Python (PyJWT)
import jwt
try:
payload = jwt.decode(token, "your-secret-key", algorithms=["HS256"])
print(payload["sub"])
except jwt.ExpiredSignatureError:
print("Token has expired")
except jwt.InvalidTokenError as e:
print(f"Invalid token: {e}")
Go (golang-jwt/jwt)
import "github.com/golang-jwt/jwt/v5"
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte("your-secret-key"), nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
fmt.Println(claims["sub"])
}
The alg: none Vulnerability
Some early JWT libraries had a catastrophic flaw: if the alg header was set to "none", they would accept the token without verifying the signature — allowing anyone to craft arbitrary tokens.
Always explicitly specify the expected algorithm when verifying:
# Wrong — accepts any algorithm including "none"
jwt.decode(token, key)
# Correct — only accepts HS256
jwt.decode(token, key, algorithms=["HS256"])
Never allow "none" as a valid algorithm in your verification code.
Where to Store JWTs in the Browser
| Storage | XSS Risk | CSRF Risk | Notes |
|---|---|---|---|
localStorage | High | None | XSS can steal the token; no mitigation possible |
sessionStorage | High | None | Same as localStorage |
HttpOnly cookie | Low | Medium | XSS cannot read it; use SameSite=Strict to mitigate CSRF |
The safest option is an HttpOnly; Secure; SameSite=Strict cookie. JWTs in localStorage are accessible to any JavaScript on the page — a single XSS vulnerability exposes all tokens.
JWT vs Session Tokens
JWTs are stateless: the server does not need to look them up in a database on every request, which scales well. But this means you cannot revoke a JWT before it expires.
If you need immediate revocation (e.g., log out, user banned, password changed), you have two options:
- Keep JWTs short-lived (5-15 minutes) and use refresh tokens
- Maintain a server-side revocation list (denylist) — this makes JWTs essentially stateful
Debugging Tips
When a JWT-related error occurs in production:
- Decode the token with ZeroTool JWT Decoder — check
exp,iss,aud - Check clock skew — if your server clock differs from the issuer’s,
expchecks will fail inconsistently - Verify the algorithm — make sure the token’s
algmatches what your code expects - Check the audience — if
audis set, your verification must pass the expected audience
Summary
| Task | Tool / Approach |
|---|---|
| Inspect a JWT payload | ZeroTool JWT Decoder |
| Verify in Node.js | jsonwebtoken library |
| Verify in Python | PyJWT library |
| Algorithm for new systems | RS256 (asymmetric) or HS256 (symmetric with strong secret) |
| Token storage | HttpOnly cookie |
| Revocation | Short expiry + refresh tokens |
JWTs are powerful and widely supported, but they carry subtle security risks that bite developers who treat them as opaque blobs. Understanding what is inside — and what verification actually means — is the difference between secure and broken authentication.