Skip to content
agentgateway has joined the Agentic AI FoundationLearn more

For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.

Page as Markdown

Cross App Access (ID-JAG)

Verified Code examples on this page have been automatically tested and verified.

Call a downstream API as the authenticated end user with the OAuth Identity Assertion Authorization Grant.

Set up Cross App Access (ID-JAG) with an AgentgatewayPolicy.

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:

  1. Authenticate the user. The inbound request carries the user’s OIDC ID token, validated by a route-level JWT authentication policy. The validated token is the subject of the exchange. The JWT policy must validate an OIDC ID token, not an arbitrary access token, because the identity provider expects an ID token as the subject.
  2. Exchange the token. The gateway performs two sequential exchanges:
    1. 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.
    2. 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.
  3. 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.

Note

To keep the demo self-contained, a single Keycloak instance acts as both parties: the user’s IdP and the resource authorization server. In production, these parties are typically separate trust domains. For examples against separate providers, review the traffic-cross-app-access examples in the upstream agentgateway repository. That example uses xaa-dev (hosted IdenX IdP + hosted resource authorization server) and okta-auth0 (Okta IdP + Auth0 resource authorization server). These IdPs illustrate the two-party topology, but note that the configs are written for standalone mode, not Kubernetes.

Before you begin

  1. Follow the Get started guide to install agentgateway.

  2. Follow the Sample app guide to create a gateway proxy with an HTTP listener and deploy the httpbin sample app.

  3. Get the external address of the gateway and save it in an environment variable.

    Kind cluster? Kind does not support LoadBalancer services by default. To use this option with a Kind cluster, install and run cloud-provider-kind.
    export INGRESS_GW_ADDRESS=$(kubectl get svc -n agentgateway-system agentgateway-proxy -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
    echo $INGRESS_GW_ADDRESS  

Deploy Keycloak

Deploy a Keycloak authorization server into your cluster to act as both the user’s identity provider and the resource authorization server. This walkthrough uses a single Keycloak that performs both legs of the exchange.

Issuing and consuming an ID-JAG requires the experimental identity-assertion-jwt Keycloak feature, which is not in a stock Keycloak release. The demo uses the ceposta/keycloak:id-jag image, which bakes in the feature.

  1. Deploy Keycloak and its Service into the httpbin namespace. The KC_HOSTNAME variable pins the token issuer to the in-cluster DNS name, so that the ID token you mint through a port-forward and the gateway’s in-cluster exchange calls agree on the issuer (iss). The ID-JAG’s issuer must be stable, because the resource authorization server validates it against the same issuer.

    kubectl apply -f- <<EOF
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: keycloak
      namespace: httpbin
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: keycloak
      template:
        metadata:
          labels:
            app: keycloak
        spec:
          containers:
          - name: keycloak
            image: ceposta/keycloak:id-jag
            args: ["start-dev", "--http-port=8080"]
            env:
            - name: KC_BOOTSTRAP_ADMIN_USERNAME
              value: admin
            - name: KC_BOOTSTRAP_ADMIN_PASSWORD
              value: admin
            - name: KC_HOSTNAME
              value: "http://keycloak.httpbin.svc.cluster.local:8080"
            - name: KC_HOSTNAME_STRICT
              value: "false"
            - name: KC_HOSTNAME_BACKCHANNEL_DYNAMIC
              value: "false"
            ports:
            - containerPort: 8080
            # Report ready only once Keycloak is actually serving, so that later
            # steps (realm configuration, JWKS fetch) do not race its startup.
            readinessProbe:
              httpGet:
                path: /realms/master
                port: 8080
              initialDelaySeconds: 15
              periodSeconds: 5
              failureThreshold: 60
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: keycloak
      namespace: httpbin
    spec:
      selector:
        app: keycloak
      ports:
      - name: http
        port: 8080
        targetPort: 8080
    EOF
  2. Wait for Keycloak to be ready. The readiness probe reports the pod ready only after Keycloak serves requests, so this command returns when Keycloak is available to configure. Startup can take a few minutes.

    kubectl rollout status deployment/keycloak -n httpbin --timeout=300s
  3. Configure the idjag-demo realm with a script from the agentgateway repository. Clone the repository and go to the example setup scripts.

    git clone https://github.com/agentgateway/agentgateway.git
    cd agentgateway/examples/traffic-cross-app-access/keycloak
  4. Run the configuration script against the in-cluster Keycloak. The KCADM variable runs the Keycloak admin CLI inside the running pod, and SERVER points at the in-cluster hostname so that the realm’s self-referential identity provider uses the same issuer as KC_HOSTNAME. The script creates the idjag-demo realm, the alice/alice user, an agent-client (the gateway’s requesting client, which mints the ID-JAG through token exchange), and a resource-client (the resource authorization server client, which consumes the ID-JAG through the JWT-bearer grant).

    POD=$(kubectl get pod -n httpbin -l app=keycloak -o jsonpath='{.items[0].metadata.name}')
    KCADM="kubectl exec -i -n httpbin $POD -- /opt/keycloak/bin/kcadm.sh" \
    SERVER="http://keycloak.httpbin.svc.cluster.local:8080" \
    KC_AGENT_SECRET=agent-secret KC_RESOURCE_SECRET=resource-secret \
      ./configure-keycloak.sh

    Example 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)

