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:
- Marks the used refresh token as
rotatedin the database. - Issues a new refresh token in the same chain (linked via
parent_id). - Returns the new tokens.
If you ever submit a refresh token that has already been rotated:
- We return
400 invalid_grant. - We revoke the entire chain (current + all descendants).
- 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/refreshpath of your server, withSecure,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.