Overview
auth_type: "token_exchange" lets each caller reach an upstream MCP server as themselves, without Bifrost ever storing a per-user credential. On every tool call, Bifrost takes the caller’s own identity-provider (IdP) token and exchanges it — via RFC 8693 Token Exchange or the equivalent on-behalf-of grant — for a short-lived token scoped to that MCP server’s audience. The exchanged token is sent upstream; the caller’s original token never is.
This is strictly delegated: there is no shared service-account fallback. A caller with no identity token cannot use a token_exchange server — see Identity requirements.
Requires an enabled SCIM identity provider (enterprise). Token exchange runs against whichever IdP handles your SSO, using its token endpoint. See Prerequisites.
If the upstream MCP server doesn’t trust your SCIM identity provider’s issuer — most third-party SaaS services — token exchange cannot work there. Use Per-User OAuth instead.
This auth type is only valid for HTTP and SSE connections.
Prerequisites
Token exchange needs an application registered at your identity provider with the token-exchange (or on-behalf-of) grant enabled and permission to mint tokens for your MCP servers’ audiences. Each MCP client picks which application performs its exchange viatoken_exchange.use_idp_credentials:
use_idp_credentials: false(default) — a dedicated application, separate from your SSO login application, configured per MCP client viatoken_exchange.client_id/client_secret. This is the right default for most providers: it keeps each MCP server’s OBO grant scoped independently and limits what a leaked secret can reach.use_idp_credentials: true— reuses your SSO login application’s own credentials instead;client_id/client_secretare then ignored.
Bifrost derives the token endpoint and picks the correct grant shape automatically from your enabled SCIM provider — you never configure an endpoint or grant type directly.
Okta: per-resource Authorization Server
Skip this section if your exchange application’s audience is registered on the same Authorization Server your SSO login uses — the defaults just work. If you followed Okta’s own guidance and created a separate Custom Authorization Server for the resource (e.g. one per MCP server, or one shared “internal APIs” server distinct from your SSO login’s server), all three of the following are required:- Point the exchange at that Authorization Server explicitly. Set
token_exchange.authorization_server_urlto its issuer URL (e.g.https://your-domain.okta.com/oauth2/your-auth-server-id). Without this, Bifrost sends the exchange request to the same Authorization Server your SSO login uses — which has never heard of an audience registered on a different one, and Okta rejects it withinvalid_target: Token Exchange requests must include a valid audience of the authorization server. - Register your SSO login’s Authorization Server as a Trusted Server on the resource’s Authorization Server: Security → API → (the resource’s Authorization Server) → Trusted servers → Add Server, and add the Authorization Server your SSO login uses (often
default). Without this, Okta rejects the caller’s identity token as an untrusted subject withinvalid_request: 'subject_token' is invalid— the audience and endpoint can be entirely correct and this still fails, since it’s a separate cross-server trust check. - Define a custom scope on that Authorization Server and include it in
token_exchange.scopes. Standard OIDC scopes (openid,profile,email,offline_access) aren’t valid for a service-app token exchange — Okta rejects the request withinvalid_scope: ... 'scope' must be providedif none is set, or rejects an OIDC scope outright. Add a scope under Security → API → (the resource’s Authorization Server) → Scopes → Add Scope (e.g.your-resource.access), grant it to the exchange application in that Authorization Server’s Access Policy, and list it inscopes.
invalid_dpop_proof: The DPoP proof JWT header is missing.
Microsoft Entra ID: known setup gotchas
Entra’s OBO flow surfaces several sharp edges that don’t show up with Okta/Auth0/Keycloak. All of these were hit while setting up a real tenant end-to-end; fix them in this order if you see the matching error.- Set
token_exchange.use_idp_credentials: true. This is not optional for Entra — see the warning in Prerequisites above for why a dedicated exchange application structurally cannot work.client_id/client_secretare ignored once this is set; leave them unset. - Token issuer mismatch —
oidc: id token issued by a different provider. Whether an app’s tokens use the v1 issuer (https://sts.windows.net/<tenant>/) or the v2 issuer (https://login.microsoftonline.com/<tenant>/v2.0) is controlled by the resource app’s manifest, not by which endpoint you called. If your MCP server validates against the v2 issuer (the standard OIDC discovery shape) but gets v1 tokens, open the resource app’s registration → Manifest → set"requestedAccessTokenVersion": 2under theapiblock → Save. - Audience mismatch —
oidc: expected audience "api://..." got ["..."]. Once an app issues v2 tokens, its access tokens carry the app’s client ID (bare GUID) inaud, not theapi://...Application ID URI you registered — even though you requested the scope using the URI form. Settoken_exchange.audience(and the MCP server’s own audience-validation config) to the bare GUID, notapi://<guid>. AADSTS240002: Input id_token cannot be used as 'urn:ietf:params:oauth:grant-type:jwt-bearer' grant. The assertion Entra’s OBO endpoint accepts must be an access token, never anid_token. Bifrost’s SSO session already stores the right one for real caller traffic; this only bites when hand-crafting a token for manual testing — if you hit it, you grabbed theid_tokeninstead of theaccess_token.AADSTS501461: AcceptMappedClaims is only supported for a token audience matching the application GUID.... If your SSO app has custom claims mapping enabled (acceptMappedClaims: truein its manifest — typically on if you’re mappingroles/groupsclaims for SCIM attribute mappings), requesting an access token audienced to that app’s ownapi://<guid>Application ID URI is rejected outright. This only comes up when manually minting a test token against the SSO app’s own scope (e.g.<client-id>/access_as_user, notapi://<client-id>/access_as_user— Entra treats an app’s own client ID as an implicit alternate resource identifier alongside its App ID URI, and that form isn’t subject to the same restriction); Bifrost’s own runtime exchange isn’t affected.- Adding a scope silently drops resource access. Entra’s
.defaultscope (whataudiencebuilds by default) cannot be combined with other delegated scopes — Microsoft’s own OBO docs are explicit about this (AADSTS70011otherwise). Bifrost handles the one documented exception automatically:scopes: ["offline_access"]combines with the default (<audience>/.default offline_access) rather than replacing it. Any other scope you configure fully replaces the default instead, per Entra’s own rule — there is no way to request.defaultalongside a named custom scope in the same call. If you need narrower, specific permissions rather than everything.defaultgrants, expose named scopes on the resource app (Expose an API → Add a scope) and list only those inscopes, without.defaultat all.
Setup
Each MCP client scopes its own exchange: which resource (audience) the token is minted for, and which IdP application performs the exchange.
- Web UI
- config.json

- Navigate to MCP Gateway in the sidebar
- Click New MCP Server
- Pick HTTP or SSE as the connection type, fill in the Connection URL
- Set Auth Type to Token Exchange (On-Behalf-Of) — only shown when an identity provider is configured
- Fill in:
- Audience — the resource identifier this server is registered as at your identity provider (e.g.
api://jira-mcpfor Okta/Auth0/Keycloak). For Microsoft Entra ID, this is the resource app’s Application (client) ID — a bare GUID, not theapi://...Application ID URI shown under “Expose an API”; see Audience mismatch below for why - Exchange application — Dedicated application (default) or Identity provider application. Pick the latter, which reuses your SSO login application’s credentials, if your provider requires it — Microsoft Entra ID always does; see Prerequisites
- Exchange Client ID (required) / Exchange Client Secret (optional for public clients) — only shown when Dedicated application is selected
- Authorization Server URL (optional) — only needed if this audience is registered on a different Authorization Server than your SSO login uses; see Okta: per-resource Authorization Server
- Scopes (optional, comma-separated) — include
offline_accesswhere your identity provider supports it, so exchanges issue refresh tokens and the retained admin discovery credential stays self-renewing. For Microsoft Entra ID specifically:offline_accessis the only additional scope that combines with the audience’s default resource access — leave it as the sole entry to keep both; any other scope here replaces the default resource scope entirely rather than adding to it, and Entra rejects a.default+ custom-scope combination outright — if you need narrower, specific permissions instead of the audience’s default access, list only named resource scopes here (e.g.api://jira-mcp/access_as_user), with no.defaultentry at all
- Audience — the resource identifier this server is registered as at your identity provider (e.g.
- Click Create — Bifrost exchanges your own signed-in identity token, verifies the upstream connection, and discovers tools
How it works
- The caller sends a request carrying their own identity-provider token (
Authorization: Bearer <token>), or an SSO dashboard session. - Bifrost checks its in-memory cache for an exchanged token already minted for this
(identity, mcp_client)pair. - On a miss, Bifrost exchanges the caller’s token at the identity provider’s token endpoint, scoped to the client’s configured
audience. - The exchanged token is used on an ephemeral upstream connection for that call only — nothing about it is written to the database.
- The cache holds the exchanged token until shortly before it expires, so repeat calls from the same caller don’t re-exchange every time.
Verification
Bifrost needs to prove the exchange works once before serving traffic: exchange a sample token, connect to the upstream server, and discover its tools. There is no manual token to paste — verification always runs as the signed-in admin, using the admin’s own identity-provider token (already present on their dashboard session or API request).pending_verification— shown for a newly created orconfig.json-declared client with no discovered tools yet. Click Verify as me on the client sheet.needs_reauth— shown when the retained admin discovery credential (see below) has died and needs repair. Click Re-verify as me.
POST /api/mcp/client/{id}/verify-exchange, with no request body.

Verification requires an identity-authenticated session or request (SSO login, or a request bearing your own IdP token). An API-key-authenticated request has no identity token to exchange and verification fails with a message asking you to sign in with your identity provider first.
Admin discovery credential
The token exchanged during verification is retained as a client-level admin discovery credential, used only for periodic tool-list refresh — never for end-user tool calls, which always exchange the caller’s own token. It renews automatically via the refresh token your identity provider issues alongside the exchange, provided your configuredscopes include offline_access (or your provider’s equivalent).
Without a refresh token, the admin credential expires within its own lifetime and the client shows needs_reauth until an admin clicks Re-verify as me. End-user tool calls are unaffected either way — only the periodic tool-list refresh pauses.

Identity requirements
Unlike per-user OAuth and per-user headers, token exchange has no virtual-key-only path. A caller must present a validated identity-provider token — a virtual key alone establishes governance (budgets, rate limits, tool scoping) but carries nothing to exchange.Authorization: Bearer <IdP JWT>— validated against your SCIM identity provider, then used as the exchange subject.- A dashboard SSO session (cookie) — the session’s stored identity-provider token is used automatically; no header needed.
- A virtual key alone — resolves identity and governance, but a tool call against a
token_exchangeserver fails:subject_token_missing.
dual_credential_conflict_behavior decides which one governance attributes the request to, while the identity token is used for the exchange either way.
Configuration reference
client_id and client_secret are redacted in API responses, same as other MCP credential fields.
Try it yourself
examples/mcps/token-exchange-demo-server is a runnable MCP server for testing this end-to-end against a real identity provider (Okta, Entra, Keycloak, Auth0, or any generic OIDC provider). It validates every request’s bearer token via real OIDC discovery and JWKS signature verification — no shortcuts — and exposes a whoami tool that echoes back the caller’s identity claims, so you can confirm each caller’s own identity reached the upstream server.
Next Steps
- Per-User OAuth — when the upstream server runs its own OAuth and doesn’t trust your IdP
- MCP Authentication Overview — compare all auth types
- Gateway Authentication — identity modes and dual-credential behavior

