Skip to content

Refresh Tokens

Refresh tokens allow relying parties to obtain new access tokens after the current one expires, without sending the user back through the login flow.

sequenceDiagram
    participant App as Relying Party
    participant IdP as Autentico

    App->>IdP: POST /oauth2/token<br/>(grant_type=refresh_token)
    IdP->>IdP: Validate refresh token<br/>Check not revoked
    IdP->>App: New access_token + refresh_token
Terminal window
curl -X POST https://auth.example.com/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=YOUR_REFRESH_TOKEN" \
-d "client_id=YOUR_CLIENT_ID"
# Public clients identify themselves with client_id; no secret is required.

Every refresh request must identify the same registered client that received the token. Public clients send client_id. Confidential clients must also authenticate, using HTTP Basic authentication or client_id plus client_secret form fields. The client must be active and allow the refresh_token grant. PKCE verifies the initial authorization-code exchange; it does not replace confidential-client authentication during refresh.

For a confidential client using HTTP Basic authentication:

Terminal window
curl -X POST https://auth.example.com/oauth2/token \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=YOUR_REFRESH_TOKEN"

Missing client identification or invalid credentials return invalid_client. Incomplete, repeated or conflicting credential sets return invalid_request; credentials must be in one Basic header or the POST body. A token issued to another client, or an older token with no client binding, returns invalid_grant. Requests rejected by these checks do not rotate the token. Users holding an unbound token must sign in again; it cannot be reassigned to a client.

Response:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

A new refresh token is issued with each refresh. Store the new refresh token — the old one is invalidated. Send one refresh request at a time: concurrent requests using the same token have one winner, and replay detection revokes all of the user’s tokens. Rotation and replacement token/session storage commit together, so a storage failure leaves the original token available for retry. An invalid scope request also leaves it usable.

Access and ID tokens retain original auth_time across code exchange and every refresh. Their iat describes the new token’s issuance. Waiting at consent, using SSO or refreshing does not create a new authentication event (OIDC Core §12.2).

New refresh credentials carry signed authentication context internally. On upgrade, older credentials recover that time from a linked browser session or original, unrotated password grant. An already-rotated older credential without a browser link has no recoverable authentication time and returns invalid_grant; the user must sign in again. This rejection leaves the credential unconsumed and does not invalidate other sessions. Treat refresh tokens as opaque values.

The default refresh token lifetime is 30 days (720h). Configure globally via the refresh_token_expiration runtime setting, or per-client via the refresh_token_expiration override field.

Refresh tokens can be revoked explicitly via Token Revocation, or implicitly when the user logs out. Revoked refresh tokens cannot be used to obtain new access tokens.