Refresh tokens

Keep users signed in beyond the access token lifetime with OneiD refresh tokens, and handle rotation, reuse and failures correctly.

View as Markdown

A refresh token lets your application get a new access token without sending the user back to OneiD. OneiD rotates refresh tokens on every use and ends a chain of refreshes when the original lifetime runs out. This guide shows how to request, use, store and revoke them.

Get a refresh token

OneiD issues a refresh token when both are true:

  • the client has the refresh token grant, set by your administrator, and
  • the authorisation request includes the offline_access scope.
GET /connect/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20offline_access&state=hX3n9qT2vYw8LkP0&nonce=Qm4rT8zWc1JpVb6s&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256 HTTP/1.1
Host: YOUR_ONEID

The token response after the code exchange then contains refresh_token. See Authorization code flow with PKCE for the full flow.

If the client does not have the refresh token grant, or you leave out offline_access, there is no refresh token. Client credentials never return one.

Refresh tokens are opaque. Do not parse them, and never send them to an API.

Use a refresh token

Send it to the token endpoint with grant_type=refresh_token.

A public client sends its client ID:

POST /connect/token HTTP/1.1
Host: YOUR_ONEID
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=EXAMPLE_OPAQUE_REFRESH_TOKEN_7Hq2Lm9Xv4Rk
&client_id=YOUR_CLIENT_ID

A confidential client authenticates as it does for the code exchange:

curl -s https://YOUR_ONEID/connect/token \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d refresh_token=EXAMPLE_OPAQUE_REFRESH_TOKEN_7Hq2Lm9Xv4Rk

The response contains a new access token and a new refresh token:

{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVYQU1QTEVfS0lEIn0.EXAMPLE_ACCESS_TOKEN_PAYLOAD.EXAMPLE_SIGNATURE",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile offline_access",
  "refresh_token": "EXAMPLE_OPAQUE_REFRESH_TOKEN_Np5Wc8Ds1Ky3"
}

Checkpoint The refresh_token in the response differs from the one you sent. Your application now stores the new value and discards the old one.

Rotation: always store the new refresh token

Every successful refresh returns a new refresh token. The one you sent is now used. Replace it in your store before you do anything else with the response.

If the same refresh token is used again after a short grace period of about 30 seconds, OneiD:

  1. refuses the request with invalid_grant, and
  2. revokes the whole chain, including the newer refresh token issued from it.

The user must then sign in again. This protects users when a refresh token is stolen: either the thief or your application uses it second, and the chain ends.

Avoid accidental reuse

Reuse usually comes from your own application refreshing twice at once, for example two browser tabs or two worker threads that notice an expired access token at the same moment.

  • Serialise refreshes. Let one caller refresh, and let the others wait for its result. In a browser application, coordinate tabs, for example with a lock or by keeping tokens in one tab.
  • Write the new token before you release the lock. A second caller must read the new refresh token, not the one that was just used.
  • Do not rely on the grace period. It covers a lost response or a retry within a few seconds. It is not a design for parallel refreshes.
  • Refresh shortly before expiry, not on every request. Use expires_in to know when the access token runs out.

Lifetime: not sliding

Refresh tokens last 14 days by default; your administrator can change this per client. The lifetime is not sliding: each new refresh token in a chain expires when the original one would have. When the chain reaches the end, the refresh fails with invalid_grant and the user signs in again.

Plan for this. Users of an application they open every day still see a sign-in at the end of the refresh token lifetime. If they still have a OneiD session, that sign-in completes without a password prompt.

Each refresh checks the user

OneiD re-checks the user on every refresh. The refresh fails with invalid_grant when the user:

  • has been deleted
  • is locked
  • must change their password
  • must enrol in MFA

OneiD also revokes refresh tokens itself when the user’s password changes, an administrator removes a role, locks the user, resets the user’s MFA or sets a temporary password, and when the user signs out of your application. A refresh after any of these fails with invalid_grant.

Handle a failed refresh

Treat invalid_grant from a refresh as “this session is over”:

  1. Delete the stored refresh token and access token.
  2. End the user’s session in your application.
  3. Send the user to sign in again with a new authorisation request.

Do not retry the same refresh token. Retrying cannot succeed, and after the grace period it counts as reuse.

Other responses:

Response Meaning What to do
invalid_grant Expired, used, revoked, or the user can no longer sign in. Sign the user in again.
invalid_client Wrong secret, expired secret or disabled client. Fix the configuration; ask your administrator.
HTTP 429 with slow_down Too many requests. Wait for Retry-After seconds.

See Errors and troubleshooting.

Revoke on sign-out

When the user signs out, end the refresh token too. Two ways do this:

  • Sign out through OneiD. Signing out with the end session endpoint revokes your application’s tokens for that user. See Sign-out.
  • Revoke the token directly. If your application only clears its own session, revoke the refresh token first:
POST /connect/revoke HTTP/1.1
Host: YOUR_ONEID
Authorization: Basic WU9VUl9DTElFTlRfSUQ6WU9VUl9DTElFTlRfU0VDUkVU
Content-Type: application/x-www-form-urlencoded

token=EXAMPLE_OPAQUE_REFRESH_TOKEN_Np5Wc8Ds1Ky3&token_type_hint=refresh_token

A public client sends client_id=YOUR_CLIENT_ID in the body instead of the Authorization header.

Storage

  • Server-side applications: keep refresh tokens on the server, for example in the session store or an encrypted database column. Never send them to the browser.
  • Browser applications: keep tokens in memory. Avoid localStorage for refresh tokens, because any script on the page can read it. See Browser applications and CORS.
  • Mobile and desktop applications: use the platform’s secure storage, such as the iOS Keychain or Android Keystore.

Learn more