Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Fixed non-JSON error responses (e.g. plain-text "Invalid Token" with HTTP 403) producing `Unknown` instead of the correct typed exception (`PermissionDenied`, `Unauthenticated`, etc.). The error message no longer contains Jackson deserialization internals.
* Added `X-Databricks-Org-Id` header to deprecated workspace SCIM APIs (Groups, ServicePrincipals, Users) for SPOG host compatibility.
* Fixed Databricks CLI authentication to detect when the cached token's scopes don't match the SDK's configured scopes. Previously, a scope mismatch was silently ignored, causing requests to use wrong permissions. The SDK now raises an error with instructions to re-authenticate.
* Made `clientId` optional in `DatabricksOAuthTokenSource` to support web browser OIDC federation flows where no client ID is available. The `client_id` parameter is now only included in token exchange requests when non-null and non-empty, per RFC 8693 ([#757](https://github.com/databricks/databricks-sdk-java/issues/757)).

### Security Vulnerabilities

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static class Builder {
/**
* Creates a new Builder with required parameters.
*
* @param clientId OAuth client ID.
* @param clientId OAuth client ID, or null for web OAuth2 flows that don't require one.
* @param host Databricks host URL.
* @param endpoints OpenID Connect endpoints configuration.
* @param idTokenSource Source of ID tokens.
Expand Down Expand Up @@ -145,15 +145,11 @@ public DatabricksOAuthTokenSource build() {
*/
@Override
public Token getToken() {
Objects.requireNonNull(clientId, "ClientID cannot be null");
Objects.requireNonNull(host, "Host cannot be null");
Objects.requireNonNull(endpoints, "Endpoints cannot be null");
Objects.requireNonNull(idTokenSource, "IDTokenSource cannot be null");
Objects.requireNonNull(httpClient, "HttpClient cannot be null");

if (clientId.isEmpty()) {
throw new IllegalArgumentException("ClientID cannot be empty");
}
if (host.isEmpty()) {
throw new IllegalArgumentException("Host cannot be empty");
}
Expand All @@ -166,7 +162,9 @@ public Token getToken() {
params.put(SUBJECT_TOKEN_PARAM, idToken.getValue());
params.put(SUBJECT_TOKEN_TYPE_PARAM, SUBJECT_TOKEN_TYPE);
params.put(SCOPE_PARAM, String.join(" ", scopes));
params.put(CLIENT_ID_PARAM, clientId);
if (!Strings.isNullOrEmpty(clientId)) {
params.put(CLIENT_ID_PARAM, clientId);
}

OAuthResponse response;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private static Stream<TestCase> provideTestCases() throws MalformedURLException
final String errorJson = mapper.writeValueAsString(errorResponse);
final String successJson = mapper.writeValueAsString(successResponse);

// Create the expected request that will be used in all test cases
// Create the expected request with client_id for standard test cases
Map<String, String> formParams = new HashMap<>();
formParams.put("client_id", TEST_CLIENT_ID);
formParams.put("subject_token", TEST_ID_TOKEN);
Expand All @@ -117,6 +117,14 @@ private static Stream<TestCase> provideTestCases() throws MalformedURLException
formParams.put("scope", "all-apis");
FormRequest expectedRequest = new FormRequest(TEST_TOKEN_ENDPOINT, formParams);

// Create the expected request without client_id for null/empty client ID cases
Map<String, String> noClientIdFormParams = new HashMap<>();
noClientIdFormParams.put("subject_token", TEST_ID_TOKEN);
noClientIdFormParams.put("subject_token_type", "urn:ietf:params:oauth:token-type:jwt");
noClientIdFormParams.put("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange");
noClientIdFormParams.put("scope", "all-apis");
FormRequest noClientIdRequest = new FormRequest(TEST_TOKEN_ENDPOINT, noClientIdFormParams);

return Stream.of(
// Token exchange test cases
new TestCase(
Expand Down Expand Up @@ -198,27 +206,27 @@ private static Stream<TestCase> provideTestCases() throws MalformedURLException
DatabricksException.class),
// Parameter validation test cases
new TestCase(
"Null client ID",
"Null client ID omits client_id from request",
null,
TEST_HOST,
testEndpoints,
testIdTokenSource,
createMockHttpClient(expectedRequest, 200, successJson),
null,
createMockHttpClient(noClientIdRequest, 200, successJson),
null,
null,
NullPointerException.class),
TEST_TOKEN_ENDPOINT,
null),
new TestCase(
"Empty client ID",
"Empty client ID omits client_id from request",
"",
TEST_HOST,
testEndpoints,
testIdTokenSource,
createMockHttpClient(expectedRequest, 200, successJson),
null,
createMockHttpClient(noClientIdRequest, 200, successJson),
null,
null,
IllegalArgumentException.class),
TEST_TOKEN_ENDPOINT,
null),
new TestCase(
"Null host",
TEST_CLIENT_ID,
Expand Down
Loading