Keycloak Token Exchange: Practical Implementation Guide

Guilliano Molaire Guilliano Molaire Updated June 1, 2026 9 min read

Last updated: March 2026

Token exchange allows one service to trade a token it already holds for a different token with different claims, audiences, or permissions. Defined in RFC 8693, it solves a class of problems that arise in microservice architectures: a frontend service needs to call a backend service on behalf of a user, an admin needs to impersonate a user for troubleshooting, or tokens need to cross trust boundaries between realms.

Keycloak implements token exchange as a preview feature. This guide covers enabling it, configuring the three main exchange types, and integrating exchange flows into Node.js and Java applications.

Enabling Token Exchange in Keycloak

Token exchange is not enabled by default. You must activate it during the Keycloak build:

# Enable token exchange feature
KC_FEATURES=token-exchange

If you are using a Docker-based setup, add it to your Dockerfile:

FROM quay.io/keycloak/keycloak:26.1.0 AS builder

ENV KC_DB=postgres
ENV KC_FEATURES=token-exchange

RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:26.1.0
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
CMD ["start", "--optimized"]

For local testing, start Keycloak in dev mode with the feature enabled:

docker run -p 8080:8080 
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin 
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin 
  -e KC_FEATURES=token-exchange 
  quay.io/keycloak/keycloak:26.1.0 start-dev

Or use the Keycloak Docker Compose Generator and add the feature flag to the generated configuration.

The Token Exchange Protocol

All token exchange requests go to the standard token endpoint with grant_type=urn:ietf:params:oauth:grant-type:token-exchange. The key parameters are:

Parameter Description
subject_token The token you currently hold
subject_token_type Type of the subject token (typically urn:ietf:params:oauth:token-type:access_token)
requested_token_type Type of token you want back
audience The target client (service) the new token is for
requested_subject User ID when performing impersonation
scope Scopes for the new token

Exchange Type 1: Internal Token Exchange (Delegation)

Internal token exchange is the most common pattern. Service A has a user’s access token and needs to call Service B on behalf of that user. Service A exchanges its token for a new one scoped to Service B.

Why Not Just Forward the Original Token?

You could forward the original access token to downstream services, but this has problems:

  1. Over-scoped access: The original token may have permissions the downstream service should not see.
  2. Audience mismatch: The downstream service may reject tokens not issued for its audience.
  3. No audit trail: You cannot distinguish between the user calling Service B directly and Service A calling on the user’s behalf.

Keycloak Configuration

Step 1: Configure the target client (Service B). In the Keycloak admin console, navigate to Service B’s client configuration. Under Permissions, enable permissions and configure a token exchange policy that allows Service A to exchange tokens for Service B’s audience.

Go to the client details for service-b:

  1. Navigate to Permissions tab.
  2. Enable Permissions Enabled.
  3. Click on token-exchange permission.
  4. Create a Client policy that allows service-a.

Step 2: Grant the exchange role to Service A. In the realm’s client scopes or via the Admin REST API, ensure Service A’s service account has the token-exchange role for Service B.

The Exchange Request

# Service A exchanges a user's token for one scoped to Service B
curl -X POST https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token 
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" 
  -d "client_id=service-a" 
  -d "client_secret=service-a-secret" 
  -d "subject_token=eyJhbGciOi..." 
  -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" 
  -d "audience=service-b" 
  -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token"

The response contains a new access token with service-b as the audience and only the permissions relevant to Service B:

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "openid",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}

Decode the new token with the JWT Token Analyzer to verify the audience and claims are correct.

Node.js Implementation

import axios from 'axios';

const KEYCLOAK_URL = 'https://keycloak.example.com';
const REALM = 'myrealm';
const TOKEN_URL = `${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token`;

async function exchangeToken(subjectToken, targetAudience) {
  const params = new URLSearchParams({
    grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
    client_id: 'service-a',
    client_secret: process.env.SERVICE_A_SECRET,
    subject_token: subjectToken,
    subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
    audience: targetAudience,
    requested_token_type: 'urn:ietf:params:oauth:token-type:access_token',
  });

  const response = await axios.post(TOKEN_URL, params, {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  });

  return response.data.access_token;
}

