TL;DR
A JWT is a three-part token (Header.Payload.Signature) encoded in Base64URL. You can decode any JWT without a key to read its claims. Use the DevToolBox JWT Decoder to inspect tokens instantly. Always verify the signature server-side before trusting any claim for authorization. This guide covers JWT anatomy, decoding vs verifying, JavaScript (manual + jsonwebtoken + jose), Python (PyJWT), claims reference, algorithms, security best practices, debugging, and real-world patterns.
1. What Is a JWT? Anatomy of a JSON Web Token
A JSON Web Token (JWT), standardized in RFC 7519, is a compact, self-contained token for securely transmitting information as a JSON object. JWTs are used for authentication (OAuth 2.0, OpenID Connect), API authorization (Bearer tokens), and SSO. A JWT looks like three Base64URL-encoded strings separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiJBbGljZSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cThe Three Parts
Header β Base64URL-decoded, the header is a JSON object specifying the algorithm and token type:
{
"alg": "HS256", // Algorithm: HS256, RS256, ES256, etc.
"typ": "JWT" // Token type
}Payload β Contains the claims: assertions about the user and additional metadata:
{
"sub": "user_123", // Subject: who this token is about
"name": "Alice", // Custom claim
"iat": 1700000000, // Issued At (Unix timestamp)
"exp": 1700003600, // Expiration (1 hour later)
"iss": "https://auth.example.com",
"aud": "https://api.example.com"
}Signature β Created by the issuer using their secret/private key. For HS256:
HMACSHA256(
base64url(header) + "." + base64url(payload),
secret
)JWT vs Session Tokens vs API Keys
| Property | JWT | Session Token | API Key |
|---|---|---|---|
| Stateless | Yes | No (server stores session) | No (server stores key) |
| Built-in Expiry | Yes (exp claim) | Server-side controlled | Usually no |
| Self-contained | Yes (claims in token) | No (data in DB) | No (opaque) |
| Revocation Support | Hard (needs blocklist) | Easy (delete session) | Easy (delete key) |
| Payload Size | Medium (grows with claims) | Tiny (just an ID) | Tiny |
| Use Case | Auth, SSO, microservices | Traditional web apps | Server-to-server |
2. Decoding vs Verifying β A Critical Distinction
Many developers confuse decoding and verifying a JWT. Understanding the difference is essential for building secure applications.
| Action | What It Does | Key Required? | Safe for AuthZ? |
|---|---|---|---|
| Decode | Base64URL-decodes header + payload to read JSON | No | NEVER |
| Verify | Validates signature + checks exp, iss, aud claims | Yes | YES |
When to decode only: Debugging, logging, displaying user info in a UI after the token has already been verified server-side, reading non-security-sensitive metadata.
When to verify: Any time you make an authorization decision based on a JWT claim. If you call jwt.decode() instead of jwt.verify() in your authorization middleware, an attacker can craft a token with arbitrary claims and bypass security entirely.
3. Decode JWT Online with DevToolBox
The DevToolBox JWT Decoder is a free, client-side tool that lets you paste any JWT and instantly see:
- Header β algorithm (
alg), token type (typ), key ID (kid) - Payload β all claims with human-readable timestamps for
exp,iat,nbf - Signature β raw signature bytes (for visual inspection)
- Expiration status β whether the token is currently valid, expired, or not yet active
All processing happens in your browser β tokens are never sent to any server. This makes it safe for inspecting tokens in development and staging environments.
4. JavaScript β Decode a JWT Without a Library
Since a JWT is just Base64URL-encoded JSON, you can decode the header and payload with standard built-in functions β no library needed. Base64URL differs from Base64 by using - instead of + and _ instead of /, with no padding.
Browser (using atob)
function decodeJWT(token) {
const [headerB64, payloadB64, signature] = token.split('.');
function base64UrlDecode(str) {
// Replace URL-safe chars, add padding
const base64 = str
.replace(/-/g, '+')
.replace(/_/g, '/')
.padEnd(str.length + (4 - (str.length % 4)) % 4, '=');
return JSON.parse(atob(base64));
}
return {
header: base64UrlDecode(headerB64),
payload: base64UrlDecode(payloadB64),
signature,
};
}
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTcwMDAwMzYwMH0.abc';
const decoded = decodeJWT(token);
console.log(decoded.header); // { alg: 'HS256', typ: 'JWT' }
console.log(decoded.payload); // { sub: 'user_123', exp: 1700003600 }
// Check expiration
const isExpired = decoded.payload.exp * 1000 < Date.now();
console.log('Expired:', isExpired);Node.js (using Buffer)
function decodeJWTNode(token) {
const [headerB64, payloadB64, signature] = token.split('.');
function base64UrlDecode(str) {
// Buffer.from handles base64url directly in Node.js
return JSON.parse(Buffer.from(str, 'base64url').toString('utf8'));
}
return {
header: base64UrlDecode(headerB64),
payload: base64UrlDecode(payloadB64),
signature,
};
}
// Usage
const { header, payload } = decodeJWTNode(token);
console.log(header); // { alg: 'HS256', typ: 'JWT' }
console.log(payload); // { sub: 'user_123', iat: 1700000000, exp: 1700003600 }
// Human-readable expiration
const expDate = new Date(payload.exp * 1000);
console.log('Expires:', expDate.toISOString());5. JavaScript β The jsonwebtoken Library
The jsonwebtoken package is the most popular JWT library for Node.js with millions of weekly downloads. Install it with npm install jsonwebtoken.
const jwt = require('jsonwebtoken');
// ESM: import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET; // Never hardcode in production
// --- SIGN a token ---
const payload = {
sub: 'user_123',
name: 'Alice',
role: 'admin',
};
const token = jwt.sign(payload, SECRET, {
algorithm: 'HS256',
expiresIn: '1h', // or 3600 (seconds)
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
// --- VERIFY a token (validates signature + claims) ---
try {
const verified = jwt.verify(token, SECRET, {
algorithms: ['HS256'], // Explicitly whitelist algorithms!
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
console.log('Valid token. Subject:', verified.sub);
} catch (err) {
if (err.name === 'TokenExpiredError') {
console.error('Token has expired at:', err.expiredAt);
} else if (err.name === 'JsonWebTokenError') {
console.error('Invalid token:', err.message);
} else if (err.name === 'NotBeforeError') {
console.error('Token not active yet:', err.date);
}
}
// --- DECODE only (no verification β for inspection/logging) ---
const decoded = jwt.decode(token, { complete: true });
console.log(decoded.header); // { alg: 'HS256', typ: 'JWT' }
console.log(decoded.payload); // { sub: 'user_123', iat: ..., exp: ... }
// WARNING: jwt.decode() is NOT safe for authorization!Async Callback Style
// Promisified verify
const verifyAsync = (token, secret, options) =>
new Promise((resolve, reject) => {
jwt.verify(token, secret, options, (err, decoded) => {
if (err) reject(err);
else resolve(decoded);
});
});
// Usage with async/await
async function authenticate(token) {
const payload = await verifyAsync(token, SECRET, {
algorithms: ['HS256'],
});
return payload;
}6. JavaScript β The jose Library (Modern, Edge-Compatible)
The jose library is a modern, dependency-free JWT library that runs in Node.js, browser, Deno, Cloudflare Workers, and other edge runtimes. It supports all JWA algorithms and is ideal for RS256/ES256 with JWKS. Install with npm install jose.
import { jwtVerify, SignJWT, createRemoteJWKSet, decodeJwt } from 'jose';
// --- SIGN with HS256 ---
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const token = await new SignJWT({ sub: 'user_123', role: 'admin' })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('1h')
.setIssuer('https://auth.example.com')
.setAudience('https://api.example.com')
.sign(secret);
// --- VERIFY with HS256 ---
const { payload, protectedHeader } = await jwtVerify(token, secret, {
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
console.log(payload.sub); // 'user_123'
// --- VERIFY with RS256/ES256 using JWKS endpoint ---
// Ideal for OAuth 2.0 / OpenID Connect
const JWKS = createRemoteJWKSet(
new URL('https://auth.example.com/.well-known/jwks.json')
);
const { payload: oauthPayload } = await jwtVerify(accessToken, JWKS, {
issuer: 'https://auth.example.com',
audience: 'your-client-id',
algorithms: ['RS256'],
});
// --- DECODE only (no verification) ---
const claims = decodeJwt(token);
console.log(claims.exp); // Expiration timestampNext.js Middleware with jose
// middleware.ts (runs at the Edge)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
export async function middleware(req: NextRequest) {
const token = req.cookies.get('access_token')?.value;
if (!token) return NextResponse.redirect(new URL('/login', req.url));
try {
const { payload } = await jwtVerify(token, secret);
// Attach user info to headers for downstream handlers
const headers = new Headers(req.headers);
headers.set('x-user-id', payload.sub as string);
return NextResponse.next({ request: { headers } });
} catch {
return NextResponse.redirect(new URL('/login', req.url));
}
}
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};7. Python β PyJWT
PyJWT is the standard JWT library for Python. Install it with pip install PyJWT. For RS256/ES256, also install pip install cryptography.
import jwt
import os
from datetime import datetime, timedelta, timezone
SECRET = os.environ["JWT_SECRET"]
# --- ENCODE (sign) a token ---
payload = {
"sub": "user_123",
"name": "Alice",
"iat": datetime.now(tz=timezone.utc),
"exp": datetime.now(tz=timezone.utc) + timedelta(hours=1),
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
}
token = jwt.encode(payload, SECRET, algorithm="HS256")
print(token) # "eyJhbGci..."
# --- DECODE and VERIFY ---
try:
decoded = jwt.decode(
token,
SECRET,
algorithms=["HS256"], # Explicitly whitelist!
audience="https://api.example.com",
issuer="https://auth.example.com",
)
print("Subject:", decoded["sub"])
except jwt.ExpiredSignatureError:
print("Token has expired")
except jwt.InvalidAudienceError:
print("Audience mismatch")
except jwt.InvalidIssuerError:
print("Issuer mismatch")
except jwt.InvalidTokenError as e:
print("Invalid token:", e)
# --- DECODE only (no verification) ---
# options={"verify_signature": False} disables sig check
unverified = jwt.decode(
token,
options={"verify_signature": False},
algorithms=["HS256"],
)
print("Unverified payload:", unverified)
# WARNING: Only use this for inspection/debugging!Python β RS256 with a Public Key
from cryptography.hazmat.primitives import serialization
# Load RSA public key (for verification only)
with open("public_key.pem", "rb") as f:
public_key = serialization.load_pem_public_key(f.read())
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="https://api.example.com",
)
# Load RSA private key (for signing)
with open("private_key.pem", "rb") as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
token = jwt.encode(payload, private_key, algorithm="RS256")8. JWT Claims Reference
The JWT specification defines a set of registered claim names with well-known meanings. All timestamps are Unix timestamps (seconds since epoch, not milliseconds).
| Claim | Full Name | Description | Type | Required? |
|---|---|---|---|---|
| iss | Issuer | Who issued the token (URL or identifier) | String | Recommended |
| sub | Subject | Who the token is about (user ID) | String | Recommended |
| aud | Audience | Intended recipient(s) of the token | String/Array | Recommended |
| exp | Expiration | When the token expires (Unix timestamp) | Number | Strongly recommended |
| iat | Issued At | When the token was created | Number | Recommended |
| nbf | Not Before | Token not valid before this time | Number | Optional |
| jti | JWT ID | Unique identifier (prevents replay attacks) | String | Optional |
Validating Claims Manually (JavaScript)
function validateClaims(payload, options = {}) {
const now = Math.floor(Date.now() / 1000); // Current Unix timestamp
// Check expiration (exp)
if (payload.exp && payload.exp < now) {
throw new Error(`Token expired at ${new Date(payload.exp * 1000).toISOString()}`);
}
// Check not-before (nbf)
if (payload.nbf && payload.nbf > now) {
throw new Error(`Token not yet valid until ${new Date(payload.nbf * 1000).toISOString()}`);
}
// Check issuer (iss)
if (options.issuer && payload.iss !== options.issuer) {
throw new Error(`Invalid issuer: expected ${options.issuer}, got ${payload.iss}`);
}
// Check audience (aud)
if (options.audience) {
const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
if (!aud.includes(options.audience)) {
throw new Error(`Invalid audience: ${payload.aud}`);
}
}
return true;
}9. JWT Signing Algorithms Compared
| Algorithm | Type | Key Type | Best For | Security Level |
|---|---|---|---|---|
| HS256 | Symmetric (HMAC) | Shared secret | Single service (same signer + verifier) | Good (if secret is strong) |
| HS384 | Symmetric (HMAC) | Shared secret | Same as HS256, larger digest | Good |
| HS512 | Symmetric (HMAC) | Shared secret | Same as HS256, largest digest | Good |
| RS256 | Asymmetric (RSA) | Private + Public key | Microservices, OAuth 2.0, OIDC | Strong |
| RS384 | Asymmetric (RSA) | Private + Public key | Higher security RSA | Strong |
| ES256 | Asymmetric (ECDSA) | EC Private + Public key | Mobile, IoT, performance-critical | Very Strong |
| EdDSA | Asymmetric (Ed25519) | Ed25519 key pair | Modern systems, small tokens | Very Strong |
| none | No signature | None | NEVER use in production | None (dangerous) |
When to use HS256: When both the issuer and verifier are the same service and you can securely share a secret. Simple, fast, no key infrastructure needed.
When to use RS256/ES256: In distributed systems where multiple services need to verify tokens but only one service should sign them. The auth server holds the private key; all other services use the public key (often fetched from a JWKS endpoint).
alg: 'none' and no signature, treating them as valid. Always explicitly whitelist allowed algorithms and reject none. Never accept the algorithm from the token header without cross-checking it against your expected algorithm.10. JWT Security Best Practices
- Always verify the signature β never use
decode()for authorization; always useverify(). - Validate critical claims β always check
exp,iss, andaudduring verification. - Whitelist algorithms β explicitly specify allowed algorithms in your library, e.g.,
algorithms: ['HS256']. Never acceptnone. - Use HTTPS always β JWTs sent over HTTP can be intercepted. Always use TLS.
- Short-lived access tokens β keep access token expiry to 15 minutes or 1 hour. Use refresh tokens for session persistence.
- Rotate refresh tokens β issue a new refresh token on every use (refresh token rotation) and invalidate the old one.
- Store tokens securely β prefer httpOnly, Secure, SameSite=Strict cookies over localStorage to prevent XSS access.
- No sensitive data in payload β the payload is only Base64URL-encoded, not encrypted. Anyone who obtains the token can decode it. Never put passwords, SSNs, or PII.
- Use strong secrets β for HS256, use at least 256 bits (32 bytes) of random entropy. Never use a dictionary word or short string.
- Implement token revocation β maintain a blocklist (Redis with TTL) for cases where you need to invalidate tokens before expiry (logout, password change).
- Clock skew tolerance β allow a small clock skew (up to 30 seconds) for
nbfandexpvalidation when services have slightly different system clocks.
localStorage vs httpOnly Cookies β Tradeoffs
| Aspect | localStorage | httpOnly Cookie |
|---|---|---|
| XSS protection | Vulnerable (JS can read it) | Protected (JS cannot read it) |
| CSRF protection | Immune (not auto-sent) | Requires CSRF token or SameSite |
| CORS handling | Easy | Requires credentials: include |
| Persistence | Until cleared | Controlled by Max-Age/Expires |
| Mobile app use | Easy | Requires custom handling |
| Recommendation | Avoid for sensitive tokens | Preferred for web apps |
11. Debugging Common JWT Errors
| Error | Cause | Fix |
|---|---|---|
| TokenExpiredError | exp claim is in the past | Refresh the token using a refresh token or re-authenticate |
| JsonWebTokenError: invalid signature | Token tampered with, or wrong key used | Check that the same key used to sign is used to verify |
| JsonWebTokenError: invalid algorithm | alg in token header does not match expected | Explicitly set algorithms whitelist; check token header |
| NotBeforeError | nbf claim is in the future | Allow clock skew tolerance (e.g., clockTolerance: 30) |
| Invalid audience | aud claim does not match expected value | Ensure aud in token matches the audience you specify during verify |
| Malformed JWT | Token does not have 3 dot-separated parts | Check token is not truncated; ensure Bearer prefix is stripped |
| Invalid issuer | iss claim does not match expected issuer | Check issuer URL matches exactly (trailing slash matters) |
| Clock skew | Server clocks out of sync causing exp/nbf issues | Sync clocks with NTP; add clockTolerance in verification options |
Debug Checklist
// 1. Decode the token first to inspect claims
const decoded = jwt.decode(token, { complete: true });
console.log('Header:', decoded?.header);
console.log('Payload:', decoded?.payload);
// 2. Check expiration manually
const now = Math.floor(Date.now() / 1000);
console.log('Current time:', now);
console.log('Token exp:', decoded?.payload?.exp);
console.log('Expired:', decoded?.payload?.exp < now);
// 3. Check issuer and audience
console.log('iss:', decoded?.payload?.iss);
console.log('aud:', decoded?.payload?.aud);
// 4. Verify with explicit options to get specific errors
try {
jwt.verify(token, SECRET, {
algorithms: ['HS256'],
// Comment these out temporarily to isolate issues:
// issuer: 'https://auth.example.com',
// audience: 'https://api.example.com',
});
} catch (err) {
console.error('Verification error name:', err.name);
console.error('Verification error message:', err.message);
}12. Real-World JWT Patterns
Express.js Auth Middleware
// middleware/auth.js
const jwt = require('jsonwebtoken');
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
}
const token = authHeader.slice(7); // Remove "Bearer " prefix
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: process.env.JWT_ISSUER,
audience: process.env.JWT_AUDIENCE,
});
req.user = payload;
next();
} catch (err) {
const status = err.name === 'TokenExpiredError' ? 401 : 403;
return res.status(status).json({ error: err.message });
}
}
// Role-based access control
function authorize(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.get('/admin', authenticate, authorize('admin'), (req, res) => {
res.json({ message: 'Admin area', user: req.user.sub });
});Refresh Token Rotation Pattern
// POST /auth/refresh
async function refreshTokens(req, res) {
const { refreshToken } = req.cookies; // httpOnly cookie
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
// 1. Verify the refresh token
let payload;
try {
payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET, {
algorithms: ['HS256'],
});
} catch {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// 2. Check it exists in the database (rotation tracking)
const stored = await db.refreshTokens.findOne({ token: refreshToken, userId: payload.sub });
if (!stored) return res.status(401).json({ error: 'Refresh token reuse detected' });
// 3. Delete the used refresh token (invalidate)
await db.refreshTokens.deleteOne({ token: refreshToken });
// 4. Issue new token pair
const newAccessToken = jwt.sign(
{ sub: payload.sub, role: payload.role },
process.env.JWT_SECRET,
{ expiresIn: '15m', algorithms: ['HS256'] }
);
const newRefreshToken = jwt.sign(
{ sub: payload.sub },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);
// 5. Store new refresh token
await db.refreshTokens.insertOne({ token: newRefreshToken, userId: payload.sub });
// 6. Set httpOnly cookie + return access token
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: newAccessToken });
}JWKS Endpoint for Microservices (jose)
// In any microservice β verify tokens issued by auth service
import { createRemoteJWKSet, jwtVerify } from 'jose';
// Fetch public keys from auth server's JWKS endpoint
// Keys are cached automatically by jose
const JWKS = createRemoteJWKSet(
new URL(`${process.env.AUTH_SERVER}/.well-known/jwks.json`)
);
async function verifyToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: process.env.AUTH_SERVER,
audience: process.env.SERVICE_NAME,
algorithms: ['RS256'],
});
return payload;
}
// Each microservice can verify tokens independently
// without sharing any secret β only the auth server has the private keyKey Takeaways
- A JWT has three Base64URL-encoded parts: Header, Payload, and Signature, joined by dots.
- Decoding reads the claims without verifying authenticity. Verifying validates the cryptographic signature β always do this for authorization.
- Use the DevToolBox JWT Decoder to inspect tokens instantly β all client-side, nothing sent to servers.
- In JavaScript, use jsonwebtoken for Node.js or jose for edge runtimes. In Python, use PyJWT.
- Standard claims:
iss,sub,aud,exp,iat,nbf,jtiβ always validate them. - Use HS256 for single-service auth, RS256/ES256 for distributed/microservice architectures.
- Never trust
alg: none. Always whitelist algorithms explicitly. Never put sensitive data in the payload. - Store JWTs in httpOnly cookies (not localStorage) to protect against XSS attacks.
- Use short-lived access tokens (15mβ1h) with refresh token rotation for long-lived sessions.