From 94e0b53f74aa7ff7acf53d192f0c48233a8e0938 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 16 Apr 2026 13:46:26 +0000 Subject: [PATCH 1/2] Make clientId optional in DatabricksOAuthTokenSource Users authenticating via web browser OIDC federation flows do not have a client ID in their IdP JWT token. RFC 8693 makes client_id optional for token exchange requests. The SDK was over-constraining by requiring it. Skip the client_id parameter when it is null or empty. Fixes #757 --- .../oauth/DatabricksOAuthTokenSource.java | 10 +++---- .../oauth/DatabricksOAuthTokenSourceTest.java | 26 ++++++++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSource.java index 026197e12..1e685e6be 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSource.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSource.java @@ -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. @@ -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"); } @@ -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 { diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSourceTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSourceTest.java index ee226cd42..22b2d33f9 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSourceTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSourceTest.java @@ -108,7 +108,7 @@ private static Stream 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 formParams = new HashMap<>(); formParams.put("client_id", TEST_CLIENT_ID); formParams.put("subject_token", TEST_ID_TOKEN); @@ -117,6 +117,14 @@ private static Stream 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 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( @@ -198,27 +206,27 @@ private static Stream 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, From 0c221fa8ad13ad5bc67105d6193618e9ea8b748e Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 16 Apr 2026 17:33:41 +0000 Subject: [PATCH 2/2] Add changelog entry for optional clientId fix --- NEXT_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 8017d10a0..41a819592 100755 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -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