Impersonation
Configure agentgateway to perform RFC 8693 impersonation, replacing a user’s IdP token with a new STS-signed JWT that carries only the user’s identity.
About impersonation
When an agent calls a downstream service such as an MCP server or API on behalf of a user, the downstream service typically requires a token to verify who is making the request and to decide what that caller is allowed to do.
According to the RFC 8693, impersonation token exchange is a security mechanism where an AI agent or client exchanges a user’s initial authentication token, such as a JWT from Keycloak, for a new, short-lived token with the same sub. The JWT is minted and signed by the STS before the agentgateway proxy forwards the token to the downstream system. The new token allows the agent to act on behalf of the user, accessing downstream APIs and services with the user’s permissions while ensuring auditability and adhering to security boundaries.
Without a valid token, the downstream service rejects the request. The agent cannot simply forward the user’s original IdP token due to the following reasons:
- The downstream might not trust the IdP that issued the original token.
- The token might be scoped to a different audience that the downstream does not accept.
- Passing raw IdP tokens through the MCP stack can create security risks.
On-behalf-of (OBO) token exchange solves these challenges by using the agentgateway STS to validate the user’s IdP token and issue a new token that the downstream service can verify. The new token is signed by the STS’s private key and carries only the user’s identity (sub). No agent identity is included. From the downstream service’s perspective, the request appears to come directly from the user. The agent is indistinguishable from the user within that context as shown in the following flow.
sequenceDiagram
participant User
participant Agent
participant STS as agentgateway STS
participant Downstream as MCP server
User->>Agent: Request with IdP JWT <br/>(e.g. Keycloak token)
Agent->>STS: Token exchange request <br/>(grant_type=token-exchange,<br/>subject_token=user JWT,<br/>no actor_token)
STS->>STS: Validate user JWT signature and claims
STS->>STS: Mint new JWT<br/>(sub=user subject,<br/>iss = agentgateway STS,<br/>no act claim)
STS-->>Agent: New STS-signed JWT
Agent->>Downstream: Request with STS-signed JWT
Downstream->>Downstream: Check user identity <br/>Agent identity is not tracked<br/>(Agent is indistinguishable from user)
Downstream-->>Agent: Response
Agent-->>User: Result
Example token without impersonation:
Without impersonation, the downstream service receives the upstream IdP token directly. The upstream token includes the original issuer (iss) and audience (aud) claim. The downstream service does not accept tokens with these claims. It is configured to only accept tokens that are issued by STS and that include an aud claim for the target downstream service.
{
"iss": "https://keycloak.example.com/realms/my-realm",
"sub": "user@example.com",
"aud": "my-app",
"scope": "read write",
"exp": 1735678400
}Example token after impersonation:
After the STS exchanges and mints a new token, the downstream service receives a JWT with the correct issuer and audience claims. The original user claim (sub) is preserved.
{
"iss": "agentgateway.your-cluster.svc:7777",
"sub": "user@example.com",
"aud": "urn:example:target-service",
"scope": "read write",
"exp": 1735678400,
"iat": 1735592000
}Impersonation can be useful in the following scenarios:
- The downstream service only needs to know the user’s identity, not which agent acted.
- Audit requirements at the downstream service do not need agent-level traceability.
- You want to normalize tokens across identity providers. For example, instead of configuring all downstream systems to trust all the IdPs that you use in your environment, you instead use a standard agentgateway-signed JWT for all downstream requests. The downstreams need to be configured to trust the STS as a token issuer.
act claim to the issued token. If you need to preserve the identity of the agent that performs the call for auditing purposes, use delegation instead.Before you begin
- Set up the STS infrastructure
- Complete the static MCP guide.
Configure the proxy for token exchange
Set up the agentgateway proxy to perform token exchange by using the built-in STS server.
kubectl apply -f- <<EOF
apiVersion: enterpriseagentgateway.solo.io/v1alpha1
kind: EnterpriseAgentgatewayParameters
metadata:
name: agw-params
namespace: agentgateway-system
spec:
env:
- name: STS_URI
value: http://enterprise-agentgateway.agentgateway-system.svc.cluster.local:7777/oauth2/token
- name: STS_AUTH_TOKEN
value: /var/run/secrets/xds-tokens/xds-token
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: agentgateway-proxy
namespace: agentgateway-system
spec:
gatewayClassName: enterprise-agentgateway
infrastructure:
parametersRef:
group: enterpriseagentgateway.solo.io
kind: EnterpriseAgentgatewayParameters
name: agw-params
listeners:
- name: http
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: All
EOFOptional: Test the STS server locally
You can optionally test the STS locally to verify that your JWT is minted as expected. These steps serve as confirmation that the JWT is correctly issued by the STS and contains the claims that your downstream services expect. The following example assumes that you set up Keycloak as the IdP.
Generate a JWT token for the
user1user that you set up. You use this JWT token to trigger the OBO token exchange flow.export USER_TOKEN=$(curl -d 'client_id=fe-client-1' \ -d 'username=user1' \ -d 'password=Password1!' \ -d 'grant_type=password' "$KEYCLOAK_URL/realms/agentgateway/protocol/openid-connect/token" | jq -r .access_token) echo $USER_TOKENReview the JWT that you got.
echo "$USER_TOKEN" | python3 -c " import base64, json, sys payload = sys.stdin.read().strip().split('.')[1] payload += '=' * (4 - len(payload) % 4) print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2)) "Port-forward the STS server on port 7777.
kubectl port-forward -n agentgateway-system svc/enterprise-agentgateway 7777:7777 export STS_URL="http://localhost:7777"Send a token exchange request to the STS server. Provide the JWT that you created earlier. Note that this step serves as a confirmation that your STS is configured correctly. The token exchange request to the STS is automatically sent if the request passes through the agentgateway proxy. No user interaction is required.
export STS_RESPONSE=$(curl -s -X POST "${STS_URL}/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: application/json" \ -H "Authorization: Bearer ${USER_TOKEN}" \ -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ -d "subject_token=${USER_TOKEN}" \ -d "subject_token_type=urn:ietf:params:oauth:token-type:jwt") echo "$STS_RESPONSE" | jq '.' 2>/dev/null || echo "$STS_RESPONSE"Example output:
{ "access_token": "eyJ...", "issued_token_type": "urn:ietf:params:oauth:token-type:jwt", "token_type": "Bearer" }Extract the access token from the STS token and decode it. Verify that your token has the same
subas your original Keycloak token and that the issuer is set to agentgateway.export NEW_TOKEN=$(echo "$STS_RESPONSE" | jq -r '.access_token') echo "$NEW_TOKEN" | python3 -c " import base64, json, sys payload = sys.stdin.read().strip().split('.')[1] payload += '=' * (4 - len(payload) % 4) print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2)) "Example output:
{ "exp": 1775270769, "iat": 1775184369, "iss": "enterprise-agentgateway.agentgateway-system.svc.cluster.local:7777", "nbf": 1775184369, "scope": "groups profile email", "sub": "e07b0795-6192-441a-ae9a-d8cf37b3e790" }
Set up impersonation
Create an EnterpriseAgentgatewayPolicy resource with your OBO policy. In this example, you protect the MCP server that you set up before you began. Because the policy is set to
ExchangeOnlywithout a specific provider, the STS defaults to the standard OBO flow and uses the STS as the token issuer.kubectl apply -f- <<EOF apiVersion: enterpriseagentgateway.solo.io/v1alpha1 kind: EnterpriseAgentgatewayPolicy metadata: name: impersonation namespace: default spec: targetRefs: - kind: AgentgatewayBackend group: agentgateway.dev name: mcp-backend # works the same for non-MCP routes backend: tokenExchange: mode: ExchangeOnly EOFConfigure the downstream service to trust the agentgateway STS. The STS exposes two standard discovery endpoints that you point the downstream service’s JWT validator to, as described in the following table.
Endpoint Purpose /.well-known/oauth-authorization-serverOAuth 2.0 server , such as https://enterprise-agentgateway.agentgateway-system.svc.cluster.local:7777/.well-known/oauth-authorization-server./.well-known/jwks.jsonThe STS public key set for JWT signature verification, such as https://enterprise-agentgateway.agentgateway-system.svc.cluster.local:7777/.well-known/jwks.json.Enable debug logging on the controller.
kubectl port-forward deploy/enterprise-agentgateway \ -n agentgateway-system 9095 & PF_PID=$! && sleep 1 && curl -X PUT "http://localhost:9095/logging?level=debug" ; kill $PF_PIDLook at the logs.
kubectl logs deploy/enterprise-agentgateway -n agentgateway-system | tail -3Example output:
{"time":"2026-04-03T22:12:25.304167298Z","level":"debug","msg":"processing token exchange request","component":"sts/handler","resource":"default/mcp-backend","audience":"","has_actor_token":false} {"time":"2026-04-03T22:12:25.304188215Z","level":"debug","msg":"no provider mapping found for resource, falling back to standard OBO","component":"sts/handler","resource":"default/mcp-backend"} {"time":"2026-04-03T22:12:25.307276507Z","level":"info","msg":"request","component":"request","method":"POST","path":"/oauth2/token","StatusCode":200,"latency":3143417,"clientIP":"10.244.0.18"}Generate a JWT token for the
user1user that you set up. You use this JWT token to trigger the token exchange flow.export USER_TOKEN=$(curl -d 'client_id=fe-client-1' \ -d 'username=user1' \ -d 'password=Password1!' \ -d 'grant_type=password' "$KEYCLOAK_URL/realms/agentgateway/protocol/openid-connect/token" | jq -r .access_token) echo $USER_TOKENSend a request to the MCP server without an
Authorizationtoken. Verify that this request fails. The token exchange policy that you configured enforces JWT authentication to the MCP backend.curl -vik -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "mcp-protocol-version: 2025-06-18" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "curl-client", "version": "1.0"} } }'Example output:
< HTTP/1.1 500 Internal Server Error HTTP/1.1 500 Internal Server Error ... < * Connection #0 to host localhost left intact {"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"failed to send message: send error: http upstream error: http request failed: invalid request"}}%Send a request to the MCP server with a valid
Authorizationheader. Verify that this time, the request succeeds. The provided token is validated by the Keycloak JWKS and then automatically minted by the STS for a new token.curl -vik -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "mcp-protocol-version: 2025-06-18" \ -H "Authorization: Bearer $USER_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "curl-client", "version": "1.0"} } }'Example output:
< HTTP/1.1 200 OK HTTP/1.1 200 OK ... data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"experimental":{},"tools":{"listChanged":false}},"serverInfo":{"name":"mcp-website-fetcher","version":"1.14.1"}}}
Cleanup
You can optionally remove the resources that you set up as part of this guide.kubectl delete enterpriseagentgatewaypolicy impersonation