Refresh Token Rotation

Last updated

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

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, Redis Keys

中文