// Express middleware example
async function callServiceB(req, res) {
  const userToken = req.headers.authorization?.replace('Bearer ', '');

  // Exchange the user's token for one scoped to service-b
  const serviceBToken = await exchangeToken(userToken, 'service-b');

  // Call Service B with the exchanged token
  const result = await axios.get('https://service-b.internal/api/data', {
    headers: { Authorization: `Bearer ${serviceBToken}` },
  });

  res.json(result.data);
}

Java Implementation with Spring WebClient

import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;

public class TokenExchangeService {

    private final WebClient webClient;
    private final String tokenUrl;
    private final String clientId;
    private final String clientSecret;

    public TokenExchangeService(String keycloakUrl, String realm,
                                 String clientId, String clientSecret) {
        this.webClient = WebClient.builder().build();
        this.tokenUrl = keycloakUrl + "/realms/" + realm
                        + "/protocol/openid-connect/token";
        this.clientId = clientId;
        this.clientSecret = clientSecret;
    }

    public String exchangeToken(String subjectToken, String targetAudience) {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type",
                   "urn:ietf:params:oauth:grant-type:token-exchange");
        params.add("client_id", clientId);
        params.add("client_secret", clientSecret);
        params.add("subject_token", subjectToken);
        params.add("subject_token_type",
                   "urn:ietf:params:oauth:token-type:access_token");
        params.add("audience", targetAudience);
        params.add("requested_token_type",
                   "urn:ietf:params:oauth:token-type:access_token");

        var response = webClient.post()
            .uri(tokenUrl)
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .body(BodyInserters.fromFormData(params))
            .retrieve()
            .bodyToMono(TokenResponse.class)
            .block();

        return response.getAccessToken();
    }
}

Exchange Type 2: Impersonation

Impersonation lets an admin or support agent obtain a token as if they were a specific user. This is invaluable for debugging issues that are user-specific — permissions problems, missing attributes, or broken flows.

Keycloak Configuration

The requesting client needs the impersonation role from the realm-management client:

  1. Open the requesting client’s Service Account Roles tab.
  2. In Client Roles, select realm-management.
  3. Assign the impersonation role.

Additionally, configure a fine-grained permission on the target user’s realm that allows the requesting client to impersonate.

The Impersonation Request

curl -X POST https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token 
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" 
  -d "client_id=admin-tool" 
  -d "client_secret=admin-tool-secret" 
  -d "requested_subject=user-uuid-here" 
  -d "subject_token=eyJhbGciOi..." 
  -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" 
  -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token"

The requested_subject is the user’s UUID in Keycloak. The subject_token is the admin’s token. The response is an access token that represents the target user.

Security Considerations for Impersonation

Impersonation is powerful and dangerous. Protect it carefully:

  • Audit every impersonation. Keycloak logs impersonation events. Configure your audit logs to alert on these events in real time.
  • Restrict the impersonation role to a dedicated admin client, not your general-purpose admin account.
  • Consider disabling impersonation if you do not need it: KC_FEATURES_DISABLED=impersonation.
  • Use time-bounded tokens. Exchanged tokens should have very short lifetimes (1-2 minutes).

Exchange Type 3: Cross-Realm Exchange

Cross-realm token exchange allows a token issued in one realm to be exchanged for a token in a different realm. This is useful in multi-tenant architectures where different business units or customers use separate realms but need to access shared services.

Configuration

Step 1: Create a cross-realm trust. In the target realm, create an Identity Provider that points to the source realm’s OIDC discovery endpoint.

Step 2: Configure token exchange permissions. In the target realm, set up token exchange permissions that reference the Identity Provider.

Step 3: Exchange the token.

curl -X POST https://keycloak.example.com/realms/target-realm/protocol/openid-connect/token 
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" 
  -d "client_id=shared-service" 
  -d "client_secret=shared-service-secret" 
  -d "subject_token=eyJhbGciOi..." 
  -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" 
  -d "subject_issuer=source-realm-idp-alias" 
  -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token"

The subject_issuer must match the alias of the Identity Provider configured in the target realm.

Multi-Tenant Architecture Example

