JSON Web Tokens (JWTs) are everywhere. If you've built a modern web application, integrated with an OAuth provider, or worked with any API that requires authentication, you've almost certainly encountered JWTs. Yet despite their ubiquity, JWTs are one of the most misunderstood and misused tools in web development.
In this guide, we'll break down exactly what JWTs are, how they work, how to decode and debug them, and — critically — how to use them securely. Want to inspect a token right now? Paste it into our JWT Decoder to see the header, payload, and expiration instantly.
What Is a JWT?
A JSON Web Token is a compact, URL-safe means of representing claims between two parties. It's defined in RFC 7519 and is the de facto standard for stateless authentication in web applications.
A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
It's three Base64URL-encoded strings separated by dots. That's it. No magic — just encoded JSON with a cryptographic signature appended.
JWT Structure: The Three Parts
Every JWT consists of three parts: Header, Payload, and Signature.
1. Header
The header typically contains two fields:
{
"alg": "HS256",
"typ": "JWT"
}
alg— The signing algorithm (HS256, RS256, ES256, etc.)typ— The token type (always "JWT")
This JSON is Base64URL-encoded to form the first part of the token. You can decode it with our Base64 Encoder/Decoder or directly with the JWT Decoder.
2. Payload (Claims)
The payload contains the actual data — called claims. There are three types:
Registered claims (standardized, optional but recommended):
{
"iss": "https://auth.example.com", // Issuer
"sub": "user-123", // Subject (user ID)
"aud": "https://api.example.com", // Audience
"exp": 1748505600, // Expiration (Unix timestamp)
"nbf": 1748419200, // Not Before
"iat": 1748419200, // Issued At
"jti": "unique-token-id" // JWT ID (for revocation)
}
Public claims (custom, registered in the IANA JWT Claims Registry):
{
"name": "John Doe",
"email": "[email protected]"
}
Private claims (custom, agreed between parties):
{
"role": "admin",
"permissions": ["read", "write", "delete"],
"tenant_id": "acme-corp"
}
Important: the payload is not encrypted. Anyone who has the token can decode and read the payload. Never put sensitive data (passwords, API keys, PII) in a JWT payload. The signature only ensures the data hasn't been tampered with, not that it's secret.
3. Signature
The signature verifies that the token hasn't been modified. It's computed by:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
For asymmetric algorithms (RS256, ES256), the signature is created with a private key and verified with the corresponding public key. This is essential for microservices architectures where the signing service and verifying services are separate.
How JWT Authentication Works
Here's the typical JWT authentication flow:
1. User sends credentials (email + password) to /auth/login
2. Server validates credentials against database
3. Server creates a JWT with user claims and signs it
4. Server returns the JWT to the client
5. Client stores the JWT (localStorage, cookie, or memory)
6. Client sends JWT in Authorization header: Bearer <token>
7. Server verifies JWT signature and extracts claims
8. Server processes the request using the claims (user ID, role, etc.)
The key advantage: the server doesn't need to store session data. The JWT itself contains all the information needed to authenticate the request. This makes JWTs ideal for stateless, horizontally scalable architectures.
Signing Algorithms Explained
Choosing the right algorithm is critical for security. Here's what each algorithm family does:
HMAC (HS256, HS384, HS512)
Symmetric signing — the same secret key is used to create and verify the signature. Simple and fast, but every service that needs to verify the token must have the secret key.
// Node.js example
const jwt = require('jsonwebtoken');
const token = jwt.sign({ userId: '123' }, 'your-secret-key', { expiresIn: '1h' });
const decoded = jwt.verify(token, 'your-secret-key');
Best for: monolithic applications, single-service architectures.
RSA (RS256, RS384, RS512)
Asymmetric signing — a private key creates the signature, a public key verifies it. The auth server keeps the private key; all other services only need the public key.
// Sign with private key
const token = jwt.sign({ userId: '123' }, privateKey, { algorithm: 'RS256' });
// Verify with public key (any service can do this)
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Best for: microservices, multi-service architectures, OAuth 2.0 providers.
ECDSA (ES256, ES384, ES512)
Asymmetric signing using elliptic curves. Same security as RSA but with smaller keys and faster operations.
Best for: mobile applications, IoT devices, performance-sensitive systems.
Decoding and Debugging JWTs
Debugging JWTs is straightforward because the header and payload are just Base64URL-encoded JSON. Here are several ways to inspect tokens:
Using DevToolkit's JWT Decoder
Our JWT Decoder provides instant decoding with:
- Header and payload parsed and formatted
- Expiration time displayed in human-readable format
- Token validity status (expired or active)
- 100% client-side — your tokens never leave your browser
Using the Command Line
# Decode header
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# Decode payload
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0" | base64 -d
# Full decode with jq
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
Using JavaScript
function decodeJwt(token) {
const parts = token.split('.');
return {
header: JSON.parse(atob(parts[0])),
payload: JSON.parse(atob(parts[1])),
};
}
const { header, payload } = decodeJwt(token);
console.log('Expires:', new Date(payload.exp * 1000));
Note: atob() decodes standard Base64. JWTs use Base64URL encoding (replacing + with - and / with _). Most browsers handle this correctly, but for edge cases you may need to normalize first. Use our Base64 tool for quick encoding/decoding.
JWT Security Best Practices
JWTs are secure when used correctly, but dangerous when misused. Follow these rules:
1. Always Verify the Signature
Never trust a JWT without verifying its signature. The whole point of the signature is to prove the token hasn't been tampered with. Always specify the expected algorithm to prevent algorithm confusion attacks:
// GOOD: specify expected algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] });
// BAD: accept any algorithm (vulnerable to alg:none attack)
jwt.verify(token, secret);
2. Set Short Expiration Times
JWTs can't be revoked after issuance (without a blocklist). Keep expiration times short:
- Access tokens: 15 minutes to 1 hour
- Refresh tokens: 7 to 30 days (stored securely, used to get new access tokens)
Always check the exp claim. Our Timestamp Converter can help you convert Unix timestamps to human-readable dates for debugging.
3. Use Strong Secrets
For HMAC algorithms, use a randomly generated secret of at least 256 bits (32 bytes). Never use dictionary words or short strings. Generate secure random strings with our Password Generator.
4. Store Tokens Securely
// BEST: HttpOnly cookie (not accessible to JavaScript, immune to XSS)
Set-Cookie: token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
// OK: Memory (lost on page refresh, but immune to XSS and CSRF)
let token = null; // in-memory variable
// AVOID: localStorage (accessible to any JavaScript on the page)
localStorage.setItem('token', jwt); // Vulnerable to XSS
5. Never Put Sensitive Data in the Payload
The payload is Base64-encoded, not encrypted. Anyone with the token can read it. Never include:
- Passwords or password hashes
- API keys or secrets
- Personal data beyond what's necessary (SSN, credit cards)
- Internal system details
6. Validate All Claims
Don't just verify the signature — also validate:
exp— Token isn't expirediss— Token was issued by a trusted authorityaud— Token is intended for your servicenbf— Token is valid for use (not too early)
JWT vs Sessions vs API Keys
When should you use JWTs vs other authentication methods?
JWT Sessions API Keys
Stateless Yes No Yes
Scalable High Medium High
Revocable Hard Easy Easy
Size Medium Small (ID only) Small
Best for SPAs, APIs, Traditional Server-to-server,
microservices web apps third-party access
JWTs are ideal when you need stateless, scalable authentication — especially for SPAs and microservices. For traditional server-rendered applications with simple session needs, cookie-based sessions may be simpler and more secure.
Common JWT Vulnerabilities
Algorithm Confusion (alg:none)
Some libraries accept tokens with "alg": "none", which means no signature verification. Always whitelist allowed algorithms:
jwt.verify(token, secret, { algorithms: ['HS256'] });
Key Confusion (RS256 to HS256)
If a server expects RS256 (asymmetric) but an attacker sends HS256 (symmetric) and signs with the public key (which is publicly available), the server might accept it. Fix: always specify the expected algorithm.
Token Leakage
JWTs in URLs, logs, or error messages can be stolen. Always transmit via HTTPS, use Authorization headers (not query parameters), and sanitize logs.
Missing Expiration
A JWT without exp never expires. Always set and validate expiration. Use our JWT Decoder to quickly check if a token has an expiration claim.
Implementing JWT in Production
Node.js (Express)
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET; // 256-bit random secret
// Issue token
app.post('/auth/login', async (req, res) => {
const user = await authenticateUser(req.body.email, req.body.password);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const token = jwt.sign(
{ sub: user.id, role: user.role },
SECRET,
{ expiresIn: '1h', issuer: 'https://api.example.com' }
);
res.cookie('token', token, {
httpOnly: true, secure: true, sameSite: 'strict', maxAge: 3600000
});
res.json({ success: true });
});
// Verify middleware
function authMiddleware(req, res, next) {
const token = req.cookies.token || req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token' });
try {
req.user = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
Python (FastAPI)
from jose import jwt, JWTError
from datetime import datetime, timedelta
SECRET_KEY = os.environ["JWT_SECRET"]
ALGORITHM = "HS256"
def create_token(user_id: str, role: str) -> str:
payload = {
"sub": user_id,
"role": role,
"exp": datetime.utcnow() + timedelta(hours=1),
"iss": "https://api.example.com",
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def verify_token(token: str) -> dict:
try:
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
Refresh Token Pattern
Since access tokens should be short-lived, use refresh tokens to get new access tokens without re-authenticating:
1. User logs in → receives access token (15 min) + refresh token (30 days)
2. Access token expires → client sends refresh token to /auth/refresh
3. Server validates refresh token → issues new access token
4. Refresh token expires → user must log in again
Store refresh tokens in a database so they can be revoked. This gives you the stateless benefits of JWTs for most requests, with the ability to revoke access when needed.
FAQ
Can JWTs be decoded without the secret key?
Yes. The header and payload are Base64-encoded, not encrypted. Anyone can decode them. The secret key is only needed to verify the signature (i.e., confirm the token hasn't been tampered with).
Are JWTs encrypted?
Standard JWTs (JWS) are signed but not encrypted. If you need encrypted tokens, use JWE (JSON Web Encryption), defined in RFC 7516. However, this adds complexity — in most cases, just don't put sensitive data in the payload.
How do I revoke a JWT?
You can't revoke a JWT directly — it's valid until it expires. Workarounds: use short expiration times, maintain a token blocklist (checked on every request), or use a version counter in the user record.
Should I use JWT for session management?
It depends. For SPAs and APIs, JWTs work well. For traditional server-rendered apps, cookie-based sessions are simpler and easier to revoke. Don't use JWTs just because they're trendy — use them when stateless authentication is a genuine requirement.
What is the difference between JWS and JWE?
JWS (JSON Web Signature) is what most people mean when they say "JWT." It signs the token so you can verify it hasn't been tampered with, but the payload remains readable by anyone. JWE (JSON Web Encryption) encrypts the entire payload so only the intended recipient can read it. In practice, JWS covers the vast majority of use cases — just avoid putting secrets in the payload. If you truly need confidential claims, consider JWE or encrypt the payload before embedding it.
Can I use JWT with GraphQL APIs?
Absolutely. JWTs work with any transport layer. In a GraphQL setup, the client sends the JWT via the Authorization: Bearer <token> header just like a REST API. Your GraphQL server middleware verifies the token and attaches the decoded claims to the request context. This lets resolvers access user information (ID, role, permissions) without an extra database lookup on every query.
How do I handle JWT token size issues?
JWTs grow as you add more claims. A token with dozens of permissions or nested objects can exceed cookie size limits (typically 4 KB). To keep tokens small: store only essential claims (user ID, role), move detailed permissions to a server-side cache keyed by user ID, and avoid duplicating data already available from an API call. If your token is large, paste it into our JWT Decoder to see exactly which claims are consuming space.
JWT Alternatives: PASETO and Opaque Tokens
JWTs are the dominant standard, but they are not the only option. Two notable alternatives address specific shortcomings of the JWT specification.
PASETO (Platform-Agnostic Security Tokens)
PASETO was created as a direct response to JWT's flexibility-related security pitfalls. Where JWT lets developers choose from many algorithms (including the dangerous "alg": "none"), PASETO restricts choices to a small set of vetted, versioned protocols. Each PASETO version locks in a specific set of cryptographic primitives, eliminating algorithm confusion attacks entirely.
A PASETO token looks similar to a JWT but uses a version prefix:
v4.public.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0...
PASETO supports two purposes: local (symmetric authenticated encryption — the payload is encrypted and authenticated) and public (asymmetric signing — like JWS, the payload is readable but tamper-proof). The "local" purpose gives you encryption by default, something that requires the separate JWE spec in the JWT ecosystem.
The trade-off is ecosystem maturity. JWT libraries exist for virtually every language and framework. PASETO libraries are available for most popular languages but have a smaller community and fewer battle-tested integrations with identity providers and API gateways.
Opaque Tokens
Opaque tokens are the conceptual opposite of JWTs. Instead of encoding claims into the token itself, an opaque token is simply a random string (e.g., a UUID or a cryptographically random hex string) that maps to session data stored server-side.
// Opaque token example
Authorization: Bearer 7f3c8a2b-4d1e-9f6c-a5b0-3e8d7c2f1a4b
The server looks up this token in a database or cache (like Redis) to retrieve the associated user data. This makes revocation trivial — just delete the record. It also means no sensitive data can leak from the token, because the token contains no data at all.
The downside is that every request requires a lookup, which adds latency and creates a single point of failure (your token store). For high-throughput APIs or microservices architectures where you want to avoid per-request database calls, JWTs remain the better choice.
In many production systems, the best approach is a hybrid: use short-lived JWTs as access tokens (stateless, fast verification) and opaque strings as refresh tokens (stored server-side, easily revocable).
JWT Implementation Security Checklist
Before deploying JWTs in production, walk through this checklist. Each item addresses a real-world vulnerability or misconfiguration that has led to breaches.
- Algorithm whitelist: Explicitly specify accepted algorithms in your verification logic (e.g.,
{ algorithms: ['RS256'] }). Never allow the token'salgheader to dictate which algorithm the server uses. - Reject
alg: none: Ensure your JWT library does not accept unsigned tokens. Test this by crafting a token with"alg": "none"and an empty signature — your server must reject it. - Strong signing keys: For HMAC, use at least 256 bits of cryptographic randomness. For RSA, use 2048-bit keys minimum (4096 recommended). For ECDSA, use P-256 or stronger curves. Generate keys with our Password Generator or a dedicated key generation tool.
- Short access token lifetimes: Set
expto 15 minutes for access tokens. Use the refresh token pattern for longer sessions. - Validate all registered claims: Check
exp,iss,aud, andnbfon every request. Do not skip audience validation — a token meant for Service A should not be accepted by Service B. - Secure token storage: Use
HttpOnly,Secure, andSameSite=Strictcookies for browser-based apps. AvoidlocalStoragein XSS-prone environments. - HTTPS only: Never transmit JWTs over unencrypted HTTP. A single intercepted token grants full access until expiration.
- No sensitive data in payload: The payload is only Base64-encoded, not encrypted. Decode any token with our JWT Decoder to verify no secrets are exposed.
- Token revocation strategy: Decide upfront how you will handle compromised tokens. Options include short expiration, a server-side blocklist, or a per-user token version counter.
- Log sanitization: Strip or mask JWTs from application logs, error messages, and URLs. A leaked log file should not grant authentication access.
- Key rotation plan: Rotate signing keys periodically. Use JWKS (JSON Web Key Sets) endpoints so verifying services can discover new keys without downtime. During rotation, accept tokens signed with both old and new keys for a grace period.
- Rate-limit token issuance: Protect your
/auth/loginand/auth/refreshendpoints with rate limiting to prevent brute-force attacks and token flooding.
Ready to inspect your own tokens? Head over to our JWT Decoder to paste and decode any JWT instantly — everything runs in your browser, so your tokens stay private.