# Refresh tokens

Access tokens issued by Truora Pass expire after 60 minutes by default. The [Authorization Code flow](/truora-pass/authorization_code_flow/) also issues a `refresh_token`, which lets your backend obtain fresh access tokens without sending the user through authorization again.

Refresh tokens are long-lived (365 days by default), single-use, and rotated on every exchange.

**Note**: every [Authorization Code](/truora-pass/authorization_code_flow/) token exchange returns a refresh token, so any application using that flow can use this grant. The CIBA flow does not issue refresh tokens.

## Exchanging a refresh token

Send a **JSON** request to the token endpoint:

```bash
curl -X POST https://api.pass.truora.com/v1/oauth2/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "<your most recent refresh token>",
    "client_id": "WLT_APP_your_client_id",
    "client_secret": "your_client_secret"
  }'
```

| Field | Required | Description |
|---|---|---|
| `grant_type` | Yes | Must be `refresh_token`. |
| `refresh_token` | Yes | The most recently issued refresh token. |
| `client_id` | Yes | Your application's client ID — must be the client the token was issued to. |
| `client_secret` | Yes | Your application's client secret. |
| `scope` | No | Space-delimited subset of the originally granted scopes, to narrow the new access token. |

On success:

```json
{
  "access_token": "<new access token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile email",
  "refresh_token": "<NEW refresh token — replace the old one>"
}
```

**Note**: the refresh grant never returns an `id_token`. Read up-to-date identity claims from `GET /v1/oauth2/userinfo` with the new access token — see [UserInfo and claims](/truora-pass/userinfo_and_claims/).

## Rotation: every exchange returns a new token

Refresh tokens are **single-use**. Every successful exchange consumes the token you sent and returns a new one belonging to the same token family. Always persist the `refresh_token` from the latest response and discard the previous one — the old token cannot be used again.

The token lifetime slides with rotation: each successful exchange gives the new token a full lifetime (365 days by default) from the moment of rotation. A user who returns at least once a year therefore never needs to re-authorize; a family that goes unused for the full lifetime expires.

## Reuse detection

If a refresh token that was already used is presented again, Truora Pass treats it as a possible token leak and **revokes the entire token family** — including the currently valid newest token. The exchange fails with `invalid_grant` (`refresh token already used`), and every subsequent exchange in that family fails too.

To recover, send the user through the [Authorization Code flow](/truora-pass/authorization_code_flow/) again to obtain a new grant.

**Note**: reuse detection means a buggy retry (for example, replaying a request after a timeout whose original attempt actually succeeded) can revoke your tokens. Persist the new refresh token atomically with marking the old one as used on your side.

## Client binding

A refresh token can only be exchanged by the application it was issued to. Presenting it with a different `client_id` fails with `invalid_grant` (`refresh token was not issued to this client`).

## Down-scoping

The optional `scope` field lets you request an access token with **fewer** scopes than originally granted:

```json
{
  "grant_type": "refresh_token",
  "refresh_token": "<your most recent refresh token>",
  "client_id": "WLT_APP_your_client_id",
  "client_secret": "your_client_secret",
  "scope": "openid email"
}
```

The requested scopes must be a subset of the scopes in the original grant; anything outside that set fails with `invalid_scope`. Omitting `scope` keeps the originally granted scopes.

## Errors

This grant answers errors in the standard OAuth 2.0 (RFC 6749) envelope, `{"error": "...", "error_description": "..."}`:

| HTTP | `error` | When |
|---|---|---|
| 401 | `invalid_client` | Wrong `client_id` / `client_secret`. |
| 400 | `invalid_grant` | The refresh token is invalid or expired; was not issued to this client; or was already used (reuse detected — the whole family is revoked). |
| 400 | `invalid_scope` | The requested `scope` is not a subset of the originally granted scopes. |

On `invalid_grant`, discard your stored tokens for that user and re-run the [Authorization Code flow](/truora-pass/authorization_code_flow/).
