Support RFC 8693 token exchange for enterprise IdP flows (SEP-990)
Summary
Implements SEP-990 (enterprise IdP policy controls during MCP OAuth flows). The wire mechanism is the RFC 8693 token-exchange grant (urn:ietf:params:oauth:grant-type:token-exchange): a client exchanges an enterprise IdP-issued Identity Assertion Authorization Grant (ID-JAG) for an MCP access token at the authorization server's token endpoint.
This is additive and opt-in on both sides; existing flows are unchanged.
Client
TokenExchangeOAuthProvider (mcp.client.auth.extensions.token_exchange) is an httpx.Auth that posts the exchange, mirroring the client_credentials extension. The ID-JAG is supplied lazily via an async subject_token_provider(audience) callback — the SDK does not implement IdP login or the first exchange against the IdP (deployment-specific). Public (default, none) and confidential (client_secret_post/client_secret_basic) clients are supported; requested_token_type defaults to the access-token URN.
Server
OAuthAuthorizationServerProvider.exchange_token validates the subject token (signature/issuer/audience/expiry/policy — the provider's responsibility) and issues the token. Gated by AuthSettings(token_exchange_enabled=True), which:
- advertises the grant + the public-client
noneauth method in AS metadata, and - gates the
/tokenendpoint: with the flag off,/tokenrejects the grant withunsupported_grant_typeeven if the provider implementsexchange_token.
TokenError gains RFC 8693's invalid_target; TokenExchangeToken carries the required issued_token_type (the handler defaults it so responses are compliant regardless of provider). TokenExchangeRequest enforces RFC 8693 actor-token pairing.
The provider owns scope decisions: requested scopes are never granted verbatim. The example server narrows the request against an allowed set so a valid ID-JAG cannot be exchanged for broader access than policy permits.
Review
Reviewed by Codex across two rounds. Round one raised seven should-fix findings; all were addressed (dispatch gating, issued_token_type, scope-escalation hardening in the example, invalid_target, actor-token pairing, Basic-auth confidential client, public-client advertisement). The follow-up review confirmed every finding resolved with no new Critical/Should-fix issues.
Testing
- Unit + ASGI HTTP tests for the client extension and the server token handler (13 server, 7 client, plus shared-model coverage).
- Verified end to end over real Uvicorn sockets with a real RS256-signed JWT acting as the ID-JAG: AS metadata discovery, raw
/tokenexchange, forged-signature rejection, and a full MCP session (discovery → 401 → token exchange → bearer-gated tool call) all pass. ./scripts/test: 100% coverage,strict-no-coverclean,pyrightclean.
This exercise surfaced a separate AS-metadata issuer normalization bug, fixed in #2987 (merged); this branch builds on it.
Docs: new SEP-990 section in docs/migration.md; client + server snippets under examples/snippets/.
AI Disclaimer
This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.