patterntypescriptMajor
Refresh Token Rotation with Silent Re-authentication
Viewed 0 times
refresh tokentoken rotationsilent refreshaccess token expiryreuse detectionhttpOnly cookie
Error Messages
Problem
Access tokens expire and users are abruptly logged out. Storing long-lived tokens defeats security. Manual re-login creates poor UX.
Solution
Issue a short-lived access token (15 min) and a long-lived refresh token (7–30 days). On access token expiry, use the refresh token to silently obtain a new pair. Invalidate the old refresh token immediately (rotation) to detect reuse attacks.
Why
Refresh token rotation means each refresh token can only be used once. If a stolen refresh token is used after the legitimate client has already rotated it, the server detects the reuse and can revoke the entire family.
Gotchas
- Store the refresh token in an httpOnly, Secure, SameSite=Strict cookie — never in localStorage
- Implement a refresh token family: if reuse is detected, revoke all tokens issued from the same root
- Race conditions during concurrent requests can cause multiple refresh attempts — use a mutex or queue on the client side
Code Snippets
NextAuth JWT callback implementing refresh token rotation
async jwt({ token, account }) {
// Initial sign-in
if (account) {
return {
...token,
accessToken: account.access_token,
accessTokenExpires: Date.now() + (account.expires_in ?? 3600) * 1000,
refreshToken: account.refresh_token,
};
}
// Access token is still valid
if (Date.now() < (token.accessTokenExpires as number)) return token;
// Rotate expired access token
const response = await fetch('https://oauth2.provider.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: token.refreshToken as string,
client_id: process.env.CLIENT_ID!,
client_secret: process.env.CLIENT_SECRET!,
}),
});
const tokens = await response.json();
if (!response.ok) throw tokens;
return {
...token,
accessToken: tokens.access_token,
accessTokenExpires: Date.now() + tokens.expires_in * 1000,
refreshToken: tokens.refresh_token ?? token.refreshToken,
};
}Revisions (0)
No revisions yet.