Token Exchange and Refresh Tokens
Token exchange, refresh-token rotation, and the rotation reuse window for Keycloak and Microsoft Entra deployments.
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 asAUTH_SERVICE_PRIVATE_KEYandtokenExchange.keyIdasAUTH_SERVICE_KEY_ID. It readsauth-service.config.tokenExchange.privateKey, or else thetoken-exchange-private-keyentry of the identity provider’s secret: the Keycloak secret, filled fromglobal.secrets.auth.keycloak.values.tokenExchangePrivateKey, or the Microsoft Entra secret, filled fromglobal.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_URLis the issuer of exchanged tokens, and Auth Service fetches its own public key fromAUTH_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:
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"
}'{
"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:
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"'"}'{
"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 has the full request and error schemas.
Related
- Token Flows — how token exchange fits with the other credential flows
- Identity Providers — which providers support token exchange
- Key Configuration — the DID signing key created for each user at token exchange
- Keycloak — setting up Keycloak for a deployment
- Microsoft Entra ID — setting up Microsoft Entra ID for a deployment