In modern web and mobile security, short-lived access tokens protect APIs from hijacked credentials, but asking users to re-enter their passwords every 10 minutes ruins user experience. Refresh tokens resolve this dilemma.

A refresh token allows a client application to seamlessly obtain new access tokens behind the scenes. However, because refresh tokens are long-lived, securing them requires specialized patterns like Refresh Token Rotation (RTR) and Automatic Reuse Detection.

1. Why Do We Need Refresh Tokens?

Stateless access tokens (JWTs) cannot be readily revoked once issued. If an attacker intercepts a token via network sniffing, malware, or an open log file, they have complete access until the token expires.

To minimize this exposure window, access tokens are granted a very short lifespan: typically 5 to 15 minutes. But how do we keep the user logged in without interruption? By pairing the access token with a long-lived credential called a Refresh Token (typically valid for 7 to 30 days).

2. The Refresh Token Lifecycle

The lifecycle works as a two-tier authentication loop:

Refresh Token Exchange Flow
Client Application                     API Gateway                  Auth Server
       |                                    |                            |
       |--- 1. POST /login --------------------------------------------->|
       |    (username + password)           |                            |
       |<-- 2. Return Access Token (15m) + Refresh Token (14d) ----------|
       |                                    |                            |
       |=== NORMAL API REQUESTS (15 minutes) ============================|
       |--- 3. GET /api/dashboard --------->|                            |
       |    Authorization: Bearer   |--+ Validates signature     |
       |<-- 4. HTTP 200 OK -----------------|<--+ Returns protected data |
       |                                    |                            |
       |=== ACCESS TOKEN EXPIRES ========================================|
       |--- 5. GET /api/dashboard --------->|                            |
       |<-- 6. HTTP 401 Unauthorized -------| (TokenExpiredError)        |
       |                                    |                            |
       |=== SILENT TOKEN RENEWAL ========================================|
       |--- 7. POST /oauth/token --------------------------------------->|
       |    grant_type=refresh_token&refresh_token=rt_xyz123             |
       |                                    |--+ Verifies in database    |
       |                                    |  | Issues new access token |
       |<-- 8. Returns New Access Token (15m) + New Refresh Token -------|
       |                                    |                            |
       |--- 9. Retry GET /api/dashboard --->|                            |
       |<-- 10. HTTP 200 OK ----------------|                            |

3. Refresh Token Rotation (RTR)

In older systems, refresh tokens were static: the same refresh token was reused over and over for weeks. If that token was stolen from a user's browser, the attacker had persistent access.

🔁 Refresh Token Rotation (RTR) & Reuse Detection

Security Lifecycle
1 Initial Login Token A (15m) + Refresh Token R1 (7d) 2 Token A Expires Client exchanges R1 POST /oauth/token 3 RTR Rotation R1 invalidated! New Token B + New R2 🛡️ Automatic Token Reuse Detection If an attacker replays previously consumed R1, the server detects the breach and revokes R2 instantly.
Figure 1: Refresh Token Rotation (RTR) invalidates the previous refresh token on every single exchange, detecting token theft automatically.

The modern standard is Refresh Token Rotation (RFC 6749 Section 10.4):

4. Automatic Token Reuse Detection

What happens if an attacker intercepts a refresh token right before the legitimate user uses it? This is where Automatic Reuse Detection provides a powerful defense mechanism:

🛡️ How Reuse Detection Shuts Down Token Theft

Imagine User Alice holds Refresh Token RT-1. An attacker intercepts RT-1.

  1. The attacker uses RT-1 to get a new token pair. The server invalidates RT-1 and gives the attacker RT-2.
  2. Ten minutes later, Alice's browser attempts to use RT-1 (which Alice still believes is valid).
  3. The authorization server sees that RT-1 has already been used!
  4. The server detects a replay attack, immediately flags the entire token family as compromised, and revokes all active access and refresh tokens associated with that user session.
  5. Both Alice and the attacker are logged out. The attacker is immediately blocked from further access, and Alice is prompted to log in again with multi-factor authentication.

5. Secure Storage: Where Should Refresh Tokens Live?

Because refresh tokens grant the ability to generate new credentials, their storage location is vital:

6. Node.js Express Implementation Outline

Here is how a production refresh route handles token rotation and revocation in Node.js:

POST /auth/refresh Handler
app.post("/auth/refresh", async (req, res) => {
  const incomingToken = req.cookies.refreshToken;
  if (!incomingToken) return res.status(401).json({ error: "Missing refresh token" });

  // 1. Look up the token in your database
  const tokenRecord = await db.findRefreshToken(incomingToken);

  // 2. Check for token reuse / compromise
  if (!tokenRecord) {
    // Token not found or previously rotated! Possible theft!
    await db.revokeTokenFamily(incomingToken);
    res.clearCookie("refreshToken");
    return res.status(403).json({ error: "Token reuse detected. Session terminated." });
  }

  // 3. Invalidate the old token (Single-use rotation)
  await db.markTokenAsUsed(tokenRecord.id);

  // 4. Generate new token pair
  const newAccessToken = signAccessToken({ sub: tokenRecord.userId });
  const newRefreshToken = generateSecureRandomToken();

  // 5. Save new refresh token linked to the family
  await db.createRefreshToken({
    token: newRefreshToken,
    userId: tokenRecord.userId,
    familyId: tokenRecord.familyId
  });

  // 6. Set new cookie and return access token
  res.cookie("refreshToken", newRefreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: "strict",
    path: "/auth"
  });

  return res.json({ accessToken: newAccessToken });
});