Improve OAuth protected resource metadata URL construction per RFC 9728 (#1407)

This commit is contained in:
Marcus Shu 2025-10-06 20:52:44 +08:00 • committed by GitHub
commit df3e428ee8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 192 additions and 32 deletions

View file

@ -1,5 +1,6 @@
from collections.abc import Awaitable, Callable
from typing import Any
from urllib.parse import urlparse
from pydantic import AnyHttpUrl
from starlette.middleware.cors import CORSMiddleware
@ -186,6 +187,25 @@ def build_metadata(
return metadata
def build_resource_metadata_url(resource_server_url: AnyHttpUrl) -> AnyHttpUrl:
"""
Build RFC 9728 compliant protected resource metadata URL.
Inserts /.well-known/oauth-protected-resource between host and resource path
as specified in RFC 9728 §3.1.
Args:
resource_server_url: The resource server URL (e.g., https://example.com/mcp)
Returns:
The metadata URL (e.g., https://example.com/.well-known/oauth-protected-resource/mcp)
"""
parsed = urlparse(str(resource_server_url))
# Handle trailing slash: if path is just "/", treat as empty
resource_path = parsed.path if parsed.path != "/" else ""
return AnyHttpUrl(f"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-protected-resource{resource_path}")
def create_protected_resource_routes(
resource_url: AnyHttpUrl,
authorization_servers: list[AnyHttpUrl],
@ -218,9 +238,15 @@ def create_protected_resource_routes(
handler = ProtectedResourceMetadataHandler(metadata)
# RFC 9728 §3.1: Register route at /.well-known/oauth-protected-resource + resource path
metadata_url = build_resource_metadata_url(resource_url)
# Extract just the path part for route registration
parsed = urlparse(str(metadata_url))
well_known_path = parsed.path
return [
Route(
"/.well-known/oauth-protected-resource",
well_known_path,
endpoint=cors_middleware(handler.handle, ["GET", "OPTIONS"]),
methods=["GET", "OPTIONS"],
)

View file

@ -825,11 +825,10 @@ class FastMCP(Generic[LifespanResultT]):
# Determine resource metadata URL
resource_metadata_url = None
if self.settings.auth and self.settings.auth.resource_server_url:
from pydantic import AnyHttpUrl
from mcp.server.auth.routes import build_resource_metadata_url
resource_metadata_url = AnyHttpUrl(
str(self.settings.auth.resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource"
)
# Build compliant metadata URL for WWW-Authenticate header
resource_metadata_url = build_resource_metadata_url(self.settings.auth.resource_server_url)
# Auth is enabled, wrap the endpoints with RequireAuthMiddleware
routes.append(
@ -938,11 +937,10 @@ class FastMCP(Generic[LifespanResultT]):
# Determine resource metadata URL
resource_metadata_url = None
if self.settings.auth and self.settings.auth.resource_server_url:
from pydantic import AnyHttpUrl
from mcp.server.auth.routes import build_resource_metadata_url
resource_metadata_url = AnyHttpUrl(
str(self.settings.auth.resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource"
)
# Build compliant metadata URL for WWW-Authenticate header
resource_metadata_url = build_resource_metadata_url(self.settings.auth.resource_server_url)
routes.append(
Route(
@ -961,23 +959,13 @@ class FastMCP(Generic[LifespanResultT]):
# Add protected resource metadata endpoint if configured as RS
if self.settings.auth and self.settings.auth.resource_server_url:
from mcp.server.auth.handlers.metadata import ProtectedResourceMetadataHandler
from mcp.server.auth.routes import cors_middleware
from mcp.shared.auth import ProtectedResourceMetadata
from mcp.server.auth.routes import create_protected_resource_routes
protected_resource_metadata = ProtectedResourceMetadata(
resource=self.settings.auth.resource_server_url,
authorization_servers=[self.settings.auth.issuer_url],
scopes_supported=self.settings.auth.required_scopes,
)
routes.append(
Route(
"/.well-known/oauth-protected-resource",
endpoint=cors_middleware(
ProtectedResourceMetadataHandler(protected_resource_metadata).handle,
["GET", "OPTIONS"],
),
methods=["GET", "OPTIONS"],
routes.extend(
create_protected_resource_routes(
resource_url=self.settings.auth.resource_server_url,
authorization_servers=[self.settings.auth.issuer_url],
scopes_supported=self.settings.auth.required_scopes,
)
)