JWT Security: Best Practices for JSON Web Token Implementation
In this tutorial, you will learn about JWT Security: Best Practices for JSON Web Token Implementation. We cover key concepts, practical examples, and best practices to help you master this topic.
JSON Web Tokens (JWT) are a compact, URL-safe token format used for authentication and information exchange. While JWTs are widely used, many implementations have critical security flaws: weak algorithms, missing validation, secret leakage, and improper token storage.
flowchart TB
subgraph Secure JWT Flow
Login[User Login] --> Issue[Server Issues JWT]
Issue --> Claims[Include: sub, iat, exp, jti]
Claims --> Sign[Sign with RS256 or ES256]
Sign --> Client[Client Stores in httpOnly Cookie]
Client --> Request[API Request with JWT]
Request --> Validate[Server Validates: sig, exp, iss, aud]
Validate -->|Valid| Process[Process Request]
Validate -->|Invalid| Reject[401 Unauthorized]
end
subgraph Common Attacks
None[alg: none Attack]
Weak[alg: HS256 with Public Key]
XSS[XSS Steals Token]
Exp[No Expiration Validation]
end
What You'll Learn
- Secure JWT signing algorithms (RS256, ES256 vs HS256)
- Token validation: signature, expiration, issuer, audience
- Secure token storage: httpOnly cookies vs localStorage
- Refresh token rotation and revocation
Why It Matters
JWT vulnerabilities are among the most common authentication flaws. The "alg: none" attack, algorithm confusion attacks, and token theft via XSS have compromised countless applications. Proper JWT implementation is essential for secure token-based authentication.
Real-World Use
A payments API uses RS256-signed JWTs with 15-minute expiry. Access tokens are stored in httpOnly, Secure, SameSite cookies. Refresh tokens are stored in the database with a device fingerprint. On logout or password change, all refresh tokens for the user are revoked.
Secure JWT Implementation
Secure Token Generation (RS256)
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// Generate RSA key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
function generateAccessToken(user) {
return jwt.sign(
{
sub: user.id,
role: user.role,
email: user.email,
type: 'access'
},
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'https://api.example.com',
jwtid: crypto.randomUUID()
}
);
}
function generateRefreshToken(user) {
return jwt.sign(
{
sub: user.id,
type: 'refresh',
tokenVersion: user.tokenVersion
},
privateKey,
{
algorithm: 'RS256',
expiresIn: '7d',
issuer: 'https://api.example.com',
jwtid: crypto.randomUUID()
}
);
}
Expected output:
Access token: RS256-signed, 15-min expiry, includes sub, role, jti.
Refresh token: RS256-signed, 7-day expiry, includes tokenVersion for revocation.
Token Validation Middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://api.example.com',
maxAge: '15m'
});
if (decoded.type !== 'access') {
return res.status(401).json({ error: 'Invalid token type' });
}
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
Expected output:
Valid token: req.user populated, request proceeds.
Expired token: 401 with code TOKEN_EXPIRED.
Invalid signature: 403 Forbidden.
Refresh Token Rotation
const crypto = require('crypto');
// Store refresh tokens with rotation
const refreshTokens = new Map(); // Use Redis in production
async function rotateRefreshToken(oldToken, user) {
// Verify old refresh token
let decoded;
try {
decoded = jwt.verify(oldToken, publicKey, {
algorithms: ['RS256'],
issuer: 'https://api.example.com'
});
} catch (err) {
return { error: 'Invalid refresh token' };
}
// Check if token exists and is not revoked
const stored = refreshTokens.get(decoded.jti);
if (!stored || stored.revoked) {
// Possible token theft: revoke all tokens for user
revokeAllUserTokens(user.id);
return { error: 'Token reuse detected' };
}
// Revoke old token
refreshTokens.set(decoded.jti, { ...stored, revoked: true });
// Issue new tokens
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
const rtDecoded = jwt.decode(refreshToken);
refreshTokens.set(rtDecoded.jti, {
userId: user.id,
deviceFingerprint: req.headers['user-agent'],
createdAt: new Date(),
revoked: false
});
return { accessToken, refreshToken };
}
Expected output:
Old refresh token is revoked (consumed). New access + refresh token pair is issued. If old token is reused, all tokens for user are revoked.
Common Mistakes
- Using HS256 (symmetric) with a weak or leaked secret — use RS256 or ES256 (asymmetric) so the public key can validate without exposing the private key.
- Not validating the
algheader — attackers can changealgtononeor switch from RS256 to HS256 if they know the public key. - Storing tokens in localStorage, which is accessible to any JavaScript on the same origin (XSS vulnerability).
- Using excessively long token expiry (days or weeks) without refresh token rotation.
- Not including a
jti(JWT ID) claim, making token revocation impossible without tracking all issued tokens.
Practice Questions
- Why is RS256 more secure than HS256 for JWT signing?
- What is the "alg: none" attack and how do you prevent it?
- Why should access tokens have short expiry (e.g., 15 minutes)?
- How does refresh token rotation prevent token theft?
- Why is httpOnly cookie storage safer than localStorage for tokens?
Challenge
Build a complete JWT authentication system with: RS256 signing, access tokens (15min) + refresh tokens (7 days), refresh token rotation with reuse detection, token revocation on password change, and httpOnly cookie storage. Write tests for token expiry, invalid signature, and token reuse.
FAQ
Mini Project
Build a secure JWT authentication service. Implement: RS256 key pair generation, short-lived access tokens (15min), refresh tokens (7 days) with rotation, token blocklist in Redis for immediate revocation, httpOnly cookie storage, and comprehensive validation middleware.
What's Next
Continue to SSL/TLS to learn about securing data in transit.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro