Fix building auth metadata paths (#779)

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
This commit is contained in:
Pedro Rodrigues
2025-05-26 06:19:18 -07:00
committed by GitHub
parent 8a2359ffeb
commit 6e418e62f9
2 changed files with 85 additions and 21 deletions

View File

@@ -147,31 +147,19 @@ def create_auth_routes(
return routes return routes
def modify_url_path(url: AnyHttpUrl, path_mapper: Callable[[str], str]) -> AnyHttpUrl:
return AnyHttpUrl.build(
scheme=url.scheme,
username=url.username,
password=url.password,
host=url.host,
port=url.port,
path=path_mapper(url.path or ""),
query=url.query,
fragment=url.fragment,
)
def build_metadata( def build_metadata(
issuer_url: AnyHttpUrl, issuer_url: AnyHttpUrl,
service_documentation_url: AnyHttpUrl | None, service_documentation_url: AnyHttpUrl | None,
client_registration_options: ClientRegistrationOptions, client_registration_options: ClientRegistrationOptions,
revocation_options: RevocationOptions, revocation_options: RevocationOptions,
) -> OAuthMetadata: ) -> OAuthMetadata:
authorization_url = modify_url_path( authorization_url = AnyHttpUrl(
issuer_url, lambda path: path.rstrip("/") + AUTHORIZATION_PATH.lstrip("/") str(issuer_url).rstrip("/") + AUTHORIZATION_PATH
) )
token_url = modify_url_path( token_url = AnyHttpUrl(
issuer_url, lambda path: path.rstrip("/") + TOKEN_PATH.lstrip("/") str(issuer_url).rstrip("/") + TOKEN_PATH
) )
# Create metadata # Create metadata
metadata = OAuthMetadata( metadata = OAuthMetadata(
issuer=issuer_url, issuer=issuer_url,
@@ -193,14 +181,14 @@ def build_metadata(
# Add registration endpoint if supported # Add registration endpoint if supported
if client_registration_options.enabled: if client_registration_options.enabled:
metadata.registration_endpoint = modify_url_path( metadata.registration_endpoint = AnyHttpUrl(
issuer_url, lambda path: path.rstrip("/") + REGISTRATION_PATH.lstrip("/") str(issuer_url).rstrip("/") + REGISTRATION_PATH
) )
# Add revocation endpoint if supported # Add revocation endpoint if supported
if revocation_options.enabled: if revocation_options.enabled:
metadata.revocation_endpoint = modify_url_path( metadata.revocation_endpoint = AnyHttpUrl(
issuer_url, lambda path: path.rstrip("/") + REVOCATION_PATH.lstrip("/") str(issuer_url).rstrip("/") + REVOCATION_PATH
) )
metadata.revocation_endpoint_auth_methods_supported = ["client_secret_post"] metadata.revocation_endpoint_auth_methods_supported = ["client_secret_post"]

View File

@@ -10,9 +10,12 @@ from urllib.parse import parse_qs, urlparse
import httpx import httpx
import pytest import pytest
from inline_snapshot import snapshot
from pydantic import AnyHttpUrl from pydantic import AnyHttpUrl
from mcp.client.auth import OAuthClientProvider from mcp.client.auth import OAuthClientProvider
from mcp.server.auth.routes import build_metadata
from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions
from mcp.shared.auth import ( from mcp.shared.auth import (
OAuthClientInformationFull, OAuthClientInformationFull,
OAuthClientMetadata, OAuthClientMetadata,
@@ -905,3 +908,76 @@ class TestOAuthClientProvider:
await oauth_provider._exchange_code_for_token( await oauth_provider._exchange_code_for_token(
"invalid_auth_code", oauth_client_info "invalid_auth_code", oauth_client_info
) )
@pytest.mark.parametrize(
(
"issuer_url",
"service_documentation_url",
"authorization_endpoint",
"token_endpoint",
"registration_endpoint",
"revocation_endpoint",
),
(
pytest.param(
"https://auth.example.com",
"https://auth.example.com/docs",
"https://auth.example.com/authorize",
"https://auth.example.com/token",
"https://auth.example.com/register",
"https://auth.example.com/revoke",
id="simple-url",
),
pytest.param(
"https://auth.example.com/",
"https://auth.example.com/docs",
"https://auth.example.com/authorize",
"https://auth.example.com/token",
"https://auth.example.com/register",
"https://auth.example.com/revoke",
id="with-trailing-slash",
),
pytest.param(
"https://auth.example.com/v1/mcp",
"https://auth.example.com/v1/mcp/docs",
"https://auth.example.com/v1/mcp/authorize",
"https://auth.example.com/v1/mcp/token",
"https://auth.example.com/v1/mcp/register",
"https://auth.example.com/v1/mcp/revoke",
id="with-path-param",
),
),
)
def test_build_metadata(
issuer_url: str,
service_documentation_url: str,
authorization_endpoint: str,
token_endpoint: str,
registration_endpoint: str,
revocation_endpoint: str,
):
metadata = build_metadata(
issuer_url=AnyHttpUrl(issuer_url),
service_documentation_url=AnyHttpUrl(service_documentation_url),
client_registration_options=ClientRegistrationOptions(
enabled=True, valid_scopes=["read", "write", "admin"]
),
revocation_options=RevocationOptions(enabled=True),
)
assert metadata == snapshot(
OAuthMetadata(
issuer=AnyHttpUrl(issuer_url),
authorization_endpoint=AnyHttpUrl(authorization_endpoint),
token_endpoint=AnyHttpUrl(token_endpoint),
registration_endpoint=AnyHttpUrl(registration_endpoint),
scopes_supported=["read", "write", "admin"],
grant_types_supported=["authorization_code", "refresh_token"],
token_endpoint_auth_methods_supported=["client_secret_post"],
service_documentation=AnyHttpUrl(service_documentation_url),
revocation_endpoint=AnyHttpUrl(revocation_endpoint),
revocation_endpoint_auth_methods_supported=["client_secret_post"],
code_challenge_methods_supported=["S256"],
)
)