Skip to main content

Use the agentgateway token-exchange policy

Agentgateway v1.5.0 and later include an RFC 8693 client: the oauthTokenExchange backend authentication policy. Agentgateway calls the broker POST /oauth2/token endpoint itself. You do not deploy the ExtProc sidecar.

Agentgateway authenticates to the broker with private_key_jwt. It signs each client assertion with a key that it holds locally. The broker verifies the assertion against an inline key set. No service has to publish the gateway's public key.

This guide is tested with agentgateway v1.5.0.

Choose the policy or the ExtProc sidecar​

Both options perform the same broker exchange. They differ in what the agent sees and in the extra gates that they offer.

oauthTokenExchange policyExtProc sidecar
Extra componentNoneextproc-token-exchange gRPC sidecar
Gateway authenticationprivate_key_jwt, signed by agentgatewayClient assertion from an upstream client-credentials grant
Resource selectionFixed resources list on the backendCEL resource_uri expression on the route
Broker rejectionHTTP 500 with JSON-RPC error -32603. The broker error_uri is not passed on.JSON-RPC error -32042 URL elicitation with the broker error_uri
OPA gate and tool approvalsNot availableOptional

Use the policy when you need only transparent exchange. Use the ExtProc sidecar when agents must receive the re-consent or re-authentication link, or when you need the OPA gate or tool approvals.

What you need​

  • Agentgateway v1.5.0 or later.
  • A reachable broker token endpoint at POST /oauth2/token on end-user port 8000.
  • An RSA key pair for agentgateway. Agentgateway reads the private key from a PEM file. The broker receives the public key as a JWK.
  • A registered third-party service whose protected_resources contains each URI in the agentgateway resources list. See Manage agents and services.
  • Agents that send agentgateway a subject token that the broker accepts. Its aud must contain token_exchange.expected_audience, token-exchange-broker by default. See OAuth2 server modes.
  • The issuer and JWKS URL of those subject tokens, so that agentgateway can validate them.

How the exchange flows​

  1. The agent calls agentgateway with its broker or upstream token as the bearer credential.
  2. jwtAuth validates the credential. An expired or invalid credential gets HTTP 401 and never reaches the broker.
  3. Agentgateway sends the credential unchanged as subject_token. The broker validates it again.
  4. Agentgateway signs a client assertion. Its iss and sub are the configured clientId.
  5. The broker verifies the assertion, the subject token, the user grant, and the stored provider session. It then returns the stored third-party token.
  6. Agentgateway sends Authorization: Bearer <third-party token> to the backend.

Step 1: Create the gateway key pair​

Create an RSA private key for agentgateway. Then write its public half as a JWK Set. The kid and alg values must match the agentgateway clientAuth settings in step 2.

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out gateway-signing.key

N=$(openssl rsa -in gateway-signing.key -noout -modulus | cut -d= -f2 \
| python3 -c 'import base64, sys; print(base64.urlsafe_b64encode(bytes.fromhex(sys.stdin.read().strip())).rstrip(b"=").decode())')
printf '{"keys":[{"kty":"RSA","kid":"gateway-2026-09","alg":"RS256","use":"sig","e":"AQAB","n":"%s"}]}\n' "$N" > gateway-jwks.json

e is AQAB because openssl genpkey uses the public exponent 65537. gateway-signing.key is a secret. Mount it only into agentgateway. gateway-jwks.json holds only public key material.

Step 2: Configure agentgateway​

Validate the subject token with jwtAuth on the route. Add the oauthTokenExchange policy to the backend that receives the exchanged token:

binds:
- port: 4000
listeners:
- routes:
- policies:
jwtAuth:
mode: strict
preserveToken: true # the exchange reads the subject token from Authorization
providers:
- issuer: https://broker.example.com
audiences:
- token-exchange-broker
jwks:
url: https://broker.example.com/oauth2/jwks.json
backends:
- mcp:
targets:
- name: tools
mcp:
host: https://mcp.example.com/mcp
policies:
backendAuth:
oauthTokenExchange:
host: https://broker.example.com
path: /oauth2/token
resources:
- https://api.example.com
clientAuth:
method: privateKeyJwt
clientId: https://gateway.example.com
assertionAudience: https://broker.example.com/oauth2/token
signingKey:
file: /etc/agentgateway/keys/gateway-signing.key
alg: RS256
kid: gateway-2026-09

