Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungI audited my own ML linter and had to withdraw its best evidence(21.09.2026 um 22:54 Uhr)
Sichere ProgrammierungQuantum Result Validation for Distributed Computing Systems(21.09.2026 um 22:54 Uhr)
Sichere ProgrammierungJWT Authentication and Role-Based Access Control in LocalHands(21.09.2026 um 22:56 Uhr)
Sichere ProgrammierungStochastic Parrot or Alien Mind?(21.09.2026 um 22:56 Uhr)
Sichere ProgrammierungBuilding AI for the Physical World Is a Different Engineering Problem(21.09.2026 um 22:58 Uhr)
Sichere ProgrammierungI audited my own ML linter and had to withdraw its best evidence(21.09.2026 um 22:54 Uhr)
Sichere ProgrammierungQuantum Result Validation for Distributed Computing Systems(21.09.2026 um 22:54 Uhr)
Sichere ProgrammierungJWT Authentication and Role-Based Access Control in LocalHands(21.09.2026 um 22:56 Uhr)
Sichere ProgrammierungStochastic Parrot or Alien Mind?(21.09.2026 um 22:56 Uhr)
Sichere ProgrammierungBuilding AI for the Physical World Is a Different Engineering Problem(21.09.2026 um 22:58 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Implementing RFC 8693 Token Exchange in AgentGateway: A Complete Tutorial

Table of Contents Introduction Why Token Exchange Matters for Agentic AI The Problem The Solution: RFC 8693 Token Exchange AgentGateway's Three Exchange Grants Prerequisites Architecture Overview Step 1: Clone and Build…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!




Table of Contents




  1. Introduction


  2. Why Token Exchange Matters for Agentic AI


    • The Problem

    • The Solution: RFC 8693 Token Exchange

    • AgentGateway's Three Exchange Grants



  3. Prerequisites

  4. Architecture Overview

  5. Step 1: Clone and Build AgentGateway

  6. Step 2: Start Keycloak and the Echo Upstream

  7. Step 3: Understand the Configuration

  8. Step 4: Run AgentGateway


  9. Step 5: Test the Token Exchange Flow


    • 5.1 Get a User Token

    • 5.2 Call AgentGateway

    • 5.3 Verify the Token Transformation

    • 5.4 Test Token Caching



  10. Step 6: Understand the Security Boundary


  11. Step 7: Advanced Configuration


    • JWT Bearer (RFC 7523)

    • Microsoft Entra OBO

    • Custom Subject Token Source

    • Delegation with Actor Token



  12. Step 8: Troubleshooting

  13. Step 9: Cleanup

  14. Conclusion

  15. Production Considerations






Introduction



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?



The 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:




"Shielding AI agents from sensitive MCP / API credentials (secrets, API keys, tokens, etc) is the prevailing best practice."




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 claim), but the token's audience, scope, and lifetime are reshaped for the specific downstream service.



In this tutorial, I walk through how to implement token exchange in AgentGateway, 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 policy. By the end, you will have a working end-to-end setup where:




  1. A user authenticates with an identity provider (Keycloak)

  2. AgentGateway exchanges the user's token for a downstream-scoped token

  3. An upstream service receives only the exchanged token: never the original

  4. All exchanges are cached for performance






Why Token Exchange Matters for Agentic AI






The Problem



Agents 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:





  • Who is the user? (sub claim)


  • What can they do? (scopes/permissions)


  • Is this token meant for me? (audience validation)


  • Who is acting on their behalf? (the agent)



