Agent Token Exchange
In this demo, we’ll explore how agent identity works when user consent is required and needs to propagate across agents/resources. This builds on the authorization with user consent flow but adds one more piece: an agent acting on behalf of another. When an agent needs to act on behalf of a user, even across service hops, the authorization server (Keycloak) enables token exchange.
Watch the demo
Token Exchange: Supply Chain Agent Calls Market Analysis Agent
When the supply-chain-agent (SCA) receives the request from backend, it needs to call the market-analysis-agent (MAA) to get market data. But SCA is now acting as both a resource (receiving the backend’s request) and an agent (making its own request to MAA). To do this, it must be authorized to act on behalf of the user so it will need to request a token exchange:
Here’s the complete flow:
sequenceDiagram
participant BE as Backend
participant SCA as "Supply-Chain Agent"
participant MAA as "Market-Analysis Agent"
participant KC as Keycloak
BE->>SCA: Request with auth_token sub user agent backend
SCA->>MAA: Initial request jwks_uri scheme
MAA-->>SCA: 401 AAuth challenge plus resource token
Note over SCA: Exchange upstream token at auth server
SCA->>KC: Token exchange upstream auth_token plus resource_token
KC-->>SCA: New auth_token aud MAA agent SCA
SCA->>MAA: Retry with exchanged token
MAA-->>SCA: 200 OK plus market data
SCA-->>BE: 200 OK plus optimization result
Step 1: MAA issues challenge
When SCA first calls MAA, it receives 401 with an AAuth: require=auth-token; resource-token="..."; auth-server="..." response (same challenge shape as in the autonomous authorization and resource authorization flows). The body indicates authorization is required until a suitable auth token is presented.
INFO:agent_executor:🔐 Authorization required: scheme=jwt needed but received jwks_uri
INFO:resource_token_service:✅ Resource token generated successfully
INFO:agent_executor:🔐 Issuing resource_token for agent: http://supply-chain-agent.localhost:3000
The resource token identifies SCA as the requesting agent:
{
"iss": "http://market-analysis-agent.localhost:3000",
"aud": "http://localhost:8080/realms/aauth-test",
"agent": "http://supply-chain-agent.localhost:3000",
"agent_jkt": "9aOuAvaRr0YVHxiZqIpJvDf9hjg2uvKw1FVVMzDiOwg",
"exp": 1770659003,
"scope": "market-analysis:analyze"
}
Step 2: Token exchange request
SCA now performs a token exchange. On the signed POST to the token endpoint it sends (see Token exchange):
upstream_token: The auth token SCA received frombackend(proves upstream authorization and user context the auth server already issued)resource_token: The token from MAA (proves Resource 2 challenged SCA for access)
From the logs in SCA:
INFO:agent_executor:🔐 Exchanging upstream auth_token for MAA token
INFO:aauth.tokens:🔐 Token exchange: upstream_auth_token=eyJhbGci..., resource_token=eyJhbGci...
The token exchange request uses the JWT scheme in Signature-Key, and SCA signs the HTTP request with its own key. The Auth server reviews the JWT from the Signature-Key header and sees that the token was issued with an aud of supply-chain-agent. The Auth server, knowing this is a token exchange request, will use the aud claim to retrieve the JWKS of the supply-chain-agent.
INFO:aauth_token_service:🔐 Signing with JWT scheme for token exchange
INFO:aauth.signing:🔐 Line 3: '"signature-key": sig1=(scheme=jwt jwt="eyJhbGci...")'
Step 3: Keycloak issues exchanged token
Keycloak validates, in line with call-chaining token exchange:
- The HTTP message signature on the exchange request
- The
upstream_token(auth token for the upstream audience, issued by this auth server) - The
resource_tokenfrom MAA (includingagent/agent_jktbinding to SCA) - Policy: whether SCA is allowed to receive an auth token for MAA with the requested scopes
It then issues a new auth token for the downstream hop. That token’s claims describe the immediate caller and the resource audience—not a nested “actor” object. As in flow-05-token-ex, the exchanged JWT does not carry a legacy act claim; provenance of the chain is established when the auth server validates upstream_token and resource_token together.
Keycloak issues a new auth_token shaped like this (illustrative):
{
"iss": "http://localhost:8080/realms/aauth-test",
"aud": "http://market-analysis-agent.localhost:3000",
"jti": "abdceb92-5458-4fbc-9403-7c0d8255526d",
"sub": "00b519e8-f409-4201-8911-1cb408e8a082",
"agent": "http://supply-chain-agent.localhost:3000",
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "jqjPR5broSbHfpXZaGpTrBcem4DX6gbWEQsDWEyZMG0",
"kid": "-DN2FPpkqklNWGbl9yYVuH4ONyIgBp36hU4nJJKUARY"
}
},
"iat": 1770659000,
"exp": 1770662600,
"scope": "market-analysis:analyze"
}
What MAA can rely on in the exchanged token
| Claim | Role |
|---|---|
agent |
Verifiable immediate caller — SCA (matches who must sign requests with this token) |
aud |
Intended resource — MAA |
sub |
User identity when the upstream grant was user-delegated (same meaning as elsewhere in auth tokens) |
cnf.jwk |
Proof-of-possession — only SCA’s key can be used with this token |
scope |
What MAA authorized for this hop |
The backend does not appear as a nested field in this JWT. The auth server already used the upstream_token (audience backend → SCA, user in sub, etc.) when deciding to mint this token. Audit and policy at MAA focus on who is calling now (agent), for which user (sub when present), and what is allowed (scope, aud).
Step 4: Supply-chain agent calls market-analysis agent again
SCA retries the request with the exchanged token:
INFO:agent_executor:✅ Token exchange successful, retrying MAA request with exchanged token
INFO:aauth_interceptor:🔐 AAuth: Signing request with JWT scheme (auth_token present)
MAA validates the exchanged token and grants access:
INFO:agent_executor:🔐 JWT scheme detected: verifying auth_token
INFO:agent_executor:✅ Auth token verified successfully
Summary
When an agent needs to call another agent or MCP server after receiving user consent, it typically obtains a new auth token for that audience via token exchange at the auth server: signed POST with resource_token and upstream_token. The new token names the immediate agent and resource aud; it does not embed a nested act claim. The same pattern extends to cross-domain cases (not shown here) where different authorization servers participate in the chain.
Use user-delegated mode when: Agents must act on behalf of a specific user (accessing user data, making decisions with user accountability, compliance requirements).
In the next and final post, we’ll dig into using Agentgateway for policy control.