Support RFC 8693 token exchange for enterprise IdP flows (SEP-990) (#2988)

Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
This commit is contained in:
Marcelo Trylesinski 2026-06-26 17:57:10 +02:00 • committed by GitHub
commit c0ecb70e24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -15,6 +15,7 @@ from mcp.server.auth.provider import (
AccessToken,
AuthorizationCode,
AuthorizationParams,
IdentityAssertionParams,
OAuthAuthorizationServerProvider,
RefreshToken,
TokenError,
@ -25,6 +26,10 @@ from tests.interaction._connect import BASE_URL
_TOKEN_LIFETIME_SECONDS = 3600
# The only ID-JAG assertion the in-memory provider accepts; any other value is rejected with
# invalid_grant, standing in for the signature/policy validation a real AS performs.
VALID_ASSERTION = "valid-id-jag"
class InMemoryAuthorizationServerProvider(
OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]
@ -71,6 +76,9 @@ class InMemoryAuthorizationServerProvider(
self.codes: dict[str, AuthorizationCode] = {}
self.refresh_tokens: dict[str, RefreshToken] = {}
self.access_tokens: dict[str, AccessToken] = {}
# The most recent jwt-bearer request the SDK handler passed to exchange_identity_assertion,
# for tests to assert what the client sent (None until the first exchange).
self.last_assertion_params: IdentityAssertionParams | None = None
def _next_expires_in(self) -> int:
self._tokens_issued += 1
@ -187,6 +195,28 @@ class InMemoryAuthorizationServerProvider(
refresh_token=new_refresh,
)
async def exchange_identity_assertion(
self, client: OAuthClientInformationFull, params: IdentityAssertionParams
) -> OAuthToken:
"""Validate the ID-JAG assertion and mint an MCP access token (RFC 7523 jwt-bearer / SEP-990).
Records `params` for inspection and rejects any assertion other than `VALID_ASSERTION` with
invalid_grant (standing in for signature/policy validation). The granted scopes are exactly
those the client requested; a real provider would derive them from the validated ID-JAG.
"""
self.last_assertion_params = params
assert client.client_id is not None
if params.assertion != VALID_ASSERTION:
raise TokenError(error="invalid_grant", error_description="assertion is not valid")
scopes = params.scopes if params.scopes is not None else self._default_scopes
access = self.mint_access_token(client_id=client.client_id, scopes=scopes, resource=params.resource)
return OAuthToken(
access_token=access,
token_type="Bearer",
expires_in=self._next_expires_in(),
scope=" ".join(scopes),
)
async def revoke_token(self, token: AccessToken | RefreshToken) -> None:
"""Not exercised by this suite; revocation is out of scope for the interaction tests."""
raise NotImplementedError