# Refresh tokens are rotated on use

- **Pattern:** `ab-000379` (`api-security.auth.refresh-token-rotation`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000379
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000379)

## Why it matters

Refresh tokens that can be reused indefinitely negate the security benefit of short-lived access tokens entirely. CWE-384 (Session Fixation) captures the structural problem: a compromised token remains valid forever as a fixed key to the account. OWASP API2 (Broken Authentication) specifically calls out refresh token replay as an authentication weakness. A single leaked refresh token — from a log, a compromised client, or a man-in-the-middle — gives an attacker persistent access that survives every access token expiry. Rotation with detection forces the attacker to race the legitimate user, and a race loss invalidates the entire session.

## Severity rationale

High because a stolen refresh token grants indefinite re-authentication capability, making it functionally equivalent to a never-expiring credential.

## Remediation

Treat each refresh token as single-use: when it's consumed to issue a new access token, delete the old one from the database and persist the replacement. Detect replay attacks by flagging token reuse.

```ts
// src/app/api/auth/refresh/route.ts
export async function POST(req: Request) {
  const { refreshToken } = await req.json()
  const stored = await db.refreshToken.findUnique({ where: { token: refreshToken } })

  if (!stored) {
    // Token not found — could be a replay attack; invalidate all sessions for safety
    return new Response('Unauthorized', { status: 401 })
  }

  // Delete old token before issuing new one (rotation)
  await db.refreshToken.delete({ where: { id: stored.id } })

  const newRefreshToken = crypto.randomUUID()
  await db.refreshToken.create({
    data: { token: newRefreshToken, userId: stored.userId, expiresAt: addDays(new Date(), 30) }
  })

  return Response.json({
    accessToken: signAccessToken(stored.userId),
    refreshToken: newRefreshToken
  })
}
```

## Detection

- **ID:** `refresh-token-rotation`
- **Severity:** `high`
- **What to look for:** Enumerate every relevant item. Look for refresh token handling logic. If your API issues access tokens and refresh tokens, check that when a refresh token is used to issue a new access token, the old refresh token is invalidated and a new one is issued.
- **Pass criteria:** At least 1 of the following conditions is met. Refresh token endpoint invalidates the old refresh token after issuing a new access token. Replayed refresh tokens are rejected.
- **Fail criteria:** The same refresh token can be used multiple times without invalidation, or no refresh token rotation is implemented.
- **Skip (N/A) when:** The API uses short-lived access tokens without refresh tokens, or relies on server-side session management with automatic timeout.
- **Cross-reference:** For user-facing accessibility and compliance, the Accessibility Basics audit covers foundational requirements.
- **Detail on fail:** `"Refresh tokens are not rotated — the same refresh token can be used indefinitely to issue new access tokens"` or `"Refresh token endpoint does not invalidate old tokens after rotation"`
- **Remediation:** Implement refresh token rotation by invalidating the old token when a new one is issued:

  ```ts
  export default async (req, res) => {
    const { refreshToken } = req.body
    const session = await db.refreshToken.findUnique({ where: { token: refreshToken } })

    if (!session) {
      return res.status(401).json({ error: 'Invalid refresh token' })
    }

    // Delete old token
    await db.refreshToken.delete({ where: { id: session.id } })

    // Issue new tokens
    const newAccessToken = jwt.sign({ userId: session.userId }, process.env.JWT_SECRET, { expiresIn: '1h' })
    const newRefreshToken = crypto.randomUUID()
    await db.refreshToken.create({ data: { token: newRefreshToken, userId: session.userId } })

    res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken })
  }
  ```

## External references

- cwe CWE-287
- cwe CWE-384
- owasp A07
- owasp API2

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-000379
