73 lines
2.6 KiB
Python
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}"'
|