Wire SEP-990 enterprise-managed-authorization conformance fixture (#3007)

This commit is contained in:
Max
2026-06-29 11:42:37 +02:00
committed by GitHub
parent e942d00b98
commit fe9723853d
3 changed files with 70 additions and 12 deletions
+68 -1
View File
@@ -25,6 +25,7 @@ Scenarios:
sep-2322-client-request-state - Drive the MRTR auto-loop (SEP-2322)
auth/client-credentials-jwt - Client credentials with private_key_jwt
auth/client-credentials-basic - Client credentials with client_secret_basic
auth/enterprise-managed-authorization - SEP-990 ID-JAG (RFC 8693 + RFC 7523 jwt-bearer)
auth/* - Authorization code flow (default for auth scenarios)
"""
@@ -48,6 +49,8 @@ from mcp.client.auth.extensions.client_credentials import (
PrivateKeyJWTOAuthProvider,
SignedJWTParameters,
)
from mcp.client.auth.extensions.identity_assertion import IdentityAssertionOAuthProvider
from mcp.client.auth.utils import build_protected_resource_metadata_discovery_urls
from mcp.client.client import Client
from mcp.client.context import ClientRequestContext
from mcp.client.streamable_http import streamable_http_client
@@ -454,6 +457,70 @@ async def run_client_credentials_basic(server_url: str) -> None:
await _run_auth_session(server_url, oauth_auth)
@register("auth/enterprise-managed-authorization")
async def run_enterprise_managed_authorization(server_url: str) -> None:
"""SEP-990 enterprise-managed authorization: RFC 8693 token-exchange at the
enterprise IdP for an ID-JAG, then RFC 7523 jwt-bearer at the MCP
authorization server."""
context = get_conformance_context()
client_id = context.get("client_id")
client_secret = context.get("client_secret")
idp_client_id = context.get("idp_client_id")
idp_id_token = context.get("idp_id_token")
idp_token_endpoint = context.get("idp_token_endpoint")
if not client_id:
raise RuntimeError("MCP_CONFORMANCE_CONTEXT missing 'client_id'")
if not client_secret:
raise RuntimeError("MCP_CONFORMANCE_CONTEXT missing 'client_secret'")
if not idp_client_id:
raise RuntimeError("MCP_CONFORMANCE_CONTEXT missing 'idp_client_id'")
if not idp_id_token:
raise RuntimeError("MCP_CONFORMANCE_CONTEXT missing 'idp_id_token'")
if not idp_token_endpoint:
raise RuntimeError("MCP_CONFORMANCE_CONTEXT missing 'idp_token_endpoint'")
# IdentityAssertionOAuthProvider takes the AS issuer as configuration (the
# SEP-990 trust model: the resource server is never asked which AS to use).
# The harness does not put the issuer in context, so for conformance we
# learn it from the harness's PRM document (RFC 9728); production
# deployments would supply it as static configuration instead.
prm_url = build_protected_resource_metadata_discovery_urls(None, server_url)[0]
async with httpx.AsyncClient(timeout=30.0) as http:
prm = (await http.get(prm_url)).raise_for_status().json()
as_issuer = prm["authorization_servers"][0]
async def fetch_id_jag(audience: str, resource: str) -> str:
"""Leg 1 - RFC 8693 token-exchange at the enterprise IdP."""
async with httpx.AsyncClient(timeout=30.0) as http:
resp = await http.post(
idp_token_endpoint,
data={
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"requested_token_type": "urn:ietf:params:oauth:token-type:id-jag",
"subject_token": idp_id_token,
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
"audience": audience,
"resource": resource,
"client_id": idp_client_id,
},
)
resp.raise_for_status()
return resp.json()["access_token"]
oauth_auth = IdentityAssertionOAuthProvider(
server_url=server_url,
storage=InMemoryTokenStorage(),
client_id=client_id,
client_secret=client_secret,
issuer=as_issuer,
assertion_provider=fetch_id_jag,
token_endpoint_auth_method="client_secret_basic",
)
await _run_auth_session(server_url, oauth_auth)
async def run_auth_code_client(server_url: str) -> None:
"""Authorization code flow (default for auth/* scenarios)."""
callback_handler = ConformanceOAuthCallbackHandler()
@@ -496,7 +563,7 @@ async def run_auth_code_client(server_url: str) -> None:
await _run_auth_session(server_url, oauth_auth)
async def _run_auth_session(server_url: str, oauth_auth: OAuthClientProvider) -> None:
async def _run_auth_session(server_url: str, oauth_auth: httpx.Auth) -> None:
"""Common session logic for all OAuth flows."""
http_client = httpx.AsyncClient(auth=oauth_auth, timeout=30.0)
transport = streamable_http_client(url=server_url, http_client=http_client)
@@ -20,12 +20,7 @@
# (the runner fails on stale entries), so the baseline burns down per
# milestone.
client:
[]
# auth/enterprise-managed-authorization (SEP-990) is in the 2025 baseline but
# NOT here: the harness skips it as inapplicable at --spec-version 2026-07-28
# (it is an extension scenario not carried into the 2026 wire), so it is
# neither run nor evaluated on this leg.
client: []
server:
# SEP-2322 (multi-round-trip requests / IncompleteResult): the prompt pipeline
@@ -10,11 +10,7 @@
# scenarios start passing and MUST be removed from this list (the runner fails
# on stale entries), so the baseline burns down per milestone.
client:
# --- Pre-existing scenarios that fail on checks added after conformance 0.1.15 ---
# SEP-990 (enterprise-managed authorization extension): no fixture handler /
# client support for the token-exchange + JWT bearer flow.
- auth/enterprise-managed-authorization
client: []
server:
# --- Draft-spec scenarios (in `--suite draft`; the `active` suite is green) ---