正在加载,请稍候…

JWT Refresh Token Rotation: Secure Auth with Theft Detection

Implement JWT with refresh token rotation — token families, theft detection via reuse, httpOnly cookies, and revocation strategies.

Access + Refresh Architecture

Access tokens expire in 15 minutes. Refresh tokens live 7 days but rotate on each use.

Token Generation

export function generateTokens(userId: string) {
  const accessToken = jwt.sign(
    { sub: userId, type: 'access' },
    process.env.JWT_ACCESS_SECRET!,
    { expiresIn: '15m' }
  )
  const refreshToken = jwt.sign(
    { sub: userId, type: 'refresh', jti: randomBytes(16).toString('hex') },
    process.env.JWT_REFRESH_SECRET!,
    { expiresIn: '7d' }
  )
  return { accessToken, refreshToken }
}

Rotation with Theft Detection

app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies

  try {
    const payload = verifyRefreshToken(refreshToken)

    // Token already used? = theft detected
    const stored = await db.refreshToken.findFirst({ where: { jti: payload.jti } })
    if (!stored) {
      await db.refreshToken.deleteMany({ where: { userId: payload.sub } })
      return res.status(401).json({ error: 'Token reuse detected' })
    }

    await db.refreshToken.delete({ where: { jti: payload.jti } })

    const tokens = generateTokens(payload.sub)
    const newPayload = verifyRefreshToken(tokens.refreshToken)

    await db.refreshToken.create({
      data: { jti: newPayload.jti, userId: payload.sub, expiresAt: new Date(Date.now() + 7 * 86400_000) }
    })

    res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, secure: true, sameSite: 'strict' })
    return res.json({ accessToken: tokens.accessToken })
  } catch {
    return res.status(401).json({ error: 'Invalid token' })
  }
})

Storage Best Practices

Location Access Token Refresh Token
Memory Best Lost on page reload
httpOnly Cookie Good Best
localStorage XSS risk Never

-> Parse JWT tokens with the JWT Parser.