JWT Decoding Tutorial: How to Read and Debug JSON Web Tokens
Learn how to decode JWT tokens, understand their structure, debug authentication issues, and implement JWT security best practices in your applications.
JSON Web Tokens (JWT) are the standard for secure authentication in modern web applications. From single sign-on (SSO) systems to REST API authorization, JWTs are everywhere in today's distributed architecture.
Understanding how to decode and debug JWT tokens is crucial for developers working with APIs and authentication systems. In this tutorial, you'll learn the complete anatomy of a JWT, how to decode tokens in multiple languages, common debugging workflows, security vulnerabilities to watch for, and best practices used by production systems.
What is a JWT?
A JWT is a compact, URL-safe token format that contains claims (data) as a JSON object. The token consists of three parts separated by dots:
Header.Payload.Signature
Example JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQsW5c
JWT Structure
Header
The header typically consists of two parts: the type of token (JWT) and the signing algorithm used.
{
"alg": "HS256",
"typ": "JWT"
}
This JSON is Base64Url encoded to form the first part of the token.
Payload
The payload contains the claims — statements about an entity (typically the user) and additional metadata.
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242822,
"role": "admin"
}
Common claim types:
- Registered claims:
iss(issuer),exp(expiration),sub(subject),aud(audience) - Public claims: Custom claims registered with IANA
- Private claims: Custom claims agreed between parties
Signature
The signature is created by taking the encoded header, encoded payload, a secret, and the algorithm specified in the header:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
How to Decode a JWT
Manual Decoding
Since JWT parts are just Base64Url encoded, you can decode them manually:
- Split the token by
. - Base64Url decode each part
- Parse the JSON
Note: JWT uses Base64Url encoding (not standard Base64) — it replaces + with - and / with _, and removes padding = characters.
Using JavaScript
function decodeJWT(token) {
const parts = token.split('.');
// Base64Url to Base64: replace - with + and _ with /
const base64 = (str) =>
str.replace(/-/g, '+').replace(/_/g, '/') +
'==='.slice((str.length + 3) % 4);
const header = JSON.parse(atob(base64(parts[0])));
const payload = JSON.parse(atob(base64(parts[1])));
return { header, payload, signature: parts[2] };
}
const decoded = decodeJWT('eyJhbGci...');
console.log(decoded.payload.name); // "John Doe"
console.log(decoded.payload.exp); // 1516242822 (Unix timestamp)
Using Python
import base64
import json
def decode_jwt(token: str) -> dict:
parts = token.split('.')
# Add padding if needed
def b64_decode(s):
s += '=' * (4 - len(s) % 4)
return base64.urlsafe_b64decode(s)
header = json.loads(b64_decode(parts[0]))
payload = json.loads(b64_decode(parts[1]))
return {'header': header, 'payload': payload, 'signature': parts[2]}
decoded = decode_jwt('eyJhbGci...')
print(decoded['payload']['name']) # "John Doe"
Using Node.js (with jsonwebtoken library)
const jwt = require('jsonwebtoken');
const token = 'eyJhbGci...';
const decoded = jwt.decode(token, { complete: true });
console.log(decoded.header); // { alg: 'HS256', typ: 'JWT' }
console.log(decoded.payload); // { sub: '1234567890', name: 'John Doe', ... }
Using Online Tools
For quick debugging, online JWT decoders let you paste your token and instantly see the decoded contents. This is invaluable for:
- Debugging authentication errors in development
- Verifying token expiration and issued-at timestamps
- Inspecting custom claims and roles
- Checking the signing algorithm used
- Sharing decoded output with teammates during troubleshooting
JWT Security Best Practices
1. Always Validate on Server
Never trust JWT data on the client side. Always validate the signature and claims on your server. Decoding is not the same as verifying — anyone can read the payload, but only the server with the correct key can verify the signature.
// ❌ Wrong: just decoding without verification
const payload = jwt.decode(token);
// ✅ Right: verify signature + claims
const payload = jwt.verify(token, secretKey, {
algorithms: ['HS256'], // whitelist acceptable algorithms
issuer: 'https://auth.example.com',
audience: 'https://api.example.com'
});
2. Set Expiration Times
Always include exp claims to limit token validity:
{
"exp": 1516242822, // Short-lived access tokens
"iat": 1516239022
}
3. Use HTTPS
Always transmit JWTs over HTTPS to prevent interception.
4. Store Securely
- Browser: Use httpOnly cookies, not localStorage
- Mobile: Use secure storage (Keychain/Keystore)
5. Implement Refresh Tokens
Use short-lived access tokens with longer-lived refresh tokens:
Access Token: 15 minutes
Refresh Token: 7 days
Common JWT Errors
"Token expired"
The exp claim has passed. Implement token refresh logic:
async function fetchWithAuth(url, options = {}) {
let token = getAccessToken();
// Check if token is about to expire (within 60 seconds)
const payload = decodeJWT(token).payload;
if (payload.exp * 1000 - Date.now() < 60000) {
token = await refreshAccessToken();
}
return fetch(url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${token}` }
});
}
"Invalid signature"
The token was tampered with or the signing key is incorrect. Common causes:
- Server restarted and lost its signing key (in-memory keys)
- Multiple servers using different keys
- Key rotation without updating all instances
"Malformed token"
The token structure is invalid — missing parts or incorrect encoding. Check for:
- Truncated tokens (copy-paste errors)
- Extra whitespace or newline characters
- Tokens from a different environment (dev vs production)
JWT vs Session Cookies: When to Use Which
| Feature | JWT | Session Cookie |
|---|---|---|
| State | Stateless (no server storage) | Stateful (server stores session) |
| Scalability | Excellent (no lookup needed) | Requires shared session store |
| Size | Larger (contains data) | Small (just session ID) |
| Revocation | Difficult (can't invalidate before expiry) | Easy (delete server session) |
| Best for | APIs, microservices, mobile apps | Traditional web apps |
Modern recommendation: Use JWT for API communication and short-lived access tokens (15 min) combined with refresh tokens stored server-side for revocability.
Common JWT Attacks to Watch For
The "none" Algorithm Attack
Some libraries accept alg: "none", effectively disabling signature verification. Always whitelist acceptable algorithms on your server.
Key Confusion Attack
An attacker changes the algorithm from RS256 (asymmetric) to HS256 (symmetric) and signs the token using the public key as the HMAC secret. Mitigation: always specify expected algorithms server-side.
Token Leakage
JWTs in URLs (via Authorization header in redirects) can leak through browser history, logs, or referrer headers. Best practices:
- Use POST requests with tokens in headers, never GET query params
- Implement short expiration times
- Use httpOnly cookies when possible
Conclusion
Understanding JWT structure and decoding is essential for modern web development. Whether you're debugging authentication issues, implementing secure APIs, or troubleshooting SSO integrations, knowing how to inspect and validate JWT tokens will make you a more effective developer.
Remember: decoding is free, verification costs a key. Always verify signatures server-side, use short-lived tokens, and never store sensitive data in the payload that could be read by anyone who intercepts the token.
Need to decode a JWT token? Try our free JWT Decoder — instantly decode and inspect JWT tokens in your browser, completely client-side with no data sent to any server.
Try It Yourself
Put what you've learned into practice with our free online tools.
Related Articles
Creating Secure Passwords: Best Practices for 2024
Learn how to create strong, secure passwords and protect your online accounts from brute-force attacks and data breaches.
HTTP Status Codes Explained: What Every 404, 301, and 500 Really Means
From 200 OK to 503 Service Unavailable, HTTP status codes tell the story of every web request. This guide decodes every important status code with real-world examples and debugging tips.
Base64 Encoding Explained: A Complete Guide for Developers
Learn everything about Base64 encoding, when to use it, common use cases, and how to encode and decode Base64 strings in JavaScript, Python, and more.