"""Single authoritative OAuth contract for the public ChatGPT MCP surface.""" from __future__ import annotations import os from dataclasses import dataclass from urllib.parse import urlparse DEFAULT_PUBLIC_MCP_ORIGIN = "https://mcp.example.com" DEFAULT_CHATGPT_OIDC_ISSUER = "https://auth.example-host.com/application/o/chatgpt-itworx-gitea/" CHATGPT_OAUTH_SCOPES = ("openid", "profile", "email", "offline_access") @dataclass(frozen=True) class OAuthMetadataSettings: """Validated public values shared by metadata and Bearer challenges.""" public_origin: str issuer: str @classmethod def from_environment(cls) -> OAuthMetadataSettings: public_origin = ( os.environ.get("HUB_PUBLIC_MCP_URL", "").strip() or DEFAULT_PUBLIC_MCP_ORIGIN ).rstrip("/") issuer = ( os.environ.get("HUB_MCP_ADDITIONAL_OIDC_ISSUER", "").strip() or DEFAULT_CHATGPT_OIDC_ISSUER ) public = urlparse(public_origin) authorization_server = urlparse(issuer) if not ( public.scheme == "https" and public.hostname and not public.username and not public.password and public.path in {"", "/"} and not public.query and not public.fragment ): raise ValueError("HUB_PUBLIC_MCP_URL must be a safe HTTPS origin") if not ( authorization_server.scheme == "https" and authorization_server.hostname and not authorization_server.username and not authorization_server.password and authorization_server.path.endswith("/") and not authorization_server.query and not authorization_server.fragment ): raise ValueError("HUB_MCP_ADDITIONAL_OIDC_ISSUER must be a safe HTTPS issuer") return cls(public_origin=public_origin, issuer=issuer) def protected_resource_metadata( resource_path: str, settings: OAuthMetadataSettings ) -> dict[str, object]: """Return RFC 9728 metadata for one public MCP resource.""" return { "resource": f"{settings.public_origin}/mcp/{resource_path}", "authorization_servers": [settings.issuer], "scopes_supported": list(CHATGPT_OAUTH_SCOPES), } def bearer_challenge(resource_path: str, settings: OAuthMetadataSettings) -> str: """Advertise the same refresh-capable scope contract on a 401 response.""" scopes = " ".join(CHATGPT_OAUTH_SCOPES) metadata = f"{settings.public_origin}/.well-known/oauth-protected-resource/mcp/{resource_path}" return f'Bearer resource_metadata="{metadata}", scope="{scopes}"'