Wire SEP-990 enterprise-managed-authorization conformance fixture (#3007)
This commit is contained in:
@@ -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) ---
|
||||
|
||||
Reference in New Issue
Block a user