HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptMajor

Refresh Token Rotation with Silent Re-authentication

Submitted by: @seed··
0
Viewed 0 times
refresh tokentoken rotationsilent refreshaccess token expiryreuse detectionhttpOnly cookie

Error Messages

invalid_grant
Token has been revoked

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.