From fd2d465a4c11fd00757946c0df72c6788a8e74dc Mon Sep 17 00:00:00 2001 From: Enjoy Kumawat Date: Wed, 8 Apr 2026 01:20:00 +0530 Subject: [PATCH 1/2] 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 | 52 +++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/mcp/server/auth/routes.py b/src/mcp/server/auth/routes.py index fa88dddcf4..e21cac27d0 100644 --- a/src/mcp/server/auth/routes.py +++ b/src/mcp/server/auth/routes.py @@ -85,13 +85,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"], @@ -99,14 +104,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, identity_assertion_enabled=identity_assertion_enabled @@ -124,7 +129,7 @@ def create_auth_routes( ) routes.append( Route( - REGISTRATION_PATH, + issuer_path + REGISTRATION_PATH, endpoint=cors_middleware( registration_handler.handle, ["POST", "OPTIONS"], @@ -137,7 +142,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 58685c64c7..2588bfcc1f 100644 --- a/tests/server/auth/test_routes.py +++ b/tests/server/auth/test_routes.py @@ -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(): @@ -70,3 +71,52 @@ 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.""" + 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 From a3918ebe403bc1207fd85c4417fe92e9a8a6da9d Mon Sep 17 00:00:00 2001 From: Enjoy Kumawat Date: Wed, 5 Aug 2026 15:55:56 +0530 Subject: [PATCH 2/2] fix: place well-known discovery route per RFC 8414 3.1 The .well-known/oauth-authorization-server route was prefixed with the issuer's base path *after* the well-known suffix (e.g. /custom/path/.well-known/oauth-authorization-server), which RFC 8615 3 does not recognize as a well-known URI. RFC 8414 3.1 requires the suffix to be inserted between the authority and the path component instead: /.well-known/oauth-authorization-server/custom/path. The /authorize, /token, /register, /revoke routes are unaffected - they're plain URLs under the issuer's namespace, not well-known URIs. --- src/mcp/server/auth/routes.py | 5 ++++- tests/server/auth/test_routes.py | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/mcp/server/auth/routes.py b/src/mcp/server/auth/routes.py index e21cac27d0..d634c3924b 100644 --- a/src/mcp/server/auth/routes.py +++ b/src/mcp/server/auth/routes.py @@ -96,7 +96,10 @@ def create_auth_routes( # where the client runs in a web browser. routes = [ Route( - issuer_path + "/.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"], diff --git a/tests/server/auth/test_routes.py b/tests/server/auth/test_routes.py index 2588bfcc1f..8ba2bcddca 100644 --- a/tests/server/auth/test_routes.py +++ b/tests/server/auth/test_routes.py @@ -91,7 +91,11 @@ def test_create_auth_routes_default_paths(): def test_create_auth_routes_custom_base_path(): - """Auth routes are prefixed with the issuer_url path for gateway deployments.""" + """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, @@ -100,7 +104,7 @@ def test_create_auth_routes_custom_base_path(): revocation_options=RevocationOptions(enabled=True), ) paths = [route.path for route in routes] - assert "/custom/path/.well-known/oauth-authorization-server" in paths + 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 @@ -117,6 +121,6 @@ def test_create_auth_routes_trailing_slash_stripped(): revocation_options=RevocationOptions(enabled=True), ) paths = [route.path for route in routes] - assert "/base/.well-known/oauth-authorization-server" in paths + assert "/.well-known/oauth-authorization-server/base" in paths assert "/base/authorize" in paths assert "/base/token" in paths