---
title: "Token Exchange and Refresh Tokens"
description: "Token exchange, refresh-token rotation, and the rotation reuse window for Keycloak and Microsoft Entra deployments."
source: "Guardian Platform"
---
Token exchange lets supported deployments trade a provider-issued user token for an Auth Service-issued platform token. Token exchange is registered only for Keycloak and Microsoft Entra provider modes, and Auth Service-issued refresh tokens are available only to deployments that use token exchange.

Auth0 and generic OIDC deployments do not expose the token-exchange workflow. They rely on provider-issued access tokens for user authentication, and on the identity provider's token behavior instead of Auth Service refresh-token workflows.

## When to Use Token Exchange

Use token exchange when the deployment requires Auth Service-issued access tokens, Auth Service-issued refresh tokens, or platform signing behavior tied to Keycloak or Microsoft Entra identity.

Do not configure token exchange for a provider mode that does not support it.

## Operational Requirements

- Auth Service must trust the configured identity provider.
- Every Auth Service replica must sign with the same RSA key. Set `auth-service.config.tokenExchange.enabled: true`; only then does the chart pass the key as `AUTH_SERVICE_PRIVATE_KEY` and `tokenExchange.keyId` as `AUTH_SERVICE_KEY_ID`. It reads `auth-service.config.tokenExchange.privateKey`, or else the `token-exchange-private-key` entry of the identity provider's secret: the Keycloak secret, filled from `global.secrets.auth.keycloak.values.tokenExchangePrivateKey`, or the Microsoft Entra secret, filled from `global.secrets.auth.entra.values.tokenExchangePrivateKey`.
- Without a configured key, each pod generates its own at `AUTH_SERVICE_KEY_PATH` (`/tmp/auth-service-key.pem`, key ID in `/tmp/auth-service-key.pem.kid`). With more than one replica, tokens fail validation intermittently, and every token stops validating when the pod that signed it is replaced.
- `AUTH_SERVICE_URL` is the issuer of exchanged tokens, and Auth Service fetches its own public key from `AUTH_SERVICE_URL` + `/.well-known/jwks.json`, so that URL must be reachable from Auth Service itself.
- Platform services must trust Auth Service-issued tokens.
- Refresh-token policy should be reviewed before production use.

## Refresh Tokens

Refresh tokens allow sessions or integrations to obtain a new access token without asking the user to sign in again. Rotation reduces risk by replacing a refresh token after use.

The reuse window limits how long a rotated token stays valid. After rotation, the old token keeps working only for a short grace period so concurrent requests can finish (30 seconds by default, set by `REFRESH_TOKEN_REUSE_WINDOW`; `0` keeps the default), and is rejected after that. A rejected replay does not revoke the newer token. Setting `REFRESH_TOKEN_ROTATION=false` turns rotation off, and a refresh returns the same token until it expires.

Handle refresh tokens as follows:

- Treat refresh tokens as secrets.
- Store them only in approved secure storage.
- Rotate and revoke them according to customer policy.
- Investigate repeated reuse or unexpected refresh failures.

## Request Format

Both grants use `POST /api/v1/auth/token` on Auth Service, at your platform host followed by `/authService`; `POST /api/v1/auth/token-exchange` is an alias kept for older clients. The endpoint takes a JSON body, not the form encoding RFC 8693 describes. `GET /api/v1/auth/.well-known/jwks.json` publishes the public key that validates the tokens it issues.

To exchange a provider access token for a platform token:

```bash
curl -X POST "https://guardian.example.com/authService/api/v1/auth/token" \
  -H "Content-Type: application/json" \
  --data '{
    "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
    "subject_token": "'"$IDP_TOKEN"'",
    "subject_token_type": "urn:ietf:params:oauth:token-type:access_token"
  }'
```

```json
{
  "access_token": "eyJhbGciOi…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt_…",
  "refresh_expires_in": 2591999,
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "scope": "org:1:role:organization_owner"
}
```

`expires_in` defaults to one hour, and an exchanged token never outlives the provider token it came from. `refresh_expires_in` counts down from 30 days by default.

To refresh, send the refresh token to the same endpoint, and keep the `refresh_token` that comes back in place of the old one:

```bash
curl -X POST "https://guardian.example.com/authService/api/v1/auth/token" \
  -H "Content-Type: application/json" \
  --data '{"grant_type": "refresh_token", "refresh_token": "'"$REFRESH_TOKEN"'"}'
```

```json
{
  "access_token": "eyJhbGciOi…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt_…",
  "refresh_expires_in": 2591999,
  "scope": "org:1:role:organization_owner"
}
```

A refreshed token always gets the full default lifetime; the provider token's expiry no longer caps it.

## Troubleshooting

If token exchange fails, confirm the provider mode is Keycloak or Microsoft Entra, the incoming user token is valid for that provider, and the token signing key is set.

If refresh fails, confirm the deployment uses Keycloak or Microsoft Entra token exchange, the refresh token has not expired or been reused after its reuse window, and the user still exists. With `REFRESH_TOKEN_BIND_TO_IP=true`, a refresh from a different address than the one the token was issued to returns `invalid_grant`. A refresh token chain holds at most `REFRESH_TOKEN_MAX_CHAIN_LENGTH` tokens (100 by default, so 99 refreshes), and then the user must sign in again.

The [Auth Service API reference](../../reference/auth-service/api-reference/) has the full request and error schemas.

## Related

- [Token Flows](./token-flows.mdx) — how token exchange fits with the other credential flows
- [Identity Providers](./identity-providers.mdx) — which providers support token exchange
- [Key Configuration](./key-configuration.mdx) — the DID signing key created for each user at token exchange
- [Keycloak](../deployment/keycloak.mdx) — setting up Keycloak for a deployment
- [Microsoft Entra ID](../deployment/microsoft-entra-id.mdx) — setting up Microsoft Entra ID for a deployment