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.

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.

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 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 .
  • 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 , 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 (2026-07-27) with Kubernetes controller support; v1.4.1 made a missing subject token fail closed; 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.

FieldWhat it doesDoc
grantTypetokenExchange (RFC 8693, default, sends subject_token) or jwtBearer (RFC 7523, sends assertion). K spells the enums TokenExchange / JwtBearerS standard , S JWT bearer
subjectTokenWhere the incoming token comes from: header, query parameter, cookie or a CEL expression. Defaults to the Authorization bearer token, typed as an access tokenS standard
actorTokenOptional delegation token, token exchange grant only, no default sourceK standard
audiences, scopes, resourcesSent as audience, scope and resource (RFC 8707)S standard
clientAuthHow the gateway authenticates to the token endpoint: clientSecretBasic (default), clientSecretPost or privateKeyJwtS standard
additionalParamsExtra form parameters as CEL expressionsS JWT bearer
cacheExchanged 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

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 ), the subject token is read with jwt.rawToken.unredacted(). Hosts are my local mocks.

- 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 ) 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 (PR #2027, by @mjungsbluth , Magnus Jungsbluth of Zalando per his GitHub profile).

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

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 documents this Entra example. Runnable upstream examples live in examples/traffic-token-exchange , and the agentgateway blog has a walkthrough of token exchange, JWT assertion and Entra OBO .

Kubernetes mode (AgentgatewayPolicy)

The same method as an AgentgatewayPolicy, abridged from the Kubernetes standard guide :

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 ).

Same feature, different mode: what changes

Standalone (S)Kubernetes (K)
Config surfacebackendAuth.oauthTokenExchange in the YAML config, camelCase enumsAgentgatewayPolicy spec.backend.auth.oauthTokenExchange, PascalCase enums, secrets via secretRef
Token endpointhost + pathbackendRef + path, or url
Where it attachesA route backendA 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 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 . For the kagent client side of RFC 8693 with RFC 8707 audiences, see 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 . 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 .

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 ; its steps deploy the agent with kagent). I wrote up running that token service as a standalone binary in 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 , is two legs: an RFC 8693 exchange at the enterprise IdP for an Identity Assertion JWT, then an RFC 7523 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 and Kubernetes pages and 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 (post by Jan Brennenstuhl and Magnus Jungsbluth; MIT-licensed Go repo ). 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 ). 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 ).
  • What it uses from agentgateway. Its gateway guide 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 agentgatewayWho mints the backend tokenWhere provider tokens liveConsentRevocationMode
Native oauthTokenExchange (OSS)Your IdP or authorization serverNot held; minted per exchangeYour IdP’sYour IdP’s; exchanged tokens are cached by the gatewayS+K
Broker via extProc (Zalando, as documented)The provider, via the brokerEncrypted in the broker’s sessionsPer user and agent grant in the brokerGrant revocation stops future exchanges; issued tokens live until expiry (Zalando)S as published
Built-in token service (Solo Enterprise for agentgateway)Solo Enterprise token serviceNot covered hereNot covered hereNot covered hereK per the Solo doc

The full series lives at 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 ).

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:

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)"
TestWhat I checkedResultGrade
T1RFC 8693, default subject source200. 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)
T2JWT bearer, Entra OBO shape200. Authorization server got grant_type jwt-bearer, assertion, requested_token_use=on_behalf_of, client secret in the form, scopeTested (S), mock authorization server
T3Forged token without and with jwtAuthWithout (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: 200Tested (S)
T4Missing subject token400 for a missing configured header and for no Authorization. Authorization server not calledTested (S)
T5actorToken from a header200. Authorization server got actor_token and actor_token_type jwt. act came from the mockTested (S), gateway side only
T1, T3 in KKubernetes modeNot runDocumented: K standard
Cross App Access, real IdP, revocation timing, brokerNot runDocumented 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.

Frequently asked questions

How can an AI agent act on behalf of a user without holding the user's provider tokens?

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, using OAuth 2.0 token exchange (RFC 8693), the JWT bearer grant (RFC 7523) or Cross App Access, for a token scoped to that one backend. The backend sees a user-scoped credential and the agent process never sees it. That also keeps you inside the MCP authorization rule that servers must not accept or pass through tokens issued for someone else.

What does agentgateway token exchange do, and does it work in Kubernetes and standalone modes?

agentgateway's backendAuth token exchange takes a credential from the incoming request (by default the Authorization bearer token), sends it to an authorization server as an RFC 8693 subject token or an RFC 7523 assertion, and attaches the returned token to the upstream request. It supports audiences, scopes, RFC 8707 resources, an optional actor token for delegation, client authentication with a client secret or a private-key JWT, and caching. It shipped in open-source agentgateway v1.4.0 and is configured in YAML in standalone mode or with an AgentgatewayPolicy on Kubernetes. Add JWT authentication on the route so the token is verified before it is exchanged.

Does agentgateway verify the incoming token before it exchanges it?

Not by itself. The Kubernetes token exchange doc says the exchange presents the incoming token to the authorization server exactly as it arrived and does not verify its signature first. In my standalone test on v1.5.0, a token signed by the wrong key was forwarded to the authorization server when the route had no JWT authentication. With jwtAuth in strict mode on the route, the same token got a 401 and the authorization server was never called. Configure JWT authentication on every route that exchanges.

How do I express delegation, an agent acting for a user, with agentgateway?

Configure the optional actorToken on the token exchange grant. It has no default source, so you name the header, query parameter, cookie or CEL expression that carries the agent's token. agentgateway sends it as actor_token with its token type. Whether the issued token carries an act claim is decided by the authorization server, not by the gateway.

What is an agent identity broker, and how does it relate to the gateway?

An agent identity broker collects a user's consent once, stores the resulting third-party provider tokens encrypted, and returns a per-request token to approved agents, so the agent never holds long-lived provider credentials. The gateway stays the enforcement point: it validates the caller and asks the broker for the right token per request. Zalando's open-source Agentic Identity Broker works this way on kagent and agentgateway, and its gateway guide calls the broker through agentgateway's external processing (extProc) policy. Zalando states that revoking a grant stops future exchanges but cannot revoke a provider token that was already issued.

Can I revoke an agent's access to a user's account, and what happens to tokens already issued?

Revoking at the authorization server or broker stops new exchanges. A token that was already issued stays usable until it expires unless the backend checks revocation. agentgateway also caches exchanged tokens: the standalone docs give a cache of 8192 entries with a 300-second TTL when the token response omits expires_in. Plan revocation around the token lifetime your authorization server issues and the cache settings you choose.

Is gateway token exchange the same as the MCP rule against token passthrough?

It is one way to follow that rule. The MCP authorization specification says clients must not send an MCP server tokens other than ones issued by that server's authorization server, and that MCP servers must not accept or transit any other tokens. Exchanging at the gateway means the MCP server receives a token minted for its own audience instead of the user's original token.