Skip to content

Commit 5250a9c

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 d5b9155 commit 5250a9c

File tree

2 files changed

+62
-6
lines changed

2 files changed

+62
-6
lines changed

src/mcp/server/auth/routes.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,28 +80,33 @@ def create_auth_routes(
8080
)
8181
client_authenticator = ClientAuthenticator(provider)
8282

83+
# Extract the base path from the issuer URL so that auth routes are
84+
# registered under the same prefix. This is necessary when the server
85+
# sits behind a gateway with a custom base path (e.g., /custom/path).
86+
issuer_path = urlparse(str(issuer_url)).path.rstrip("/")
87+
8388
# Create routes
8489
# Allow CORS requests for endpoints meant to be hit by the OAuth client
8590
# (with the client secret). This is intended to support things like MCP Inspector,
8691
# where the client runs in a web browser.
8792
routes = [
8893
Route(
89-
"/.well-known/oauth-authorization-server",
94+
issuer_path + "/.well-known/oauth-authorization-server",
9095
endpoint=cors_middleware(
9196
MetadataHandler(metadata).handle,
9297
["GET", "OPTIONS"],
9398
),
9499
methods=["GET", "OPTIONS"],
95100
),
96101
Route(
97-
AUTHORIZATION_PATH,
102+
issuer_path + AUTHORIZATION_PATH,
98103
# do not allow CORS for authorization endpoint;
99104
# clients should just redirect to this
100105
endpoint=AuthorizationHandler(provider).handle,
101106
methods=["GET", "POST"],
102107
),
103108
Route(
104-
TOKEN_PATH,
109+
issuer_path + TOKEN_PATH,
105110
endpoint=cors_middleware(
106111
TokenHandler(provider, client_authenticator).handle,
107112
["POST", "OPTIONS"],
@@ -117,7 +122,7 @@ def create_auth_routes(
117122
)
118123
routes.append(
119124
Route(
120-
REGISTRATION_PATH,
125+
issuer_path + REGISTRATION_PATH,
121126
endpoint=cors_middleware(
122127
registration_handler.handle,
123128
["POST", "OPTIONS"],
@@ -130,7 +135,7 @@ def create_auth_routes(
130135
revocation_handler = RevocationHandler(provider, client_authenticator)
131136
routes.append(
132137
Route(
133-
REVOCATION_PATH,
138+
issuer_path + REVOCATION_PATH,
134139
endpoint=cors_middleware(
135140
revocation_handler.handle,
136141
["POST", "OPTIONS"],

tests/server/auth/test_routes.py

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

4-
from mcp.server.auth.routes import validate_issuer_url
4+
from mcp.server.auth.routes import create_auth_routes, validate_issuer_url
5+
from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions
6+
from tests.server.mcpserver.auth.test_auth_integration import MockOAuthProvider
57

68

79
def test_validate_issuer_url_https_allowed():
@@ -45,3 +47,52 @@ def test_validate_issuer_url_fragment_rejected():
4547
def test_validate_issuer_url_query_rejected():
4648
with pytest.raises(ValueError, match="query"):
4749
validate_issuer_url(AnyHttpUrl("https://example.com/path?q=1"))
50+
51+
52+
def test_create_auth_routes_default_paths():
53+
"""Auth routes are registered at root when issuer_url has no path."""
54+
provider = MockOAuthProvider()
55+
routes = create_auth_routes(
56+
provider,
57+
issuer_url=AnyHttpUrl("https://example.com"),
58+
client_registration_options=ClientRegistrationOptions(enabled=True),
59+
revocation_options=RevocationOptions(enabled=True),
60+
)
61+
paths = [route.path for route in routes]
62+
assert "/.well-known/oauth-authorization-server" in paths
63+
assert "/authorize" in paths
64+
assert "/token" in paths
65+
assert "/register" in paths
66+
assert "/revoke" in paths
67+
68+
69+
def test_create_auth_routes_custom_base_path():
70+
"""Auth routes are prefixed with the issuer_url path for gateway deployments."""
71+
provider = MockOAuthProvider()
72+
routes = create_auth_routes(
73+
provider,
74+
issuer_url=AnyHttpUrl("https://example.com/custom/path"),
75+
client_registration_options=ClientRegistrationOptions(enabled=True),
76+
revocation_options=RevocationOptions(enabled=True),
77+
)
78+
paths = [route.path for route in routes]
79+
assert "/custom/path/.well-known/oauth-authorization-server" in paths
80+
assert "/custom/path/authorize" in paths
81+
assert "/custom/path/token" in paths
82+
assert "/custom/path/register" in paths
83+
assert "/custom/path/revoke" in paths
84+
85+
86+
def test_create_auth_routes_trailing_slash_stripped():
87+
"""Trailing slash on issuer_url path is stripped to avoid double slashes."""
88+
provider = MockOAuthProvider()
89+
routes = create_auth_routes(
90+
provider,
91+
issuer_url=AnyHttpUrl("https://example.com/base/"),
92+
client_registration_options=ClientRegistrationOptions(enabled=True),
93+
revocation_options=RevocationOptions(enabled=True),
94+
)
95+
paths = [route.path for route in routes]
96+
assert "/base/.well-known/oauth-authorization-server" in paths
97+
assert "/base/authorize" in paths
98+
assert "/base/token" in paths

0 commit comments

Comments
 (0)