Skip to content

Commit 92cb5f1

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 57394b0 commit 92cb5f1

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
@@ -94,29 +94,34 @@ def create_auth_routes(
9494
client_authenticator = ClientAuthenticator(provider)
9595
token_handler = TokenHandler(provider, client_authenticator, identity_assertion_enabled=identity_assertion_enabled)
9696

97+
# Extract the base path from the issuer URL so that auth routes are
98+
# registered under the same prefix. This is necessary when the server
99+
# sits behind a gateway with a custom base path (e.g., /custom/path).
100+
issuer_path = urlparse(str(issuer_url)).path.rstrip("/")
101+
97102
# Create routes
98103
# Allow CORS requests for endpoints meant to be hit by the OAuth client
99104
# (with the client secret). This is intended to support things like MCP Inspector,
100105
# where the client runs in a web browser. CORS is the outermost wrapper so that
101106
# responses produced by inner layers (such as a 413) still carry CORS headers.
102107
routes = [
103108
Route(
104-
"/.well-known/oauth-authorization-server",
109+
issuer_path + "/.well-known/oauth-authorization-server",
105110
endpoint=cors_middleware(
106111
MetadataHandler(metadata).handle,
107112
["GET", "OPTIONS"],
108113
),
109114
methods=["GET", "OPTIONS"],
110115
),
111116
Route(
112-
AUTHORIZATION_PATH,
117+
issuer_path + AUTHORIZATION_PATH,
113118
# do not allow CORS for authorization endpoint;
114119
# clients should just redirect to this
115120
endpoint=_body_limited(request_response(AuthorizationHandler(provider).handle)),
116121
methods=["GET", "POST"],
117122
),
118123
Route(
119-
TOKEN_PATH,
124+
issuer_path + TOKEN_PATH,
120125
endpoint=_cors(_body_limited(request_response(token_handler.handle)), ["POST", "OPTIONS"]),
121126
methods=["POST", "OPTIONS"],
122127
),
@@ -129,7 +134,7 @@ def create_auth_routes(
129134
)
130135
routes.append(
131136
Route(
132-
REGISTRATION_PATH,
137+
issuer_path + REGISTRATION_PATH,
133138
endpoint=_cors(_body_limited(request_response(registration_handler.handle)), ["POST", "OPTIONS"]),
134139
methods=["POST", "OPTIONS"],
135140
)
@@ -139,7 +144,7 @@ def create_auth_routes(
139144
revocation_handler = RevocationHandler(provider, client_authenticator)
140145
routes.append(
141146
Route(
142-
REVOCATION_PATH,
147+
issuer_path + REVOCATION_PATH,
143148
endpoint=_cors(_body_limited(request_response(revocation_handler.handle)), ["POST", "OPTIONS"]),
144149
methods=["POST", "OPTIONS"],
145150
)

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)