Skip to main content

Refresh Tokens

Refresh tokens let your application obtain new access (and ID) tokens without re-prompting the user. ChainIT issues refresh tokens from the authorization_code grant (IDP / Hosted Auth) and re-issues them from the refresh_token grant on the OAuth token endpoint.

This page covers:

  • How to refresh tokens — endpoint, request parameters, sample integrations, response shape, and error codes.
  • Refresh token rotation — ChainIT's default rotation behaviour, the optional compatibility mode, and the per-application toggle.
Rotation is on by default

Every successful refresh_token grant returns a new refresh token and revokes the old one — your client must persist the latest value from each response. See Refresh token rotation if you need to opt out for a specific OAuth app.


How to refresh tokens

Regardless of the rotation setting, clients always refresh tokens with the same OAuth 2.0 token endpoint and the refresh_token grant.

Endpoint

POST https://staging-api.chainit.online/oauth/token

Content-Type: application/x-www-form-urlencoded

The exact URL is advertised by the server metadata document as token_endpoint.

Request parameters

ParameterRequiredDescription
grant_typeMust be the literal string refresh_token.
refresh_tokenThe refresh token previously issued to this client_id.
client_idOAuth client ID that received the refresh token.
scopeOptional. A space-separated subset of the originally granted scopes.
No client_secret on refresh

The refresh_token grant does not require client_secret, even for confidential clients (M2M, Hosted Auth, IDP server-side). The refresh token itself is the proof of possession — it is bound to the issuing client_id and is single-use when rotation is enabled. Use client_secret only on the authorization_code and client_credentials grants.

Where the refresh token comes from

You receive the refresh_token from either the authorization_code exchange (IDP / Hosted Auth) or a previous refresh_token grant. For rotation-enabled apps, always persist the latest value returned and discard the one you sent.

Sample integration

curl

curl -X POST "https://staging-api.chainit.online/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=<<your_client_id>>" \
-d "refresh_token=<<your_refresh_token>>"

Node.js

import axios from "axios";

const params = new URLSearchParams({
grant_type: "refresh_token",
client_id: process.env.CLIENT_ID,
refresh_token: currentRefreshToken,
});

const { data } = await axios.post(
"https://staging-api.chainit.online/oauth/token",
params,
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } },
);

// Persist the latest refresh_token — required when rotation is enabled.
saveTokens(data.access_token, data.refresh_token, data.id_token);

Python

import requests

response = requests.post(
"https://staging-api.chainit.online/oauth/token",
data={
"grant_type": "refresh_token",
"client_id": CLIENT_ID,
"refresh_token": current_refresh_token,
},
)
tokens = response.json()

# Persist the latest refresh_token — required when rotation is enabled.
save_tokens(tokens["access_token"], tokens["refresh_token"], tokens.get("id_token"))

Success response

The response shape is identical in both modes — only the refresh_token value differs.

Rotation enabled (default):

{
"access_token": "<<new_access_token>>",
"refresh_token": "<<new_refresh_token>>",
"id_token": "<<new_id_token>>",
"expires_in": 3600,
"token_type": "Bearer"
}

Rotation disabled:

{
"access_token": "<<new_access_token>>",
"refresh_token": "<<same_refresh_token_submitted_on_request>>",
"id_token": "<<new_id_token>>",
"expires_in": 3600,
"token_type": "Bearer"
}

Errors

All error responses follow the OAuth 2.0 shape defined by RFC 6749 §5.2:

{
"error": "invalid_grant",
"error_description": "Refresh token is invalid or expired"
}
HTTPCodeReason
400invalid_requestA required parameter is missing — grant_type, refresh_token, or client_id.
400invalid_clientclient_id is unknown to the authorization server.
400invalid_grantRefresh token is invalid, expired, or revoked.
400invalid_grantRefresh token does not belong to the supplied client_id.
400invalid_grantReuse detected — an already-rotated refresh token was submitted; the entire family is now revoked.
400invalid_scopeRequested scope is not a subset of the client's allowed scopes (or of the originally granted scopes, when known).

Refresh-token grant failures for a known client_id are recorded in OAuth activity logs as TOKEN_VALIDATION_FAILED with metadata including grantType, tokenType, oauthError, and oauthErrorDescription. Reuse and client-mismatch cases also emit security logs.


Refresh token rotation

Refresh-token rotation controls what ChainIT returns when a client calls /oauth/token with grant_type=refresh_token. It is enabled by default and aligns with the OAuth 2.0 Security BCP — rotation limits the lifetime of a stolen refresh token and enables automatic replay detection.

Default behaviour: rotation enabled

When rotateRefreshTokens is true:

  1. The submitted refresh token is validated.
  2. ChainIT revokes the submitted refresh token.
  3. ChainIT blacklists the old token group so previously issued access tokens stop working.
  4. ChainIT returns a new access token, a new refresh token, and an ID token when applicable.

Clients must persist the latest refresh_token from every successful refresh response. Reusing an older refresh token is treated as refresh-token reuse and can revoke the session.

Reuse detection

If a previously rotated (already revoked) refresh token is submitted, ChainIT treats it as a possible theft replay: the entire refresh-token family for that user and client is revoked and the affected session must re-authenticate.

Compatibility mode: rotation disabled

Some clients cannot safely persist a newly returned refresh token after every refresh call. For those clients, disable rotation on the OAuth application.

When rotateRefreshTokens is false:

  1. The submitted refresh token is validated.
  2. ChainIT returns a new access token and ID token when applicable.
  3. ChainIT returns the same refresh token value that was submitted.
  4. Previously issued access tokens are not blacklisted by the refresh call; they expire naturally.
Security trade-off

Disabling rotation is a compatibility option. It removes automatic replay detection for normal refresh-token reuse, so only use it for clients that cannot reliably store rotated refresh tokens.

Configure the toggle

Configure rotateRefreshTokens from the Developer Portal:

  1. Open the application in Applications.
  2. Open Advanced SettingsRefresh Token Rotation.
  3. Toggle rotation on or off and save.

If you leave the toggle untouched on a newly created application, rotation defaults to on (true).

See Create application — step-by-step for the full registration walkthrough.


Next steps