Passing the user's original IdP token to every service fails all four:




  • The audience is wrong (it's scoped for the gateway, not the downstream)

  • The scopes are too broad (full IdP permissions, not service-specific)

  • The expiration is too long (hours, not minutes)

  • There's no indication an agent is involved (no act claim)



Without gateway-managed exchange, as Posta notes, "you get N credential stores and zero consistent audit: exactly the leak surface" you want to avoid.






The Solution: RFC 8693 Token Exchange



RFC 8693 defines a standard OAuth 2.0 grant type (urn:ietf:params:oauth:grant-type:token-exchange) that lets a client (the gateway) exchange one security token (the user's JWT) for another (a downstream-scoped JWT).



The gateway becomes the security boundary:




User JWT (broad, long-lived)

AgentGateway exchanges at IdP

Downstream JWT (scoped, short-lived, audience-specific)

Upstream service






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






AgentGateway's Three Exchange Grants



As of the July 12, 2026 release, AgentGateway supports three exchange mechanisms under backendAuth.oauthTokenExchange:
































Grant Spec Subject Parameter Typical IdP

Token exchange (default)
RFC 8693 subject_token Keycloak, Okta, ZITADEL, Auth0
JWT bearer RFC 7523 assertion Keycloak JWT Authorization Grant
Entra OBO Microsoft's jwt-bearer variant assertion Microsoft Entra ID


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






Prerequisites



Before starting, ensure you have:





  • Rust toolchain: rustc >= 1.91.1 (check with rustc --version)


  • Docker or Podman: For running Keycloak (I used Podman 5.8.2 on RHEL 9)


  • Git: To clone the AgentGateway repository


  • curl & jq: For testing API calls and parsing JSON


  • Basic understanding of:


    • OAuth 2.0 flows

    • JWT structure (header, payload, signature)

    • HTTP headers and status codes








Architecture Overview



Here is what I built:



Architecture Overview

Flow Details:




  1. User authenticates with Keycloak → receives JWT (audience: requester-client)

  2. User calls AgentGateway /exchange with Authorization: Bearer <user-jwt>

  3. AgentGateway extracts the user's JWT from the Authorization header

  4. AgentGateway calls Keycloak's token endpoint with the RFC 8693 grant

  5. Keycloak validates the user's JWT and issues a new JWT with aud=target-client

  6. AgentGateway caches the exchanged token (keyed by subject + grant params)

  7. AgentGateway forwards the request to upstream with Authorization: Bearer <exchanged-token>

  8. Upstream validates the exchanged token (audience, issuer, signature)





Step 1: Clone and Build AgentGateway



Clone the AgentGateway repository:




git clone https://github.com/agentgateway/agentgateway.git
cd agentgateway






Build AgentGateway from source:




# Ensure Rust toolchain is installed (if not already)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"

# Build in release mode (this takes several minutes on first run)
cargo build --release






Actual output from my build on RHEL 9:




$ rustc --version
rustc 1.97.0 (2d8144b78 2026-07-07)

$ cargo build --release
Compiling agentgateway v0.0.0 (/home/margonza/agentgateway/crates/agentgateway)
Compiling agentgateway-app v0.0.0 (/home/margonza/agentgateway/crates/agentgateway-app)
Finished `release` profile [optimized] target(s) in 23m 19s






The first build takes ~20 minutes as it compiles all dependencies. Subsequent builds are incremental and much faster.






Step 2: Start Keycloak and the Echo Upstream



AgentGateway ships with a complete working example in examples/traffic-token-exchange/oauth-rfc8693/. This is the one I used.



If you have Docker:




docker compose -f examples/traffic-token-exchange/oauth-rfc8693/docker-compose.yaml up -d






If you have Podman (as I did on RHEL 9, without compose), start the containers manually:




# Create an isolated network
podman network create agw-token-exchange

# Start Keycloak with the pre-built realm import
podman run -d \
--name backend-oauth-keycloak \
--network agw-token-exchange \
-e KC_HOSTNAME=localhost \
-e KC_HOSTNAME_PORT=7080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
-e KC_HEALTH_ENABLED=true \
-e KC_LOG_LEVEL=info \
-v ./examples/traffic-token-exchange/oauth-rfc8693/backend-oauth-realm.json:/opt/keycloak/data/import/backend-oauth-realm.json:ro,Z \
-p 7080:7080 \
-p 7443:7443 \
quay.io/keycloak/keycloak:26.3 \
start-dev --import-realm --http-port 7080 --https-port 7443

# Start the echo upstream (reflects request headers back to the caller)
podman run -d \
--name backend-oauth-upstream \
--network agw-token-exchange \
-p 18080:8080 \
registry.istio.io/testing/app






What this sets up:





  • Keycloak on http://localhost:7080, realm backend-oauth, pre-seeded with:



    • initial-client / initial-secret: user authenticates here first


    • requester-client / requester-secret: AgentGateway uses this to request exchanges


    • target-client / target-secret: audience for exchanged tokens

    • Test user: testuser / testpass




  • Echo upstream on http://localhost:18080: reflects request headers so you can see the exchanged token



Wait for Keycloak to be ready (it takes about 15-20 seconds):




until curl -sf http://localhost:7080/realms/backend-oauth/.well-known/openid-configuration > /dev/null; do
echo "Waiting for Keycloak..."
sleep 3
done
echo "Keycloak is ready!"






Actual output:




Waiting for Keycloak...
Waiting for Keycloak...
Waiting for Keycloak...
Keycloak is ready!






Verify the realm is imported:




curl -s http://localhost:7080/realms/backend-oauth/.well-known/openid-configuration | jq -r '.issuer, .token_endpoint'






Actual output:




http://localhost:7080/realms/backend-oauth
http://localhost:7080/realms/backend-oauth/protocol/openid-connect/token






Keycloak logs will show the realm import succeeded:




2026-07-13T10:00:09.577 INFO  [org.keycloak.exportimport.dir.DirImportProvider] Importing from directory /opt/keycloak/data/import
2026-07-13T10:00:09.582 INFO [org.keycloak.services] KC-SERVICES0050: Initializing master realm
2026-07-13T10:00:11.161 INFO [org.keycloak.services] KC-SERVICES0030: Full model import requested. Strategy: IGNORE_EXISTING
2026-07-13T10:00:12.465 INFO [org.keycloak.exportimport.util.ImportUtils] Realm 'backend-oauth' imported
2026-07-13T10:00:12.470 INFO [org.keycloak.services] KC-SERVICES0032: Import finished successfully
2026-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.









Step 3: Understand the Configuration



The configuration file at examples/traffic-token-exchange/oauth-rfc8693/config.yaml is remarkably concise: AgentGateway's built-in backendAuth.oauthTokenExchange policy handles the RFC 8693 flow:




config: {}
binds:
- port: 3000
listeners:
- name: default
protocol: HTTP
routes:
- name: token-exchange
matches:
- path:
pathPrefix: /exchange
backends:
- host: localhost:18080
policies:
backendAuth:
oauthTokenExchange:
host: localhost:7080
tokenEndpointPath: /realms/backend-oauth/protocol/openid-connect/token
clientAuth:
clientId: requester-client
clientSecret: requester-secret
method: clientSecretBasic
audiences:
- target-client









Configuration Breakdown



Route match: Requests to /exchange trigger token exchange:




matches:
- path:
pathPrefix: /exchange






Backend + policy: The backendAuth.oauthTokenExchange block is where the magic happens:





  • host + tokenEndpointPath: Where to send the exchange request (Keycloak's token endpoint)


  • clientAuth: The gateway authenticates to Keycloak as a confidential client (requester-client), using HTTP Basic auth (clientSecretBasic)


  • audiences: The target audience for the exchanged token: this becomes the aud claim in the new JWT



Under the hood, AgentGateway:




  1. Reads the inbound Authorization: Bearer <token> header

  2. POSTs to the token endpoint with grant_type=urn:ietf:params:oauth:grant-type:token-exchange

  3. Includes the user's token as subject_token

  4. Requests a token scoped to audience=target-client

  5. Caches the result (keyed by subject + grant params, TTL capped by subject JWT exp)

  6. Replaces the Authorization header with the exchanged token before forwarding upstream



Compare this to the alternative extAuthz + CEL approach which requires ~40 lines of hand-written CEL expressions to achieve the same result. The built-in policy is the recommended approach.






Step 4: Run AgentGateway



Start AgentGateway with the token exchange configuration:




cargo run --release -- -f examples/traffic-token-exchange/oauth-rfc8693/config.yaml






Actual output:




2026-07-13T10:25:26.999303Z  info  agentgateway_app::commands::run  version: {
"version": "d46bc5cc05429a9b3d16180ac9de4ef46e65e857",
"rust_version": "1.97.0",
"build_profile": "release",
"build_target": "x86_64-unknown-linux-gnu"
}
2026-07-13T10:25:27.001726Z info state_manager loaded config from File("examples/traffic-token-exchange/oauth-rfc8693/config.yaml")
2026-07-13T10:25:27.002446Z info agent_core::readiness Task 'agentgateway' complete (3.448656ms), still awaiting 1 tasks
2026-07-13T10:25:27.002522Z info agent_core::readiness Task 'state manager' complete (3.524277ms), marking server ready
2026-07-13T10:25:27.002588Z info proxy::gateway started bind bind="bind/3000"






The gateway hot-reloads the config file on save, so you can edit routes and params and re-curl without restarting it.



Leave this terminal running. Open a new terminal for testing.






Step 5: Test the Token Exchange Flow






5.1 Get a User Token



Authenticate as the test user to get an initial JWT:




SUBJECT_TOKEN="$(curl -s http://localhost:7080/realms/backend-oauth/protocol/openid-connect/token \
-u initial-client:initial-secret \
-d grant_type=password \
-d username=testuser \
-d password=testpass | jq -r .access_token)"

echo "User token obtained: ${SUBJECT_TOKEN:0:50}..."






Actual output:




User token obtained: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6IC...






Decode the user token to inspect its claims:




# Decode the JWT payload (with proper base64 padding)
PAYLOAD=$(echo $SUBJECT_TOKEN | cut -d'.' -f2)
MOD=$((${#PAYLOAD} % 4))
if [ $MOD -eq 2 ]; then PAYLOAD="${PAYLOAD}=="; elif [ $MOD -eq 3 ]; then PAYLOAD="${PAYLOAD}="; fi
echo $PAYLOAD | base64 -d 2>/dev/null | jq .






Actual output from my run:




{
"exp": 1783938629,
"iat": 1783938329,
"jti": "onrtro:a83f2b10-5d4e-6c1a-9e87-3b2c1d4e5f6a",
"iss": "http://localhost:7080/realms/backend-oauth",
"aud": "requester-client",
"typ": "Bearer",
"azp": "initial-client",
"sid": "8ced65e7-d7ba-4793-896c-ad3df3b09994",
"scope": ""
}






Key observations:





  • aud = "requester-client": the audience is scoped for the gateway, not for any downstream service


  • azp = "initial-client": the authorized party (who requested this token)


  • exp - iat = 300 seconds: token expires in 5 minutes

  • No sub claim: this is a client credentials grant, not user-bound yet






5.2 Call AgentGateway (Token Exchange Happens Here)



Call AgentGateway's /exchange endpoint with the user's token:




curl -s http://localhost:3000/exchange \
-H "authorization: Bearer $SUBJECT_TOKEN"






Actual output (from the echo upstream: it reflects the request headers it received):




ServiceVersion=
ServicePort=8080
Host=localhost
URL=/exchange
Method=GET
Proto=HTTP/1.1
IP=10.89.2.3
RequestHeader=Accept:*/*
RequestHeader=Authorization:Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSl...
RequestHeader=User-Agent:curl/7.76.1
Hostname=ed81d5bb758c






Notice the Authorization header contains a different token than what I sent. The original user token had aud=requester-client, but the upstream received a token with aud=target-client. AgentGateway exchanged it transparently.



The gateway's access log confirms the exchange:




2026-07-13T10:25:29.848Z  info  request
gateway=default/default listener=default route=default/token-exchange
endpoint=localhost:18080 src.addr=[::1]:49202
http.method=GET http.path=/exchange http.status=200
protocol=http duration=17ms






The first request took 17ms: this includes the round-trip to Keycloak's token endpoint for the exchange.






5.3 Verify the Token Transformation



To see exactly what changed, I performed the token exchange directly against Keycloak:




EXCHANGED_TOKEN="$(curl -s http://localhost:7080/realms/backend-oauth/protocol/openid-connect/token \
-u requester-client:requester-secret \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "subject_token=$SUBJECT_TOKEN" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
-d "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \
-d "audience=target-client" | jq -r .access_token)"






Decoded exchanged token (this is what the upstream service sees):




{
"exp": 1783938629,
"iat": 1783938329,
"jti": "ntrtte:5172a3d7-4def-7ec0-d6b1-4ad6adab761c",
"iss": "http://localhost:7080/realms/backend-oauth",
"aud": "target-client",
"sub": "feac01a0-48f7-4139-9b2d-171237c2f5e5",
"typ": "Bearer",
"azp": "requester-client",
"sid": "8ced65e7-d7ba-4793-896c-ad3df3b09994",
"scope": ""
}









Before vs. After Comparison


















































Claim Original Token Exchanged Token Changed?
iss http://localhost:7080/realms/backend-oauth Same No
aud "requester-client" "target-client"
Yes: audience now matches downstream
sub (absent) "feac01a0-48f7-4139-9b2d-171237c2f5e5"
Yes: user identity added
azp "initial-client" "requester-client"
Yes: shows who requested the exchange
sid "0ce5ad6d-..." "8ced65e7-..." Varies per session
jti "onrtro:5447afe4-..." "ntrtte:5172a3d7-..."
Yes: new unique token ID


Key takeaways:





  • Identity preserved: The sub claim links to the same user (feac01a0-... = testuser)


  • Audience changed: aud now matches the target service, not the gateway


  • Requester tracked: azp shows the gateway (requester-client) requested this exchange


  • New token ID: Each exchanged token has a unique jti for audit trails






5.4 Test Token Caching



Call the endpoint multiple times with the same user token:




for i in {1..3}; do
curl -s -w "Request $i: %{time_total}s\n" -o /dev/null \
http://localhost:3000/exchange \
-H "authorization: Bearer $SUBJECT_TOKEN"
done






Actual output (these ran after the first /exchange call already populated the cache):




Request 1: 0.001003s   <- cache hit
Request 2: 0.001595s <- cache hit
Request 3: 0.001129s <- cache hit






The gateway access logs confirm the difference between cache miss and cache hit:




# First request (cache miss: exchange round-trip to Keycloak):
duration=17ms

# Subsequent requests (cache hit: no Keycloak call):
duration=0ms
duration=1ms
duration=0ms






The cache reduces per-request latency from 17ms to under 1ms. The TTL is capped by the subject JWT's exp claim, so exchanged tokens are automatically refreshed when the original token expires.






Step 6: Understand the Security Boundary



Before Token Exchange (Insecure):




User -> [JWT: aud=requester-client] -> Gateway -> [Same JWT] -> Upstream






Problem: Upstream receives a token with the wrong audience, over-privileged scopes, and no indication that a gateway is involved.



After Token Exchange (Secure):




User -> [JWT: aud=requester-client] -> Gateway -> Keycloak exchange -> [JWT: aud=target-client] -> Upstream






Benefits:




  • Upstream receives a token scoped for it (aud=target-client)

  • User identity is preserved (sub claim)

  • Original user token never leaves the gateway

  • Short-lived exchanged tokens reduce risk

  • Auditable: azp shows who performed the exchange






Step 7: Advanced Configuration






Switching to JWT Bearer (RFC 7523)



For IdPs that support JWT bearer (e.g., Keycloak 26.5+ JWT Authorization Grant), change the grant type:




backendAuth:
oauthTokenExchange:
host: localhost:7080
tokenEndpointPath: /realms/backend-oauth/protocol/openid-connect/token
grantType: jwtBearer
clientAuth:
clientId: requester-client
clientSecret: requester-secret
method: clientSecretBasic
audiences:
- target-client






The key difference: the inbound credential is sent as assertion instead of subject_token.






Microsoft Entra On-Behalf-Of (OBO)



Microsoft Entra does not speak RFC 8693. It uses a jwt-bearer variant with a vendor-specific requested_token_use=on_behalf_of parameter:




backendAuth:
oauthTokenExchange:
host: login.microsoftonline.com:443
tokenEndpointPath: /<TENANT_ID>/oauth2/v2.0/token
grantType: jwtBearer
clientAuth:
clientId: <CLIENT_ID>
clientSecret: <CLIENT_SECRET>
method: clientSecretPost
scopes:
- https://graph.microsoft.com/.default
additionalParams:
requested_token_use: '"on_behalf_of"'






As Christian Posta notes: "SSO gets the user into the gateway; OBO gets the user out to Graph."






Custom Subject Token Source



By default, the gateway reads the subject token from the Authorization: Bearer header. You can change this:




subjectToken:
source:
header:
name: x-user-token
tokenType: urn:ietf:params:oauth:token-type:access_token









Output Location



Put the exchanged token in a custom header instead of Authorization:




authorizationLocation:
header:
name: x-upstream-auth









Delegation with Actor Token



For RFC 8693 delegation (the act claim), specify an actor token:




actorToken:
source:
header:
name: x-actor-token
tokenType: urn:ietf:params:oauth:token-type:jwt






The exchanged token will include an act claim showing the agent's identity: auditable proof that "agent A called this API on behalf of user B."






Disabling the Cache



To force every request to hit the token endpoint (useful for debugging):




cache:
inMemory:
maxEntries: 0









Step 8: Troubleshooting






Issue 1: 401 Unauthorized from Keycloak



Symptom: AgentGateway returns 401 when calling /exchange



Check:





  1. Client credentials: Verify requester-client:requester-secret matches Keycloak


  2. Token expired: Re-mint the subject token (they expire in ~5 minutes)




   echo $SUBJECT_TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .exp
date +%s # compare timestamps








  1. Token exchange not enabled: In Keycloak admin (http://localhost:7080/admin, admin/admin), navigate to backend-oauth realm -> Clients -> requester-client -> Capability config, and verify "Standard Token Exchange" is enabled






Issue 2: Upstream Receives Wrong Token



Symptom: Upstream echo shows the original token, not the exchanged one



Check: Verify the backendAuth block is under backends[].policies, not under routes[].policies:




# CORRECT: policy is per-backend
backends:
- host: localhost:18080
policies:
backendAuth:
oauthTokenExchange: ...

# WRONG: this level won't trigger token exchange
policies:
backendAuth: ...
backends:
- host: localhost:18080









Issue 3: Keycloak Container Fails to Start on SELinux Systems



Symptom: Container exits with "Failed to run import"



Fix: On SELinux-enabled systems (RHEL, Fedora), add the :Z suffix to the volume mount:




-v ./backend-oauth-realm.json:/opt/keycloak/data/import/backend-oauth-realm.json:ro,Z






Without :Z, SELinux blocks the container from reading the mounted file. This is what happened to me on RHEL 9 before I added the label.






Step 9: Cleanup






# Stop AgentGateway (Ctrl+C in the running terminal, or)
pkill -f 'target/release/agentgateway'

# Stop and remove containers (Podman)
podman rm -f backend-oauth-keycloak backend-oauth-upstream
podman network rm agw-token-exchange

# Or with Docker Compose:
# docker compose -f examples/traffic-token-exchange/oauth-rfc8693/docker-compose.yaml down









Conclusion



I 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:





  • Why token exchange matters: Decouples user identity from service-specific credentials, keeping secrets out of agents


  • How RFC 8693 works: Exchange one token for another with different audience/scopes


  • AgentGateway's built-in policy: backendAuth.oauthTokenExchange handles the entire flow in ~10 lines of YAML


  • Three grant types: RFC 8693 (default), RFC 7523 (jwt-bearer), and Microsoft Entra OBO


  • Real token analysis: Before/after JWT comparison showing exactly what changes


  • Production patterns: See the Production Considerations appendix below



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






Next Steps





  1. Try the extAuthz + CEL approach: See examples/traffic-token-exchange/extauthz/ for the hand-written version (useful when you need custom logic)


  2. Try JWT bearer: See examples/traffic-token-exchange/jwt-authz-grant/ for RFC 7523


  3. Explore MCP authentication: See examples/mcp-authentication/ for agent-to-agent auth


  4. Read the blog post: Agentgateway adds token exchange, jwt-assertion, and Entra OBO


  5. Read the RFCs:








Resources








Production Considerations



The tutorial above runs everything locally. When moving to production, keep these in mind:



Use environment variables for secrets. Never hardcode client secrets in YAML committed to version control:




clientAuth:
clientId: requester-client
clientSecret: $OAUTH_CLIENT_SECRET
method: clientSecretBasic






Kubernetes deployment. On Kubernetes, apply token exchange via AgentgatewayPolicy targeting a Service:




apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
name: okta-token-exchange
spec:
targetRefs:
- group: ""
kind: Service
name: my-backend
backend:
auth:
oauthTokenExchange:
tokenEndpoint:
group: agentgateway.dev
kind: AgentgatewayBackend
name: okta-token-endpoint
port: 443
path: /oauth2/default/v1/token






Monitor exchange latency. Token exchanges add latency on cache miss. Track:




  • Cache hit rate (should be >80%)

  • Exchange latency (should be <100ms)

  • 401/403 errors from IdP (may indicate expired tokens or misconfigured clients)



Token lifetime tuning. Balance security vs. cache efficiency:





  • Short-lived tokens (1-5 minutes): More secure, more IdP calls


  • Longer-lived tokens (15-60 minutes): Fewer IdP calls, higher cache hit rate

  • AgentGateway caps cache TTL by the subject JWT's exp: a safety net against stale tokens



Multi-backend routing. Different backends can have different audiences. Define separate backendAuth policies per backend:




routes:
- name: service-a
matches:
- path: { pathPrefix: /service-a }
backends:
- host: service-a.local
policies:
backendAuth:
oauthTokenExchange:
audiences: [service-a]

- name: service-b
matches:
- path: { pathPrefix: /service-b }
backends:
- host: service-b.local
policies:
backendAuth:
oauthTokenExchange:
audiences: [service-b]






Each backend receives a token scoped specifically for it.






About the Author: Marco Gonzalez (@mgonzalezo) is an AAIF Ambassador focusing on AgentGateway, MCP, Goose, and AGENTS.md. He contributes tutorials, blog posts, and PRs to the AAIF ecosystem.



License: This tutorial is licensed under CC-BY-4.0. Code examples are MIT licensed.



Happy Learning! 🚀

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Implementing RFC 8693 Token Exchange in AgentGateway: A Complete Tutorial

Thematisch verwandte Begriffe: Implementing, 8693, Token, Exchange · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-79918 | MaxKB is an open-source AI assistant for enterprise. Prior to version 2.…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick