OAuth2 refresh tokens
Renewing access tokens without forcing the user back through login
A refresh token lets your app obtain a new access token without going through the full Authorization Code flow again. Olympus issues refresh tokens by default when your client requests the offline_access scope.
Getting a refresh token
Request offline_access in the initial Authorization Code flow:
GET /oauth2/auth?
client_id=YOUR_CLIENT
&response_type=code
&scope=openid profile email offline_access
&redirect_uri=https://app.example.com/callback
&code_challenge=... # PKCE for public clients
&code_challenge_method=S256
&state=...After exchanging the code for tokens, you'll receive refresh_token in addition to access_token and id_token.
Storing refresh tokens
Confidential clients (server-side): store refresh tokens in your database, encrypted at rest, scoped to the user. Treat them as you'd treat a password.
Public clients (SPA/native): store the refresh token in an HttpOnly, Secure, SameSite=Strict cookie set by your backend, not in localStorage (XSS would steal it). For SPAs, this means your backend issues its own session cookie and proxies refresh requests; you don't expose the refresh token to JavaScript.
For native mobile, use the platform's secure credential store (iOS Keychain, Android Keystore).
Using a refresh token
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=ABC123...
&client_id=YOUR_CLIENT
&client_secret=YOUR_SECRET # confidential clientsFor public clients, no client_secret; PKCE isn't required on refresh.
Response:
{
"access_token": "...",
"refresh_token": "...",
"id_token": "...",
"token_type": "bearer",
"expires_in": 3600
}Refresh token rotation
Hydra rotates refresh tokens on every use, the response includes a new refresh token, and the one you sent is invalidated.
This means:
- After every refresh, store the new refresh token; the old one is dead.
- If two requests use the same refresh token (race), the second one fails.
- If you ever see a refresh token reuse attempt, that's evidence of theft, the original token has been compromised. Revoke the entire chain.
You can detect rotation-violation in your backend logs and trigger a session revocation:
# Detect that refresh_token X has already been used
curl -X POST http://localhost:3103/oauth2/revoke \
-H 'Authorization: Basic ...' \
-d "token=X"TTLs
Defaults configured in hydra.yml:
- Access token: 1 hour
- Refresh token: 30 days (rotated on use, so the family lives 30 days from initial issuance even with frequent refreshes)
- ID token: 1 hour
You can configure shorter or longer TTLs per OAuth2 client via the token_endpoint_auth_method-related fields when creating the client.
When the refresh fails
| Error | Cause |
|---|---|
invalid_grant with refresh_token has been revoked | The token has been explicitly revoked, or its rotation chain was broken. The user must log in again. |
invalid_grant with expired | The refresh token has aged out (default 30 days). User must log in. |
invalid_grant with mismatching client_id | Sending the refresh from a different client than it was issued for. |
invalid_client | Client credentials wrong (confidential clients). |
For all of these, the correct app behavior is: clear local tokens, redirect to login.
Programmatic refresh in the background
For SPAs, the typical pattern:
- On every access-token expiry (or proactively at 80% of TTL), the SPA's API client requests a refresh from its backend.
- The backend, holding the refresh token in an HttpOnly cookie, exchanges it with Hydra.
- The backend returns the new access token to the SPA in a short-lived in-memory store (not a cookie, not localStorage).
This avoids exposing the refresh token to JavaScript while keeping access tokens fresh.
Revoking a refresh token
To log a user out everywhere, revoke their refresh tokens:
# Revoke a specific refresh token
curl -X POST http://localhost:3102/oauth2/revoke \
-H 'Authorization: Basic ...' \
-d "token=<refresh_token>" \
-d "token_type_hint=refresh_token"
# Or revoke all sessions/tokens for an identity (Athena)
curl -X DELETE http://localhost:3001/api/identities/IDENTITY_ID/sessions \
-H 'Cookie: athena-session=...'Related
- Integrate, OAuth2 overview, when to use what.
- Integrate, RP-initiated logout, sign-out flow.
- Reference, Hydra token endpoint.
- Reference, Hydra revoke token.