Skip to content
Version 2026.6.3 is the latest release with the newest features, but gets no long-term support. To stay supported, upgrade with every new release or use a quarterly stable release instead.

Impersonation

Page as Markdown

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.
Impersonation is intended for internal services that you control. Because the agentgateway STS is the token issuer, the downstream service must be configured to trust it by pointing the downstream’s JWT validator at the STS JWKS endpoint. If you need to call an external SaaS API that is protected by a JWT validator that you cannot configure, use external identity provider exchange instead.
Impersonation does not add an 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

  1. Set up the STS infrastructure
  2. 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
EOF

Optional: 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.

  1. Generate a JWT token for the user1 user 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_TOKEN
  2. Review 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))
    "
  3. 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"
  4. 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"
    }
    
  5. Extract the access token from the STS token and decode it. Verify that your token has the same sub as 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

  1. 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 ExchangeOnly without 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
    EOF
  2. Configure 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.

    EndpointPurpose
    /.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.
  3. 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_PID
  4. Look at the logs.

    kubectl logs deploy/enterprise-agentgateway -n agentgateway-system | tail -3

    Example 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"}
    
  5. Generate a JWT token for the user1 user 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_TOKEN
  6. Send a request to the MCP server without an Authorization token. 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"}}%
    
  7. Send a request to the MCP server with a valid Authorization header. 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