"""Protected Resource Metadata for OAuth 2.1 Resource Servers.
Implements RFC 9728 Protected Resource Metadata, allowing MCP clients
to discover the authorization server and supported scopes for this
resource server.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from sso_mcp_server.config import Settings
@dataclass
class ProtectedResourceMetadata:
"""RFC 9728 Protected Resource Metadata.
Represents the metadata document that Resource Servers publish
at their well-known endpoint to describe their OAuth requirements.
Attributes:
resource: The resource identifier (this server's URL).
authorization_servers: List of authorization server issuer URLs.
scopes_supported: List of OAuth scopes supported by this resource.
bearer_methods_supported: Methods for transmitting Bearer tokens.
"""
resource: str
authorization_servers: list[str] = field(default_factory=list)
scopes_supported: list[str] = field(default_factory=list)
bearer_methods_supported: list[str] = field(default_factory=lambda: ["header"])
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization.
Returns:
Dictionary suitable for JSON response.
"""
result: dict = {
"resource": self.resource,
}
if self.authorization_servers:
result["authorization_servers"] = self.authorization_servers
if self.scopes_supported:
result["scopes_supported"] = self.scopes_supported
if self.bearer_methods_supported:
result["bearer_methods_supported"] = self.bearer_methods_supported
return result
def generate_metadata(settings: Settings) -> ProtectedResourceMetadata:
"""Generate Protected Resource Metadata from settings.
Creates an RFC 9728 compliant metadata document based on the
server's configuration.
Args:
settings: Application settings with cloud mode configuration.
Returns:
ProtectedResourceMetadata instance.
"""
return ProtectedResourceMetadata(
resource=settings.resource_identifier,
authorization_servers=settings.allowed_issuers,
scopes_supported=settings.scopes_supported,
bearer_methods_supported=["header"],
)
def build_www_authenticate_header(
settings: Settings,
error: str | None = None,
error_description: str | None = None,
scope: str | None = None,
) -> str:
"""Build WWW-Authenticate header for 401/403 responses.
Constructs an RFC 6750 compliant WWW-Authenticate header with
optional error details and resource metadata URL.
Args:
settings: Application settings.
error: OAuth error code (e.g., "invalid_token", "insufficient_scope").
error_description: Human-readable error description.
scope: Required scope(s) for insufficient_scope errors.
Returns:
WWW-Authenticate header value.
Example:
Bearer realm="sso-mcp-server",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
error="invalid_token",
error_description="Token has expired"
"""
parts = ['Bearer realm="sso-mcp-server"']
# Add resource metadata URL
if settings.resource_identifier:
base_url = settings.resource_identifier.rstrip("/")
metadata_url = f"{base_url}/.well-known/oauth-protected-resource"
parts.append(f'resource_metadata="{metadata_url}"')
# Add error details if present
if error:
parts.append(f'error="{error}"')
if error_description:
# Escape quotes in description
safe_desc = error_description.replace('"', '\\"')
parts.append(f'error_description="{safe_desc}"')
if scope:
parts.append(f'scope="{scope}"')
return ", ".join(parts)