Skip to content

Commit fd2d465

Browse files
committed
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
1 parent a4f4ccd commit fd2d465

2 files changed

Lines changed: 61 additions & 6 deletions

File tree

src/mcp/server/auth/routes.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,28 +85,33 @@ def create_auth_routes(
8585
)
8686
client_authenticator = ClientAuthenticator(provider)
8787

88+
# Extract the base path from the issuer URL so that auth routes are
89+
# registered under the same prefix. This is necessary when the server
90+
# sits behind a gateway with a custom base path (e.g., /custom/path).
91+
issuer_path = urlparse(str(issuer_url)).path.rstrip("/")
92+
8893
# Create routes
8994
# Allow CORS requests for endpoints meant to be hit by the OAuth client
9095
# (with the client secret). This is intended to support things like MCP Inspector,
9196
# where the client runs in a web browser.
9297
routes = [
9398
Route(
94-
"/.well-known/oauth-authorization-server",
99+
issuer_path + "/.well-known/oauth-authorization-server",
95100
endpoint=cors_middleware(
96101
MetadataHandler(metadata).handle,
97102
["GET", "OPTIONS"],
98103
),
99104
methods=["GET", "OPTIONS"],
100105
),
101106
Route(
102-
AUTHORIZATION_PATH,
107+
issuer_path + AUTHORIZATION_PATH,
103108
# do not allow CORS for authorization endpoint;
104109
# clients should just redirect to this
105110
endpoint=AuthorizationHandler(provider).handle,
106111
methods=["GET", "POST"],
107112
),
108113
Route(
109-
TOKEN_PATH,
114+
issuer_path + TOKEN_PATH,
110115
endpoint=cors_middleware(
111116
TokenHandler(
112117
provider, client_authenticator, identity_assertion_enabled=identity_assertion_enabled
@@ -124,7 +129,7 @@ def create_auth_routes(
124129
)
125130
routes.append(
126131
Route(
127-
REGISTRATION_PATH,
132+
issuer_path + REGISTRATION_PATH,
128133
endpoint=cors_middleware(
129134
registration_handler.handle,
130135
["POST", "OPTIONS"],
@@ -137,7 +142,7 @@ def create_auth_routes(
137142
revocation_handler = RevocationHandler(provider, client_authenticator)
138143
routes.append(
139144
Route(
140-
REVOCATION_PATH,
145+
issuer_path + REVOCATION_PATH,
141146
endpoint=cors_middleware(
142147
revocation_handler.handle,
143148
["POST", "OPTIONS"],

tests/server/auth/test_routes.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import pytest
22
from pydantic import AnyHttpUrl
33

4-
from mcp.server.auth.routes import build_metadata, validate_issuer_url
4+
from mcp.server.auth.routes import build_metadata, create_auth_routes, validate_issuer_url
55
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions, RevocationOptions
6+
from tests.server.mcpserver.auth.test_auth_integration import MockOAuthProvider
67

78

89
def test_validate_issuer_url_https_allowed():
@@ -70,3 +71,52 @@ def test_build_metadata_serves_issuer_without_trailing_slash():
7071
assert served["issuer"] == "https://as.example.com"
7172
assert served["authorization_endpoint"] == "https://as.example.com/authorize"
7273
assert served["token_endpoint"] == "https://as.example.com/token"
74+
75+
76+
def test_create_auth_routes_default_paths():
77+
"""Auth routes are registered at root when issuer_url has no path."""
78+
provider = MockOAuthProvider()
79+
routes = create_auth_routes(
80+
provider,
81+
issuer_url=AnyHttpUrl("https://example.com"),
82+
client_registration_options=ClientRegistrationOptions(enabled=True),
83+
revocation_options=RevocationOptions(enabled=True),
84+
)
85+
paths = [route.path for route in routes]
86+
assert "/.well-known/oauth-authorization-server" in paths
87+
assert "/authorize" in paths
88+
assert "/token" in paths
89+
assert "/register" in paths
90+
assert "/revoke" in paths
91+
92+
93+
def test_create_auth_routes_custom_base_path():
94+
"""Auth routes are prefixed with the issuer_url path for gateway deployments."""
95+
provider = MockOAuthProvider()
96+
routes = create_auth_routes(
97+
provider,
98+
issuer_url=AnyHttpUrl("https://example.com/custom/path"),
99+
client_registration_options=ClientRegistrationOptions(enabled=True),
100+
revocation_options=RevocationOptions(enabled=True),
101+
)
102+
paths = [route.path for route in routes]
103+
assert "/custom/path/.well-known/oauth-authorization-server" in paths
104+
assert "/custom/path/authorize" in paths
105+
assert "/custom/path/token" in paths
106+
assert "/custom/path/register" in paths
107+
assert "/custom/path/revoke" in paths
108+
109+
110+
def test_create_auth_routes_trailing_slash_stripped():
111+
"""Trailing slash on issuer_url path is stripped to avoid double slashes."""
112+
provider = MockOAuthProvider()
113+
routes = create_auth_routes(
114+
provider,
115+
issuer_url=AnyHttpUrl("https://example.com/base/"),
116+
client_registration_options=ClientRegistrationOptions(enabled=True),
117+
revocation_options=RevocationOptions(enabled=True),
118+
)
119+
paths = [route.path for route in routes]
120+
assert "/base/.well-known/oauth-authorization-server" in paths
121+
assert "/base/authorize" in paths
122+
assert "/base/token" in paths

0 commit comments

Comments
 (0)