Configure Cross App Access

Configure agentgateway to validate the inbound ID token and perform the two-leg exchange.

  1. Create an AgentgatewayBackend for the token endpoint, pointing at the in-cluster Keycloak Service. Both legs of the exchange use the same Keycloak, so they share this AgentgatewayBackend.

    kubectl apply -f- <<EOF
    apiVersion: agentgateway.dev/v1alpha1
    kind: AgentgatewayBackend
    metadata:
      name: keycloak-token-endpoint
      namespace: httpbin
    spec:
      static:
        host: keycloak.httpbin.svc.cluster.local
        port: 8080
    EOF
  2. Create Kubernetes Secrets with the two OAuth client secrets, one for each leg. These match the client secrets that the configuration script created.

    kubectl apply -f- <<EOF
    apiVersion: v1
    kind: Secret
    metadata:
      name: agent-oauth-client
      namespace: httpbin
    type: Opaque
    stringData:
      clientSecret: agent-secret
    ---
    apiVersion: v1
    kind: Secret
    metadata:
      name: resource-oauth-client
      namespace: httpbin
    type: Opaque
    stringData:
      clientSecret: resource-secret
    EOF
  3. Create a route-level AgentgatewayPolicy that validates the inbound OIDC ID token. The audiences value matches the agent-client that mints the user’s ID token, and the validated token becomes the subject of the exchange.

    kubectl apply -f- <<EOF
    apiVersion: agentgateway.dev/v1alpha1
    kind: AgentgatewayPolicy
    metadata:
      name: jwt-idtoken
      namespace: httpbin
    spec:
      targetRefs:
      - group: gateway.networking.k8s.io
        kind: HTTPRoute
        name: httpbin
      traffic:
        jwtAuthentication:
          mode: Strict
          providers:
          - issuer: "http://keycloak.httpbin.svc.cluster.local:8080/realms/idjag-demo"
            audiences:
            - agent-client
            jwks:
              remote:
                jwksPath: "/realms/idjag-demo/protocol/openid-connect/certs"
                cacheDuration: "5m"
                backendRef:
                  group: ""
                  kind: Service
                  name: keycloak
                  namespace: httpbin
                  port: 8080
    EOF
  4. Create a backend-level AgentgatewayPolicy that attaches the crossAppAccess method to the httpbin Service. The identityProvider leg authenticates as agent-client to mint the ID-JAG, and the resourceAuthorizationServer leg authenticates as resource-client to exchange the ID-JAG for a backend access token. The audience must equal the resource identifier that resource-client is registered with.

    kubectl apply -f- <<EOF
    apiVersion: agentgateway.dev/v1alpha1
    kind: AgentgatewayPolicy
    metadata:
      name: backend-cross-app-access
      namespace: httpbin
    spec:
      targetRefs:
      - group: ""
        kind: Service
        name: httpbin
      backend:
        auth:
          crossAppAccess:
            identityProvider:
              backendRef:
                group: agentgateway.dev
                kind: AgentgatewayBackend
                name: keycloak-token-endpoint
              path: /realms/idjag-demo/protocol/openid-connect/token
              clientAuth:
                clientId: agent-client
                method: ClientSecretBasic
                secretRef:
                  name: agent-oauth-client
            resourceAuthorizationServer:
              backendRef:
                group: agentgateway.dev
                kind: AgentgatewayBackend
                name: keycloak-token-endpoint
              path: /realms/idjag-demo/protocol/openid-connect/token
              clientAuth:
                clientId: resource-client
                method: ClientSecretBasic
                secretRef:
                  name: resource-oauth-client
            audience: https://resource.idjag.demo
            scopes:
            - todos.read
    EOF

    Review the following table to understand this configuration. For more information, see the API docs.

    FieldDescription
    identityProviderThe user’s IdP authorization server token endpoint, referenced as an AgentgatewayBackend through backendRef, with the token endpoint path. Agentgateway sends the validated ID token as the RFC 8693 subject_token on this leg.
    resourceAuthorizationServerThe 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.
    clientAuthClient authentication for each token endpoint. method is ClientSecretBasic (default), ClientSecretPost, or PrivateKeyJwt. Use secretRef to read the client secret from a Kubernetes Secret.
    audienceRequired identifier of the resource authorization server. The issued ID-JAG is bound to this value.
    resourcesOptional protected resource or API identifiers (RFC 8707), sent on the token exchange leg. Configure these explicitly when the authorization server expects them.
    scopesOptional scopes to request. The authorization server might grant a subset.
    cacheOptional 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.

