Access tokens and refresh tokens explained
access token lifetime, refresh token lifetime, token rotation, refresh endpoint, silent refresh, token family tracking, refresh token reuse detection
Two-Token Strategy
A single long-lived JWT is a liability — if it is stolen, the attacker has access until it expires. The solution is two tokens with different lifetimes and purposes.
Access token — short-lived (15–60 minutes), sent with every API request, verified quickly without a database lookup.
Refresh token — long-lived (7–30 days), stored more carefully, sent only to a single /auth/refresh endpoint to obtain a new access token.
Refresh Endpoint
app.post('/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).end();
try {
const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
// Optionally: check token exists in DB and has not been revoked
const newAccessToken = jwt.sign(
{ sub: payload.sub },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
Refresh Token Rotation
On each refresh, issue a new refresh token and invalidate the old one. If an old refresh token is ever used again, it signals theft — immediately invalidate all tokens for that user. This is called refresh token reuse detection.
