"""OCI OIDC provider for FastMCP. The pull request for the provider is submitted to fastmcp. This module provides OIDC Implementation to integrate MCP servers with OCI. You only need OCI Identity Domain's discovery URL, client ID, client secret, and base URL. Post Authentication, you get OCI IAM domain access token. That is not authorized to invoke OCI control plane. You need to exchange the IAM domain access token for OCI UPST token to invoke OCI control plane APIs. The sample code below has get_oci_signer function that returns OCI TokenExchangeSigner object. You can use the signer object to create OCI service object. Example: ```python from fastmcp import FastMCP from fastmcp.server.auth.providers.oci import OCIProvider from fastmcp.server.dependencies import get_access_token from fastmcp.utilities.logging import get_logger import os import oci from oci.auth.signers import TokenExchangeSigner logger = get_logger(__name__) # Load configuration from environment config_url = os.environ.get("OCI_CONFIG_URL") # OCI IAM Domain OIDC discovery URL client_id = os.environ.get("OCI_CLIENT_ID") # Client ID configured for the OCI IAM Domain Integrated Application client_secret = os.environ.get("OCI_CLIENT_SECRET") # Client secret configured for the OCI IAM Domain Integrated Application iam_guid = os.environ.get("OCI_IAM_GUID") # IAM GUID configured for the OCI IAM Domain # Simple OCI OIDC protection auth = OCIProvider( config_url=config_url, # config URL is the OCI IAM Domain OIDC discovery URL client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application required_scopes=["openid", "profile", "email"], redirect_path="/auth/callback", base_url="http://localhost:8000", ) # NOTE: For production use, replace this with a thread-safe cache implementation # such as threading.Lock-protected dict or a proper caching library _global_token_cache = {} # In memory cache for OCI session token signer def get_oci_signer() -> TokenExchangeSigner: authntoken = get_access_token() tokenID = authntoken.claims.get("jti") token = authntoken.token # Check if the signer exists for the token ID in memory cache cached_signer = _global_token_cache.get(tokenID) logger.debug(f"Global cached signer: {cached_signer}") if cached_signer: logger.debug(f"Using globally cached signer for token ID: {tokenID}") return cached_signer # If the signer is not yet created for the token then create new OCI signer object logger.debug(f"Creating new signer for token ID: {tokenID}") signer = TokenExchangeSigner( jwt_or_func=token, oci_domain_id=iam_guid.split(".")[0] if iam_guid else None, # This is same as IAM GUID configured for the OCI IAM Domain client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application ) logger.debug(f"Signer {signer} created for token ID: {tokenID}") #Cache the signer object in memory cache _global_token_cache[tokenID] = signer logger.debug(f"Signer cached for token ID: {tokenID}") return signer mcp = FastMCP("My Protected Server", auth=auth) ``` """ from typing import Literal from key_value.aio.protocols import AsyncKeyValue from pydantic import AnyHttpUrl from fastmcp.server.auth.oidc_proxy import ( DEFAULT_OIDC_DISCOVERY_TIMEOUT_SECONDS, OIDCProxy, ) from fastmcp.utilities.auth import parse_scopes from fastmcp.utilities.logging import get_logger logger = get_logger(__name__) class OCIProvider(OIDCProxy): """An OCI IAM Domain provider implementation for FastMCP. This provider is a complete OCI integration that's ready to use with just the configuration URL, client ID, client secret, and base URL. Example: ```python from fastmcp import FastMCP from fastmcp.server.auth.providers.oci import OCIProvider import os # Load configuration from environment auth = OCIProvider( config_url=os.environ.get("OCI_CONFIG_URL"), # OCI IAM Domain OIDC discovery URL client_id=os.environ.get("OCI_CLIENT_ID"), # Client ID configured for the OCI IAM Domain Integrated Application client_secret=os.environ.get("OCI_CLIENT_SECRET"), # Client secret configured for the OCI IAM Domain Integrated Application base_url="http://localhost:8000", required_scopes=["openid", "profile", "email"], redirect_path="/auth/callback", ) mcp = FastMCP("My Protected Server", auth=auth) ``` """ def __init__( self, *, config_url: AnyHttpUrl | str, client_id: str, client_secret: str, timeout_seconds: int | None = DEFAULT_OIDC_DISCOVERY_TIMEOUT_SECONDS, base_url: AnyHttpUrl | str, resource_base_url: AnyHttpUrl | str | None = None, audience: str | None = None, issuer_url: AnyHttpUrl | str | None = None, required_scopes: list[str] | None = None, redirect_path: str | None = None, allowed_client_redirect_uris: list[str] | None = None, client_storage: AsyncKeyValue | None = None, jwt_signing_key: str | bytes | None = None, require_authorization_consent: bool | Literal["remember", "external"] = True, consent_csp_policy: str | None = None, forward_resource: bool = True, fallback_refresh_token_expiry_seconds: int | None = None, fastmcp_access_token_expiry_seconds: int | None = None, token_expiry_threshold_seconds: int = 0, ) -> None: """Initialize OCI OIDC provider. Args: config_url: OCI OIDC Discovery URL client_id: OCI IAM Domain Integrated Application client id client_secret: OCI Integrated Application client secret timeout_seconds: Timeout, in seconds, for the OIDC discovery request made during construction. Defaults to 10 seconds so a slow or unreachable issuer cannot block server startup indefinitely. Pass None to fall back to the HTTP client's own default timeout. base_url: Public URL where OIDC endpoints will be accessible (includes any mount path) resource_base_url: Optional public base URL for the protected resource metadata and token audience. Defaults to ``base_url``. audience: OCI API audience (optional) issuer_url: Issuer URL for OCI IAM Domain metadata. This will override issuer URL from the discovery URL. required_scopes: Required OCI scopes (defaults to ["openid"]) redirect_path: Redirect path configured in OCI IAM Domain Integrated Application. The default is "/auth/callback". allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. fallback_refresh_token_expiry_seconds: Lifetime for the FastMCP-issued refresh token when the upstream provider omits `refresh_expires_in` (e.g. Cognito, GitHub, many OIDC IdPs). Defaults to 1 year. The upstream refresh remains the source of truth. See `OAuthProxy` for details. fastmcp_access_token_expiry_seconds: Lifetime for the FastMCP-issued access token, decoupling it from the upstream provider's `expires_in`. Defaults to None (mirror the upstream lifetime). Set this for bridges whose upstream issues short-lived access tokens that some MCP clients can't refresh gracefully (e.g. `mcp-remote`). See `OAuthProxy` for details. token_expiry_threshold_seconds: Number of seconds before actual expiry to treat a token as expired, refreshing early to avoid races. Defaults to 0. """ # Parse scopes if provided as string oci_required_scopes = ( parse_scopes(required_scopes) if required_scopes is not None else ["openid"] ) super().__init__( config_url=config_url, client_id=client_id, client_secret=client_secret, audience=audience, timeout_seconds=timeout_seconds, base_url=base_url, resource_base_url=resource_base_url, issuer_url=issuer_url, redirect_path=redirect_path, required_scopes=oci_required_scopes, allowed_client_redirect_uris=allowed_client_redirect_uris, client_storage=client_storage, jwt_signing_key=jwt_signing_key, require_authorization_consent=require_authorization_consent, consent_csp_policy=consent_csp_policy, forward_resource=forward_resource, fallback_refresh_token_expiry_seconds=fallback_refresh_token_expiry_seconds, fastmcp_access_token_expiry_seconds=fastmcp_access_token_expiry_seconds, token_expiry_threshold_seconds=token_expiry_threshold_seconds, ) logger.debug( "Initialized OCI OAuth provider for client %s with scopes: %s", 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 []