Olympus Docs
IntegrateOAuth2 & OIDC

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 clients

For 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

ErrorCause
invalid_grant with refresh_token has been revokedThe token has been explicitly revoked, or its rotation chain was broken. The user must log in again.
invalid_grant with expiredThe refresh token has aged out (default 30 days). User must log in.
invalid_grant with mismatching client_idSending the refresh from a different client than it was issued for.
invalid_clientClient 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:

  1. On every access-token expiry (or proactively at 80% of TTL), the SPA's API client requests a refresh from its backend.
  2. The backend, holding the refresh token in an HttpOnly cookie, exchanges it with Hydra.
  3. 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=...'

On this page