JWT Complete Guide 2026 - Structure, Security, and Best Practices
From JSON Web Token structure and how it works to security vulnerabilities and best practices. Master JWT authentication with practical code examples.
Toolypet Team
Development Team
JWT Complete Guide 2026
"JWT? Isn't it just a token?"
Yes, it is. But when that "just a token" isn't implemented correctly, it becomes a serious security issue. As of 2026, research shows that 48% of AI-generated authentication code contains security vulnerabilities.
This guide covers everything from JWT structure to practical security best practices.
What is JWT?
JWT (JSON Web Token) is a compact and self-contained method for securely transmitting information between two parties.
Key Features
| Feature | Description |
|---|---|
| Stateless | No session storage needed on server |
| Self-contained | All necessary information included in token |
| Compact | Usable in URLs, HTTP headers |
| Signed | Tampering can be detected |
Use Cases
- API Authentication: API requests with Bearer token
- SSO (Single Sign-On): Sharing authentication across multiple services
- Information Exchange: Transmitting signed data
- Authorization: Including permissions/role information
JWT Structure
JWT consists of three Base64URL-encoded parts separated by . (dots).
xxxxx.yyyyy.zzzzz
| | |
Header Payload Signature
1. Header
{
"alg": "HS256",
"typ": "JWT"
}
| Field | Description |
|---|---|
alg | Signature algorithm (HS256, RS256, etc.) |
typ | Token type (JWT) |
2. Payload (Claims)
{
"sub": "user123",
"name": "John Doe",
"email": "john@example.com",
"role": "admin",
"iat": 1708502400,
"exp": 1708588800
}
Registered Claims
| Claim | Description | Example |
|---|---|---|
iss | Issuer | "https://auth.example.com" |
sub | Subject | "user123" |
aud | Audience | "https://api.example.com" |
exp | Expiration Time | 1708588800 |
nbf | Not Before | 1708502400 |
iat | Issued At | 1708502400 |
jti | JWT ID | "abc123" |
Public/Private Claims
{
"role": "admin", // Private claim
"email": "user@example.com",
"permissions": ["read", "write"]
}
3. Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Signature Algorithms
Symmetric Key
| Algorithm | Description | Use Case |
|---|---|---|
| HS256 | HMAC + SHA-256 | Single server, simple implementation |
| HS384 | HMAC + SHA-384 | Stronger security |
| HS512 | HMAC + SHA-512 | Highest level security |
// Node.js - HS256
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ userId: 123, role: 'admin' },
'your-256-bit-secret', // Verified with same secret
{ algorithm: 'HS256', expiresIn: '1h' }
);
Asymmetric Key
| Algorithm | Description | Use Case |
|---|---|---|
| RS256 | RSA + SHA-256 | Microservices, public verification |
| RS384 | RSA + SHA-384 | High security requirements |
| RS512 | RSA + SHA-512 | Highest level security |
| ES256 | ECDSA + P-256 | Short keys, mobile |
// RS256 - Asymmetric
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
// Issue (Private Key)
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// Verify (Public Key)
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Algorithm Selection Guide
| Scenario | Recommended Algorithm |
|---|---|
| Single server | HS256 |
| Microservices | RS256 |
| Public verification needed | RS256 / ES256 |
| Mobile/IoT | ES256 (short keys) |
Practical Implementation
Node.js (Express)
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const SECRET = process.env.JWT_SECRET; // Load from environment variable
// Token issuance
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// User verification (example)
const user = await verifyCredentials(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role,
},
SECRET,
{
expiresIn: '1h',
issuer: 'your-app-name',
}
);
res.json({ token });
});
// Middleware verification
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET, {
issuer: 'your-app-name',
algorithms: ['HS256'],
});
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
};
// Protected route
app.get('/protected', authMiddleware, (req, res) => {
res.json({ message: `Hello, ${req.user.email}` });
});
Python (FastAPI)
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from datetime import datetime, timedelta
import os
app = FastAPI()
security = HTTPBearer()
SECRET = os.environ.get("JWT_SECRET")
# Token issuance
def create_token(user_id: str, role: str) -> str:
payload = {
"sub": user_id,
"role": role,
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(hours=1),
}
return jwt.encode(payload, SECRET, algorithm="HS256")
# Token verification
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
# Protected endpoint
@app.get("/protected")
def protected_route(user: dict = Depends(verify_token)):
return {"message": f"Hello, {user['sub']}"}
Security Vulnerabilities and Countermeasures
1. Algorithm None Attack
// Malicious header
{
"alg": "none",
"typ": "JWT"
}
Countermeasure:
// Specify allowed algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] }); // OK
jwt.verify(token, secret); // Dangerous
2. Weak Secret Key
// Dangerous
const SECRET = 'secret123';
// Safe (minimum 256 bits)
const SECRET = crypto.randomBytes(32).toString('hex');
3. Sensitive Information Exposure
// Dangerous: Payload can be Base64 decoded
{
"password": "user_password", // Never do this!
"creditCard": "1234-5678-9012-3456"
}
// Safe
{
"sub": "user123",
"role": "admin"
}
4. Token Theft
Countermeasure strategies:
- Short expiration time (15 minutes ~ 1 hour)
- Use Refresh Token
- HTTPS required
- HttpOnly cookies (XSS prevention)
5. JWT Replay Attack
// Use jti (JWT ID)
const token = jwt.sign({
sub: user.id,
jti: crypto.randomUUID(), // Unique ID
}, SECRET);
// Manage jti blacklist on server
Access Token + Refresh Token
Why is it needed?
| Token | Lifespan | Purpose |
|---|---|---|
| Access Token | 15 min ~ 1 hour | API authentication |
| Refresh Token | 7 ~ 30 days | Renew Access Token |
Implementation Example
// Issue both tokens on login
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body);
const accessToken = jwt.sign(
{ sub: user.id, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Refresh Token as HttpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken });
});
// Token renewal
app.post('/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ sub: decoded.sub, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
JWT Debugging
Token Decoding
Paste your token in JWT Decoder to:
- Check Header
- Check Payload
- Check expiration time
- Verify signature (when secret is provided)
CLI Decoding
# Decode Header
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# Decode Payload
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ" | base64 -d
Best Practices Checklist
Issuance
- Strong secret key (256 bits or more)
- Specify algorithm (HS256, RS256)
- Short expiration time (15 min ~ 1 hour)
- Include required claims (sub, iat, exp, iss)
- No sensitive information
Verification
- Algorithm whitelist (
algorithms: ['HS256']) - Expiration time validation
- Issuer (iss) validation
- Audience (aud) validation
Storage and Transmission
- HTTPS required
- HttpOnly cookie (XSS prevention)
- SameSite attribute (CSRF prevention)
- Avoid localStorage (XSS vulnerable)
FAQ
Q1: JWT vs Session, when to use which?
A:
- JWT: Stateless API, microservices, mobile apps
- Session: Traditional web apps, server-side rendering, immediate logout required
Q2: What if JWT is stolen?
A: Access Token remains valid until expiration. Therefore:
- Set short expiration time (15 min)
- Refresh Token Rotation
- Invalidate all tokens when suspicious
Q3: Why shouldn't I store in localStorage?
A: It's vulnerable to XSS attacks. Since JavaScript can access it, malicious scripts can steal tokens. HttpOnly cookies are safer.
Q4: Is RS256 safer than HS256?
A: It's not "safer" but "different purpose".
- HS256: Same secret shared, single server
- RS256: Verify with public key, multiple services
Q5: Do I need a token blacklist?
A: You need it if immediate logout is required. But it reduces the advantage of stateless. Short expiration + Refresh Token is an alternative.
Conclusion
Key points of JWT security:
- Strong secret: Random 256 bits or more
- Algorithm verification: Whitelist approach
- Short lifespan: Access 15 min, Refresh 7 days
- Safe storage: HttpOnly + Secure cookies
- No sensitive info: Payload is decodable by anyone
Related Tools
| Tool | Purpose |
|---|---|
| JWT Decoder | JWT decoding and verification |
| Base64 Encoder | Base64 encoding/decoding |
| Hash Generator | Hash generation |
About the Author
Toolypet Team
Development Team
The Toolypet Team creates free, privacy-focused web tools for developers and designers. All tools run entirely in your browser with no data sent to servers.