{"slug": "agentgateway-adds-token-exchange-jwt-assertion-and-entra-obo", "title": "Agentgateway adds token exchange, JWT-assertion, and Entra OBO", "summary": "Agentgateway has open-sourced three new token exchange capabilities—RFC 8693 token exchange, RFC 7523 JWT assertion, and Microsoft Entra on-behalf-of—enabling AI agents to call downstream APIs as the user without custom code. The features are part of the upcoming agentgateway release under the backendAuth.oauthTokenExchange configuration block.", "body_md": "# Agentgateway adds token exchange, jwt-assertion, and Entra OBO\n\nNative backend auth for RFC 8693 token exchange, RFC 7523 JWT bearer (jwt-assertion), and Microsoft Entra on-behalf-of — so agents can call downstream APIs as the user without hand-rolled ext_proc or custom agent code\n\n[Back to blog](/blog/)\n\nShielding AI agents from sensitive MCP / API credentials (secrets, API keys, tokens, etc) is the prevailing best practice pattern to keep sensitive secrets from leaking into AI model conversations or across AI agent boundaries. OpenClaw, for example, has direct file system access in its most general deployment and can read files/secrets/env variables, etc and could easily send these secrets in any requests.\n\nThe right way in an enterprise environment: user identity + agent identity tied to an authorization grant, which determines what is allowed to be done. Any calls to MCP servers or APIs have their credentials injected transparently by the infrastructure. The cornerstone of this injection is exchanging an agent or user’s identity for the correct secrets/API keys/tokens.\n\nIn the upcoming agentgateway release, we have opensourced two new exchange capabilities, including a bonus third exchange opportunity with Microsoft Entra — all under `backendAuth.oauthTokenExchange`\n\n:\n\n| Grant | Spec | Subject sent as | Typical IdP |\n|---|---|---|---|\n| Token exchange |\n|\n\n`subject_token`\n\n[RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523)`assertion`\n\n`assertion`\n\nEntra is the important enterprise wrinkle: **it does not speak RFC 8693**. Its OBO flow is jwt-bearer with `requested_token_use=on_behalf_of`\n\n. Same outcome (user-scoped downstream token), different grant shape — so both grants live under one `oauthTokenExchange`\n\nblock.\n\n## RFC 8693 token exchange\n\nDefault grant. The gateway POSTs the inbound bearer as `subject_token`\n\nto your IdP’s token endpoint and attaches the returned access token upstream.\n\n```\nbackendAuth:\n  oauthTokenExchange:\n    host: idp.example.com:443\n    tokenEndpointPath: /oauth2/default/v1/token\n    clientAuth:\n      clientId: gateway-client\n      clientSecret: $OAUTH_CLIENT_SECRET\n      method: clientSecretBasic\n    audiences:\n    - upstream-api\n    scopes:\n    - read\n```\n\nWhat the gateway sends (conceptually):\n\n```\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange\nsubject_token=<inbound bearer>\nsubject_token_type=urn:ietf:params:oauth:token-type:access_token\naudience=upstream-api\nscope=read\n```\n\nOptional knobs that matter in agentic setups:\n\n— RFC 8693 delegation (`actorToken`\n\n`act`\n\nclaim). Useful when an agent identity is acting*for*a user.—`resources`\n\n[RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707)resource indicators when the AS expects them (common for MCP / API audiences).— read the subject from a non-default header, cookie, or CEL expression instead of`subjectToken.source`\n\n`Authorization`\n\n.— in-memory cache keyed by subject + grant params; TTL capped by the subject JWT`cache`\n\n`exp`\n\n.\n\nRunnable example: [ examples/traffic-token-exchange/oauth-rfc8693](https://github.com/agentgateway/agentgateway/tree/main/examples/traffic-token-exchange/oauth-rfc8693).\n\n## JWT assertion (RFC 7523 jwt-bearer)\n\nFlip `grantType: jwtBearer`\n\nand the same policy speaks JWT bearer instead of token exchange. The inbound credential becomes the `assertion`\n\n(not `subject_token`\n\n). `requestedTokenType`\n\n/ `actorToken`\n\nare rejected for this grant — they belong to RFC 8693.\n\n```\nbackendAuth:\n  oauthTokenExchange:\n    host: idp.example.com:443\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\nForm shape:\n\n```\ngrant_type=urn:ietf:params:oauth:grant-type:jwt-bearer\nassertion=<inbound token>\naudience=target-client\n```\n\nThis grant works for any authorization servers that support JWT Assertion Grant (RFC 7523). This is the grant Keycloak’s [JWT Authorization Grant](https://www.keycloak.org/docs/latest/server_admin/#_jwt-authorization-grant) (preview in 26.5+) implements across realms — present a JWT from realm A, get a token from realm B that trusts A. Full two-realm walkthrough: [ examples/traffic-token-exchange/jwt-authz-grant](https://github.com/agentgateway/agentgateway/tree/main/examples/traffic-token-exchange/jwt-authz-grant).\n\n## Microsoft Entra OBO\n\nEntra OBO is jwt-bearer with provider-specific extras. The hand-written request looks like:\n\n```\ncurl -X POST \"https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token\" \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"client_id=<CLIENT_ID>\" \\\n  -d \"client_secret=<CLIENT_SECRET>\" \\\n  -d \"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer\" \\\n  -d \"requested_token_use=on_behalf_of\" \\\n  -d \"scope=https://graph.microsoft.com/.default\" \\\n  -d \"assertion=<USER_ACCESS_TOKEN>\"\n```\n\nAgentgateway produces that same request from config:\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   # credentials in the BODY (Entra expects this shape)\n    scopes:\n    - https://graph.microsoft.com/.default\n    additionalParams:\n      requested_token_use: '\"on_behalf_of\"'   # CEL string literal\n```\n\n| Microsoft form field | Produced by |\n|---|---|\n`grant_type=...jwt-bearer` | `grantType: jwtBearer` |\n`assertion=<user token>` | inbound bearer (jwt-bearer sends subject as `assertion` ) |\n`scope=...` | `scopes:` (joined into one space-delimited `scope` ) |\n`client_id` + `client_secret` in body | `clientAuth.method: clientSecretPost` |\n`requested_token_use=on_behalf_of` | `additionalParams` (CEL) |\n\nGotchas worth calling out:\n\n`requested_token_use`\n\nis a vendor extension, so it lives in`additionalParams`\n\n, not a first-class field. Values are**CEL expressions**— a literal string needs the inner quotes:`'\"on_behalf_of\"'`\n\n.- Prefer\n`clientSecretPost`\n\nfor Entra so credentials land in the form body (matching Microsoft’s docs). Default`clientSecretBasic`\n\nwould put them in an`Authorization: Basic`\n\nheader instead. - Inbound JWT validation still belongs on the\n*route*(`jwtAuth`\n\n/ MCP auth). Exchange only runs after the gateway has accepted the user.\n\nThis is the missing piece after [enterprise MCP SSO with Entra](/blog/2026-01-26-enterprise-mcp-sso/): SSO gets the user *into* the gateway; OBO gets the user *out* to Graph (or any Entra-protected API / MCP server) without re-prompting login.\n\n## Kubernetes: same knobs as a policy\n\nOn Kubernetes the same exchange attaches via `AgentgatewayPolicy`\n\non a Service or Backend:\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        clientAuth:\n          clientId: \"<okta-client-id>\"\n          secretRef:\n            name: okta-oauth-client\n        scopes:\n        - \"<scope>\"\n```\n\nToken endpoints are backend references (not raw URLs), so TLS, DNS, and ReferenceGrant rules stay consistent with the rest of the data plane. Client secrets come from Kubernetes Secrets; `privateKeyJwt`\n\nis available when your AS (Okta, etc.) requires signed client assertions.\n\n## Beyond single-leg exchange\n\nIf every MCP tool call just reused the inbound enterprise JWT, every upstream would see a token that was never minted for it. If every agent SDK keeps its own token vault, you get N credential stores and zero consistent audit which is exactly the OpenClaw-style leak surface from the earlier in this post. Gateway-side exchange keeps secrets out of the agent, preserves user identity (and optional `actorToken`\n\nfor agent-on-behalf-of-user), and caches so agent loops don’t hammer the token endpoint.\n\nThese single-leg grants are also the building blocks for Cross App Access / ID-JAG (a two-leg composition of token exchange and JWT-bearer). See the [ traffic-cross-app-access](https://github.com/agentgateway/agentgateway/tree/main/examples/traffic-cross-app-access) examples if you want to explore that path. Will cover that in depth in future blogs.\n\n## Try it\n\n- Standalone examples:\n[RFC 8693](https://github.com/agentgateway/agentgateway/tree/main/examples/traffic-token-exchange/oauth-rfc8693)·[JWT bearer + Entra OBO shape](https://github.com/agentgateway/agentgateway/tree/main/examples/traffic-token-exchange/jwt-authz-grant) - Backend auth docs:\n[standalone backend authentication](https://agentgateway.dev/docs/standalone/main/configuration/security/backend-authn/) - Related reading:\n[Enterprise MCP SSO with Entra](/blog/2026-01-26-enterprise-mcp-sso/)·[PR #2189](https://github.com/agentgateway/agentgateway/pull/2189)(data plane) ·[PR #2458](https://github.com/agentgateway/agentgateway/pull/2458)(Kubernetes controller)\n\n- Explore the\n[docs](https://agentgateway.dev/docs/)and[get started](https://agentgateway.dev/#getting-started)today. - Star and contribute on\n[GitHub](https://github.com/agentgateway/agentgateway). - Join the conversation on\n[Discord](https://discord.gg/y9efgEmppm). - Attend our weekly\n[community meetings](https://github.com/agentgateway/agentgateway?tab=readme-ov-file#community-meetings).", "url": "https://wpnews.pro/news/agentgateway-adds-token-exchange-jwt-assertion-and-entra-obo", "canonical_source": "https://agentgateway.dev/blog/2026-07-12-agentgateway-token-exchange-jwt-assertion-entra-obo/", "published_at": "2026-07-13 12:20:15+00:00", "updated_at": "2026-07-13 12:35:03.719365+00:00", "lang": "en", "topics": ["ai-agents", "ai-infrastructure", "developer-tools"], "entities": ["Agentgateway", "OpenClaw", "Microsoft Entra"], "alternates": {"html": "https://wpnews.pro/news/agentgateway-adds-token-exchange-jwt-assertion-and-entra-obo", "markdown": "https://wpnews.pro/news/agentgateway-adds-token-exchange-jwt-assertion-and-entra-obo.md", "text": "https://wpnews.pro/news/agentgateway-adds-token-exchange-jwt-assertion-and-entra-obo.txt", "jsonld": "https://wpnews.pro/news/agentgateway-adds-token-exchange-jwt-assertion-and-entra-obo.jsonld"}}