Skip to content

Commit 33545ab

Browse files
authored
fix: allow passing query params in OAuthProxy upstream authorization url (#1630)
1 parent 465e403 commit 33545ab

File tree

2 files changed

+42
-3
lines changed

2 files changed

+42
-3
lines changed

src/fastmcp/server/auth/oauth_proxy.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -474,9 +474,8 @@ async def authorize(
474474
query_params["scope"] = " ".join(scopes_to_use)
475475

476476
# Build the upstream authorization URL
477-
upstream_url = (
478-
f"{self._upstream_authorization_endpoint}?{urlencode(query_params)}"
479-
)
477+
separator = "&" if "?" in self._upstream_authorization_endpoint else "?"
478+
upstream_url = f"{self._upstream_authorization_endpoint}{separator}{urlencode(query_params)}"
480479

481480
logger.debug(
482481
"Starting OAuth transaction %s for client %s, redirecting to IdP",

tests/server/auth/test_oauth_proxy.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,46 @@ def test_redirect_path_normalization(self, jwt_verifier):
114114
)
115115
assert proxy2._redirect_path == "/auth/callback"
116116

117+
async def test_authorize_url_with_ampersand_separator(self, jwt_verifier):
118+
"""Test that authorize builds URLs with & separator when upstream endpoint has existing query parameters."""
119+
# Test case: upstream endpoint with existing query parameters
120+
proxy = OAuthProxy(
121+
upstream_authorization_endpoint="https://auth.example.com/authorize?version=2.0",
122+
upstream_token_endpoint="https://auth.example.com/token",
123+
upstream_client_id="client-123",
124+
upstream_client_secret="secret-456",
125+
token_verifier=jwt_verifier,
126+
base_url="https://myserver.com",
127+
)
128+
129+
client = OAuthClientInformationFull(
130+
client_id="test-client",
131+
client_secret="test-secret",
132+
redirect_uris=[AnyUrl("http://localhost:54321/callback")],
133+
)
134+
135+
params = AuthorizationParams(
136+
redirect_uri=AnyUrl("http://localhost:54321/callback"),
137+
redirect_uri_provided_explicitly=True,
138+
state="client-state",
139+
code_challenge="challenge",
140+
scopes=["read"],
141+
)
142+
143+
# Should use "&" separator
144+
redirect_url = await proxy.authorize(client, params)
145+
parsed = urlparse(redirect_url)
146+
query_params = parse_qs(parsed.query)
147+
148+
assert parsed.scheme == "https"
149+
assert parsed.netloc == "auth.example.com"
150+
assert parsed.path == "/authorize"
151+
# Params in the original url are kept
152+
assert "version" in query_params
153+
assert query_params["version"] == ["2.0"]
154+
# New params added correctly
155+
assert query_params["response_type"] == ["code"]
156+
117157
def test_dcr_always_enabled(self, jwt_verifier):
118158
"""Test that DCR is always enabled for OAuth Proxy."""
119159
proxy = OAuthProxy(

0 commit comments

Comments
 (0)