From 5250a9c16a5650575a32ffd5cd54a8cfb7fdf514 Mon Sep 17 00:00:00 2001 From: Enjoy Kumawat Date: Wed, 8 Apr 2026 01:20:00 +0530 Subject: [PATCH] fix: prefix auth routes with issuer_url base path When an MCP server is deployed behind a gateway with a custom base path (e.g., /custom/path), the OAuth auth routes (.well-known, /authorize, /token, /register, /revoke) were hardcoded at root, making them unreachable through the gateway. Extract the path component from issuer_url and prefix it to all auth route registrations. This matches the metadata URLs already built by build_metadata(), which correctly use issuer_url + path. Backward compatible: when issuer_url has no path, routes stay at root. Github-Issue: #1335 Reported-by: whitewg77 --- src/mcp/server/auth/routes.py | 15 ++++++--- tests/server/auth/test_routes.py | 53 +++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/mcp/server/auth/routes.py b/src/mcp/server/auth/routes.py index a72e81947..18457fc37 100644 --- a/src/mcp/server/auth/routes.py +++ b/src/mcp/server/auth/routes.py @@ -80,13 +80,18 @@ 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", + issuer_path + "/.well-known/oauth-authorization-server", endpoint=cors_middleware( MetadataHandler(metadata).handle, ["GET", "OPTIONS"], @@ -94,14 +99,14 @@ def create_auth_routes( 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).handle, ["POST", "OPTIONS"], @@ -117,7 +122,7 @@ def create_auth_routes( ) routes.append( Route( - REGISTRATION_PATH, + issuer_path + REGISTRATION_PATH, endpoint=cors_middleware( registration_handler.handle, ["POST", "OPTIONS"], @@ -130,7 +135,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"], diff --git a/tests/server/auth/test_routes.py b/tests/server/auth/test_routes.py index 3d13b5ba5..41fad1b40 100644 --- a/tests/server/auth/test_routes.py +++ b/tests/server/auth/test_routes.py @@ -1,7 +1,9 @@ import pytest from pydantic import AnyHttpUrl -from mcp.server.auth.routes import validate_issuer_url +from mcp.server.auth.routes import create_auth_routes, validate_issuer_url +from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions +from tests.server.mcpserver.auth.test_auth_integration import MockOAuthProvider def test_validate_issuer_url_https_allowed(): @@ -45,3 +47,52 @@ def test_validate_issuer_url_fragment_rejected(): def test_validate_issuer_url_query_rejected(): with pytest.raises(ValueError, match="query"): validate_issuer_url(AnyHttpUrl("https://example.com/path?q=1")) + + +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.""" + 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 "/custom/path/.well-known/oauth-authorization-server" 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 "/base/.well-known/oauth-authorization-server" in paths + assert "/base/authorize" in paths + assert "/base/token" in paths