Consider a SaaS application with these realms:

  • tenant-acme — Acme Corp’s users
  • tenant-globex — Globex Inc’s users
  • shared-services — Shared analytics and reporting

When a user in tenant-acme needs to access the shared analytics dashboard:

  1. The frontend authenticates the user against tenant-acme.
  2. The analytics service exchanges the tenant-acme token for a shared-services token.
  3. The analytics service serves data based on the exchanged token’s claims.

For more on multi-tenant patterns, see Multitenancy in Keycloak Using the Organizations Feature.

External Token Exchange

Keycloak can also exchange external tokens (from Google, GitHub, or other OIDC providers) for internal tokens. This is useful when your mobile app authenticates with a social provider’s native SDK and needs to exchange that token for a Keycloak token.

Configuration

  1. Configure an Identity Provider in Keycloak for the external provider (e.g., Google).
  2. Enable token exchange on the Identity Provider.
  3. Configure the client that will perform the exchange.
curl -X POST https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token 
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" 
  -d "client_id=mobile-app" 
  -d "subject_token=google-id-token-here" 
  -d "subject_token_type=urn:ietf:params:oauth:token-type:id_token" 
  -d "subject_issuer=google" 
  -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token"

Keycloak validates the external token, links or creates a user, and issues its own tokens. For configuring identity providers, see Skycloak’s Identity Providers feature.

Error Handling

Token exchange can fail for several reasons. Here are the common error responses and what they mean:

Error Cause Fix
not_allowed Client lacks token-exchange permission Configure permissions on the target client
invalid_token Subject token is expired or malformed Check token expiration; use JWT Token Analyzer to decode
access_denied Impersonation role not assigned Grant impersonation role to the service account
invalid_target Target audience (client) not found Verify client ID in the audience parameter

Retry Logic

async function exchangeTokenWithRetry(subjectToken, audience, maxRetries = 2) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await exchangeToken(subjectToken, audience);
    } catch (error) {
      if (error.response?.data?.error === 'invalid_token' && attempt < maxRetries) {
        // Subject token may have expired; refresh it first
        subjectToken = await refreshSubjectToken();
        continue;
      }
      throw error;
    }
  }
}

Security Best Practices

  1. Principle of least privilege. Only grant token-exchange permissions to clients that genuinely need them. Do not enable it realm-wide.

  2. Audit exchange events. Monitor the TOKEN_EXCHANGE event type in Keycloak’s event log. Unexpected exchanges may indicate a compromised service credential.

  3. Short-lived exchanged tokens. Configure the target client to issue tokens with a short lifespan (1-5 minutes). Exchanged tokens should be used immediately.

  4. mTLS between services. Token exchange happens between backend services. Use mutual TLS to authenticate the transport layer in addition to the OAuth client credentials.

  5. Scope reduction. When exchanging tokens, request only the scopes the target service needs. Do not request openid if you only need api:read.

  6. Validate the act claim. For delegation, the exchanged token may contain an act (actor) claim identifying the original service. Validate this on the resource server.

Monitoring Token Exchange

Track these metrics for token exchange operations:

  • Exchange success rate: Drop below 99% indicates configuration drift or expired credentials.
  • Exchange latency: Should be under 100ms. Higher values suggest network or database issues.
  • Exchange volume per client: Sudden spikes may indicate a retry loop or abuse.

Use Keycloak’s event system to stream TOKEN_EXCHANGE events to your SIEM or monitoring platform. For managed monitoring, see Skycloak Insights.

When to Use Token Exchange vs. Alternatives

Pattern Use When Avoid When
Token Exchange Services need scoped, user-specific tokens for downstream calls You control all services and trust the original token
Token Forwarding All services trust the same token issuer and audience Different services need different permissions
Service Account The downstream call is not on behalf of a user You need to preserve user context

Token exchange adds complexity. Use it when you need audience restriction, scope reduction, or cross-realm trust. For simple architectures where all services share a Keycloak realm and trust the same tokens, forwarding the original token is simpler and sufficient.


Need token exchange without the operational overhead? Skycloak provides managed Keycloak with token exchange enabled and ready to configure. Check our pricing to see how managed Keycloak compares to self-hosting.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman