I’m implementing JWT authentication in a web application for a university project, and I’ve encountered an issue with the management of refresh tokens when a user clears their cookies before these tokens naturally expire. I would appreciate some guidance or suggestions on alternative approaches.
Scenario:
- Users receive an Access Token (AT) and a Refresh Token (RT) upon login.
- The AT has a lifespan of 10 minutes, while the RT lasts for 1 hour.
- If a user clears their browser cookies 5 minutes after logging in, both tokens are removed client-side. However, the RT still exists in the database with 55 minutes until its expiration.
Issue: When the AT expires and the user attempts another request, the backend prompts for a token refresh by making a POST request to the /refresh-token
endpoint, triggering the exports.refreshTokens
function. However, since the RT cookie was cleared, no RT can be sent to the server, leading to a 403 response and effectively logging the user out. Meanwhile, the RT in the database remains valid until its set expiration, posing a potential security risk if maliciously used, along with its corresponding sessionLog database entry.
Questions:
- Is there a recommended approach to immediately invalidate the RT in the database when cookies are cleared by the user, or when no RT is present during a refresh request?
- How can the backend detect cookie clearance or handle scenarios securely, considering the RT might still be active?
- Would setting a shorter lifespan for the RT mitigate the issue sufficiently, or are there better strategies to handle this potential vulnerability?
Current Implementation (simplified):
const jwt = require('jsonwebtoken');
const config = require('../config/auth.config');
const db = require('../models');
exports.refreshTokens = async (req, res) => {
const refreshToken = req.cookies['refreshToken'];
if (!refreshToken) {
return res.status(403).send("Session invalid, please log in again.");
}
try {
const existingToken = await db.Token.findOne({
where: { tokenKey: refreshToken, tokenType: 'refresh' }
});
if (!existingToken || existingToken.expiresAt < new Date() || existingToken.invalidated) {
if (existingToken) {
await db.Token.update({ invalidated: true }, { where: { tokenKey: refreshToken } });
}
return res.status(403).send("Token expired or invalidated, please log in again.");
}
const { id, session } = jwt.verify(refreshToken, config.secret);
const newAccessToken = jwt.sign({ id, session }, config.secret, { expiresIn: '10m' });
const newRefreshToken = jwt.sign({ id, session }, config.secret, { expiresIn: '1h' });
res.cookie('accessToken', newAccessToken, { httpOnly: true, secure: true });
res.cookie('refreshToken', newRefreshToken, { httpOnly: true, secure: true, path: '/refresh' });
res.status(200).json({ success: true });
} catch ( error ) {
console.error("Failed to refresh tokens:", error);
res.status(403).send({ message: "Failed to refresh tokens. Please log in again." });
}
};
- I’ve implemented a Node-cron job for scheduled cleanup to invalidate entries of refresh tokens that have reached their expiration dates (and their corresponding sessionLog entries), but this does not mitigate the risk associated with the gap between cookie clearance and token expiration effectively.
- For a bit more thorough understanding, here are the structures of my
token
andsessionLog
MySQL tables: sessionLog
: sessionId PK, userId FK, startTime, endTime (null by default), ipAddress, deviceInfotoken
: tokenKey (PK), userId FK, tokenType (‘refresh’ for refresh tokens), expiresAt, lastUsedAt (null by default), invalidated (boolean), sessionId FK.
Any insights or recommendations on how to handle this securely would be greatly appreciated!
grassa is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.