Introduction
JWT (JSON Web Token) has become the standard for modern web authentication. It's a compact, URL-safe way to transmit information between parties as a JSON object. Used for single sign-on, API authentication, and information exchange, understanding JWT is essential for building secure modern applications.
What is a JWT?
A JWT is a string consisting of three parts separated by dots:
xxxxx.yyyyy.zzzzz │ │ │ │ │ └── Signature │ └──────── Payload └────────────── Header
Each part is Base64-encoded JSON. Together, they create a token that can be verified but not easily forged.
JWT Structure
Header
Specifies the token type and signing algorithm:
{
"alg": "HS256",
"typ": "JWT"
}Base64-encoded: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload
Contains claims (user data and metadata):
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}Base64-encoded (can be decoded and read by anyone!)
Signature
Verifies the token wasn't tampered with. Created by:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
Only verifiable with the secret key - cannot be decoded.
Standard Claims
| Claim | Name | Description |
|---|---|---|
| iss | Issuer | Who issued the token |
| sub | Subject | User ID or identifier |
| aud | Audience | Intended recipients |
| exp | Expiration | When token expires (Unix timestamp) |
| iat | Issued At | When token was created |
| nbf | Not Before | Token valid after this time |
| jti | JWT ID | Unique identifier for the token |
Security Considerations
- JWT payload is NOT encrypted: Anyone can decode and read it. Never put sensitive data (passwords, secrets) in the payload.
- Signature prevents tampering: Without the secret, attackers cannot modify the token without detection.
- Always set expiration: Tokens without expiration never expire - stolen tokens remain valid forever.
- Use HTTPS: JWTs sent over HTTP can be intercepted. Always use TLS for transmission.
Critical Security Vulnerabilities
JWT implementations are frequently the target of attacks. Understanding these vulnerabilities is essential for building secure authentication systems.
1. Algorithm Confusion Attack (alg: none)
The most famous JWT vulnerability. The JWT specification allows the alg header to be set to "none", meaning no signature is used. An attacker can modify the payload and set alg to "none" to bypass signature verification entirely.
// Attacker modifies the header:
{
"alg": "none",
"typ": "JWT"
}
// And changes the payload to escalate privileges:
{
"sub": "admin",
"role": "superuser",
"exp": 9999999999
}
// The resulting token has an empty signature:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJzdXBlcnVzZXIiLCJleHAiOjk5OTk5OTk5OTl9.Fix: Always whitelist allowed algorithms
// Node.js (jsonwebtoken)
jwt.verify(token, secret, { algorithms: ['HS256'] });
// Never accept "none" algorithm
// Never let the token header decide the algorithm2. RS256/HS256 Algorithm Confusion
When a server uses RS256 (asymmetric), the public key is used to verify tokens. An attacker can change the alg header to HS256 (symmetric) and sign the token using the public key as the HMAC secret. If the server naively uses the algorithm from the header, it will verify the forged token successfully.
Fix: Explicitly specify the expected algorithm
// Always specify the algorithm explicitly
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// Never use the algorithm from the token header
// Never use the same verification function for different algorithms3. Weak Secret Keys
HS256 tokens are only as secure as their secret key. Common mistakes include using short passwords, dictionary words, or keys found in public repositories. Tools likejwt-cracker can brute-force weak HS256 secrets in minutes.
Fix: Use cryptographically strong secrets
// Generate a strong secret (Node.js)
const crypto = require('crypto');
const secret = crypto.randomBytes(64).toString('hex');
// Result: 128 hex characters = 512 bits of entropy
// NEVER use:
// - "secret", "password", "my-secret-key"
// - Keys shorter than 256 bits (32 bytes)
// - Keys committed to public repositories4. Token Theft and Revocation
JWTs are stateless - once issued, they're valid until expiration. If a token is stolen, there's no built-in way to revoke it. This is a fundamental design tradeoff.
Fix: Implement token revocation strategies
- Short expiration - Access tokens expire in 15 minutes, use refresh tokens for longer sessions
- Token blacklist - Store revoked token IDs in Redis/database until they expire
- Refresh token rotation - Issue a new refresh token on each use, invalidate old ones
- Secure storage - Store tokens in HttpOnly cookies, not localStorage
5. JWK Set Injection
Some JWT libraries support embedding public keys in the header via the jwk orjku fields. An attacker can inject their own public key and sign a token that the server will verify using the attacker's key.
Fix: Disable JWK header processing
// Never trust keys from the token header // Only use keys from your own trusted key store // Disable jwk and jku header processing in your JWT library
JWT Best Practices Checklist
- Always whitelist algorithms - Never accept
alg: "none". Specify expected algorithms explicitly in verification. - Use strong secrets - Minimum 256-bit random keys for HS256. Use RS256/ES256 for distributed systems.
- Set short expiration - Access tokens: 15 minutes. Refresh tokens: 7 days with rotation.
- Store tokens securely - Use HttpOnly, Secure, SameSite cookies. Avoid localStorage for access tokens.
- Validate all claims - Check
iss,aud,exp, andnbfon every request. - Never put secrets in payload - JWT payload is Base64-encoded, not encrypted. Anyone can read it.
- Use JWE for sensitive data - If you need encrypted content, use JSON Web Encryption (JWE) instead of JWS.
- Implement refresh token rotation - Issue a new refresh token on each use and invalidate the old one.
Choosing the Right Algorithm
| Algorithm | Type | Key | Best For |
|---|---|---|---|
| HS256 | Symmetric | Shared secret | Single server, simple setups |
| RS256 | Asymmetric | Public/private key pair | Microservices, third-party verification |
| ES256 | Asymmetric | EC public/private key | Shorter tokens, modern systems |
For most applications, RS256 is the best choice. It allows you to share the public key for verification while keeping the private key secure on the auth server. ES256 produces shorter signatures but requires more complex key management.
Implementation Example (Node.js)
import jwt from "jsonwebtoken";
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!;
// Generate tokens
function generateTokens(userId: string) {
const accessToken = jwt.sign(
{ sub: userId, type: "access" },
ACCESS_TOKEN_SECRET,
{ algorithm: "HS256", expiresIn: "15m" }
);
const refreshToken = jwt.sign(
{ sub: userId, type: "refresh" },
REFRESH_TOKEN_SECRET,
{ algorithm: "HS256", expiresIn: "7d" }
);
return { accessToken, refreshToken };
}
// Verify access token
function verifyAccessToken(token: string) {
try {
const payload = jwt.verify(token, ACCESS_TOKEN_SECRET, {
algorithms: ["HS256"], // Always whitelist!
});
return { valid: true, payload };
} catch (error) {
return { valid: false, error };
}
}Related Tools
Conclusion
JWT provides a compact, self-contained way to transmit authenticated information. But its simplicity can be deceptive - JWT implementations are frequent targets for attacks like algorithm confusion, weak secrets, and token theft.
The most important rules: always whitelist allowed algorithms, use strong secrets, set short expiration times, and never trust keys from the token header. Remember that JWTs are encoded (Base64), not encrypted - the payload can be read by anyone. Security comes from the signature, which prevents tampering.
Use our JWT Debugger to inspect and understand tokens before implementing them in your applications.
Written by Zhisan
Independent Developer · Last updated June 2026