Keycloak Token Exchange: Practical Implementation Guide
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:
- Over-scoped access: The original token may have permissions the downstream service should not see.
- Audience mismatch: The downstream service may reject tokens not issued for its audience.
- 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:
- Navigate to Permissions tab.
- Enable Permissions Enabled.
- Click on token-exchange permission.
- 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:
- Open the requesting client’s Service Account Roles tab.
- In Client Roles, select
realm-management. - Assign the
impersonationrole.
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 userstenant-globex— Globex Inc’s usersshared-services— Shared analytics and reporting
When a user in tenant-acme needs to access the shared analytics dashboard:
- The frontend authenticates the user against
tenant-acme. - The analytics service exchanges the
tenant-acmetoken for ashared-servicestoken. - 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
- Configure an Identity Provider in Keycloak for the external provider (e.g., Google).
- Enable token exchange on the Identity Provider.
- 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
-
Principle of least privilege. Only grant token-exchange permissions to clients that genuinely need them. Do not enable it realm-wide.
-
Audit exchange events. Monitor the
TOKEN_EXCHANGEevent type in Keycloak’s event log. Unexpected exchanges may indicate a compromised service credential. -
Short-lived exchanged tokens. Configure the target client to issue tokens with a short lifespan (1-5 minutes). Exchanged tokens should be used immediately.
-
mTLS between services. Token exchange happens between backend services. Use mutual TLS to authenticate the transport layer in addition to the OAuth client credentials.
-
Scope reduction. When exchanging tokens, request only the scopes the target service needs. Do not request
openidif you only needapi:read. -
Validate the
actclaim. For delegation, the exchanged token may contain anact(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.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.