Skip to content

Commit 35ecbd2

Browse files
fix: preserve query component of discovered token endpoint
exchange_token_for_api_key() posted to only the path of the discovered token_endpoint, dropping any params/query. If a server advertises a token endpoint with a query component, the exchange would hit the wrong URL. Reconstruct the full request target (path, params, query) via urlunparse. Found by roborev review (job 65). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 079d141 commit 35ecbd2

2 files changed

Lines changed: 14 additions & 3 deletions

File tree

rsconnect/oauth.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import webbrowser
1616
from http.server import BaseHTTPRequestHandler, HTTPServer as _HTTPServer
1717
from typing import Any, Dict, Optional, Tuple, cast
18-
from urllib.parse import parse_qs, urlencode, urlparse
18+
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
1919

2020
import click
2121

@@ -489,7 +489,9 @@ def exchange_token_for_api_key(
489489
token_endpoint = str(metadata["token_endpoint"])
490490
parsed = urlparse(token_endpoint)
491491
base = f"{parsed.scheme}://{parsed.netloc}"
492-
path = parsed.path
492+
# Preserve the full request target (path, params, and query) from the
493+
# discovered endpoint, not just the path.
494+
request_target = urlunparse(("", "", parsed.path, parsed.params, parsed.query, ""))
493495

494496
body = urlencode(
495497
{
@@ -504,7 +506,7 @@ def exchange_token_for_api_key(
504506
with server:
505507
response = server.request(
506508
"POST",
507-
path,
509+
request_target,
508510
body=body,
509511
headers={"Content-Type": "application/x-www-form-urlencoded"},
510512
)

tests/test_oauth.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,15 @@ def test_uses_discovered_token_endpoint_with_prefix(self, mock_http_server: Magi
143143
assert result == "minted-key"
144144
assert mock_http_server.request.call_args.args[1] == "/connect/oauth/v1/token"
145145

146+
def test_preserves_token_endpoint_query(self, mock_http_server: MagicMock):
147+
# A discovered token endpoint with a query component must be posted to verbatim.
148+
metadata = {**FAKE_METADATA, "token_endpoint": "https://host.example.com/connect/oauth/v1/token?tenant=acme"}
149+
self._set_metadata(mock_http_server, metadata)
150+
mock_http_server.request.return_value = _make_response(200, {"access_token": "minted-key"})
151+
result = exchange_token_for_api_key("https://host.example.com/connect", "oidc-token")
152+
assert result == "minted-key"
153+
assert mock_http_server.request.call_args.args[1] == "/connect/oauth/v1/token?tenant=acme"
154+
146155
def test_grant_type_not_supported(self, mock_http_server: MagicMock):
147156
# Preflight: the server advertises grants but not token exchange.
148157
metadata = {**FAKE_METADATA, "grant_types_supported": ["authorization_code", "refresh_token"]}

0 commit comments

Comments
 (0)