Implementing JSON Web Tokens (JWTs) in stateless architectures offers significant scalability benefits, but it introduces a critical security vulnerability: token theft. Unlike session-based systems, where the server can instantly invalidate a session on the client side, JWTs are self-contained. Once issued, they remain valid until they expire, even if the server determines the user should be logged out. This post explores robust strategies to mitigate this risk through token rotation and selective revocation.
The Stateless Dilemma
In a traditional stateless environment, the server does not store user state. The JWT acts as the source of truth, containing all necessary claims. However, if an attacker steals an access token via XSS or network interception, they can use it until its short expiry time. While limiting access token lifespan (e.g., 15 minutes) helps, it creates friction for user experience. This is where the Access Token/Refresh Token pattern becomes essential.
By issuing short-lived access tokens and long-lived refresh tokens, we limit the window of opportunity for attackers. The access token handles API requests, while the refresh token is used solely to obtain new access tokens. Crucially, the refresh token must be stored securely, preferably in HTTP-only cookies, to prevent JavaScript access.
Implementing Token Rotation
Token rotation is the practice of issuing a new refresh token every time a refresh token is used. This strategy minimizes the value of a stolen refresh token. If an attacker steals a refresh token, they can only make a single request before the legitimate user’s next refresh attempt invalidates the stolen one.
Here is a simplified Node.js example demonstrating rotation logic:
async function handleRefreshToken(req, res) {
const { refreshToken } = req.body;
// 1. Verify the token signature and expiration
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
// 2. Check if token exists in our store (Redis)
const storedToken = await redis.get(`refresh:${decoded.jti}`);
if (!storedToken) {
return res.status(401).send('Refresh token revoked or invalid');
}
// 3. Issue NEW access and refresh tokens
const newAccessToken = generateAccessToken(decoded.userId);
const newRefreshToken = generateRefreshToken(decoded.userId);
// 4. Store new refresh token and delete old one
await redis.setEx(`refresh:${newJti}`, EXPIRY, newRefreshToken);
await redis.del(`refresh:${decoded.jti}`);
res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
}
Revocation Strategies
Despite rotation, there are scenarios where immediate revocation is necessary, such as password changes or reported account compromise. Since JWTs are stateless, we cannot simply "delete" them. We need a way to check their validity before granting access.
1. Short-Lived Access Tokens
The most effective mitigation is keeping access tokens very short-lived (5-15 minutes). This ensures that even if a token is stolen, the attacker has a very limited window to exploit it. Combine this with rotation to make the window practically non-existent.
2. Token Denylist (Blacklist)
For scenarios requiring immediate logout, we can maintain a denylist of revoked token IDs (JTI). When a user logs out, the JTI is added to a Redis set with a TTL matching the token's remaining life. During each request, the server checks if the JTI exists in the denylist.
function isValidToken(jti, token) {
// Check denylist first
if (redis.sIsMember('token_denylist', jti)) {
return false;
}
return true;
}
This approach adds a tiny latency cost but ensures security. For high-traffic systems, ensure the denylist operations are optimized using Redis, which handles these checks in O(1) time complexity.
Conclusion
Securing stateless architectures requires moving beyond simple JWT issuance. By combining short-lived access tokens, aggressive refresh token rotation, and targeted revocation mechanisms, developers can significantly reduce the impact of token theft. Remember, security is a balance between usability and risk. Implementing these strategies ensures that your application remains resilient against modern threats without sacrificing the scalability benefits of JWTs.