{"slug": "implementing-rfc-8693-token-exchange-in-agentgateway-a-complete-tutorial", "title": "Implementing RFC 8693 Token Exchange in AgentGateway: A Complete Tutorial", "summary": "AgentGateway, the Rust-based agentic AI proxy from the AI Agent Infrastructure Foundation (AAIF), now supports RFC 8693 OAuth 2.0 Token Exchange to securely delegate user identity to AI agents and downstream services. The feature, introduced in the July 12, 2026 release, allows the gateway to exchange a user's broad-scoped token for a fresh, scoped, short-lived credential before it reaches the agent, preventing token leakage and impersonation. A tutorial demonstrates end-to-end setup with Keycloak as the identity provider.", "body_md": "In agentic AI systems, one of the most challenging security problems is the **identity boundary**: how do you safely delegate user identity to an agent, and then from that agent to downstream services that each require different credentials?\n\nThe naive approach: passing a user's broad-scoped IdP token through every hop, is a security disaster. That token likely grants access to dozens of services, contains sensitive claims, and was never meant to leave the user's browser. If an agent sees that token, it can impersonate the user anywhere. As Christian Posta [wrote in the AgentGateway blog](https://agentgateway.dev/blog/2026-07-12-agentgateway-token-exchange-jwt-assertion-entra-obo/):\n\n\"Shielding AI agents from sensitive MCP / API credentials (secrets, API keys, tokens, etc) is the prevailing best practice.\"\n\n**Token exchange** solves this by replacing the user's original token with a fresh, scoped, short-lived credential before it reaches the agent. The user's identity is preserved (`sub`\n\nclaim), but the token's audience, scope, and lifetime are reshaped for the specific downstream service.\n\nIn this tutorial, I walk through how to implement token exchange in [AgentGateway](https://agentgateway.dev), the Rust-based agentic AI proxy from the AI Agent Infrastructure Foundation (AAIF), using **RFC 8693 (OAuth 2.0 Token Exchange)** and the built-in `backendAuth.oauthTokenExchange`\n\npolicy. By the end, you will have a working end-to-end setup where:\n\nAgents act on behalf of users. When a user invokes an agent that calls multiple downstream services (MCP servers, APIs, databases), each service needs to know:\n\n`sub`\n\nclaim)Passing the user's original IdP token to every service fails all four:\n\n`act`\n\nclaim)Without gateway-managed exchange, as Posta notes, \"you get N credential stores and zero consistent audit: exactly the leak surface\" you want to avoid.\n\nRFC 8693 defines a standard OAuth 2.0 grant type (`urn:ietf:params:oauth:grant-type:token-exchange`\n\n) that lets a client (the gateway) exchange one security token (the user's JWT) for another (a downstream-scoped JWT).\n\nThe gateway becomes the security boundary:\n\n```\nUser JWT (broad, long-lived)\n  ↓\nAgentGateway exchanges at IdP\n  ↓\nDownstream JWT (scoped, short-lived, audience-specific)\n  ↓\nUpstream service\n```\n\nThe upstream service **never sees** the user's original token. It only sees a token minted specifically for it, with the correct audience and minimal scopes.\n\nAs of [the July 12, 2026 release](https://agentgateway.dev/blog/2026-07-12-agentgateway-token-exchange-jwt-assertion-entra-obo/), AgentGateway supports three exchange mechanisms under `backendAuth.oauthTokenExchange`\n\n:\n\n| Grant | Spec | Subject Parameter | Typical IdP |\n|---|---|---|---|\nToken exchange (default) |\nRFC 8693 | `subject_token` |\nKeycloak, Okta, ZITADEL, Auth0 |\nJWT bearer |\nRFC 7523 | `assertion` |\nKeycloak JWT Authorization Grant |\nEntra OBO |\nMicrosoft's jwt-bearer variant | `assertion` |\nMicrosoft Entra ID |\n\nThis tutorial focuses on the **RFC 8693 token exchange** grant with Keycloak as the IdP. I cover the JWT bearer and Entra OBO approaches at the end.\n\nBefore starting, ensure you have:\n\n`rustc >= 1.91.1`\n\n(check with `rustc --version`\n\n)Here is what I built:\n\n`requester-client`\n\n)`/exchange`\n\nwith `Authorization: Bearer <user-jwt>`\n\n`Authorization`\n\nheader`aud=target-client`\n\n`Authorization: Bearer <exchanged-token>`\n\nClone the AgentGateway repository:\n\n```\ngit clone https://github.com/agentgateway/agentgateway.git\ncd agentgateway\n```\n\nBuild AgentGateway from source:\n\n```\n# Ensure Rust toolchain is installed (if not already)\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\nsource \"$HOME/.cargo/env\"\n\n# Build in release mode (this takes several minutes on first run)\ncargo build --release\n```\n\n**Actual output from my build on RHEL 9:**\n\n``` bash\n$ rustc --version\nrustc 1.97.0 (2d8144b78 2026-07-07)\n\n$ cargo build --release\n   Compiling agentgateway v0.0.0 (/home/margonza/agentgateway/crates/agentgateway)\n   Compiling agentgateway-app v0.0.0 (/home/margonza/agentgateway/crates/agentgateway-app)\n    Finished `release` profile [optimized] target(s) in 23m 19s\n```\n\nThe first build takes ~20 minutes as it compiles all dependencies. Subsequent builds are incremental and much faster.\n\nAgentGateway ships with a complete working example in `examples/traffic-token-exchange/oauth-rfc8693/`\n\n. This is the one I used.\n\nIf you have Docker:\n\n```\ndocker compose -f examples/traffic-token-exchange/oauth-rfc8693/docker-compose.yaml up -d\n```\n\nIf you have Podman (as I did on RHEL 9, without compose), start the containers manually:\n\n```\n# Create an isolated network\npodman network create agw-token-exchange\n\n# Start Keycloak with the pre-built realm import\npodman run -d \\\n  --name backend-oauth-keycloak \\\n  --network agw-token-exchange \\\n  -e KC_HOSTNAME=localhost \\\n  -e KC_HOSTNAME_PORT=7080 \\\n  -e KEYCLOAK_ADMIN=admin \\\n  -e KEYCLOAK_ADMIN_PASSWORD=admin \\\n  -e KC_HEALTH_ENABLED=true \\\n  -e KC_LOG_LEVEL=info \\\n  -v ./examples/traffic-token-exchange/oauth-rfc8693/backend-oauth-realm.json:/opt/keycloak/data/import/backend-oauth-realm.json:ro,Z \\\n  -p 7080:7080 \\\n  -p 7443:7443 \\\n  quay.io/keycloak/keycloak:26.3 \\\n  start-dev --import-realm --http-port 7080 --https-port 7443\n\n# Start the echo upstream (reflects request headers back to the caller)\npodman run -d \\\n  --name backend-oauth-upstream \\\n  --network agw-token-exchange \\\n  -p 18080:8080 \\\n  registry.istio.io/testing/app\n```\n\n**What this sets up:**\n\n`http://localhost:7080`\n\n, realm `backend-oauth`\n\n, pre-seeded with:\n`initial-client`\n\n/ `initial-secret`\n\n: user authenticates here first`requester-client`\n\n/ `requester-secret`\n\n: AgentGateway uses this to request exchanges`target-client`\n\n/ `target-secret`\n\n: audience for exchanged tokens`testuser`\n\n/ `testpass`\n\n`http://localhost:18080`\n\n: reflects request headers so you can see the exchanged tokenWait for Keycloak to be ready (it takes about 15-20 seconds):\n\n```\nuntil curl -sf http://localhost:7080/realms/backend-oauth/.well-known/openid-configuration > /dev/null; do\n  echo \"Waiting for Keycloak...\"\n  sleep 3\ndone\necho \"Keycloak is ready!\"\n```\n\n**Actual output:**\n\n```\nWaiting for Keycloak...\nWaiting for Keycloak...\nWaiting for Keycloak...\nKeycloak is ready!\n```\n\nVerify the realm is imported:\n\n```\ncurl -s http://localhost:7080/realms/backend-oauth/.well-known/openid-configuration | jq -r '.issuer, .token_endpoint'\n```\n\n**Actual output:**\n\n```\nhttp://localhost:7080/realms/backend-oauth\nhttp://localhost:7080/realms/backend-oauth/protocol/openid-connect/token\n```\n\nKeycloak logs will show the realm import succeeded:\n\n```\n2026-07-13T10:00:09.577 INFO  [org.keycloak.exportimport.dir.DirImportProvider] Importing from directory /opt/keycloak/data/import\n2026-07-13T10:00:09.582 INFO  [org.keycloak.services] KC-SERVICES0050: Initializing master realm\n2026-07-13T10:00:11.161 INFO  [org.keycloak.services] KC-SERVICES0030: Full model import requested. Strategy: IGNORE_EXISTING\n2026-07-13T10:00:12.465 INFO  [org.keycloak.exportimport.util.ImportUtils] Realm 'backend-oauth' imported\n2026-07-13T10:00:12.470 INFO  [org.keycloak.services] KC-SERVICES0032: Import finished successfully\n2026-07-13T10:00:12.722 INFO  [io.quarkus] Keycloak 26.3.5 on JVM (powered by Quarkus 3.20.3) started in 11.329s. Listening on: http://0.0.0.0:7080.\n```\n\nThe configuration file at `examples/traffic-token-exchange/oauth-rfc8693/config.yaml`\n\nis remarkably concise: AgentGateway's built-in `backendAuth.oauthTokenExchange`\n\npolicy handles the RFC 8693 flow:\n\n```\nconfig: {}\nbinds:\n- port: 3000\n  listeners:\n  - name: default\n    protocol: HTTP\n    routes:\n    - name: token-exchange\n      matches:\n      - path:\n          pathPrefix: /exchange\n      backends:\n      - host: localhost:18080\n        policies:\n          backendAuth:\n            oauthTokenExchange:\n              host: localhost:7080\n              tokenEndpointPath: /realms/backend-oauth/protocol/openid-connect/token\n              clientAuth:\n                clientId: requester-client\n                clientSecret: requester-secret\n                method: clientSecretBasic\n              audiences:\n              - target-client\n```\n\n**Route match**: Requests to `/exchange`\n\ntrigger token exchange:\n\n```\nmatches:\n- path:\n    pathPrefix: /exchange\n```\n\n**Backend + policy**: The `backendAuth.oauthTokenExchange`\n\nblock is where the magic happens:\n\n`host`\n\n+ `tokenEndpointPath`\n\n: Where to send the exchange request (Keycloak's token endpoint)`clientAuth`\n\n: The gateway authenticates to Keycloak as a confidential client (`requester-client`\n\n), using HTTP Basic auth (`clientSecretBasic`\n\n)`audiences`\n\n: The target audience for the exchanged token: this becomes the `aud`\n\nclaim in the new JWTUnder the hood, AgentGateway:\n\n`Authorization: Bearer <token>`\n\nheader`grant_type=urn:ietf:params:oauth:grant-type:token-exchange`\n\n`subject_token`\n\n`audience=target-client`\n\n`exp`\n\n)`Authorization`\n\nheader with the exchanged token before forwarding upstreamCompare this to the [alternative extAuthz + CEL approach](https://github.com/agentgateway/agentgateway/tree/main/examples/traffic-token-exchange/extauthz) which requires ~40 lines of hand-written CEL expressions to achieve the same result. The built-in policy is the recommended approach.\n\nStart AgentGateway with the token exchange configuration:\n\n```\ncargo run --release -- -f examples/traffic-token-exchange/oauth-rfc8693/config.yaml\n```\n\n**Actual output:**\n\n```\n2026-07-13T10:25:26.999303Z  info  agentgateway_app::commands::run  version: {\n  \"version\": \"d46bc5cc05429a9b3d16180ac9de4ef46e65e857\",\n  \"rust_version\": \"1.97.0\",\n  \"build_profile\": \"release\",\n  \"build_target\": \"x86_64-unknown-linux-gnu\"\n}\n2026-07-13T10:25:27.001726Z  info  state_manager  loaded config from File(\"examples/traffic-token-exchange/oauth-rfc8693/config.yaml\")\n2026-07-13T10:25:27.002446Z  info  agent_core::readiness  Task 'agentgateway' complete (3.448656ms), still awaiting 1 tasks\n2026-07-13T10:25:27.002522Z  info  agent_core::readiness  Task 'state manager' complete (3.524277ms), marking server ready\n2026-07-13T10:25:27.002588Z  info  proxy::gateway  started bind  bind=\"bind/3000\"\n```\n\nThe gateway hot-reloads the config file on save, so you can edit routes and params and re-curl without restarting it.\n\nLeave this terminal running. Open a new terminal for testing.\n\nAuthenticate as the test user to get an initial JWT:\n\n```\nSUBJECT_TOKEN=\"$(curl -s http://localhost:7080/realms/backend-oauth/protocol/openid-connect/token \\\n  -u initial-client:initial-secret \\\n  -d grant_type=password \\\n  -d username=testuser \\\n  -d password=testpass | jq -r .access_token)\"\n\necho \"User token obtained: ${SUBJECT_TOKEN:0:50}...\"\n```\n\n**Actual output:**\n\n```\nUser token obtained: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6IC...\n```\n\nDecode the user token to inspect its claims:\n\n```\n# Decode the JWT payload (with proper base64 padding)\nPAYLOAD=$(echo $SUBJECT_TOKEN | cut -d'.' -f2)\nMOD=$((${#PAYLOAD} % 4))\nif [ $MOD -eq 2 ]; then PAYLOAD=\"${PAYLOAD}==\"; elif [ $MOD -eq 3 ]; then PAYLOAD=\"${PAYLOAD}=\"; fi\necho $PAYLOAD | base64 -d 2>/dev/null | jq .\n```\n\n**Actual output from my run:**\n\n```\n{\n  \"exp\": 1783938629,\n  \"iat\": 1783938329,\n  \"jti\": \"onrtro:a83f2b10-5d4e-6c1a-9e87-3b2c1d4e5f6a\",\n  \"iss\": \"http://localhost:7080/realms/backend-oauth\",\n  \"aud\": \"requester-client\",\n  \"typ\": \"Bearer\",\n  \"azp\": \"initial-client\",\n  \"sid\": \"8ced65e7-d7ba-4793-896c-ad3df3b09994\",\n  \"scope\": \"\"\n}\n```\n\n**Key observations:**\n\n`aud`\n\n= `\"requester-client\"`\n\n: the audience is scoped for the gateway, not for any downstream service`azp`\n\n= `\"initial-client\"`\n\n: the authorized party (who requested this token)`exp`\n\n- `iat`\n\n= 300 seconds: token expires in 5 minutes`sub`\n\nclaim: this is a client credentials grant, not user-bound yetCall AgentGateway's `/exchange`\n\nendpoint with the user's token:\n\n```\ncurl -s http://localhost:3000/exchange \\\n  -H \"authorization: Bearer $SUBJECT_TOKEN\"\n```\n\n**Actual output (from the echo upstream: it reflects the request headers it received):**\n\n```\nServiceVersion=\nServicePort=8080\nHost=localhost\nURL=/exchange\nMethod=GET\nProto=HTTP/1.1\nIP=10.89.2.3\nRequestHeader=Accept:*/*\nRequestHeader=Authorization:Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSl...\nRequestHeader=User-Agent:curl/7.76.1\nHostname=ed81d5bb758c\n```\n\nNotice the `Authorization`\n\nheader contains a **different token** than what I sent. The original user token had `aud=requester-client`\n\n, but the upstream received a token with `aud=target-client`\n\n. AgentGateway exchanged it transparently.\n\nThe gateway's access log confirms the exchange:\n\n```\n2026-07-13T10:25:29.848Z  info  request\n  gateway=default/default listener=default route=default/token-exchange\n  endpoint=localhost:18080 src.addr=[::1]:49202\n  http.method=GET http.path=/exchange http.status=200\n  protocol=http duration=17ms\n```\n\nThe first request took **17ms**: this includes the round-trip to Keycloak's token endpoint for the exchange.\n\nTo see exactly what changed, I performed the token exchange directly against Keycloak:\n\n```\nEXCHANGED_TOKEN=\"$(curl -s http://localhost:7080/realms/backend-oauth/protocol/openid-connect/token \\\n  -u requester-client:requester-secret \\\n  -d \"grant_type=urn:ietf:params:oauth:grant-type:token-exchange\" \\\n  -d \"subject_token=$SUBJECT_TOKEN\" \\\n  -d \"subject_token_type=urn:ietf:params:oauth:token-type:access_token\" \\\n  -d \"requested_token_type=urn:ietf:params:oauth:token-type:access_token\" \\\n  -d \"audience=target-client\" | jq -r .access_token)\"\n```\n\nDecoded exchanged token (this is what the upstream service sees):\n\n```\n{\n  \"exp\": 1783938629,\n  \"iat\": 1783938329,\n  \"jti\": \"ntrtte:5172a3d7-4def-7ec0-d6b1-4ad6adab761c\",\n  \"iss\": \"http://localhost:7080/realms/backend-oauth\",\n  \"aud\": \"target-client\",\n  \"sub\": \"feac01a0-48f7-4139-9b2d-171237c2f5e5\",\n  \"typ\": \"Bearer\",\n  \"azp\": \"requester-client\",\n  \"sid\": \"8ced65e7-d7ba-4793-896c-ad3df3b09994\",\n  \"scope\": \"\"\n}\n```\n\n| Claim | Original Token | Exchanged Token | Changed? |\n|---|---|---|---|\n`iss` |\n`http://localhost:7080/realms/backend-oauth` |\nSame | No |\n`aud` |\n`\"requester-client\"` |\n`\"target-client\"` |\nYes: audience now matches downstream |\n`sub` |\n(absent) |\n`\"feac01a0-48f7-4139-9b2d-171237c2f5e5\"` |\nYes: user identity added |\n`azp` |\n`\"initial-client\"` |\n`\"requester-client\"` |\nYes: shows who requested the exchange |\n`sid` |\n`\"0ce5ad6d-...\"` |\n`\"8ced65e7-...\"` |\nVaries per session |\n`jti` |\n`\"onrtro:5447afe4-...\"` |\n`\"ntrtte:5172a3d7-...\"` |\nYes: new unique token ID |\n\n**Key takeaways:**\n\n`sub`\n\nclaim links to the same user (`feac01a0-...`\n\n= testuser)`aud`\n\nnow matches the target service, not the gateway`azp`\n\nshows the gateway (`requester-client`\n\n) requested this exchange`jti`\n\nfor audit trailsCall the endpoint multiple times with the same user token:\n\n```\nfor i in {1..3}; do\n  curl -s -w \"Request $i: %{time_total}s\\n\" -o /dev/null \\\n    http://localhost:3000/exchange \\\n    -H \"authorization: Bearer $SUBJECT_TOKEN\"\ndone\n```\n\n**Actual output (these ran after the first /exchange call already populated the cache):**\n\n```\nRequest 1: 0.001003s   <- cache hit\nRequest 2: 0.001595s   <- cache hit\nRequest 3: 0.001129s   <- cache hit\n```\n\nThe gateway access logs confirm the difference between cache miss and cache hit:\n\n```\n# First request (cache miss: exchange round-trip to Keycloak):\nduration=17ms\n\n# Subsequent requests (cache hit: no Keycloak call):\nduration=0ms\nduration=1ms\nduration=0ms\n```\n\nThe cache reduces per-request latency from **17ms to under 1ms**. The TTL is capped by the subject JWT's `exp`\n\nclaim, so exchanged tokens are automatically refreshed when the original token expires.\n\n**Before Token Exchange (Insecure):**\n\n``` php\nUser -> [JWT: aud=requester-client] -> Gateway -> [Same JWT] -> Upstream\n```\n\nProblem: Upstream receives a token with the wrong audience, over-privileged scopes, and no indication that a gateway is involved.\n\n**After Token Exchange (Secure):**\n\n``` php\nUser -> [JWT: aud=requester-client] -> Gateway -> Keycloak exchange -> [JWT: aud=target-client] -> Upstream\n```\n\nBenefits:\n\n`aud=target-client`\n\n)`sub`\n\nclaim)`azp`\n\nshows who performed the exchangeFor IdPs that support JWT bearer (e.g., Keycloak 26.5+ JWT Authorization Grant), change the grant type:\n\n```\nbackendAuth:\n  oauthTokenExchange:\n    host: localhost:7080\n    tokenEndpointPath: /realms/backend-oauth/protocol/openid-connect/token\n    grantType: jwtBearer\n    clientAuth:\n      clientId: requester-client\n      clientSecret: requester-secret\n      method: clientSecretBasic\n    audiences:\n    - target-client\n```\n\nThe key difference: the inbound credential is sent as `assertion`\n\ninstead of `subject_token`\n\n.\n\nMicrosoft Entra does **not** speak RFC 8693. It uses a jwt-bearer variant with a vendor-specific `requested_token_use=on_behalf_of`\n\nparameter:\n\n```\nbackendAuth:\n  oauthTokenExchange:\n    host: login.microsoftonline.com:443\n    tokenEndpointPath: /<TENANT_ID>/oauth2/v2.0/token\n    grantType: jwtBearer\n    clientAuth:\n      clientId: <CLIENT_ID>\n      clientSecret: <CLIENT_SECRET>\n      method: clientSecretPost\n    scopes:\n    - https://graph.microsoft.com/.default\n    additionalParams:\n      requested_token_use: '\"on_behalf_of\"'\n```\n\nAs Christian Posta notes: \"SSO gets the user *into* the gateway; OBO gets the user *out* to Graph.\"\n\nBy default, the gateway reads the subject token from the `Authorization: Bearer`\n\nheader. You can change this:\n\n```\nsubjectToken:\n  source:\n    header:\n      name: x-user-token\n  tokenType: urn:ietf:params:oauth:token-type:access_token\n```\n\nPut the exchanged token in a custom header instead of `Authorization`\n\n:\n\n```\nauthorizationLocation:\n  header:\n    name: x-upstream-auth\n```\n\nFor RFC 8693 delegation (the `act`\n\nclaim), specify an actor token:\n\n```\nactorToken:\n  source:\n    header:\n      name: x-actor-token\n  tokenType: urn:ietf:params:oauth:token-type:jwt\n```\n\nThe exchanged token will include an `act`\n\nclaim showing the agent's identity: auditable proof that \"agent A called this API on behalf of user B.\"\n\nTo force every request to hit the token endpoint (useful for debugging):\n\n```\ncache:\n  inMemory:\n    maxEntries: 0\n```\n\n**Symptom:** AgentGateway returns 401 when calling `/exchange`\n\n**Check:**\n\n`requester-client:requester-secret`\n\nmatches Keycloak\n\n```\n   echo $SUBJECT_TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .exp\n   date +%s  # compare timestamps\n```\n\n`http://localhost:7080/admin`\n\n, admin/admin), navigate to `backend-oauth`\n\nrealm -> Clients -> `requester-client`\n\n-> Capability config, and verify \"Standard Token Exchange\" is enabled**Symptom:** Upstream echo shows the original token, not the exchanged one\n\n**Check:** Verify the `backendAuth`\n\nblock is under `backends[].policies`\n\n, not under `routes[].policies`\n\n:\n\n```\n# CORRECT: policy is per-backend\nbackends:\n- host: localhost:18080\n  policies:\n    backendAuth:\n      oauthTokenExchange: ...\n\n# WRONG: this level won't trigger token exchange\npolicies:\n  backendAuth: ...\nbackends:\n- host: localhost:18080\n```\n\n**Symptom:** Container exits with \"Failed to run import\"\n\n**Fix:** On SELinux-enabled systems (RHEL, Fedora), add the `:Z`\n\nsuffix to the volume mount:\n\n```\n-v ./backend-oauth-realm.json:/opt/keycloak/data/import/backend-oauth-realm.json:ro,Z\n```\n\nWithout `:Z`\n\n, SELinux blocks the container from reading the mounted file. This is what happened to me on RHEL 9 before I added the label.\n\n```\n# Stop AgentGateway (Ctrl+C in the running terminal, or)\npkill -f 'target/release/agentgateway'\n\n# Stop and remove containers (Podman)\npodman rm -f backend-oauth-keycloak backend-oauth-upstream\npodman network rm agw-token-exchange\n\n# Or with Docker Compose:\n# docker compose -f examples/traffic-token-exchange/oauth-rfc8693/docker-compose.yaml down\n```\n\nI walked through implementing RFC 8693 token exchange in AgentGateway: from understanding the security problem to a working end-to-end setup with real Keycloak tokens. Here is what I covered:\n\n`backendAuth.oauthTokenExchange`\n\nhandles the entire flow in ~10 lines of YAMLToken exchange is a **critical security primitive** for agentic AI. Without it, agents either see over-privileged tokens (security risk) or lose user identity (audit risk). AgentGateway makes it a configuration concern, not a code concern.\n\n`examples/traffic-token-exchange/extauthz/`\n\nfor the hand-written version (useful when you need custom logic)`examples/traffic-token-exchange/jwt-authz-grant/`\n\nfor RFC 7523`examples/mcp-authentication/`\n\nfor agent-to-agent authThe tutorial above runs everything locally. When moving to production, keep these in mind:\n\n**Use environment variables for secrets.** Never hardcode client secrets in YAML committed to version control:\n\n```\nclientAuth:\n  clientId: requester-client\n  clientSecret: $OAUTH_CLIENT_SECRET\n  method: clientSecretBasic\n```\n\n**Kubernetes deployment.** On Kubernetes, apply token exchange via `AgentgatewayPolicy`\n\ntargeting a Service:\n\n```\napiVersion: agentgateway.dev/v1alpha1\nkind: AgentgatewayPolicy\nmetadata:\n  name: okta-token-exchange\nspec:\n  targetRefs:\n  - group: \"\"\n    kind: Service\n    name: my-backend\n  backend:\n    auth:\n      oauthTokenExchange:\n        tokenEndpoint:\n          group: agentgateway.dev\n          kind: AgentgatewayBackend\n          name: okta-token-endpoint\n          port: 443\n          path: /oauth2/default/v1/token\n```\n\n**Monitor exchange latency.** Token exchanges add latency on cache miss. Track:\n\n**Token lifetime tuning.** Balance security vs. cache efficiency:\n\n`exp`\n\n: a safety net against stale tokens**Multi-backend routing.** Different backends can have different audiences. Define separate `backendAuth`\n\npolicies per backend:\n\n```\nroutes:\n- name: service-a\n  matches:\n  - path: { pathPrefix: /service-a }\n  backends:\n  - host: service-a.local\n    policies:\n      backendAuth:\n        oauthTokenExchange:\n          audiences: [service-a]\n\n- name: service-b\n  matches:\n  - path: { pathPrefix: /service-b }\n  backends:\n  - host: service-b.local\n    policies:\n      backendAuth:\n        oauthTokenExchange:\n          audiences: [service-b]\n```\n\nEach backend receives a token scoped specifically for it.\n\n**About the Author**: Marco Gonzalez ([@mgonzalezo](https://github.com/mgonzalezo)) is an AAIF Ambassador focusing on AgentGateway, MCP, Goose, and AGENTS.md. He contributes tutorials, blog posts, and PRs to the AAIF ecosystem.\n\n**License**: This tutorial is licensed under CC-BY-4.0. Code examples are MIT licensed.\n\nHappy Learning! 🚀", "url": "https://wpnews.pro/news/implementing-rfc-8693-token-exchange-in-agentgateway-a-complete-tutorial", "canonical_source": "https://dev.to/mgonzalezo/implementing-rfc-8693-token-exchange-in-agentgateway-a-complete-tutorial-3g3p", "published_at": "2026-07-13 11:22:11+00:00", "updated_at": "2026-07-13 11:46:28.682215+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-infrastructure", "developer-tools"], "entities": ["AgentGateway", "AI Agent Infrastructure Foundation", "Keycloak", "RFC 8693", "Christian Posta", "Microsoft Entra ID", "Okta", "Auth0"], "alternates": {"html": "https://wpnews.pro/news/implementing-rfc-8693-token-exchange-in-agentgateway-a-complete-tutorial", "markdown": "https://wpnews.pro/news/implementing-rfc-8693-token-exchange-in-agentgateway-a-complete-tutorial.md", "text": "https://wpnews.pro/news/implementing-rfc-8693-token-exchange-in-agentgateway-a-complete-tutorial.txt", "jsonld": "https://wpnews.pro/news/implementing-rfc-8693-token-exchange-in-agentgateway-a-complete-tutorial.jsonld"}}