docs: add token exchange (on-behalf-of) MCP auth documentation - #5729
Conversation
oauth_configs to new mcp_oauth_flows table
#5709
|
|
2622dc5 to
eb95068
Compare
4cec69f to
768ee94
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
768ee94 to
d3c936f
Compare
eb95068 to
521a4f1
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/mcp/auth/token-exchange.mdx`:
- Line 10: Update docs/mcp/auth/token-exchange.mdx at lines 10-10 to state that
Bifrost performs the token exchange on a cache miss rather than on every tool
call. At lines 133-147, update the cache diagram and explanatory cache-key text
to include the validated subject-token component alongside caller identity and
MCP client, reflecting the runtime cache isolation behavior.
- Around line 45-59: Update docs/mcp/auth/token-exchange.mdx at lines 45-59,
74-80, 110, and 190-211 to match transports/config.schema.json: remove all
authorization_server_url guidance, UI references, optional-field mentions,
example entries, and table rows. In the configuration guidance at lines 190-211,
state that token_exchange is required when auth_type is token_exchange and
forbidden for every other auth_type; leave supported optional client_secret and
scopes documented.
- Around line 1-5: Update the token-exchange documentation to remove every
authorization_server_url reference and describe configuration only with the
supported audience, client_id, client_secret, and scopes fields. Document that
the cache key includes the inbound bearer-token fingerprint together with auth
mode, identity, and MCP client, matching the runtime behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: c56ebeca-6bb4-4570-bf68-ae31bc3b16e8
📒 Files selected for processing (9)
docs/docs.jsondocs/mcp/auth/overview.mdxdocs/mcp/auth/token-exchange.mdxdocs/mcp/gateway-auth.mdxdocs/openapi/openapi.jsondocs/openapi/openapi.yamldocs/openapi/paths/management/mcp.yamldocs/openapi/schemas/management/mcp.yamldocs/openapi/schemas/management/oauth.yaml
🚧 Files skipped from review as they are similar to previous changes (7)
- docs/openapi/openapi.yaml
- docs/openapi/schemas/management/oauth.yaml
- docs/openapi/paths/management/mcp.yaml
- docs/docs.json
- docs/openapi/schemas/management/mcp.yaml
- docs/openapi/openapi.json
- docs/mcp/auth/overview.mdx
| --- | ||
| title: "Token Exchange (On-Behalf-Of)" | ||
| sidebarTitle: "Token Exchange" | ||
| description: "Each caller's identity-provider token is exchanged for a short-lived token scoped to the upstream MCP server — delegated, per-call, no shared credential." | ||
| icon: "right-left" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 6 '"mcp/auth/(token-exchange|per-user-oauth)"' docs/docs.jsonRepository: maximhq/bifrost
Length of output: 684
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- token-exchange.mdx ---'
wc -l docs/mcp/auth/token-exchange.mdx
sed -n '1,230p' docs/mcp/auth/token-exchange.mdx
printf '%s\n' '--- schema references ---'
rg -n -C 5 'token_exchange|authorization_server_url|auth_type' transports/config.schema.json
printf '%s\n' '--- runtime cache references ---'
rg -n -C 5 'token.?exchange|bearer.?token|fingerprint|cache' --glob '!docs/**' --glob '!transports/config.schema.json' .Repository: maximhq/bifrost
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- documentation ---'
rg -n -C 4 'authorization_server_url|token_exchange|cache|fingerprint|pending_verification|client_id|audience|scopes' docs/mcp/auth/token-exchange.mdx
printf '%s\n' '--- schema ---'
rg -n -C 6 'authorization_server_url|token_exchange|pending_verification|client_id|audience|scopes' transports/config.schema.json
printf '%s\n' '--- token-exchange implementation files ---'
rg -l 'MCPAuthTypeTokenExchange|GetExchangedAccessToken|BifrostInboundBearer|BifrostContextKeyMCPInboundBearer|TokenExchange' --glob '!docs/**' --glob '!transports/config.schema.json' --glob '!**/*_test.go' . | head -80Repository: maximhq/bifrost
Length of output: 37428
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- exchange cache implementation ---'
rg -n -C 12 'cache|fingerprint|BifrostContextKeyMCPInboundBearer|identity.*mcp|mcp.*identity|GetExchangedAccessToken' \
framework/oauth2/tokenexchange.go framework/oauth2/main.go core/mcp/credstore/token_exchange.go core/schemas/oauth.go core/schemas/context.go
printf '%s\n' '--- schema block and condition ---'
sed -n '4830,4878p' transports/config.schema.json
sed -n '5080,5108p' transports/config.schema.json
printf '%s\n' '--- relevant documentation lines ---'
sed -n '86,116p' docs/mcp/auth/token-exchange.mdx
sed -n '128,149p' docs/mcp/auth/token-exchange.mdx
sed -n '190,213p' docs/mcp/auth/token-exchange.mdxRepository: maximhq/bifrost
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json
import re
from pathlib import Path
schema = json.loads(Path("transports/config.schema.json").read_text())
mcp = schema["$defs"]["mcp_client_config"]
exchange = mcp["properties"]["token_exchange"]
print("schema_auth_type_token_exchange:",
"token_exchange" in mcp["properties"]["auth_type"]["enum"])
print("schema_token_exchange_required:",
exchange["required"])
print("schema_token_exchange_allowed:",
sorted(exchange["properties"]))
print("schema_token_exchange_additional_properties:",
exchange["additionalProperties"])
docs = Path("docs/mcp/auth/token-exchange.mdx").read_text()
examples = re.findall(r"```json\s*(.*?)```", docs, re.S)
for i, block in enumerate(examples, 1):
obj = json.loads(block)
config = obj.get("mcp_clients", [{}])[0] if "mcp_clients" in obj else obj
if "token_exchange" in config:
print("docs_example_token_exchange_keys:",
sorted(config["token_exchange"]))
navigation = json.loads(Path("docs/docs.json").read_text())
hits = []
def walk(value):
if isinstance(value, dict):
for key, child in value.items():
if key == "pages" and isinstance(child, list):
for page in child:
if page in ("mcp/auth/per-user-oauth", "mcp/auth/token-exchange"):
hits.append(page)
walk(child)
elif isinstance(value, list):
for child in value:
walk(child)
walk(navigation)
print("navigation_auth_pages:", hits)
PY
printf '%s\n' '--- repository stack indicators ---'
git status --short
git branch --all --no-color | head -40
git diff --statRepository: maximhq/bifrost
Length of output: 792
Align the token-exchange documentation with the schema and runtime.
- Remove all
authorization_server_urlreferences. The schema allows onlyaudience,client_id,client_secret, andscopes. - Document the inbound bearer-token fingerprint in the cache key. The runtime uses it with
(auth mode, identity, mcp client).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/mcp/auth/token-exchange.mdx` around lines 1 - 5, Update the
token-exchange documentation to remove every authorization_server_url reference
and describe configuration only with the supported audience, client_id,
client_secret, and scopes fields. Document that the cache key includes the
inbound bearer-token fingerprint together with auth mode, identity, and MCP
client, matching the runtime behavior.
Source: Path instructions
|
|
||
| ## 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](https://datatracker.ietf.org/doc/html/rfc8693) 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. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Correct the exchange cache description.
Line 10 implies that Bifrost performs an exchange on every tool call. Lines 133 and 144 define the cache only by (identity, mcp_client). Runtime first checks the cache and includes the validated inbound bearer-token fingerprint in its cache key.
State that Bifrost exchanges on a cache miss. State that cache entries are isolated by the caller identity, MCP client, and validated subject token.
docs/mcp/auth/token-exchange.mdx#L10-L10: Change the per-call exchange statement to describe cache-miss behavior.docs/mcp/auth/token-exchange.mdx#L133-L147: Add the subject-token component to the diagram and explanatory cache-key text.
📍 Affects 1 file
docs/mcp/auth/token-exchange.mdx#L10-L10(this comment)docs/mcp/auth/token-exchange.mdx#L133-L147
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/mcp/auth/token-exchange.mdx` at line 10, Update
docs/mcp/auth/token-exchange.mdx at lines 10-10 to state that Bifrost performs
the token exchange on a cache miss rather than on every tool call. At lines
133-147, update the cache diagram and explanatory cache-key text to include the
validated subject-token component alongside caller identity and MCP client,
reflecting the runtime cache isolation behavior.
| 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. The exchange application's own client ID / secret are configured per MCP client, not on the SCIM provider (see [Setup](#setup)). | ||
|
|
||
| <Warning> | ||
| **Okta specifically** ties an audience to the Authorization Server that issues it — one Authorization Server, one audience. Okta's own documented best practice is a **dedicated Custom Authorization Server per protected resource**, separate from the one your SSO login uses. If that's your setup, three additional things are required beyond the app registration above — see [Okta: per-resource Authorization Server](#okta-per-resource-authorization-server) below. Entra and Auth0 don't have this constraint: both use a single tenant-wide token endpoint regardless of target resource, so nothing extra is needed for them. | ||
| </Warning> | ||
|
|
||
| ### 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: | ||
|
|
||
| 1. **Point the exchange at that Authorization Server explicitly.** Set `token_exchange.authorization_server_url` to 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 with `invalid_target: Token Exchange requests must include a valid audience of the authorization server`. | ||
| 2. **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 with `invalid_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. | ||
| 3. **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 with `invalid_scope: ... 'scope' must be provided` if 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 in `scopes`. | ||
|
|
||
| Also check that Okta's **DPoP** (sender-constrained tokens) requirement is off for the exchange application — Bifrost's token exchange sends plain bearer tokens, not DPoP-proofed ones. If your org enforces DPoP by default, disable it specifically for this application under its General settings, or the token request fails with `invalid_dpop_proof: The DPoP proof JWT header is missing`. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Align the configuration reference with transports/config.schema.json.
The supplied schema contract permits optional client_secret and scopes. It does not define authorization_server_url. The page also does not state that token_exchange is required for auth_type: "token_exchange" and forbidden for every other auth type.
docs/mcp/auth/token-exchange.mdx#L45-L59: Remove the unsupported Authorization Server override guidance.docs/mcp/auth/token-exchange.mdx#L74-L80: Remove the Authorization Server URL UI field.docs/mcp/auth/token-exchange.mdx#L110-L110: Removeauthorization_server_urlfrom the optional-field list.docs/mcp/auth/token-exchange.mdx#L190-L211: Remove the field from the example and table. State the required and forbiddentoken_exchangeblock conditions.
As per coding guidelines, “Documentation configuration examples must match transports/config.schema.json.”
📍 Affects 1 file
docs/mcp/auth/token-exchange.mdx#L45-L59(this comment)docs/mcp/auth/token-exchange.mdx#L74-L80docs/mcp/auth/token-exchange.mdx#L110-L110docs/mcp/auth/token-exchange.mdx#L190-L211
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/mcp/auth/token-exchange.mdx` around lines 45 - 59, Update
docs/mcp/auth/token-exchange.mdx at lines 45-59, 74-80, 110, and 190-211 to
match transports/config.schema.json: remove all authorization_server_url
guidance, UI references, optional-field mentions, example entries, and table
rows. In the configuration guidance at lines 190-211, state that token_exchange
is required when auth_type is token_exchange and forbidden for every other
auth_type; leave supported optional client_secret and scopes documented.
Sources: Coding guidelines, Path instructions
521a4f1 to
1e66c03
Compare
1e66c03 to
b01a319
Compare
d3c936f to
70ce9d2
Compare
Merge activity
|
…or other auth types in MCP OpenAPI schema Add minLength: 1 to audience/client_id in MCPTokenExchangeConfig and every duplicate token_exchange block in the bundled openapi.json: required alone doesn't reject empty strings in JSON Schema, and config.schema.json documents both fields as required-and-non-empty. Add the missing counterpart conditional that rejects a token_exchange block when auth_type isn't 'token_exchange' — the existing conditional only required it FOR that auth_type, never forbade it for others.
70ce9d2 to
8b2375c
Compare
Summary
Briefly explain the purpose of this PR and the problem it solves.
Changes
Type of change
Affected areas
How to test
Describe the steps to validate this change. Include commands and expected outcomes.
If adding new configs or environment variables, document them here.
Screenshots/Recordings
If UI changes, add before/after screenshots or short clips.
Breaking changes
If yes, describe impact and migration instructions.
Related issues
Link related issues and discussions. Example: Closes #123
Security considerations
Note any security implications (auth, secrets, PII, sandboxing, etc.).
Checklist
docs/contributing/README.mdand followed the guidelines