Implement secure JWT authentication in Node.js. Learn algorithm confusion attacks, token storage, refresh token patterns, blacklisting, and JWT security best practices.
JWT Authentication Security
Secure JWT Implementation
import jwt from 'jsonwebtoken'
import crypto from 'crypto'
const JWT_SECRET = process.env.JWT_SECRET // At least 256 bits
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET
const ACCESS_TOKEN_TTL = '15m'
const REFRESH_TOKEN_TTL = '7d'
function generateTokens(payload) {
const accessToken = jwt.sign(
{ ...payload, type: 'access' },
JWT_SECRET,
{
algorithm: 'HS256',
expiresIn: ACCESS_TOKEN_TTL,
issuer: 'myapp.com',
audience: 'myapp-users',
}
)
const refreshToken = jwt.sign(
{ userId: payload.userId, type: 'refresh', jti: crypto.randomUUID() },
JWT_REFRESH_SECRET,
{ expiresIn: REFRESH_TOKEN_TTL }
)
return { accessToken, refreshToken }
}
function verifyAccessToken(token) {
return jwt.verify(token, JWT_SECRET, {
algorithms: ['HS256'], // ALWAYS specify algorithms - prevents alg:none attack
issuer: 'myapp.com',
audience: 'myapp-users',
})
}
Common JWT Vulnerabilities
// VULNERABILITY 1: Algorithm confusion (alg:none)
// BAD: Not specifying algorithm
jwt.verify(token, secret) // Vulnerable to alg:none
// GOOD: Always specify algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] })
// VULNERABILITY 2: Weak secret
// BAD:
const secret = 'secret123' // Too weak
// GOOD: Cryptographically random
const secret = crypto.randomBytes(32).toString('hex') // 256 bits
// VULNERABILITY 3: Storing JWT in localStorage
// BAD: Vulnerable to XSS
localStorage.setItem('token', jwt)
// GOOD: httpOnly cookie - XSS resistant
res.cookie('access_token', token, {
httpOnly: true, // Not accessible via JS
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
})
Refresh Token Rotation with Redis
import { createClient } from 'redis'
const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
async function saveRefreshToken(jti, userId, expiresIn) {
await redis.setEx(
`refresh_token:${jti}`,
expiresIn,
JSON.stringify({ userId, used: false })
)
}
async function useRefreshToken(refreshToken) {
const payload = jwt.verify(refreshToken, JWT_REFRESH_SECRET, {
algorithms: ['HS256']
})
const stored = await redis.get(`refresh_token:${payload.jti}`)
if (!stored) throw new Error('Invalid refresh token')
const data = JSON.parse(stored)
// Detect reuse attack
if (data.used) {
// Token reuse - revoke all user tokens
await revokeAllUserTokens(data.userId)
throw new Error('Refresh token reuse detected - all sessions revoked')
}
// Mark as used
await redis.set(`refresh_token:${payload.jti}`,
JSON.stringify({ ...data, used: true }),
{ keepTTL: true }
)
// Issue new tokens
const { accessToken, refreshToken: newRefreshToken } = generateTokens({
userId: data.userId
})
// Save new refresh token
const newPayload = jwt.decode(newRefreshToken)
await saveRefreshToken(newPayload.jti, data.userId, 7 * 24 * 3600)
return { accessToken, refreshToken: newRefreshToken }
}
JWT Blacklisting for Logout
async function logout(accessToken, refreshToken) {
// Blacklist access token until expiry
const decoded = jwt.decode(accessToken)
const ttl = decoded.exp - Math.floor(Date.now() / 1000)
if (ttl > 0) {
await redis.setEx(`blacklist:${decoded.jti}`, ttl, '1')
}
// Invalidate refresh token
const refreshDecoded = jwt.decode(refreshToken)
await redis.del(`refresh_token:${refreshDecoded.jti}`)
}
// Middleware to check blacklist
async function authMiddleware(req, res, next) {
const token = req.cookies.access_token || req.headers.authorization?.split(' ')[1]
if (!token) return res.status(401).json({ error: 'No token' })
try {
const payload = verifyAccessToken(token)
// Check blacklist
const blacklisted = await redis.get(`blacklist:${payload.jti}`)
if (blacklisted) return res.status(401).json({ error: 'Token revoked' })
req.user = payload
next()
} catch (err) {
res.status(401).json({ error: 'Invalid token' })
}
}
Asymmetric JWT with RS256
import { readFileSync } from 'fs'
// Generate keys: openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem
const privateKey = readFileSync('private.pem')
const publicKey = readFileSync('public.pem')
function signToken(payload) {
return jwt.sign(payload, privateKey, {
algorithm: 'RS256',
expiresIn: '15m',
keyid: 'key-v1', // For key rotation
})
}
function verifyToken(token) {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Only RS256 - never HS256
})
}
// RS256 benefit: Only auth service has private key,
// any service can verify with public key
Security Summary
| Vulnerability |
Prevention |
| Algorithm confusion |
Specify algorithms: ['HS256'] |
| Token theft |
httpOnly cookies, short expiry |
| Weak secret |
256-bit random secret |
| Missing validation |
Verify issuer, audience |
| No revocation |
Redis blacklist + rotation |