For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Cross App Access (ID-JAG)
Call a downstream API as the authenticated end user with the OAuth Identity Assertion Authorization Grant.
Attaches to:
About
The crossAppAccess backend authentication method implements the OAuth Identity Assertion Authorization Grant, also called “ID-JAG” or “Cross App Access” (XAA). With this method, agentgateway calls a downstream API as the authenticated end user, without requiring the user to interactively log in to that downstream app. This pattern is common in agentic scenarios where an agent calls other apps’ APIs on behalf of the user, such as per-user access to MCP servers (MCP enterprise-managed authorization).
The gateway acts as a confidential OAuth client and performs a two-leg exchange on each backend call:
- Authenticate the user. The inbound request carries the user’s OIDC ID token, validated by the
jwtAuthpolicy. The validated token is the subject of the exchange. ThejwtAuthpolicy must validate an OIDC ID token, not an arbitrary access token, because the identity provider expects an ID token as the subject. - Exchange the token. The gateway performs two sequential exchanges:
- RFC 8693 token exchange. The gateway calls the user’s identity provider (IdP) authorization server with an RFC 8693 token exchange and receives an ID-JAG assertion that is bound to the resource authorization server.
- RFC 7523 JWT-bearer grant. The gateway presents the ID-JAG to the resource’s authorization server with an RFC 7523 JWT-bearer grant and receives a Bearer access token that is scoped to the downstream API.
- Attach and cache. The Bearer token is added as
Authorization: Bearer <token>to the upstream request and cached until shortly before it expires.
flowchart LR
Client -- ID token --> AGW[Agentgateway]
AGW -- "(a) token exchange" --> IdP[Identity provider]
IdP -- ID-JAG --> AGW
AGW -- "(b) jwt-bearer grant" --> RAS[Resource authorization server]
RAS -- access token --> AGW
AGW -- "Authorization: Bearer<br>access token" --> API[Downstream API]
Cross App Access differs from OAuth token exchange in that it crosses a trust boundary: the IdP and the resource’s authorization server are separate parties, so the gateway performs two exchanges and holds two client registrations, one at each token endpoint. For a single-leg exchange at one authorization server, use oauthTokenExchange instead.
Before you begin
The following walkthrough runs a complete Cross App Access flow locally, with a Keycloak container that acts as both the IdP and the resource authorization server.
Make sure that you have the following tools installed.
- agentgateway binary
- Docker
python3andcurl
Clone the agentgateway repository and navigate to the example setup scripts.
git clone https://github.com/agentgateway/agentgateway.git cd agentgateway/examples/traffic-cross-app-access/keycloak
Local example
In this example, you deploy Keycloak to act as the IdP that authenticates the user as well as the authorization server for the token exchange.
Set up Keycloak as the IdP
Run and configure a local Keycloak that supports ID-JAG issuance. The demo uses a Keycloak build with the experimental identity-assertion-jwt feature, packaged as the ceposta/keycloak:id-jag container image.
Start Keycloak on port
8480. The same port is mapped inside and outside the container so that the token issuer URL is consistent everywhere, which the self-referential demo setup requires.docker run --name kc-idjag --rm -d -p 8480:8480 \ -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \ ceposta/keycloak:id-jag start-dev --http-port=8480Wait for Keycloak to be ready. Startup can take a few minutes, such as when the image platform does not match your machine and runs emulated.
until [ "$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8480/realms/master)" = "200" ]; do sleep 5; done; echo "Keycloak ready"Configure Keycloak with the demo realm. The script creates the
idjag-demorealm, thealice/aliceuser, anagent-client(the gateway’s requesting client, which mints the ID-JAG through token exchange), and aresource-client(the resource authorization server client, which consumes the ID-JAG through the JWT-bearer grant).KCADM="$PWD/docker/kcadm.sh" ./configure-keycloak.shExample output:
Keycloak configured: realm : idjag-demo user : alice / alice agent-client : agent-secret (requesting app; mints ID-JAG via token exchange) resource-client: resource-secret (resource AS; consumes ID-JAG via jwt-bearer) IdP : self-idjag (self-referential JWT_AUTHORIZATION_GRANT)Allow HTTP access to the realm. When Keycloak runs in Docker, requests that arrive through the port mapping can be treated as external, and Keycloak then rejects them with an
HTTPS requirederror../docker/kcadm.sh update realms/idjag-demo -s sslRequired=NONEStart the echo backend, which acts as the downstream API and reflects the request headers that it receives, so that you can inspect the token the gateway forwards.
python3 echo-backend.py &
Configure the gateway
Run agentgateway with the Cross App Access policy.
Review the gateway configuration.
jwtAuthvalidates the inbound OIDC ID token.crossAppAccessconfigures the two token endpoints:identityProviderauthenticates asagent-client.resourceAuthorizationServerauthenticates asresource-client.
audiencemust equal the resource identifier that the resource client is registered with.
# # agentgateway Cross App Access (ID-JAG) policy pointed at the local Keycloak (both legs). # # inbound: Authorization: Bearer <alice's Keycloak ID token> (validated by jwtAuth) # leg 1 : token exchange at Keycloak (agent-client) -> ID-JAG # leg 2 : jwt-bearer at Keycloak (resource-client) -> Bearer access token # upstream: GET localhost:9000 with Authorization: Bearer <access token> # # The client secrets come from env vars (expanded at load time), matching what the setup # scripts create. Run Keycloak (configured) and the echo backend first, then: # export KC_AGENT_SECRET=agent-secret KC_RESOURCE_SECRET=resource-secret # cargo run --release --bin agentgateway -- -f ./gateway.yaml binds: - port: 3030 listeners: - protocol: HTTP routes: - policies: # Validate alice's Keycloak OIDC ID token. This validated token becomes the # subject of the exchange below. jwtAuth: issuer: http://localhost:8480/realms/idjag-demo audiences: - agent-client jwks: url: http://localhost:8480/realms/idjag-demo/protocol/openid-connect/certs backendAuth: crossAppAccess: # Leg 1 — the user's IdP token endpoint, authenticated as the requesting client. identityProvider: host: localhost:8480 path: /realms/idjag-demo/protocol/openid-connect/token clientAuth: clientId: agent-client method: clientSecretBasic clientSecret: $KC_AGENT_SECRET # Leg 2 — the resource authorization server, authenticated as the resource client. resourceAuthorizationServer: host: localhost:8480 path: /realms/idjag-demo/protocol/openid-connect/token clientAuth: clientId: resource-client method: clientSecretBasic clientSecret: $KC_RESOURCE_SECRET # Must equal the resource client's configured resource-server identifier; # becomes the ID-JAG audience. audience: https://resource.idjag.demo scopes: - todos.read backends: # The downstream API the agent calls on alice's behalf (local echo server). - host: localhost:9000Export the client secrets that the configuration reads as environment variables, and start the gateway. The gateway fetches the realm’s JWKS at startup, so configure Keycloak before you start the gateway.
export KC_AGENT_SECRET=agent-secret KC_RESOURCE_SECRET=resource-secret agentgateway -f ./gateway.yaml
Verify the exchange
Send a request through the gateway as the demo user and confirm that the downstream API receives a different, backend-scoped access token.
In another terminal, mint alice’s OIDC ID token from Keycloak. This token is the inbound credential and the subject of the exchange.
ID_TOKEN=$(curl -s -X POST "http://localhost:8480/realms/idjag-demo/protocol/openid-connect/token" \ -d grant_type=password -d client_id=agent-client -d client_secret=agent-secret \ -d username=alice -d password=alice -d scope=openid \ | python3 -c 'import sys,json;print(json.load(sys.stdin)["id_token"])') echo "${ID_TOKEN:0:40}..."Send a request to the gateway with the ID token. The gateway validates the token, performs the two-leg exchange at Keycloak, and forwards the request to the echo backend with the resulting access token. The command decodes the token that the backend received.
curl -s http://localhost:3030/todos -H "Authorization: Bearer $ID_TOKEN" | python3 -c ' import sys,json,base64 d=json.load(sys.stdin) tok=(d["headers"].get("authorization") or d["headers"].get("Authorization")).split()[1] p=json.loads(base64.urlsafe_b64decode(tok.split(".")[1]+"==")) print("backend received a", p["typ"], "token for", p["preferred_username"], "| azp:", p["azp"], "| scope:", p["scope"])'In the example output, the backend received a Bearer access token for
alicethat is authorized for theresource-clientparty with thetodos.readscope. The inbound ID token was stripped and never reached the backend.backend received a Bearer token for alice | azp: resource-client | scope: email todos.read profileConfirm that the gateway rejects requests without a valid ID token before any exchange happens.
curl -s -o /dev/null -w "no token -> %{http_code}\n" http://localhost:3030/todos curl -s -o /dev/null -w "bad token -> %{http_code}\n" http://localhost:3030/todos -H "Authorization: Bearer not.a.jwt"Example output:
no token -> 400 bad token -> 401Clean up the demo when you are done. The
lsofcommand stops the echo backend that runs in the background on port9000and the gateway on port3030.docker rm -f kc-idjag lsof -ti tcp:9000,3030 | xargs kill
Try it against hosted providers
The traffic-cross-app-access examples in the agentgateway repository include two more demos of the same policy against real, separate trust domains.
xaa.dev
The xaa-dev example uses the public xaa.dev playground, with the hosted IdenX IdP issuing the ID-JAG and a hosted resource authorization server protecting a Todo API. Registration is free, and the interactive login mints the inbound ID token. Note that the exchange endpoints use the https:// host form, while the route backend uses host:port with an explicit backendTLS policy.
Okta and Auth0
The okta-auth0 example uses an enterprise topology with Okta as the IdP (Cross App Access early access) and Auth0 as the resource authorization server (XAA beta).
Configuration reference
The following table describes the most common crossAppAccess fields. For the full set of fields, see the configuration reference.
| Field | Description |
|---|---|
identityProvider | The user’s IdP authorization server token endpoint. Agentgateway sends the authenticated ID token as the RFC 8693 subject_token. Set the endpoint as a host in host:port form, or in https://host form to automatically enable TLS, and set the token endpoint path, which defaults to /. Optional connection policies such as backendTLS can be attached. |
resourceAuthorizationServer | The resource authorization server token endpoint, in the same shape as identityProvider. This leg uses the RFC 7523 JWT-bearer grant, with the ID-JAG from the IdP leg sent as the assertion. This is a separate client registration from the IdP one. |
clientAuth | Client authentication for each token endpoint. Supported methods are clientSecretBasic, clientSecretPost, and privateKeyJwt. |
audience | Required identifier of the resource authorization server. The issued ID-JAG is bound to this value. |
resources | Optional protected resource or API identifiers (RFC 8707), sent on the token exchange leg. Configure these explicitly when the authorization server expects them. |
scopes | Optional scopes to request. The authorization server might grant a subset. |
cache | Optional token cache configuration. Defaults to an in-memory cache with 8192 entries. Set cache.defaultTtl as a fallback for when the token response omits expires_in (defaults to 300s), and cache.maxEntries: 0 to disable caching. The cache duration is capped by the subject token’s JWT exp claim when present. |
Private key JWT client authentication
Enterprise IdPs such as Okta commonly require the privateKeyJwt client authentication method, in which the gateway authenticates with a signed JWT assertion instead of a client secret. Because token endpoints are configured as backend references rather than raw URLs, privateKeyJwt requires an explicit assertionAudience.
clientAuth:
clientId: gateway-at-chat
method: privateKeyJwt
signingKey:
file: /path/to/signing-key.pem
alg: RS256 # one of RS256/RS384/RS512/ES256/ES384
kid: my-signing-key # optional `kid` header
assertionAudience: https://chat.example.com/oauth2/tokenLimitations
The following parts of the Identity Assertion Authorization Grant draft are not yet supported:
- DPoP sender-constrained tokens (RFC 9449).
.well-knownendpoint discovery (RFC 8414, endpoints must be configured explicitly).- SAML or refresh-token subject types (only OIDC ID tokens are used as the subject).
Next steps
- Exchange the incoming credential for a per-backend token at a single authorization server with OAuth token exchange.
- Validate incoming JWTs with the JWT authentication policy.