EQTY LabDocs

Token Exchange and Refresh Tokens

Token exchange, refresh-token rotation, and the rotation reuse window for Keycloak and Microsoft Entra deployments.

View as Markdown

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 has the full request and error schemas.