Keep preserveToken: true. With false, jwtAuth removes the Authorization header and the exchange has no subject token: every request fails with HTTP 500. The backend still never sees the subject token, because oauthTokenExchange replaces the header.

This example validates broker-issued subject tokens. For upstream tokens, use the upstream issuer and its JWKS URL instead.

Each agentgateway setting must match a broker setting:

Agentgateway settingBroker setting
host and pathThe broker end-user URL and /oauth2/token
resourcesA protected_resources entry of the service whose token the backend needs
clientAuth.clientIdtoken_exchange.client_assertion.issuer_uri. It is also the client assertion sub that the CEL policy sees.
clientAuth.assertionAudiencetoken_exchange.client_assertion.expected_audience. Use the full broker token endpoint URL.
clientAuth.alg and clientAuth.kidalg and kid of a key in token_exchange.client_assertion.jwks

Step 3: Configure the broker​

Make the gateway clientId the client-assertion issuer. Then put the JWK Set from step 1 inline:

token_exchange:
client_assertion:
issuer_uri: "https://gateway.example.com" # agentgateway clientId; never contacted
expected_audience: "https://broker.example.com/oauth2/token" # agentgateway assertionAudience
jwks: |
{"keys":[{"kty":"RSA","kid":"gateway-2026-09","alg":"RS256","use":"sig","e":"AQAB","n":"<base64url modulus>"}]}
claim_extraction:
principal_expression: "subject_token.sub"
# Broker-issued tokens carry agent_id. Upstream tokens carry the client ID in azp.
agent_id_expression: "has(subject_token.agent_id) ? subject_token.agent_id : resolveAgentIdByClientId(subject_token.azp)"
authorization:
type: "cel"
cel:
expression: 'client_assertion.sub == "https://gateway.example.com"' # only agentgateway may exchange

The broker makes no request to issuer_uri and no key-set request for this trust anchor. You can also set the key set through IDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_JWKS. Use jwks_file to rotate keys without a restart. See Trust-anchor key sets for the key-set rules.

client_assertion.expected_audience applies only to client assertions. Subject tokens keep token_exchange.expected_audience, token-exchange-broker by default. In local mode, issue subject tokens with agent_id and that audience:

oauth2_authorization_server:
local:
token_claims_expression: '{"agent_id": agent.id, "azp": agent.id, "aud": "token-exchange-broker"}'

The request that agentgateway sends​

Agentgateway sends this form to the broker. Line breaks in the body are for display only:

POST /oauth2/token HTTP/1.1
Host: broker.example.com
Content-Type: application/x-www-form-urlencoded
Accept: application/json

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&subject_token=eyJhbGciOiJFUzI1NiIsImtpZCI6ImJyb2tlci0xIn0.SYNTHETIC-SUBJECT.sig
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&resource=https%3A%2F%2Fapi.example.com
&client_id=https%3A%2F%2Fgateway.example.com
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImdhdGV3YXktMjAyNi0wOSJ9.SYNTHETIC-ASSERTION.sig

Decoded client_assertion (synthetic):

{
"header": { "typ": "JWT", "alg": "RS256", "kid": "gateway-2026-09" },
"payload": {
"iss": "https://gateway.example.com",
"sub": "https://gateway.example.com",
"aud": "https://broker.example.com/oauth2/token",
"jti": "417174fe-17bf-4212-a4bf-46bccf33ff9e",
"nbf": 1792683407,
"iat": 1792683407,
"exp": 1792683717
}
}

The broker authenticates the gateway only through client_assertion. It ignores client_id in a token-exchange request. The response and error contract is in the token exchange reference.

Operational notes​

  • Token reuse. Agentgateway reuses an exchanged token for later requests that carry the same subject token. It does not call the broker for each request. A revoked grant stops new exchanges. It does not recall a token that agentgateway already holds.
  • Expired subject tokens. jwtAuth answers HTTP 401, so an MCP client can refresh its token and retry. Without jwtAuth, the broker rejects the token and the client receives HTTP 500 instead.
  • Failures. When the broker rejects an exchange, an MCP client receives HTTP 500 with JSON-RPC error -32603. The message names only the broker HTTP status. To find the cause, use the broker token_exchange.* log and span attributes. See diagnostic attributes.
  • Key rotation. Add the new public key to the broker key set first. Then switch the agentgateway signingKey and kid. Remove the old public key last. An inline jwks value changes only on restart. A jwks_file is re-read every jwks_min_refresh.