# agentgateway Token Exchange for AI Agents

> How AI agents act for a user without holding provider tokens: agentgateway token exchange (RFC 8693, JWT bearer, XAA) in standalone and Kubernetes.

- Canonical URL: https://webofmike.com/agentgateway-token-exchange/
- Author: Mike Moore (https://webofmike.com/about/)
- Published: 2026-10-09
- Last modified: 2026-10-09
- Tags: AI Gateways, Security, AI Agents, MCP, Kubernetes, Platform Engineering
- Cite as: Mike Moore, "agentgateway Token Exchange for AI Agents", Web of Mike (webofmike.com), 2026-10-09. https://webofmike.com/agentgateway-token-exchange/


An AI agent that calls tools for a user needs that user's authority, and the usual shortcuts are a pasted personal access token or a shared service account. The MCP authorization spec rules out passing a user's token through to a server it was not issued for. **agentgateway token exchange** moves the problem to the gateway: the agent presents the user token it already has, agentgateway trades it with RFC 8693 token exchange, the RFC 7523 JWT bearer grant or Cross App Access (ID-JAG), and the backend receives a token scoped to itself that the agent never sees.

*Disclosure: I work at Solo.io, which created agentgateway and sells Solo Enterprise for agentgateway. Everything below is open-source agentgateway unless it is labelled Solo Enterprise, and every claim links to the project's own docs or release notes as of 2026-10-01.*

Mode key used on this page: **S** is agentgateway standalone mode, **K** is Kubernetes mode, **S+K** is both.

## The short answer: exchange the user's token at the gateway

Put the exchange in the gateway. The agent carries only a token that says who the user is. When the agent calls a tool or API, agentgateway, with JWT authentication on the route, validates that token and swaps it for one scoped to that one backend, using the token endpoint of an authorization server you already trust. The backend sees a user-scoped credential and the agent process never sees it.

```text
user --(login)--> IdP
user --(user JWT)--> agent (kagent or any client)
agent --(user JWT)--> agentgateway
                        1. jwtAuth: verify the user JWT
                        2. oauthTokenExchange: POST /token (subject_token = user JWT)
                      <-- authorization server returns a backend-scoped token
agentgateway --(backend token)--> MCP server / API
```

Who holds what: the agent holds the user JWT only. The gateway holds its own client credentials for the token endpoint and, briefly, the exchanged token. The backend gets the exchanged token, never the user JWT.

## Why agents shouldn't hold provider tokens

- **The MCP spec limits which tokens a server may accept.** The [2026-07-28 MCP authorization specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) says MCP clients "MUST NOT send tokens to the MCP server other than ones issued by the MCP server's authorization server" and MCP servers "MUST NOT accept or transit any other tokens". It also requires servers to validate that a token was issued for them (RFC 8707 audience binding). I covered what the spec leaves open in [the MCP agent identity gap](/mcp-agent-identity-gap/).
- **Blast radius.** Anything in the agent's context or environment can end up in a log, a trace or a prompt-injected tool call. A token the agent never receives can't leak from the agent.
- **Per-user audit.** An exchanged token names the user (and, with delegation, the agent). A shared service account names neither.

## What agentgateway token exchange does (both modes)

For static provider keys, see [secretless AI agents](/secretless-ai-agents/), which injects one key at the gateway. This page is the per-user version of the same idea.

`backendAuth.oauthTokenExchange` (S+K) takes a credential from the incoming request, sends it to an authorization server's token endpoint, and attaches the returned token to the upstream request before it leaves the gateway. History, from the release notes: RFC 8693 and RFC 7523 token exchange plus Cross App Access shipped in [v1.4.0](https://github.com/agentgateway/agentgateway/releases/tag/v1.4.0) (2026-07-27) with Kubernetes controller support; [v1.4.1](https://github.com/agentgateway/agentgateway/releases/tag/v1.4.1) made a missing subject token fail closed; [v1.5.0](https://github.com/agentgateway/agentgateway/releases/tag/v1.5.0) (2026-08-27) added per-leg scopes, a configurable subject token type, an optional `requested_token_type` and token exchange trace spans.

| Field | What it does | Doc |
| --- | --- | --- |
| `grantType` | `tokenExchange` (RFC 8693, default, sends `subject_token`) or `jwtBearer` (RFC 7523, sends `assertion`). K spells the enums `TokenExchange` / `JwtBearer` | [S standard](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/backend-authn/token-exchange/standard/), [S JWT bearer](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/backend-authn/token-exchange/jwt-bearer/) |
| `subjectToken` | Where the incoming token comes from: header, query parameter, cookie or a CEL expression. Defaults to the `Authorization` bearer token, typed as an access token | [S standard](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/backend-authn/token-exchange/standard/) |
| `actorToken` | Optional delegation token, token exchange grant only, no default source | [K standard](https://agentgateway.dev/docs/kubernetes/latest/documentation/security/backend-authn/token-exchange/standard/) |
| `audiences`, `scopes`, `resources` | Sent as `audience`, `scope` and `resource` (RFC 8707) | [S standard](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/backend-authn/token-exchange/standard/) |
| `clientAuth` | How the gateway authenticates to the token endpoint: `clientSecretBasic` (default), `clientSecretPost` or `privateKeyJwt` | [S standard](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/backend-authn/token-exchange/standard/) |
| `additionalParams` | Extra form parameters as CEL expressions | [S JWT bearer](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/backend-authn/token-exchange/jwt-bearer/) |
| `cache` | Exchanged tokens are cached; the standalone docs give 8192 entries with a 300-second TTL when the response omits `expires_in` (the Kubernetes docs give only the 8192 entries) | [S standard](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/backend-authn/token-exchange/standard/) |

### Standalone mode (YAML, single binary)

Last checked 2026-10-01, agentgateway v1.5.0. This is the route I ran (T3 below). `jwtAuth` verifies the user JWT, then the exchange sends that raw token as the subject token. Because `jwtAuth` removes the validated credential from the request unless `preserveToken: true` is set ([JWT authentication](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/jwt-authn/)), the subject token is read with `jwt.rawToken.unredacted()`. Hosts are my local mocks.

```yaml
- name: t3-jwtauth-then-exchange
  matches: [{path: {pathPrefix: /t3}}]
  policies:
    jwtAuth:
      mode: strict
      issuer: https://idp.example.test
      audiences: [agent-client]
      jwks: {file: ./keys/idp.jwks.json}
  backends:
  - host: 127.0.0.1:18080          # mock MCP backend
    policies:
      backendAuth:
        oauthTokenExchange:
          host: 127.0.0.1:7080     # mock authorization server
          path: /token
          clientAuth: {clientId: gateway-client, clientSecret: gateway-secret, method: clientSecretBasic}
          audiences: [mcp-backend]
          subjectToken:
            source: {expression: 'jwt.rawToken.unredacted()'}
```

Since v1.4.1 ([PR #2740](https://github.com/agentgateway/agentgateway/pull/2740)) the exchange fails closed when the configured subject-token source is missing or empty, with no implicit fallback, so select `jwt.rawToken.unredacted()` explicitly. The `jwt.rawToken` CEL variable itself arrived in [v1.3.0](https://github.com/agentgateway/agentgateway/releases/tag/v1.3.0) (PR #2027, by [@mjungsbluth](https://github.com/mjungsbluth), Magnus Jungsbluth of Zalando per his GitHub profile).

The JWT bearer variant in the Entra on-behalf-of shape, also run (T2):

```yaml
backendAuth:
  oauthTokenExchange:
    host: 127.0.0.1:7080
    path: /token
    grantType: jwtBearer
    clientAuth: {clientId: gateway-client, clientSecret: gateway-secret, method: clientSecretPost}
    scopes: ["api://mcp-backend/.default"]
    additionalParams:
      requested_token_use: '"on_behalf_of"'   # CEL string literal
```

The [JWT bearer page](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/backend-authn/token-exchange/jwt-bearer/) documents this Entra example. Runnable upstream examples live in [examples/traffic-token-exchange](https://github.com/agentgateway/agentgateway/tree/main/examples/traffic-token-exchange), and the agentgateway blog has a [walkthrough of token exchange, JWT assertion and Entra OBO](https://agentgateway.dev/blog/2026-07-12-agentgateway-token-exchange-jwt-assertion-entra-obo/).

### Kubernetes mode (`AgentgatewayPolicy`)

The same method as an `AgentgatewayPolicy`, abridged from the [Kubernetes standard guide](https://agentgateway.dev/docs/kubernetes/latest/documentation/security/backend-authn/token-exchange/standard/):

```yaml
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
  name: backend-token-exchange
  namespace: httpbin
spec:
  targetRefs:
  - group: ""
    kind: Service
    name: httpbin
  backend:
    auth:
      oauthTokenExchange:
        backendRef:
          group: agentgateway.dev
          kind: AgentgatewayBackend
          name: keycloak-token-endpoint
        path: /realms/backend-oauth/protocol/openid-connect/token
        grantType: TokenExchange
        audiences: [target-client]
        clientAuth:
          clientId: requester-client
          method: ClientSecretBasic
          secretRef:
            name: oauth-client
```

The Kubernetes doc is explicit: "The exchange presents the incoming token to the authorization server exactly as it arrived, and does not verify the signature first." Its fix is a route-level `traffic.jwtAuthentication` policy with `preserveToken: true`, after which an invalid token gets a 401 and the token endpoint is never called. For MCP servers, a Kubernetes-only page attaches the same `oauthTokenExchange` to an MCP `AgentgatewayBackend` instead of a plain Service ([token exchange for MCP servers](https://agentgateway.dev/docs/kubernetes/latest/documentation/security/backend-authn/token-exchange/mcp/)).

### Same feature, different mode: what changes

| | Standalone (S) | Kubernetes (K) |
| --- | --- | --- |
| Config surface | `backendAuth.oauthTokenExchange` in the YAML config, camelCase enums | `AgentgatewayPolicy` `spec.backend.auth.oauthTokenExchange`, PascalCase enums, secrets via `secretRef` |
| Token endpoint | `host` + `path` | `backendRef` + `path`, or `url` |
| Where it attaches | A route backend | A Service or route target, or an MCP `AgentgatewayBackend` (K-only doc page) |

The docs list the same grants and fields in both modes. I ran standalone only, so treat Kubernetes runtime parity as documented, not tested.

## Delegation: who is acting for whom (`actor_token` and `act`)

[RFC 8693](https://www.rfc-editor.org/rfc/rfc8693) distinguishes impersonation (the new token simply says "user") from delegation (it says "user, with this actor acting", expressed with the `act` claim). agentgateway exposes an optional `actorToken` (S+K) on the token exchange grant. It has no default source, so you point it at the header, query parameter, cookie or CEL expression that carries the agent's own token. In my T5 run the gateway sent `actor_token` and `actor_token_type` as configured. The `act` claim in the result came from my mock authorization server: whether you get one depends on your authorization server, not on agentgateway.

Where does the agent's own token come from? On OSS agentgateway v1.5.0 standalone, I covered [SPIFFE identity for AI agents](/spiffe-identity-for-ai-agents/). For the kagent client side of RFC 8693 with RFC 8707 audiences, see [kagent audience-bound agent tokens](/kagent-audience-bound-agent-tokens/) (that post's harness is a stand-in for the kagent runtime). For federating a Kubernetes service account token to a cloud provider instead, see [workload identity federation for agents](/workload-identity-federation-agents/). For how the two divide the work, SPIFFE for who the agent is, OAuth for what it may do, see [SPIFFE vs OAuth for AI agents](/spiffe-vs-oauth-for-ai-agents/).

*Solo Enterprise aside, labelled: Solo Enterprise for agentgateway has a built-in token service that issues on-behalf-of tokens with an `act` claim ([delegation doc](https://docs.solo.io/agentgateway/kubernetes/latest/documentation/security/backend-authn/token-exchange/sts/delegation/); its steps deploy the agent with kagent). I wrote up running that token service as a standalone binary in [agent token service, standalone](/agent-token-service-standalone/), also Solo Enterprise for agentgateway. Neither is open-source evidence for this page.*

## Cross App Access (ID-JAG): enterprise-managed authorization for MCP

Cross App Access (ID-JAG, often shortened to XAA), also described as [MCP Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization), is two legs: an RFC 8693 exchange at the enterprise IdP for an Identity Assertion JWT, then an [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523) JWT bearer grant at the resource's authorization server. agentgateway (S+K) runs both legs. It needs `jwtAuth` validating an OIDC ID token, and since v1.5.0 each leg can carry its own scopes. See the [standalone](https://agentgateway.dev/docs/standalone/latest/documentation/configuration/security/backend-authn/token-exchange/cross-app-access/) and [Kubernetes](https://agentgateway.dev/docs/kubernetes/latest/documentation/security/backend-authn/token-exchange/cross-app-access/) pages and [examples/traffic-cross-app-access](https://github.com/agentgateway/agentgateway/tree/main/examples/traffic-cross-app-access). I have not run it against a specific IdP.

## When your IdP isn't enough: agent identity brokers

Native exchange works when your IdP or authorization server can mint the backend token. For third-party providers such as a SaaS API with its own OAuth server, your IdP can't mint that token. Someone has to collect the user's consent once, hold the resulting provider token, and release it per request. That is the job of an agent identity broker.

### Case study: Zalando's Agentic Identity Broker on kagent and agentgateway

On 2026-09-25 Zalando open-sourced its [Agentic Identity Broker](https://engineering.zalando.com/posts/2026/09/agentic-platform-open-sourcing-agentic-identity-broker.html) (post by Jan Brennenstuhl and Magnus Jungsbluth; [MIT-licensed Go repo](https://github.com/zalando-incubator/agentic-identity-broker)). Per Zalando's post and docs:

- **What it is.** A delegation and consent broker, not an IdP. It relies on a reverse proxy for user authentication ([why not an IdP](https://agenticidentitybroker.dev/docs/introduction/why-not-idp)). It models permission sets, grants (revocable, optionally time-limited) and user sessions that hold encrypted provider tokens, and it exposes RFC 8693 on its token endpoint, where `resource` selects which provider credential to use ([token exchange concepts](https://agenticidentitybroker.dev/docs/concepts/token-exchange)).
- **What it uses from agentgateway.** Its [gateway guide](https://agenticidentitybroker.dev/docs/guides/token-exchange-gateway) configures agentgateway v1.5.0 `jwtAuth` in strict mode plus an `extProc` policy with `failureMode: failClosed`, passing `jwt.rawToken.unredacted()` and the resource URI as metadata. The broker's own `extproc-token-exchange` sidecar performs the exchange and replaces `Authorization`. It does not use agentgateway's native `oauthTokenExchange`. Zalando also wired Open Policy Agent into agentgateway through the same external processing hook.
- **What it uses from kagent.** Zalando's platform combines kagent and agentgateway with the broker, and uses kro to translate its platform CRDs into kagent and agentgateway resources.
- **Stated limits.** "A revoked or expired grant blocks future exchanges, but it cannot revoke a provider token that has already been issued." CIBA and SPIFFE support are on their list of next steps.

I described the broker from its public docs. I didn't run it.

### Native exchange vs broker on the same gateway (facts, not advice)

| Path on agentgateway | Who mints the backend token | Where provider tokens live | Consent | Revocation | Mode |
| --- | --- | --- | --- | --- | --- |
| Native `oauthTokenExchange` (OSS) | Your IdP or authorization server | Not held; minted per exchange | Your IdP's | Your IdP's; exchanged tokens are cached by the gateway | S+K |
| Broker via `extProc` (Zalando, as documented) | The provider, via the broker | Encrypted in the broker's sessions | Per user and agent grant in the broker | Grant revocation stops future exchanges; issued tokens live until expiry (Zalando) | S as published |
| Built-in token service (**Solo Enterprise for agentgateway**) | Solo Enterprise token service | Not covered here | Not covered here | Not covered here | K per the Solo doc |

The full series lives at [agent identity](/agent-identity/).

## Gotchas

- **Verify before you exchange.** Without `jwtAuth` on the route, a forged token went to the authorization server in my standalone test (T3), matching the Kubernetes doc's warning. The standalone doc doesn't carry that warning today.
- **Name the subject token source.** A missing source fails closed with a 400 and no call to the authorization server (T4, and the v1.4.1 notes).
- **Cache vs revocation.** A repeat request with the same user token reused the cached exchange in T1. The standalone docs' 300-second default applies only when the token response omits `expires_in`. I didn't measure a revocation window.
- **`actorToken` has no default source.** Name it explicitly.
- **Entra.** v1.4.0 added a native `entra` MCP auth provider that serves RFC 8414 metadata from OIDC discovery, strips the RFC 8707 `resource` parameter and short-circuits dynamic client registration ([v1.4.0 notes](https://github.com/agentgateway/agentgateway/releases/tag/v1.4.0)).

## Run it yourself

I ran the released **agentgateway v1.5.0** Linux binary (sha256 `daca5cda76e8c5ab0c1a75912fecf2d6365095403f810db72029c49d14a37e7b`, git revision `fe67324`) in standalone mode on 2026-10-01 at 10:17 AM PT, and re-ran the same script at 10:35 AM PT with the header capture below scripted. Every status code and authorization server call count matched. The authorization server and backend were small Python mocks, not Keycloak or Entra. The mock authorization server does not verify the subject token and adds `act` itself. The backend logs the claims it receives and returns only a fingerprint of the token. The machine had no Docker, so nothing ran in Kubernetes mode.

The T1 client-side check is a response-header capture, recorded in the harness's `run-tests.sh`:

```sh
curl -s -D evidence/t1-client-response-headers.txt -o /dev/null \
  http://127.0.0.1:3000/t1 -H "Authorization: Bearer $(python3 mock/mint.py user)"
```

| Test | What I checked | Result | Grade |
| --- | --- | --- | --- |
| T1 | RFC 8693, default subject source | 200. Authorization server got `grant_type` token-exchange, `subject_token`, `audience`, `scope`, `resource`, Basic client auth. Backend got a different token (`aud` mcp-backend). Client response carried no token (by mock design: the mock backend returns only a fingerprint). Repeat request reused the cache (one authorization server call) | Tested (S) |
| T2 | JWT bearer, Entra OBO shape | 200. Authorization server got `grant_type` jwt-bearer, `assertion`, `requested_token_use=on_behalf_of`, client secret in the form, `scope` | Tested (S), mock authorization server |
| T3 | Forged token without and with `jwtAuth` | Without (route `/t1`, default subject source): 200, forwarded unverified. With strict `jwtAuth` (route `/t3`, subject from `jwt.rawToken.unredacted()`): 401 `InvalidSignature`, authorization server not called. Valid token with `jwtAuth`: 200 | Tested (S) |
| T4 | Missing subject token | 400 for a missing configured header and for no `Authorization`. Authorization server not called | Tested (S) |
| T5 | `actorToken` from a header | 200. Authorization server got `actor_token` and `actor_token_type` jwt. `act` came from the mock | Tested (S), gateway side only |
| T1, T3 in K | Kubernetes mode | Not run | Documented: [K standard](https://agentgateway.dev/docs/kubernetes/latest/documentation/security/backend-authn/token-exchange/standard/) |
| Cross App Access, real IdP, revocation timing, broker | | Not run | Documented only |

## Methodology and disclosure

Checked 2026-10-01 (PT) against agentgateway docs labelled 1.5 (latest) in both modes, release notes v1.3.0 through v1.5.0, the v1.5.0 config schema, RFCs 8693, 7523 and 8707, and the MCP authorization specification (2026-07-28). Tests ran on agentgateway v1.5.0 standalone only, as described above. Zalando's broker is described from its public post, repo and docs; I didn't run it. Solo Enterprise material is labelled and is not used as open-source evidence. Re-check when agentgateway v1.6.0 is generally available.

*Disclosure: I work at Solo.io, which created agentgateway and sells Solo Enterprise for agentgateway. Everything above is open-source agentgateway unless it is labelled Solo Enterprise, and every claim links to the project's own docs or release notes as of 2026-10-01.*

