Files
ITWorx_MCP_Hub-Public/services/runtime/oauth_metadata.py
T

73 lines
2.6 KiB
Python

"""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}"'