# Refresh Token Rotation

This page explains how `VerifyJWT()` refreshes an expired Access Token within the same request, and when the Refresh ID rotates.

## Triggers

A refresh happens only in these two cases; while the Access Token is valid, Redis refresh data is not touched:

| Scenario | Where the Refresh ID comes from |
|---|---|
| Access Token expired (`jwt expired`) | The `refresh_id` decoded from the expired token |
| No Access Token, but a Refresh ID | `X-Refresh-ID` header or the refresh cookie |

## Refresh Flow

```mermaid
sequenceDiagram
    participant C as Client
    participant A as VerifyJWT
    participant R as Redis
    participant U as checkUserExists
    C->>A: Expired Access Token or Refresh ID only
    A->>R: GET refresh:id, compare fingerprint
    A->>R: TTL refresh:id, version + 1
    opt version > 5 or remaining TTL < refreshTokenExpires / 2
        A->>R: SETEX old key 5 seconds
        A->>A: New Refresh ID, version reset to 0
    end
    A->>R: SETEX refresh:id (refreshTokenExpires)
    A->>U: Does the user exist
    A-->>C: New Access Token + Refresh ID (Set-Cookie and X-New-*)
```

## Rotation Rules

| Condition | Behavior |
|---|---|
| Every refresh | `version + 1`, `expires_at` extended, Redis TTL reset to the full `refreshTokenExpires` |
| `version > 5` | Issue a new Refresh ID |
| Remaining TTL below `refreshTokenExpires / 2` | Issue a new Refresh ID |
| On rotation | The old key stays for 5 seconds so concurrent requests can still refresh with the old ID |

`CreateJWT()` stores refresh data with `version` 1: the first 4 refreshes keep the same ID, and the 5th pushes `version` to 6 and rotates. After rotation `version` resets to 0, so the next rotation happens on the 6th refresh.

Each refresh resets the TTL to the full lifetime, so Refresh ID expiry is **sliding**: an actively used session never expires, and only goes stale after `refreshTokenExpires` of inactivity.

## What a Refresh Writes Back

| Location | Content |
|---|---|
| `X-New-Access-Token` header | The new Access Token |
| `X-New-Refresh-ID` header | The current Refresh ID (new value on rotation) |
| Access cookie | New Access Token, lifetime `accessTokenExpires` |
| Refresh cookie | Refresh ID, lifetime `refreshTokenExpires` |

The new Access Token payload is the user snapshot stored at login, plus `fp`, `version`, and `refresh_id`.

Related: [Verification Results](/verify-results), [Redis Keys](/redis-keys)
