Refresh tokens are rotated on use
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.
// 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:
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 — Improper Authentication
- cwe · CWE-384 — Session Fixation
- owasp:2021 · A07 — Identification and Authentication Failures
- owasp:2023 · API2 — Broken Authentication
Taxons
History
- 2026-04-18·v1.0.0·Initial import from api-security·automated