1. What is a JWT Token?
JWT (JSON Web Token) is an open standard (RFC 7519) for securely transmitting information between parties as a JSON object. JWTs are digitally signed, so the information can be verified and trusted.
Example JWT Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Key Characteristics of JWT:
- Compact: Small size allows transmission in URLs, headers, or POST parameters
- Self-contained: Contains all the information needed about the user
- Stateless: Server doesn't need to store session data
- Verifiable: Digital signature ensures data integrity
Common Use Cases:
- Authentication: After login, each request includes the JWT for access control
- Information Exchange: Securely transmit data between parties
- Authorization: Determine what resources a user can access
- Single Sign-On (SSO): Share authentication across multiple domains
2. JWT Structure (Header, Payload, Signature)
A JWT consists of three parts separated by dots (.): xxxxx.yyyyy.zzzzz
1. Header
Contains metadata about the token: algorithm and token type.
{
"alg": "HS256", // Signing algorithm
"typ": "JWT" // Token type
}
Base64URL encoded →
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2. Payload (Claims)
Contains the claims - statements about the user and additional metadata.
{
"sub": "1234567890", // Subject (user ID)
"name": "John Doe", // Custom claim
"admin": true, // Custom claim
"iat": 1516239022, // Issued at
"exp": 1516242622 // Expiration time
}
Base64URL encoded →
eyJzdWIiOiIxMjM0NTY3ODkwIi4uLn0
3. Signature
Verifies the token wasn't tampered with. Created by signing header + payload.
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Result →
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
⚠️ Important
The header and payload are only Base64URL encoded, NOT encrypted. Anyone can decode and read them. Never put sensitive data (passwords, credit cards) in a JWT payload!
3. JWT Authentication Flow
Step-by-step Authentication Flow:
- Login Request: User sends credentials (email/password) to server
- Verification: Server validates credentials against database
- Token Creation: Server creates JWT with user claims and signs it
- Token Response: Server sends JWT back to client
- Token Storage: Client stores JWT (cookie or localStorage)
- Authenticated Request: Client sends JWT with each request (Authorization header)
- Token Validation: Server verifies JWT signature and checks expiration
- Resource Access: Server grants/denies access based on claims
Request Header Format:
// HTTP Authorization Header
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIi4uLn0.signature
Refresh Token Flow:
For better security, use short-lived access tokens with longer-lived refresh tokens:
- Access Token: Short-lived (5-15 minutes), used for API requests
- Refresh Token: Long-lived (days/weeks), used to get new access tokens
4. Standard JWT Claims Explained
| Claim | Name | Description |
|---|---|---|
| iss | Issuer | Who created the token (e.g., your auth server) |
| sub | Subject | Who the token represents (usually user ID) |
| aud | Audience | Intended recipient (e.g., API server) |
| exp | Expiration | Unix timestamp when token expires |
| nbf | Not Before | Token not valid before this time |
| iat | Issued At | When the token was created |
| jti | JWT ID | Unique identifier for the token |
Custom Claims:
You can add any custom claims for your application:
{
"sub": "user_123",
"email": "john@example.com",
"role": "admin",
"permissions": ["read", "write", "delete"],
"organization_id": "org_456"
}
5. Signing Algorithms (HS256, RS256, ES256)
Symmetric (HMAC)
Same secret key for signing and verification
- HS256: HMAC + SHA-256
- HS384: HMAC + SHA-384
- HS512: HMAC + SHA-512
Best for: Single server, internal APIs
Asymmetric (RSA/ECDSA)
Private key signs, public key verifies
- RS256: RSA + SHA-256
- ES256: ECDSA + P-256
- PS256: RSA-PSS + SHA-256
Best for: Microservices, distributed systems
Algorithm Comparison:
| Algorithm | Type | Key Size | Recommended |
|---|---|---|---|
| HS256 | Symmetric | 256-bit secret | ✅ Single server |
| RS256 | Asymmetric | 2048-bit RSA | ✅ Distributed |
| ES256 | Asymmetric | 256-bit EC | ✅ Modern choice |
| none | - | - | ❌ Never use! |
6. Implementation in Different Languages
JavaScript (Node.js with jsonwebtoken)
const jwt = require('jsonwebtoken');
// Secret key (use environment variable in production!)
const SECRET = process.env.JWT_SECRET;
// Create a token
function createToken(user) {
const payload = {
sub: user.id,
email: user.email,
role: user.role
};
return jwt.sign(payload, SECRET, {
expiresIn: '15m', // Short-lived access token
issuer: 'myapp.com',
audience: 'myapp.com'
});
}
// Verify a token
function verifyToken(token) {
try {
const decoded = jwt.verify(token, SECRET, {
issuer: 'myapp.com',
audience: 'myapp.com'
});
return { valid: true, payload: decoded };
} catch (err) {
return { valid: false, error: err.message };
}
}
// Express middleware
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
const result = verifyToken(token);
if (!result.valid) {
return res.status(401).json({ error: result.error });
}
req.user = result.payload;
next();
}
Python (PyJWT)
import jwt
from datetime import datetime, timedelta
import os
SECRET = os.environ.get('JWT_SECRET')
def create_token(user: dict) -> str:
"""Create a JWT token for a user."""
payload = {
'sub': user['id'],
'email': user['email'],
'role': user['role'],
'iat': datetime.utcnow(),
'exp': datetime.utcnow() + timedelta(minutes=15)
}
return jwt.encode(payload, SECRET, algorithm='HS256')
def verify_token(token: str) -> dict:
"""Verify and decode a JWT token."""
try:
payload = jwt.decode(token, SECRET, algorithms=['HS256'])
return {'valid': True, 'payload': payload}
except jwt.ExpiredSignatureError:
return {'valid': False, 'error': 'Token expired'}
except jwt.InvalidTokenError as e:
return {'valid': False, 'error': str(e)}
# Flask decorator
from functools import wraps
from flask import request, jsonify
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return jsonify({'error': 'No token provided'}), 401
token = auth_header.split(' ')[1]
result = verify_token(token)
if not result['valid']:
return jsonify({'error': result['error']}), 401
request.user = result['payload']
return f(*args, **kwargs)
return decorated
Java (jjwt)
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
public class JwtUtil {
private static final Key SECRET = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final long EXPIRATION_MS = 15 * 60 * 1000; // 15 minutes
public static String createToken(User user) {
return Jwts.builder()
.setSubject(user.getId())
.claim("email", user.getEmail())
.claim("role", user.getRole())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
.signWith(SECRET)
.compact();
}
public static Claims verifyToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(SECRET)
.build()
.parseClaimsJws(token)
.getBody();
} catch (JwtException e) {
throw new RuntimeException("Invalid token: " + e.getMessage());
}
}
}
7. Security Best Practices
✅ Do: Use Strong Secrets
For HS256, use at least 256-bit
(32 character) random secrets. Generate with: openssl rand -hex 32
✅ Do: Set Short Expiration
Access tokens: 5-15 minutes. Use refresh tokens for longer sessions. Shorter = less damage if compromised.
✅ Do: Validate All Claims
Always verify: signature, expiration (exp), issuer (iss), audience (aud). Don't just decode - validate!
✅ Do: Use HTTPS
Always transmit JWTs over HTTPS. HTTP allows token interception via man-in-the-middle attacks.
❌ Don't: Store Sensitive Data
Never put passwords, credit cards, or PII in JWTs. The payload is readable by anyone!
❌ Don't: Use Weak Secrets
Never use "secret", "password", or short strings. These can be brute-forced in seconds.
❌ Don't: Accept Unsigned Tokens
Explicitly reject tokens with
"alg": "none". Always specify allowed algorithms.
8. Common JWT Vulnerabilities
CVE Algorithm Confusion Attack
Attacker changes alg from RS256 to HS256 and signs with
the public key (treating it as HMAC secret).
Fix: Always
specify allowed algorithms: algorithms=['RS256']
CVE None Algorithm Attack
Attacker sets alg to "none" and removes signature.
Vulnerable libraries accept unsigned tokens.
Fix: Reject
tokens with "alg": "none". Use updated libraries.
RISK Weak Secret Key
Short or predictable secrets can be brute-forced. "secret" or "password" cracked in seconds.
Fix: Use 256+
bit random secrets: openssl rand -hex 32
RISK Token Sidejacking
Stolen tokens via XSS or network sniffing can be replayed by attackers.
Fix: Use HttpOnly cookies, short expiration, token binding, and HTTPS.
9. Token Storage: Cookies vs localStorage
| Storage Method | XSS Protection | CSRF Protection | Best For |
|---|---|---|---|
| HttpOnly Cookie | ✅ Protected | ❌ Vulnerable | Most secure (with CSRF tokens) |
| localStorage | ❌ Vulnerable | ✅ Protected | SPAs with strict CSP |
| sessionStorage | ❌ Vulnerable | ✅ Protected | Tab-specific sessions |
| Memory (variable) | ✅ Protected | ✅ Protected | Most secure (lost on refresh) |
Recommended: HttpOnly Cookie with SameSite
// Setting a secure HttpOnly cookie (Node.js/Express)
res.cookie('accessToken', token, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 15 * 60 * 1000 // 15 minutes
});
10. Frequently Asked Questions
What is a JWT token used for?
Is JWT more secure than sessions?
How do I invalidate/revoke a JWT?
What's the difference between HS256 and RS256?
RS256 (asymmetric): Uses a private key to sign and a public key to verify. Only the auth server needs the private key; others can verify with the public key. Best for microservices and distributed systems.
Should I store JWT in localStorage or cookies?
SameSite=Strict (or
Lax) and implement CSRF tokens. If using localStorage, ensure strict
Content Security Policy.
How long should a JWT be valid?
Refresh tokens: 7-30 days (stored securely, rotated on use).
Critical applications (banking) may use even shorter access tokens (1-5 minutes). The key is balancing security (shorter) vs user experience (longer).
Can JWT be decoded without the secret?
What is a refresh token and why use it?
JWT vs OAuth vs OpenID Connect - what's the difference?
OAuth 2.0: An authorization framework - how to grant third-party access to resources without sharing passwords.
OpenID Connect: An identity layer on top of OAuth 2.0 - adds authentication and user identity.
OAuth and OIDC often use JWTs as their token format, but they're separate concepts.
What happens when a JWT expires?
exp claim timestamp is in the past, the token is invalid. The
server should reject it with a 401 Unauthorized error. The client should: (1) Attempt to
use the refresh token to get a new access token, or (2) Redirect the user to login again
if no valid refresh token exists. Implement token refresh logic before the access token
expires for seamless UX.
Need to decode a JWT?
Try our free JWT decoder tool. Decode tokens instantly, validate signatures, and inspect claims - all client-side.
Open JWT Decoder