-
Notifications
You must be signed in to change notification settings - Fork 4k
Implement SEP-990 Enterprise Managed OAuth #1721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
BinoyOza-okta
wants to merge
32
commits into
modelcontextprotocol:main
Choose a base branch
from
BinoyOza-okta:feature/sep-990-enterprise-oauth
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
3e5d7aa
- Implemented SEP-990 feature for providing support for Enterprise Ma…
BinoyOza-okta 70937cc
Added test cases for missing lines of code.
BinoyOza-okta 51f8fcc
- Added tests cases for few of the missing lines. src/mcp/client/auth…
BinoyOza-okta b8d2c97
- Fixed pre-commit errors.
BinoyOza-okta cd8d111
- Tried to fix the ruff error.
BinoyOza-okta f2421eb
- Fixed ruff errors.
BinoyOza-okta ab23ceb
- Removed server side changes for enterprise_managed_auth.py
BinoyOza-okta 204bd86
- Added README.md changes for SEP-990 implementation for enterprise m…
BinoyOza-okta add0cc2
- Resolved pyright checks error.
BinoyOza-okta 9e4c83f
- Resolved README.md file fixes for removing unused imports.
BinoyOza-okta 9da5f5b
- Resolved pyright errors.
BinoyOza-okta 5663563
- Added new test cases for the missing code lines.
BinoyOza-okta 5ef7740
- Fixed the failing test cases.
BinoyOza-okta 7587731
- Fixed the test cases.
BinoyOza-okta e57633d
- Added typing for request payload structures TokenExchangeRequestDat…
BinoyOza-okta 36d30b3
- Updated test case to include IDJAGClaims type model to verify payload.
BinoyOza-okta f86758e
feat: Add conformance tests for enterprise managed authorization (SEP…
BinoyOza-okta 2a67fcd
- Fix uv.lock.
BinoyOza-okta 649d9d8
feat(auth): migrate enterprise auth conformance tests to @modelcontex…
BinoyOza-okta 3565513
Fixed end-of-file-fixer for test_enterprise_managed_auth_client.py.
BinoyOza-okta 14dd313
Fixed pre-commit hooks issues.
BinoyOza-okta a3a9af3
Fixed Ruff formatting issues.
BinoyOza-okta 6653fb2
Fixed pyright issues.
BinoyOza-okta 57802c8
Moved the changes from README.md to README.v2.md.
BinoyOza-okta 84330a3
- Removed Unicode characters from the example snippet file.
BinoyOza-okta f01bf16
- Removed unused parameters from the EnterpriseAuthOAuthClientProvide…
BinoyOza-okta 170df60
refactor(auth): address code review feedback for enterprise managed auth
BinoyOza-okta e2f3136
- Removed the non-existing scenario from docstring.
BinoyOza-okta f3d2f53
### Code Review Fixes
BinoyOza-okta ab07f34
- Fixed end of file for test_enterprise_managed_auth_client.py.
BinoyOza-okta a67a98a
- Remove dead is_token_valid() guard from _perform_authorization; par…
BinoyOza-okta 7c9276a
fix(auth): harden enterprise managed auth with RFC compliance fixes
BinoyOza-okta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,7 @@ | |
|
|
||
| Contract: | ||
| - MCP_CONFORMANCE_SCENARIO env var -> scenario name | ||
| - MCP_CONFORMANCE_CONTEXT env var -> optional JSON (for client-credentials scenarios) | ||
| - MCP_CONFORMANCE_CONTEXT env var -> optional JSON (for auth scenarios) | ||
| - Server URL as last CLI argument (sys.argv[1]) | ||
| - Must exit 0 within 30 seconds | ||
|
|
||
|
|
@@ -16,7 +16,16 @@ | |
| elicitation-sep1034-client-defaults - Elicitation with default accept callback | ||
| auth/client-credentials-jwt - Client credentials with private_key_jwt | ||
| auth/client-credentials-basic - Client credentials with client_secret_basic | ||
| auth/cross-app-access-complete-flow - Enterprise managed OAuth (SEP-990) - v0.1.14+ | ||
| auth/* - Authorization code flow (default for auth scenarios) | ||
|
|
||
| Enterprise Auth (SEP-990): | ||
| The conformance package v0.1.14+ (https://github.com/modelcontextprotocol/conformance/pull/110) | ||
| provides the scenario 'auth/cross-app-access-complete-flow' which tests the complete | ||
| enterprise managed OAuth flow: IDP ID token → ID-JAG → access token. | ||
|
|
||
| The client receives test context (idp_id_token, idp_token_endpoint, etc.) via | ||
| MCP_CONFORMANCE_CONTEXT environment variable and performs the token exchange flows automatically. | ||
| """ | ||
|
|
||
| import asyncio | ||
|
|
@@ -314,9 +323,100 @@ async def run_auth_code_client(server_url: str) -> None: | |
| await _run_auth_session(server_url, oauth_auth) | ||
|
|
||
|
|
||
| @register("auth/cross-app-access-complete-flow") | ||
| async def run_cross_app_access_complete_flow(server_url: str) -> None: | ||
| """Enterprise managed auth: Complete SEP-990 flow (OIDC ID token → ID-JAG → access token). | ||
|
|
||
| This scenario is provided by @modelcontextprotocol/[email protected]+ (PR #110). | ||
| It tests the complete enterprise managed OAuth flow using token exchange (RFC 8693) | ||
| and JWT bearer grant (RFC 7523). | ||
| """ | ||
| from mcp.client.auth.extensions.enterprise_managed_auth import ( | ||
| EnterpriseAuthOAuthClientProvider, | ||
| TokenExchangeParameters, | ||
| ) | ||
|
|
||
| context = get_conformance_context() | ||
| # The conformance package provides these fields | ||
| idp_id_token = context.get("idp_id_token") | ||
| idp_token_endpoint = context.get("idp_token_endpoint") | ||
| idp_issuer = context.get("idp_issuer") | ||
|
|
||
| # For cross-app access, we need to determine the MCP server's resource ID and auth issuer | ||
| # The conformance package sets up the auth server, and the MCP server URL is passed to us | ||
|
|
||
| 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'") | ||
| if not idp_issuer: | ||
| raise RuntimeError("MCP_CONFORMANCE_CONTEXT missing 'idp_issuer'") | ||
|
|
||
| # Extract base URL by stripping trailing /mcp path (Python 3.9+). | ||
| # The conformance harness always serves MCP at <base>/mcp, so stripping | ||
| # the suffix gives us the auth-server base URL for fallback defaults. | ||
| base_url = server_url.removesuffix("/mcp") | ||
| auth_issuer = context.get("auth_issuer", base_url) | ||
| resource_id = context.get("resource_id", server_url) | ||
|
|
||
| logger.debug("Cross-app access flow:") | ||
| logger.debug(f" IDP Issuer: {idp_issuer}") | ||
| logger.debug(f" IDP Token Endpoint: {idp_token_endpoint}") | ||
| logger.debug(f" Auth Issuer: {auth_issuer}") | ||
| logger.debug(f" Resource ID: {resource_id}") | ||
|
|
||
| # Create token exchange parameters from IDP ID token | ||
| token_exchange_params = TokenExchangeParameters.from_id_token( | ||
| id_token=idp_id_token, | ||
| mcp_server_auth_issuer=auth_issuer, | ||
| mcp_server_resource_id=resource_id, | ||
| scope=context.get("scope"), | ||
| ) | ||
|
|
||
| # Get pre-configured client credentials from context (if provided) | ||
| client_id = context.get("client_id") | ||
| client_secret = context.get("client_secret") | ||
|
|
||
| storage = InMemoryTokenStorage() | ||
|
|
||
| # Create enterprise auth provider | ||
| enterprise_auth = EnterpriseAuthOAuthClientProvider( | ||
| server_url=server_url, | ||
| client_metadata=OAuthClientMetadata( | ||
| client_name="conformance-cross-app-client", | ||
| redirect_uris=[AnyUrl("http://localhost:3000/callback")], | ||
| grant_types=["urn:ietf:params:oauth:grant-type:jwt-bearer"], | ||
| response_types=["token"], | ||
| ), | ||
| storage=storage, | ||
| idp_token_endpoint=idp_token_endpoint, | ||
| token_exchange_params=token_exchange_params, | ||
| ) | ||
|
|
||
| # If client credentials are provided in context, use them instead of dynamic registration | ||
| if client_id and client_secret: | ||
| from mcp.shared.auth import OAuthClientInformationFull | ||
|
|
||
| logger.debug(f"Using pre-configured client credentials: {client_id}") | ||
| client_info = OAuthClientInformationFull( | ||
| client_id=client_id, | ||
| client_secret=client_secret, | ||
| token_endpoint_auth_method="client_secret_basic", | ||
| grant_types=["urn:ietf:params:oauth:grant-type:jwt-bearer"], | ||
| response_types=["token"], | ||
| redirect_uris=[AnyUrl("http://localhost:3000/callback")], | ||
| ) | ||
| enterprise_auth.context.client_info = client_info | ||
| await storage.set_client_info(client_info) | ||
|
|
||
| await _run_auth_session(server_url, enterprise_auth) | ||
|
|
||
|
|
||
| async def _run_auth_session(server_url: str, oauth_auth: OAuthClientProvider) -> None: | ||
| """Common session logic for all OAuth flows.""" | ||
| client = httpx.AsyncClient(auth=oauth_auth, timeout=30.0) | ||
| # Allow timeout to be configured via environment variable for different test scenarios | ||
| timeout = float(os.environ.get("MCP_CONFORMANCE_TIMEOUT", "30.0")) | ||
| client = httpx.AsyncClient(auth=oauth_auth, timeout=timeout) | ||
| async with streamable_http_client(url=server_url, http_client=client) as (read_stream, write_stream): | ||
| async with ClientSession( | ||
| read_stream, write_stream, elicitation_callback=default_elicitation_callback | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,4 +42,4 @@ jobs: | |
| with: | ||
| node-version: 24 | ||
| - run: uv sync --frozen --all-extras --package mcp | ||
| - run: npx @modelcontextprotocol/[email protected].13 client --command 'uv run --frozen python .github/actions/conformance/client.py' --suite all | ||
| - run: npx @modelcontextprotocol/[email protected].14 client --command 'uv run --frozen python .github/actions/conformance/client.py' --suite all | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these scenarios do not exist, only the top one does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the scenarios that do not exist, and kept only the top one.