The Challenge of Stateless JWT Invalidation
JSON Web Tokens (JWT) are popular for stateless authentication in Node.js apps. But unlike traditional session IDs stored in cookies, JWTs are self-contained and stateless, making invalidation tricky. If a user logs out, changes their password, or faces a security breach, you need to revoke their token immediately.
This post explores proven methods to invalidate JWTs while balancing security, performance, and user experience.
Why JWT Invalidation Matters
- Security Risks: Stolen or compromised tokens can grant unauthorized access.
- User Control: Users expect to log out and terminate sessions across devices.
- Compliance: Regulations often require immediate revocation of access during breaches.
Solution 1: Token Blocklist (Blacklist)
How It Works:
- Store invalidated tokens in a database (e.g., Redis) until their natural expiration.
- Check incoming tokens against the blocklist during validation.
Implementation with Redis:
const redis = require('redis');
const client = redis.createClient();
// Invalidate a token
function invalidateToken(token) {
const expiry = jwt.decode(token).exp;
client.setex(token, (expiry - Date.now() / 1000), 'invalid');
}
// Middleware to check blocklist
function checkBlocklist(req, res, next) {
const token = req.headers.authorization.split(' ')[1];
client.get(token, (err, reply) => {
if (reply === 'invalid') return res.status(401).send('Token revoked');
next();
});
}
Pros:
- Immediate invalidation.
- Minimal storage (only revoked tokens).
Cons:
- Adds a database lookup per request.
Solution 2: Refresh Tokens with Short-Lived JWTs
How It Works:
- Issue a short-lived access token (e.g., 15 minutes) and a long-lived refresh token.
- When the access token expires, the client uses the refresh token to get a new one.
- Revoke refresh tokens to invalidate all associated access tokens.
Example Flow:
Login:
app.post('/login', (req, res) => {
const accessToken = jwt.sign(user, 'secret', { expiresIn: '15m' });
const refreshToken = jwt.sign(user, 'refreshSecret', { expiresIn: '7d' });
res.json({ accessToken, refreshToken });
});
Refresh Endpoint:
app.post('/refresh', (req, res) => {
const { refreshToken } = req.body;
if (isRevoked(refreshToken)) throw new Error('Token revoked');
const accessToken = jwt.sign(user, 'secret', { expiresIn: '15m' });
res.json({ accessToken });
});
Pros:
- Limits exposure of long-lived tokens.
- Centralized control via refresh token revocation.
Solution 3: Change the JWT Secret Key
How It Works:
Rotate the server’s secret key to invalidate all existing tokens instantly.
Use Case:
- Emergency scenarios (e.g., security breach).
Drawbacks:
- Forces all users to reauthenticate.
- Not granular (cannot target specific users or tokens).
Solution 4: JWT Versioning with User Data
How It Works:
- Add a
tokenVersion
field to the user database. - Include
tokenVersion
in the JWT payload. - Increment
tokenVersion
to invalidate all existing tokens.
Implementation:
// User schema
const userSchema = new mongoose.Schema({
username: String,
password: String,
tokenVersion: { type: Number, default: 0 }
});
// Invalidate all tokens for a user
async function invalidateUserTokens(userId) {
await User.updateOne({ _id: userId }, { $inc: { tokenVersion: 1 } });
}
// Token validation middleware
function validateToken(req, res, next) {
const user = jwt.verify(token, 'secret');
const dbUser = await User.findById(user.id);
if (user.tokenVersion !== dbUser.tokenVersion) throw new Error('Token invalid');
next();
}
Pros:
- No additional storage.
- Granular control per user.
Solution 5: Use jwt-redis for Server-Side Invalidation
How It Works:
The jwt-redis
library stores tokens in Redis, enabling server-side invalidation.
Implementation:
const JWTR = require('jwt-redis').default;
const redisClient = require('redis').createClient();
const jwtr = new JWTR(redisClient);
// Create token with unique identifier
const token = await jwtr.sign({ id: user.id }, 'secret', { expiresIn: '1h' });
// Invalidate token
await jwtr.destroy(user.id);
Pros:
- Combines JWT benefits with Redis speed.
- Immediate invalidation.
Best Practices for JWT Security
- Use HTTPS: Prevent token interception.
- Set Short Expiry Times: 15 minutes for access tokens.
- Monitor Anomalies: Track IP changes or unusual activity.
- Store Tokens Securely: Use HTTP-only cookies or Secure Storage (mobile).
Common Pitfalls to Avoid
- Long-Lived Tokens: Increase risk of token theft.
- Ignoring Token Versioning: Makes bulk invalidation impossible.
- No Refresh Token Revocation: Compromised refresh tokens grant endless access.
Conclusion
JWT invalidation requires balancing stateless design with security needs. For most apps:
- Use refresh tokens for granular control.
- Pair with Redis blocklists for immediate revocation.
- Rotate secrets only in emergencies.
Keywords: Invalidate JWT, JWT blocklist, stateless session management, JWT security, refresh tokens, jwt-redis, token versioning, Node.js JWT invalidation.
Secure your Node.js apps with these strategies to ensure stolen tokens can’t compromise user data! 🔒