From cf59a4511ff1980fd932215ef886e665c829bdbf Mon Sep 17 00:00:00 2001 From: Kiran Thakkar Date: Sun, 10 May 2026 07:08:00 -0700 Subject: [PATCH] =?UTF-8?q?Fix=20OCI=20Provider=20issue=20in=203.x=20versi?= =?UTF-8?q?on.=20Add=20OCI=20auth=20provider=20example=20=E2=80=A6=20(#411?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix OCI Provider issue in 3.x version. Add OCI auth provider example and test * Fix OCI Provider issue in 3.x version. Add OCI auth provider example and test. Fixed a couple of minor issues in README. * Rerun CI --- examples/auth/oci_oauth/README.md | 51 ++++++++++++++ examples/auth/oci_oauth/client.py | 32 +++++++++ examples/auth/oci_oauth/server.py | 38 +++++++++++ src/fastmcp/server/auth/providers/oci.py | 7 ++ tests/server/auth/providers/test_oci.py | 87 ++++++++++++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 examples/auth/oci_oauth/README.md create mode 100644 examples/auth/oci_oauth/client.py create mode 100644 examples/auth/oci_oauth/server.py create mode 100644 tests/server/auth/providers/test_oci.py diff --git a/examples/auth/oci_oauth/README.md b/examples/auth/oci_oauth/README.md new file mode 100644 index 000000000..5b831770d --- /dev/null +++ b/examples/auth/oci_oauth/README.md @@ -0,0 +1,51 @@ +# Oracle (OCI IAM (Identity Domain)) OAuth Example + +This example demonstrates how to use the OCI IAM OAuth provider with FastMCP servers. + +## Setup + +### 1. OCI App Registration + +1. Login to OCI console (https://cloud.oracle.com for OCI commercial cloud). +2. From "Identity & Security" menu, open Domains page. +3. On the Domains list page, select the domain in which you want to create MCP server OAuth client. If you need help finding the list page for the domain, see [Listing Identity Domains.](https://docs.oracle.com/en-us/iaas/Content/Identity/domains/to-view-identity-domains.htm#view-identity-domains). +4. On the details page, select Integrated applications. A list of applications in the domain is displayed. +5. Select Add application. +6. In the Add application window, select Confidential Application. +7. Select Launch workflow. +8. In the Add application details page, Enter name and description and create the application. +9. Once the Integrated Application is created, Click on "OAuth configuration" tab. +10. Click on "Edit OAuth configuration" button. +11. Configure the application as OAuth client by selecting "Configure this application as a client now" radio button. +12. Select "Authorization code" grant type. If you are planning to use the same OAuth client application for token exchange, select "Client credentials" grant type as well. In the sample, we will use the same client. +13. For Authorization grant type, select redirect URL. In most cases, this will be the MCP server URL followed by "/auth/callback". For example http://localhost:8000/auth/callback +14. Click on "Submit" button to update OAuth configuration for the client application. +15. Make sure to Activate the client application. +16. Note down client ID and client secret for the application. You'll use these values when configuring the OCIProvider in the MCP server. + +For details instructions with screenshots, please refer to [FastMCP OCI Provider Documentation](https://gofastmcp.com/integrations/oci). + +### 2. Set Environment Variables + +```bash +# Required +FASTMCP_SERVER_AUTH_IDCS_CLIENT_ID=your-application-client-id +FASTMCP_SERVER_AUTH_IDCS_CLIENT_SECRET=your-client-secret-value +FASTMCP_SERVER_AUTH_IDCS_DOMAIN=your-iam-domain-url # IDCS domain URL for example idcs-abscasdwdac3432rdwsda.identity.oraclecloud.com +``` + +### 3. Run the Example + +Start the server: + +```bash +python server.py +``` + +Test with client: + +```bash +python client.py +``` + +When you run the client, it will open a browser on your machine to login to OCI IAM domain. diff --git a/examples/auth/oci_oauth/client.py b/examples/auth/oci_oauth/client.py new file mode 100644 index 000000000..d7f2b760a --- /dev/null +++ b/examples/auth/oci_oauth/client.py @@ -0,0 +1,32 @@ +"""OAuth client example for connecting to FastMCP servers. + +This example demonstrates how to connect to an OAuth-protected FastMCP server. + +To run: + python client.py +""" + +import asyncio + +from fastmcp.client import Client + +SERVER_URL = "http://localhost:8000/mcp" + + +async def main(): + try: + async with Client(SERVER_URL, auth="oauth") as client: + assert await client.ping() + print("✅ Successfully authenticated!") + + tools = await client.list_tools() + print(f"🔧 Available tools ({len(tools)}):") + for tool in tools: + print(f" - {tool.name}: {tool.description}") + except Exception as e: + print(f"❌ Authentication failed: {e}") + raise + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/auth/oci_oauth/server.py b/examples/auth/oci_oauth/server.py new file mode 100644 index 000000000..e1b09f827 --- /dev/null +++ b/examples/auth/oci_oauth/server.py @@ -0,0 +1,38 @@ +"""Oracle OCI IAM OAuth server example for FastMCP. + +This example demonstrates how to protect a FastMCP server with Oracle OCI IAM OAuth. + +Required environment variables: +- FASTMCP_SERVER_AUTH_IDCS_CLIENT_ID: Your IDCS OAuth Application clientID +- FASTMCP_SERVER_AUTH_IDCS_CLIENT_SECRET: Your IDCS client secret +- FASTMCP_SERVER_AUTH_IDCS_DOMAIN: IDCS domain URL for example idcs-abscasdwdac3432rdwsda.identity.oraclecloud.com + +To run: + python server.py +""" + +import os + +from fastmcp import FastMCP +from fastmcp.server.auth.providers.oci import OCIProvider + +auth = OCIProvider( + client_id=os.getenv("FASTMCP_SERVER_AUTH_IDCS_CLIENT_ID") or "", + client_secret=os.getenv("FASTMCP_SERVER_AUTH_IDCS_CLIENT_SECRET") or "", + config_url=f"https://{os.getenv('FASTMCP_SERVER_AUTH_IDCS_DOMAIN')}/.well-known/openid-configuration" + or "", + base_url="http://localhost:8000", + # redirect_path="/auth/callback", # Default path - change if using a different callback URL +) + +mcp = FastMCP("OCI OAuth Example Server", auth=auth) + + +@mcp.tool +def echo(message: str) -> str: + """Echo the provided message.""" + return message + + +if __name__ == "__main__": + mcp.run(transport="http", port=8000, host="localhost") diff --git a/src/fastmcp/server/auth/providers/oci.py b/src/fastmcp/server/auth/providers/oci.py index 671ad61c6..503317dce 100644 --- a/src/fastmcp/server/auth/providers/oci.py +++ b/src/fastmcp/server/auth/providers/oci.py @@ -180,3 +180,10 @@ class OCIProvider(OIDCProxy): client_id, oci_required_scopes, ) + + def _prepare_scopes_for_token_exchange(self, scopes: list[str]) -> list[str]: + """Omit scope from the upstream auth-code token exchange.""" + logger.debug( + "Omitting scope from upstream token exchange. Original scopes: %s", scopes + ) + return [] diff --git a/tests/server/auth/providers/test_oci.py b/tests/server/auth/providers/test_oci.py new file mode 100644 index 000000000..7ad134656 --- /dev/null +++ b/tests/server/auth/providers/test_oci.py @@ -0,0 +1,87 @@ +"""Unit tests for OCI OAuth provider.""" + +from unittest.mock import patch + +import pytest +from key_value.aio.stores.memory import MemoryStore + +from fastmcp.server.auth.oidc_proxy import OIDCConfiguration +from fastmcp.server.auth.providers.jwt import JWTVerifier +from fastmcp.server.auth.providers.oci import OCIProvider + +TEST_DOMAIN = "idcs-test.identity.oraclecloud.com" +TEST_CONFIG_URL = f"https://{TEST_DOMAIN}/.well-known/openid-configuration" +TEST_CLIENT_ID = "test-client-id" +TEST_CLIENT_SECRET = "test-client-secret" +TEST_AUDIENCE = "test-audience" +TEST_BASE_URL = "https://example.com:8000/" +TEST_REDIRECT_PATH = "/test/callback" +TEST_REQUIRED_SCOPES = ["openid", "profile", "email"] + + +@pytest.fixture +def valid_oidc_configuration_dict(): + """Create a valid OCI OIDC configuration dict for testing.""" + return { + "issuer": "https://identity.oraclecloud.com/", + "authorization_endpoint": f"https://{TEST_DOMAIN}/oauth2/v1/authorize", + "token_endpoint": f"https://{TEST_DOMAIN}/oauth2/v1/token", + "jwks_uri": f"https://{TEST_DOMAIN}/admin/v1/SigningCert/jwk", + "response_types_supported": ["code"], + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": ["RS256"], + } + + +class TestOCIProvider: + """Test OCIProvider initialization.""" + + def test_init_with_explicit_params(self, valid_oidc_configuration_dict): + """Test initialization with explicit parameters.""" + with patch( + "fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration" + ) as mock_get: + oidc_config = OIDCConfiguration.model_validate( + valid_oidc_configuration_dict + ) + mock_get.return_value = oidc_config + + provider = OCIProvider( + config_url=TEST_CONFIG_URL, + client_id=TEST_CLIENT_ID, + client_secret=TEST_CLIENT_SECRET, + audience=TEST_AUDIENCE, + base_url=TEST_BASE_URL, + redirect_path=TEST_REDIRECT_PATH, + required_scopes=TEST_REQUIRED_SCOPES, + client_storage=MemoryStore(), + jwt_signing_key="test-secret-key", + ) + + mock_get.assert_called_once() + + call_args = mock_get.call_args + assert str(call_args[0][0]) == TEST_CONFIG_URL + + assert provider._upstream_client_id == TEST_CLIENT_ID + assert provider._upstream_client_secret is not None + assert ( + provider._upstream_client_secret.get_secret_value() + == TEST_CLIENT_SECRET + ) + + assert ( + provider._upstream_authorization_endpoint + == f"https://{TEST_DOMAIN}/oauth2/v1/authorize" + ) + assert ( + provider._upstream_token_endpoint + == f"https://{TEST_DOMAIN}/oauth2/v1/token" + ) + + assert isinstance(provider._token_validator, JWTVerifier) + assert provider._token_validator.audience == TEST_AUDIENCE + + assert str(provider.base_url) == TEST_BASE_URL + assert provider._redirect_path == TEST_REDIRECT_PATH + assert provider._token_validator.required_scopes == TEST_REQUIRED_SCOPES