Skip to content

Refresh tokens & rotation

Refresh tokens let your backend mint new access_tokens without redirecting the user. Little X Little issues refresh tokens only when you request offline_access in the scope list.

Requesting one

php $url = $client->createAuthUrl([ 'scope' => 'openid profile email offline_access', ]);

The token response will then include:

json { "access_token": "...", "refresh_token": "rt_2x9...", "id_token": "eyJ...", "expires_in": 3600 }

Refreshing

bash curl -X POST https://id.littlexlittle.org/oidc/token \ -d "grant_type=refresh_token" \ -d "refresh_token=rt_2x9..." \ -d "client_id=YOUR_CLIENT_ID"

The response contains a new refresh token. Store it. Discard the old one.

Rotation & replay detection

Every successful refresh:

  1. Marks the used refresh token as rotated in the database.
  2. Issues a new refresh token in the same chain (linked via parent_id).
  3. Returns the new tokens.

If you ever submit a refresh token that has already been rotated:

  1. We return 400 invalid_grant.
  2. We revoke the entire chain (current + all descendants).
  3. The user must sign in again.

This is the OAuth 2.0 Security Best Current Practice §4.13.2 defense against stolen refresh tokens.

Lifetimes

Token TTL Behavior
Access token 1 hour Hard expiry.
Refresh token 30 days sliding Reset to 30d on each successful use.
Refresh chain 90 days absolute Total chain age cap, even with constant use.

After 90 days the user must re-authenticate via /oidc/authorize.

Storage recommendations

  • Server-side only. Never expose the refresh token to JavaScript.
  • HttpOnly cookie scoped to /oauth/refresh path of your server, with Secure, SameSite=Strict, and a separate cookie from the access token cookie.
  • Encrypt at rest if you persist them in a database.

Detecting compromise

If you receive a 400 invalid_grant with error_description=Refresh token was already used, you have evidence of a compromised refresh token. Force-re-auth all sessions for that user and emit a security alert.