Skip to content

docs: add token exchange (on-behalf-of) MCP auth documentation - #5729

Merged
Pratham-Mishra04 merged 1 commit into
devfrom
07-31-docs_add_token_exchange_on-behalf-of_mcp_auth_documentation
Aug 8, 2026
Merged

docs: add token exchange (on-behalf-of) MCP auth documentation#5729
Pratham-Mishra04 merged 1 commit into
devfrom
07-31-docs_add_token_exchange_on-behalf-of_mcp_auth_documentation

Conversation

@Pratham-Mishra04

Copy link
Copy Markdown
Collaborator

Summary

Briefly explain the purpose of this PR and the problem it solves.

Changes

  • What was changed and why
  • Any notable design decisions or trade-offs

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

Describe the steps to validate this change. Include commands and expected outcomes.

# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build

If adding new configs or environment variables, document them here.

Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

Breaking changes

  • Yes
  • No

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

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

This was referenced Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

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.

@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-31-docs_add_token_exchange_on-behalf-of_mcp_auth_documentation branch from 768ee94 to d3c936f Compare August 8, 2026 08:43
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-31-docs_document_make-before-break_reconnection_and_shared-client_retry_behavior branch from eb95068 to 521a4f1 Compare August 8, 2026 08:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between eb95068 and 768ee94.

📒 Files selected for processing (9)
  • docs/docs.json
  • docs/mcp/auth/overview.mdx
  • docs/mcp/auth/token-exchange.mdx
  • docs/mcp/gateway-auth.mdx
  • docs/openapi/openapi.json
  • docs/openapi/openapi.yaml
  • docs/openapi/paths/management/mcp.yaml
  • docs/openapi/schemas/management/mcp.yaml
  • docs/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

Comment on lines +1 to +5
---
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.json

Repository: 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 -80

Repository: 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.mdx

Repository: 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 --stat

Repository: maximhq/bifrost

Length of output: 792


Align the token-exchange documentation with the schema and runtime.

  • Remove all authorization_server_url references. The schema allows only audience, client_id, client_secret, and scopes.
  • 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +45 to +59
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`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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: Remove authorization_server_url from the optional-field list.
  • docs/mcp/auth/token-exchange.mdx#L190-L211: Remove the field from the example and table. State the required and forbidden token_exchange block 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-L80
  • docs/mcp/auth/token-exchange.mdx#L110-L110
  • docs/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

@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-31-docs_document_make-before-break_reconnection_and_shared-client_retry_behavior branch from 521a4f1 to 1e66c03 Compare August 8, 2026 10:12
@Pratham-Mishra04
Pratham-Mishra04 requested a review from a team as a code owner August 8, 2026 10:12
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-31-docs_document_make-before-break_reconnection_and_shared-client_retry_behavior branch from 1e66c03 to b01a319 Compare August 8, 2026 10:13
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-31-docs_add_token_exchange_on-behalf-of_mcp_auth_documentation branch from d3c936f to 70ce9d2 Compare August 8, 2026 10:13

Pratham-Mishra04 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

Merge activity

  • Aug 8, 10:33 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Aug 8, 10:35 AM UTC: Graphite rebased this pull request as part of a merge.
  • Aug 8, 10:36 AM UTC: @Pratham-Mishra04 merged this pull request with Graphite.

@Pratham-Mishra04
Pratham-Mishra04 changed the base branch from 07-31-docs_document_make-before-break_reconnection_and_shared-client_retry_behavior to graphite-base/5729 August 8, 2026 10:33
@Pratham-Mishra04
Pratham-Mishra04 changed the base branch from graphite-base/5729 to dev August 8, 2026 10:34
…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.
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-31-docs_add_token_exchange_on-behalf-of_mcp_auth_documentation branch from 70ce9d2 to 8b2375c Compare August 8, 2026 10:34
@Pratham-Mishra04
Pratham-Mishra04 merged commit 088e76f into dev Aug 8, 2026
14 of 15 checks passed
@Pratham-Mishra04
Pratham-Mishra04 deleted the 07-31-docs_add_token_exchange_on-behalf-of_mcp_auth_documentation branch August 8, 2026 10:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Files API Support

2 participants