Verify the exchange

Mint alice’s ID token, send a request through agentgateway with it, and verify that the downstream API receives a different, backend-scoped access token.

  1. Port-forward the Keycloak Service so that you can reach its token endpoint locally.

    kubectl port-forward -n httpbin svc/keycloak 8080:8080
  2. In another terminal, mint alice’s OIDC ID token from Keycloak as agent-client. This token is the inbound credential and the subject of the exchange. Tokens expire, so re-mint if you come back later.

    export ID_TOKEN=$(curl -s -X POST "http://localhost:8080/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 | jq -r .id_token)
    echo "${ID_TOKEN:0:40}..."
  3. Send a request to the httpbin /headers endpoint through the gateway, with the ID token. The gateway validates the token, performs the two-leg exchange at Keycloak, and forwards the request to httpbin with the resulting access token. Because httpbin reflects the request headers, the command decodes the token that the backend received.

    curl -s "http://$INGRESS_GW_ADDRESS:80/headers" -H "host: www.example.com" \
      -H "Authorization: Bearer $ID_TOKEN" | python3 -c '
    import sys,json,base64
    h=json.load(sys.stdin)["headers"]
    tok=(h.get("Authorization") or h.get("authorization"))
    tok=(tok[0] if isinstance(tok,list) else tok).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 alice that is authorized for the resource-client party (azp) with the todos.read scope. 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 profile
  4. Confirm 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://$INGRESS_GW_ADDRESS:80/headers" -H "host: www.example.com"
    curl -s -o /dev/null -w "bad token  -> %{http_code}\n" "http://$INGRESS_GW_ADDRESS:80/headers" -H "host: www.example.com" -H "Authorization: Bearer not.a.jwt"

    Example output:

    no token   -> 401
    bad token  -> 401

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. Store the PEM-encoded signing key in a Kubernetes Secret and reference it with signingKeyRef. Because token endpoints are configured as backend references rather than raw URLs, PrivateKeyJwt requires an explicit assertionAudience.

clientAuth:
  clientId: gateway-at-chat
  method: PrivateKeyJwt
  privateKeyJwt:
    signingKeyRef:
      name: gateway-signing-key
    alg: RS256  # one of RS256/RS384/RS512/ES256/ES384
    kid: my-signing-key  # optional `kid` header
    assertionAudience: https://chat.example.com/oauth2/token

Limitations

The following parts of the Identity Assertion Authorization Grant draft are not yet supported:

  • DPoP sender-constrained tokens (RFC 9449).
  • .well-known endpoint discovery (RFC 8414, endpoints must be configured explicitly).
  • SAML or refresh-token subject types (only OIDC ID tokens are used as the subject).

Cleanup

You can remove the resources that you created in this guide.
kubectl delete AgentgatewayPolicy backend-cross-app-access jwt-idtoken -n httpbin
kubectl delete AgentgatewayBackend keycloak-token-endpoint -n httpbin
kubectl delete secret agent-oauth-client resource-oauth-client -n httpbin
kubectl delete deployment keycloak -n httpbin
kubectl delete service keycloak -n httpbin

Next steps

Was this page helpful?
Agentgateway assistant

Ask me anything about agentgateway configuration, features, or usage.

Note: AI-generated content might contain errors; please verify and test all returned information.

Tip: one topic per conversation gives the best results. Use the + button in the chat header to start a new conversation.

Switching topics? Starting a new conversation improves accuracy.
↑↓ navigate select esc dismiss

What could be improved?

Your feedback helps us improve assistant answers and identify docs gaps we should fix.

Need more help? Join us on Discord: https://discord.gg/y9efgEmppm

Want to use your own agent? Add the Solo MCP server to query our docs directly. Get started here: https://search.solo.io/.