Skip to content

Commit 11237a2

Browse files
Properly encore query parameters in OAuth client. (#514)
## What changes are proposed in this pull request? This PR updates the OAuth client to properly encode complex query parameters. ## How is this tested? Unit and integration tests.
1 parent 38c6b50 commit 11237a2

File tree

4 files changed

+195
-7
lines changed

4 files changed

+195
-7
lines changed

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
### Bug Fixes
88

9+
* Fix OAuthClient to properly encode complex query parameters.
10+
911
### Documentation
1012

1113
### Internal Changes

databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/OAuthClient.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import com.databricks.sdk.core.DatabricksException;
55
import com.databricks.sdk.core.http.HttpClient;
66
import java.io.IOException;
7+
import java.io.UnsupportedEncodingException;
78
import java.net.MalformedURLException;
9+
import java.net.URLEncoder;
810
import java.nio.charset.StandardCharsets;
911
import java.security.MessageDigest;
1012
import java.security.NoSuchAlgorithmException;
@@ -188,15 +190,38 @@ private static byte[] sha256(byte[] input) {
188190
}
189191
}
190192

191-
private static String urlEncode(String urlBase, Map<String, String> params) {
193+
protected static String urlEncode(String urlBase, Map<String, String> params) {
194+
if (params == null || params.isEmpty()) {
195+
return urlBase;
196+
}
197+
192198
String queryParams =
193199
params.entrySet().stream()
194-
.map(entry -> entry.getKey() + "=" + entry.getValue())
200+
.sorted(Map.Entry.comparingByKey())
201+
.map(entry -> encodeParam(entry.getKey()) + "=" + encodeParam(entry.getValue()))
195202
.collect(Collectors.joining("&"));
196-
return urlBase + "?" + queryParams.replaceAll(" ", "%20");
203+
204+
String separator = urlBase.contains("?") ? "&" : "?";
205+
return urlBase + separator + queryParams;
206+
}
207+
208+
private static String encodeParam(String value) {
209+
try {
210+
return URLEncoder.encode(value, "UTF-8");
211+
} catch (UnsupportedEncodingException e) {
212+
// This should never happen. The exception is catched because it is
213+
// a "checked" exception that we do not want to propagate to the method
214+
// signature.
215+
throw new RuntimeException("UTF-8 encoding not supported", e);
216+
}
197217
}
198218

199219
public Consent initiateConsent() throws MalformedURLException {
220+
if (authUrl == null) {
221+
throw new DatabricksException(
222+
"Authorization URL is not configured. OAuth endpoints may not be available.");
223+
}
224+
200225
String state = tokenUrlSafe(16);
201226
String verifier = tokenUrlSafe(32);
202227
byte[] digest = sha256(verifier.getBytes(StandardCharsets.UTF_8));

databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/ExternalBrowserCredentialsProviderTest.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ void clientAndConsentTest() throws IOException {
2929
new FixtureServer.FixtureMapping.Builder()
3030
.validateMethod("GET")
3131
.validatePath("/oidc/.well-known/oauth-authorization-server")
32-
.withResponse("{\"token_endpoint\": \"tokenEndPointFromServer\"}", 200)
32+
.withResponse(
33+
"{\"token_endpoint\": \"tokenEndPointFromServer\", \"authorization_endpoint\": \"authEndPointFromServer\"}",
34+
200)
3335
.build();
3436
try (FixtureServer fixtures = new FixtureServer()) {
3537
fixtures.with(fixture).with(fixture);
@@ -62,7 +64,7 @@ void clientAndConsentTest() throws IOException {
6264
assertNotNull(authUrl);
6365
assertTrue(authUrl.contains("response_type=code"));
6466
assertTrue(authUrl.contains("client_id=test-client-id"));
65-
assertTrue(authUrl.contains("redirect_uri=http://localhost:8080/callback"));
67+
assertTrue(authUrl.contains("redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback"));
6668
assertTrue(authUrl.contains("scope=all-apis"));
6769
}
6870
}
@@ -73,7 +75,9 @@ void clientAndConsentTestWithCustomRedirectUrl() throws IOException {
7375
new FixtureServer.FixtureMapping.Builder()
7476
.validateMethod("GET")
7577
.validatePath("/oidc/.well-known/oauth-authorization-server")
76-
.withResponse("{\"token_endpoint\": \"tokenEndPointFromServer\"}", 200)
78+
.withResponse(
79+
"{\"token_endpoint\": \"tokenEndPointFromServer\", \"authorization_endpoint\": \"authEndPointFromServer\"}",
80+
200)
7781
.build();
7882
try (FixtureServer fixtures = new FixtureServer()) {
7983
fixtures.with(fixture).with(fixture);
@@ -108,7 +112,7 @@ void clientAndConsentTestWithCustomRedirectUrl() throws IOException {
108112
assertNotNull(authUrl);
109113
assertTrue(authUrl.contains("response_type=code"));
110114
assertTrue(authUrl.contains("client_id=test-client-id"));
111-
assertTrue(authUrl.contains("redirect_uri=http://localhost:8010"));
115+
assertTrue(authUrl.contains("redirect_uri=http%3A%2F%2Flocalhost%3A8010"));
112116
assertTrue(authUrl.contains("scope=sql"));
113117
}
114118
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.databricks.sdk.core.oauth;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import com.google.auto.value.AutoValue;
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
import java.util.stream.Stream;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.params.ParameterizedTest;
11+
import org.junit.jupiter.params.provider.MethodSource;
12+
13+
public class OAuthClientTest {
14+
15+
@AutoValue
16+
public abstract static class UrlEncodeTestCase {
17+
public abstract String description();
18+
19+
public abstract String baseUrl();
20+
21+
public abstract Map<String, String> params();
22+
23+
public abstract String expectedResult();
24+
25+
public static UrlEncodeTestCase create(
26+
String description, String baseUrl, Map<String, String> params, String expectedResult) {
27+
return new AutoValue_OAuthClientTest_UrlEncodeTestCase(
28+
description, baseUrl, params, expectedResult);
29+
}
30+
}
31+
32+
private static Stream<UrlEncodeTestCase> urlEncodeTestCases() {
33+
return Stream.of(
34+
UrlEncodeTestCase.create(
35+
"Basic parameters",
36+
"https://example.com/auth",
37+
createParams("client_id", "test-client", "response_type", "code"),
38+
"https://example.com/auth?client_id=test-client&response_type=code"),
39+
UrlEncodeTestCase.create(
40+
"Empty parameters",
41+
"https://example.com/auth",
42+
createParams(),
43+
"https://example.com/auth"),
44+
UrlEncodeTestCase.create(
45+
"Special characters in parameters",
46+
"https://example.com/auth",
47+
createParams(
48+
"redirect_uri",
49+
"http://localhost:8080/callback?extra=value",
50+
"scope",
51+
"read write",
52+
"state",
53+
"test&value=123"),
54+
"https://example.com/auth?redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback%3Fextra%3Dvalue&scope=read+write&state=test%26value%3D123"),
55+
UrlEncodeTestCase.create(
56+
"URL with existing query parameters",
57+
"https://example.com/auth?existing=param",
58+
createParams("client_id", "test-client"),
59+
"https://example.com/auth?existing=param&client_id=test-client"),
60+
UrlEncodeTestCase.create(
61+
"Complex OAuth parameters",
62+
"https://accounts.cloud.databricks.com/oidc/v1/authorize",
63+
createParams(
64+
"client_id",
65+
"databricks-client",
66+
"code_challenge",
67+
"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
68+
"code_challenge_method",
69+
"S256",
70+
"redirect_uri",
71+
"https://app.example.com/callback?session=abc123",
72+
"response_type",
73+
"code",
74+
"scope",
75+
"sql clusters repos",
76+
"state",
77+
"random-state-123"),
78+
"https://accounts.cloud.databricks.com/oidc/v1/authorize?client_id=databricks-client&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback%3Fsession%3Dabc123&response_type=code&scope=sql+clusters+repos&state=random-state-123"),
79+
UrlEncodeTestCase.create(
80+
"Parameter encoding with spaces",
81+
"https://example.com",
82+
createParams("scope", "read write admin"),
83+
"https://example.com?scope=read+write+admin"),
84+
UrlEncodeTestCase.create(
85+
"Parameter encoding with special characters",
86+
"https://example.com",
87+
createParams("state", "value&with=special?chars#fragment"),
88+
"https://example.com?state=value%26with%3Dspecial%3Fchars%23fragment"),
89+
UrlEncodeTestCase.create(
90+
"Parameter encoding with URL as value",
91+
"https://example.com",
92+
createParams("redirect_uri", "https://example.com/callback?param=value"),
93+
"https://example.com?redirect_uri=https%3A%2F%2Fexample.com%2Fcallback%3Fparam%3Dvalue"),
94+
UrlEncodeTestCase.create(
95+
"Parameter encoding with plus signs",
96+
"https://example.com",
97+
createParams("scope", "scope+with+plus"),
98+
"https://example.com?scope=scope%2Bwith%2Bplus"),
99+
UrlEncodeTestCase.create(
100+
"Parameter encoding with empty string",
101+
"https://example.com",
102+
createParams("empty", ""),
103+
"https://example.com?empty="),
104+
UrlEncodeTestCase.create(
105+
"Parameter encoding with Unicode characters",
106+
"https://example.com",
107+
createParams("unicode", "测试数据"),
108+
"https://example.com?unicode=%E6%B5%8B%E8%AF%95%E6%95%B0%E6%8D%AE"),
109+
UrlEncodeTestCase.create(
110+
"Integration OAuth flow with complex parameters",
111+
"https://accounts.cloud.databricks.com/oidc/v1/authorize",
112+
createParams(
113+
"redirect_uri",
114+
"https://app.example.com/oauth/callback?session=abc123&return=/dashboard",
115+
"scope",
116+
"sql clusters repos notebooks",
117+
"state",
118+
"test&state=value"),
119+
"https://accounts.cloud.databricks.com/oidc/v1/authorize?redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback%3Fsession%3Dabc123%26return%3D%2Fdashboard&scope=sql+clusters+repos+notebooks&state=test%26state%3Dvalue"));
120+
}
121+
122+
private static Map<String, String> createParams(String... keyValuePairs) {
123+
Map<String, String> params = new HashMap<>();
124+
for (int i = 0; i < keyValuePairs.length; i += 2) {
125+
params.put(keyValuePairs[i], keyValuePairs[i + 1]);
126+
}
127+
return params;
128+
}
129+
130+
@ParameterizedTest
131+
@MethodSource("urlEncodeTestCases")
132+
public void testUrlEncode(UrlEncodeTestCase testCase) {
133+
String result = OAuthClient.urlEncode(testCase.baseUrl(), testCase.params());
134+
assertEquals(testCase.expectedResult(), result, testCase.description());
135+
}
136+
137+
@Test
138+
public void testDeterministicParameterOrdering() {
139+
// Test that parameters are always in the same order regardless of insertion order.
140+
Map<String, String> params1 = new HashMap<>();
141+
params1.put("z_param", "value1");
142+
params1.put("a_param", "value2");
143+
params1.put("m_param", "value3");
144+
145+
Map<String, String> params2 = new HashMap<>();
146+
params2.put("m_param", "value3");
147+
params2.put("z_param", "value1");
148+
params2.put("a_param", "value2");
149+
150+
String result1 = OAuthClient.urlEncode("https://example.com", params1);
151+
String result2 = OAuthClient.urlEncode("https://example.com", params2);
152+
153+
// Both should produce identical results (sorted by key).
154+
assertEquals(result1, result2);
155+
assertEquals("https://example.com?a_param=value2&m_param=value3&z_param=value1", result1);
156+
}
157+
}

0 commit comments

Comments
 (0)