Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/mcp/server/auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,28 +85,36 @@ def create_auth_routes(
)
client_authenticator = ClientAuthenticator(provider)

# Extract the base path from the issuer URL so that auth routes are
# registered under the same prefix. This is necessary when the server
# sits behind a gateway with a custom base path (e.g., /custom/path).
issuer_path = urlparse(str(issuer_url)).path.rstrip("/")

# Create routes
# Allow CORS requests for endpoints meant to be hit by the OAuth client
# (with the client secret). This is intended to support things like MCP Inspector,
# where the client runs in a web browser.
routes = [
Route(
"/.well-known/oauth-authorization-server",
# RFC 8414 3.1: the well-known suffix goes between the authority and
# the issuer's path component, not after it — "/custom/path/.well-known/..."
# is not a valid well-known URI per RFC 8615 3.
"/.well-known/oauth-authorization-server" + issuer_path,
endpoint=cors_middleware(
MetadataHandler(metadata).handle,
["GET", "OPTIONS"],
),
methods=["GET", "OPTIONS"],
),
Route(
AUTHORIZATION_PATH,
issuer_path + AUTHORIZATION_PATH,
# do not allow CORS for authorization endpoint;
# clients should just redirect to this
endpoint=AuthorizationHandler(provider).handle,
methods=["GET", "POST"],
),
Route(
TOKEN_PATH,
issuer_path + TOKEN_PATH,
endpoint=cors_middleware(
TokenHandler(
provider, client_authenticator, identity_assertion_enabled=identity_assertion_enabled
Expand All @@ -124,7 +132,7 @@ def create_auth_routes(
)
routes.append(
Route(
REGISTRATION_PATH,
issuer_path + REGISTRATION_PATH,
endpoint=cors_middleware(
registration_handler.handle,
["POST", "OPTIONS"],
Expand All @@ -137,7 +145,7 @@ def create_auth_routes(
revocation_handler = RevocationHandler(provider, client_authenticator)
routes.append(
Route(
REVOCATION_PATH,
issuer_path + REVOCATION_PATH,
endpoint=cors_middleware(
revocation_handler.handle,
["POST", "OPTIONS"],
Expand Down
56 changes: 55 additions & 1 deletion tests/server/auth/test_routes.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import pytest
from pydantic import AnyHttpUrl

from mcp.server.auth.routes import build_metadata, validate_issuer_url
from mcp.server.auth.routes import build_metadata, create_auth_routes, validate_issuer_url
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions, RevocationOptions
from tests.server.mcpserver.auth.test_auth_integration import MockOAuthProvider


def test_validate_issuer_url_https_allowed():
Expand Down Expand Up @@ -70,3 +71,56 @@ def test_build_metadata_serves_issuer_without_trailing_slash():
assert served["issuer"] == "https://as.example.com"
assert served["authorization_endpoint"] == "https://as.example.com/authorize"
assert served["token_endpoint"] == "https://as.example.com/token"


def test_create_auth_routes_default_paths():
"""Auth routes are registered at root when issuer_url has no path."""
provider = MockOAuthProvider()
routes = create_auth_routes(
provider,
issuer_url=AnyHttpUrl("https://example.com"),
client_registration_options=ClientRegistrationOptions(enabled=True),
revocation_options=RevocationOptions(enabled=True),
)
paths = [route.path for route in routes]
assert "/.well-known/oauth-authorization-server" in paths
assert "/authorize" in paths
assert "/token" in paths
assert "/register" in paths
assert "/revoke" in paths


def test_create_auth_routes_custom_base_path():
"""Auth routes are prefixed with the issuer_url path for gateway deployments.

Per RFC 8414 3.1 / RFC 8615 3, the well-known discovery URI is rooted at the
domain and inserted *before* the issuer's path component, not after it.
"""
provider = MockOAuthProvider()
routes = create_auth_routes(
provider,
issuer_url=AnyHttpUrl("https://example.com/custom/path"),
client_registration_options=ClientRegistrationOptions(enabled=True),
revocation_options=RevocationOptions(enabled=True),
)
paths = [route.path for route in routes]
assert "/.well-known/oauth-authorization-server/custom/path" in paths
assert "/custom/path/authorize" in paths
assert "/custom/path/token" in paths
assert "/custom/path/register" in paths
assert "/custom/path/revoke" in paths


def test_create_auth_routes_trailing_slash_stripped():
"""Trailing slash on issuer_url path is stripped to avoid double slashes."""
provider = MockOAuthProvider()
routes = create_auth_routes(
provider,
issuer_url=AnyHttpUrl("https://example.com/base/"),
client_registration_options=ClientRegistrationOptions(enabled=True),
revocation_options=RevocationOptions(enabled=True),
)
paths = [route.path for route in routes]
assert "/.well-known/oauth-authorization-server/base" in paths
assert "/base/authorize" in paths
assert "/base/token" in paths
Loading