JWT Tokens: What's Actually Inside?
You've seen them — those long strings starting with eyJ that show up in Authorization headers, cookies, and local storage. JWTs (JSON Web Tokens) are everywhere in modern authentication, and understanding their structure is essential for debugging auth issues.
Anatomy of a JWT
A JWT has three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|___________ header ___________|.|_____________ payload ______________|.|_________ signature __________|Each part is Base64url-encoded JSON (not encrypted — anyone can read it).
The Header
{
"alg": "HS256",
"typ": "JWT"
}Tells you the signing algorithm. Common values:
HS256— HMAC with SHA-256 (symmetric, uses a shared secret)RS256— RSA with SHA-256 (asymmetric, uses public/private key pair)ES256— ECDSA with P-256 curve (asymmetric, smaller keys)none— DANGER: no signature. Never accept this in production.
The Payload (Claims)
{
"sub": "user-123",
"name": "Alice",
"email": "alice@example.com",
"role": "admin",
"iat": 1716239022,
"exp": 1716242622,
"iss": "auth.myapp.com"
}Standard claims:
| Claim | Meaning |
|---|---|
sub | Subject (user ID) |
iat | Issued At (Unix timestamp) |
exp | Expiration (Unix timestamp) |
iss | Issuer (who created the token) |
aud | Audience (intended recipient) |
nbf | Not Before (valid after this time) |
You can add any custom claims you want — just don't put sensitive data in them, because the payload is readable by anyone who has the token.
The Signature
This is what makes JWTs tamper-proof:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)If anyone modifies the header or payload, the signature won't match. The server rejects the token. But the signature only proves integrity — it doesn't hide the content.
Common Debugging Scenarios
"Token expired": Decode the token, check the exp claim. Convert the Unix timestamp to a date. Is your server clock off? Is the token lifetime too short?
"Invalid signature": The token was signed with a different secret/key than what your server expects. Common in multi-environment setups where staging and production use different secrets.
"Token malformed": The token doesn't have three dot-separated parts, or the Base64 decoding fails. Often caused by truncation when copying tokens.
Security Considerations
- Don't store sensitive data in the payload. It's Base64-encoded, not encrypted. Anyone with the token can read the claims.
- Always verify the signature server-side. Never trust a JWT without verification.
- Set reasonable expiration times. Short-lived access tokens (15-60 min) + long-lived refresh tokens is the standard pattern.
- Use HTTPS. JWTs in transit can be intercepted on unencrypted connections.
JWT in Code
// Node.js — verify a token
import jwt from 'jsonwebtoken';
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded.sub); // user ID
} catch (err) {
if (err.name === 'TokenExpiredError') {
// handle refresh
}
}# Python
import jwt
try:
payload = jwt.decode(token, SECRET, algorithms=['HS256'])
print(payload['sub'])
except jwt.ExpiredSignatureError:
# handle refresh
passDecode your token now: JWT Decoder — paste any JWT, see the header, payload, and expiration instantly. No data is sent to any server.