Understanding JWTs: How JSON Web Tokens Work
JSON Web Tokens (JWTs) are everywhere in modern web development. They power authentication in APIs, single sign-on systems, and OAuth flows. But most developers use JWTs without fully understanding what's inside them or how they work. This guide breaks down the structure, encoding, and security considerations of JWTs.
What Is a JWT?
A JWT is a compact, URL-safe token format defined in RFC 7519. It consists of three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c Those three parts are:
- Header — metadata about the token (algorithm and type)
- Payload — the actual data (called "claims")
- Signature — a cryptographic signature to verify the token hasn't been tampered with
Each part is Base64url-encoded JSON. That's why every JWT starts with eyJ — it's the Base64url encoding of {", the start of a JSON object.
The Header
The header is a JSON object that specifies the signing algorithm and token type:
{
"alg": "HS256",
"typ": "JWT"
} Common algorithms include:
| Algorithm | Type | Use Case |
|---|---|---|
HS256 | Symmetric (HMAC) | Single server, shared secret |
RS256 | Asymmetric (RSA) | Distributed systems, public key verification |
ES256 | Asymmetric (ECDSA) | Smaller keys, mobile-friendly |
none | No signature | Never use in production |
Security warning: The none algorithm has been the source of many JWT vulnerabilities. Always validate that the algorithm in the header matches what your server expects.
The Payload (Claims)
The payload contains the actual data. JWT defines several standard claim names:
{
"sub": "user_12345",
"name": "Jane Smith",
"email": "jane@example.com",
"iat": 1709942400,
"exp": 1710028800,
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"role": "admin"
} | Claim | Full Name | Purpose |
|---|---|---|
sub | Subject | Who the token is about (usually user ID) |
iat | Issued At | When the token was created (Unix timestamp) |
exp | Expiration | When the token expires (Unix timestamp) |
iss | Issuer | Who issued the token |
aud | Audience | Who the token is intended for |
nbf | Not Before | Token is not valid before this time |
jti | JWT ID | Unique identifier for the token |
The iat and exp claims use Unix timestamps — seconds since January 1, 1970. You can use our Epoch Converter to translate these into human-readable dates when debugging tokens.
The Signature
The signature prevents tampering. For HS256, it's calculated as:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
) This means if anyone changes a single character in the header or payload, the signature won't match and the token is rejected. The server verifies the signature using either the shared secret (HMAC) or the public key (RSA/ECDSA).
How JWT Authentication Works
A typical JWT authentication flow:
- User logs in with username/password
- Server verifies credentials, creates a JWT with user info, and signs it
- Server sends the JWT back to the client
- Client stores the JWT (typically in memory or an httpOnly cookie)
- Client includes the JWT in the
Authorizationheader of subsequent requests - Server verifies the signature and extracts user info from the payload
// Client sends:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
// Server verifies and extracts:
const payload = jwt.verify(token, SECRET_KEY);
console.log(payload.sub); // "user_12345"
console.log(payload.role); // "admin" Decoding vs Verifying
This is a critical distinction many developers miss:
- Decoding simply reads the header and payload by Base64url-decoding them. Anyone can do this — the payload is not encrypted.
- Verifying checks the cryptographic signature to ensure the token hasn't been modified and was issued by a trusted source.
Never trust a JWT's payload without verifying the signature first. An attacker could create a JWT with "role": "admin" — without signature verification, your server would blindly trust it.
// WRONG — decodes but doesn't verify
const payload = JSON.parse(atob(token.split('.')[1]));
// CORRECT — verifies signature first
const payload = jwt.verify(token, SECRET_KEY); For debugging during development, decoding is fine — use our JWT Decoder to quickly inspect the header and payload of any token.
Creating JWTs in Node.js
const jwt = require('jsonwebtoken');
// Sign (create) a token
const token = jwt.sign(
{
sub: 'user_12345',
name: 'Jane Smith',
role: 'admin'
},
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
// Verify and decode
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded.sub); // "user_12345"
} catch (err) {
if (err.name === 'TokenExpiredError') {
console.log('Token has expired');
} else {
console.log('Invalid token');
}
} Creating JWTs in Python
import jwt
from datetime import datetime, timedelta, timezone
# Sign (create) a token
payload = {
"sub": "user_12345",
"name": "Jane Smith",
"role": "admin",
"iat": datetime.now(timezone.utc),
"exp": datetime.now(timezone.utc) + timedelta(hours=24)
}
token = jwt.encode(payload, "your-secret-key", algorithm="HS256")
# Verify and decode
try:
decoded = jwt.decode(token, "your-secret-key", algorithms=["HS256"])
print(decoded["sub"]) # "user_12345"
except jwt.ExpiredSignatureError:
print("Token has expired")
except jwt.InvalidTokenError:
print("Invalid token") JWT Security Best Practices
- Always verify the signature — never just decode and trust.
- Use short expiration times — 15 minutes to 1 hour for access tokens. Use refresh tokens for longer sessions.
- Validate all claims — check
iss,aud,exp, andnbfon every request. - Use strong secrets — for HMAC algorithms, use at least 256 bits of entropy. Use our Password Generator to create strong secrets.
- Don't store sensitive data in the payload — remember, the payload is just Base64-encoded, not encrypted. Never put passwords, credit card numbers, or PII in JWTs.
- Prefer RS256 over HS256 for distributed systems — asymmetric algorithms let services verify tokens without knowing the signing secret.
- Store tokens securely — use httpOnly cookies (not localStorage) to prevent XSS attacks from stealing tokens.
- Implement token revocation — maintain a blacklist or use short-lived tokens with refresh token rotation.
JWTs vs Sessions
| Feature | JWT | Server Sessions |
|---|---|---|
| State storage | Client-side (stateless) | Server-side (database/Redis) |
| Scalability | Excellent (no shared state) | Requires shared session store |
| Revocation | Difficult (need blacklist) | Easy (delete session) |
| Size | Larger (payload in every request) | Small session ID |
| Cross-domain | Works natively | Requires CORS configuration |
JWTs excel in microservice architectures where multiple services need to verify user identity without calling a central session store. For monolithic applications, server sessions are often simpler and more secure.
Common JWT Mistakes
- Using
"alg": "none"— this disables signature verification entirely. Always reject tokens with algorithm "none." - Not checking expiration — expired tokens should always be rejected. Libraries handle this by default, but make sure it's not disabled.
- Storing in localStorage — vulnerable to XSS attacks. Prefer httpOnly cookies.
- Using JWTs for sessions — if you need to revoke access immediately (e.g., user logs out, account compromised), JWTs require extra infrastructure. Sessions are simpler for this.
- Putting too much data in the payload — large JWTs increase request size. Keep payloads minimal; look up additional data server-side.
FAQ
Are JWTs encrypted?
No, standard JWTs (JWS) are signed but not encrypted. The payload is Base64url-encoded and readable by anyone. If you need encrypted tokens, use JWE (JSON Web Encryption), though this is less common.
Can I use JWTs without a library?
You can decode a JWT manually by splitting on . and Base64-decoding each part. But you should always use a library for verification — implementing cryptographic verification correctly is error-prone.
What's the maximum size of a JWT?
There's no spec limit, but most servers and browsers limit HTTP headers to 8KB. Keep your JWT payload small to stay well under this limit.
Need to quickly inspect a JWT during development? Paste it into our JWT Decoder to instantly see the header, payload, and expiration details — all decoded right in your browser with no data sent to any server.