From 019f577b0330edf19bb8b9d69a4399c3663b50df Mon Sep 17 00:00:00 2001 From: Gopal Lal Date: Mon, 2 Mar 2026 10:17:39 +0530 Subject: [PATCH] Add EnableOAuthSecretFromPwd connection parameter (#1132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using OAuth Client Credentials (M2M), BI tools like DBeaver expose the full JDBC URL in clear text, which leaks the OAuth2Secret. This commit introduces an opt-in EnableOAuthSecretFromPwd parameter that lets the driver read the OAuth client secret from the PWD/password property instead, leveraging BI tools' built-in password masking. Behavior when EnableOAuthSecretFromPwd=1: - getClientSecret() always reads from PWD/password (pwd takes priority over password, matching getToken() behavior) - OAuth2Secret is ignored even if explicitly set — PWD always wins - If neither PWD nor password is provided, throws a DatabricksDriverException with a clear error message - Covers all flows that call getClientSecret(): M2M Standard, M2M Azure, Refresh Token, and Browser-Based (U2M) Behavior when EnableOAuthSecretFromPwd=0 (default): - No change — getClientSecret() reads from OAuth2Secret as before Files changed: - DatabricksJdbcUrlParams: add ENABLE_OAUTH_SECRET_FROM_PWD enum - IDatabricksConnectionContext: add isOAuthSecretFromPwdEnabled() - DatabricksConnectionContext: implement isOAuthSecretFromPwdEnabled(), update getClientSecret() with PWD fallback and validation - DatabricksDriverPropertyUtil: skip reporting CLIENT_SECRET as missing in CLIENT_CREDENTIALS and TOKEN_PASSTHROUGH flows when the feature is enabled and PWD/password is present - DatabricksConnectionContextTest: 12 unit tests covering all scenarios - M2MAuthIntegrationTests: 3 integration tests (secret from pwd, pwd wins over explicit secret, missing pwd throws error) - OAuthTests: 1 E2E test for M2M with secret from password - IntegrationTestUtil: add getValidM2MConnectionWithSecretFromPwd() Co-Authored-By: Claude Opus 4.6 Signed-off-by: Gopal Lal --- NEXT_CHANGELOG.md | 1 + .../api/impl/DatabricksConnectionContext.java | 16 ++ .../IDatabricksConnectionContext.java | 3 + .../jdbc/common/DatabricksJdbcUrlParams.java | 4 +- .../util/DatabricksDriverPropertyUtil.java | 12 +- .../impl/DatabricksConnectionContextTest.java | 169 ++++++++++++++++++ .../jdbc/integration/IntegrationTestUtil.java | 8 + .../jdbc/integration/e2e/OAuthTests.java | 6 + .../tests/M2MAuthIntegrationTests.java | 58 ++++++ 9 files changed, 274 insertions(+), 3 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index ddd5e0677..d92a42daa 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - Added connection property `OAuthWebServerTimeout` to configure the OAuth browser authentication timeout for U2M (user-to-machine) flows, and also updated hardcoded 1-hour timeout to default 120 seconds timeout. +- Added connection property `EnableOAuthSecretFromPwd` to allow reading the OAuth client secret from the `PWD`/`password` property instead of `OAuth2Secret`. This enables BI tools to mask the secret using their built-in password field handling. When enabled, it will only be read from `PWD`/`password`. ### Updated diff --git a/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java b/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java index c909acf40..7cf1ca728 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java +++ b/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java @@ -340,9 +340,25 @@ public List getOAuthScopesForU2M() throws DatabricksParsingException { @Override public String getClientSecret() { + if (isOAuthSecretFromPwdEnabled()) { + String pwdSecret = + getParameter(DatabricksJdbcUrlParams.PWD, getParameter(DatabricksJdbcUrlParams.PASSWORD)); + if (pwdSecret == null) { + throw new DatabricksDriverException( + "EnableOAuthSecretFromPwd is enabled but no PWD/password property was provided." + + " Set the OAuth client secret via the PWD or password connection property.", + DatabricksDriverErrorCode.INPUT_VALIDATION_ERROR); + } + return pwdSecret; + } return getParameter(DatabricksJdbcUrlParams.CLIENT_SECRET); } + @Override + public boolean isOAuthSecretFromPwdEnabled() { + return getParameter(DatabricksJdbcUrlParams.ENABLE_OAUTH_SECRET_FROM_PWD).equals("1"); + } + @Override public String getGoogleServiceAccount() { return getParameter(DatabricksJdbcUrlParams.GOOGLE_SERVICE_ACCOUNT); diff --git a/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java b/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java index 5b0fa3758..b517b0104 100644 --- a/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java +++ b/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java @@ -468,4 +468,7 @@ public interface IDatabricksConnectionContext { * @return the link prefetch window size (default: 128) */ int getLinkPrefetchWindow(); + + /** Returns whether the driver should read OAuth secret from PWD/password property. */ + boolean isOAuthSecretFromPwdEnabled(); } diff --git a/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java b/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java index 0f043428b..ac3df450a 100644 --- a/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java +++ b/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java @@ -219,7 +219,9 @@ public enum DatabricksJdbcUrlParams { NON_ROWCOUNT_QUERY_PREFIXES( "NonRowcountQueryPrefixes", "Comma-separated list of query prefixes (like INSERT,UPDATE,DELETE) that should return result sets instead of row counts", - ""); + ""), + ENABLE_OAUTH_SECRET_FROM_PWD( + "EnableOAuthSecretFromPwd", "Read OAuth secret/token from PWD/password property", "0"); private final String paramName; private final String defaultValue; diff --git a/src/main/java/com/databricks/jdbc/common/util/DatabricksDriverPropertyUtil.java b/src/main/java/com/databricks/jdbc/common/util/DatabricksDriverPropertyUtil.java index 52fad67ca..068a86688 100644 --- a/src/main/java/com/databricks/jdbc/common/util/DatabricksDriverPropertyUtil.java +++ b/src/main/java/com/databricks/jdbc/common/util/DatabricksDriverPropertyUtil.java @@ -132,7 +132,11 @@ public static List buildMissingPropertiesList( case TOKEN_PASSTHROUGH: if (connectionContext.getOAuthRefreshToken() != null) { addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_ID, true); - addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true); + if (!(connectionContext.isOAuthSecretFromPwdEnabled() + && (connectionContext.isPropertyPresent(PWD) + || connectionContext.isPropertyPresent(PASSWORD)))) { + addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true); + } handleTokenEndpointAndDiscoveryMode(missingPropertyInfos, connectionContext); } else { addMissingProperty( @@ -149,7 +153,11 @@ public static List buildMissingPropertiesList( } else if (connectionContext.getCloud() == Cloud.AZURE) { addMissingProperty(missingPropertyInfos, connectionContext, AZURE_TENANT_ID, false); } - addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true); + if (!(connectionContext.isOAuthSecretFromPwdEnabled() + && (connectionContext.isPropertyPresent(PWD) + || connectionContext.isPropertyPresent(PASSWORD)))) { + addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true); + } addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_ID, true); if (connectionContext.isPropertyPresent(USE_JWT_ASSERTION)) { diff --git a/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java b/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java index 0aa31e366..f85bfbfbc 100644 --- a/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java +++ b/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java @@ -1357,4 +1357,173 @@ public void testOAuthWebServerTimeoutCustom() throws DatabricksSQLException { TestConstants.VALID_URL_1 + ";OAuthWebServerTimeout=300", properties); assertEquals(300, connectionContext.getOAuthWebServerTimeout()); } + + // ===== OAuth Secret from PWD Tests ===== + + private static final String OAUTH_M2M_BASE_URL = + "jdbc:databricks://sample-host.cloud.databricks.com:9999/default;AuthMech=11;Auth_Flow=1;" + + "httpPath=/sql/1.0/warehouses/9999999999999999"; + + @Test + public void testGetClientSecret_WithOAuthSecretFromPwd_ReadsFromPassword() + throws DatabricksSQLException { + String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1"; + Properties props = new Properties(); + props.setProperty("password", "my-oauth-secret"); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props); + assertEquals("my-oauth-secret", ctx.getClientSecret()); + assertTrue(ctx.isOAuthSecretFromPwdEnabled()); + } + + @Test + public void testGetClientSecret_WithOAuthSecretFromPwd_PwdWinsOverExplicitSecret() + throws DatabricksSQLException { + // When feature is enabled, PWD/password always takes precedence over OAuth2Secret + String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1"; + Properties props = new Properties(); + props.setProperty("password", "password-value"); + props.setProperty("OAuth2Secret", "explicit-secret"); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props); + assertEquals("password-value", ctx.getClientSecret()); + } + + @Test + public void testGetClientSecret_WithOAuthSecretFromPwd_PwdParamWinsOverExplicitSecret() + throws DatabricksSQLException { + // Same as above but with pwd param instead of password + String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1"; + Properties props = new Properties(); + props.setProperty("pwd", "pwd-value"); + props.setProperty("OAuth2Secret", "explicit-secret"); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props); + assertEquals("pwd-value", ctx.getClientSecret()); + } + + @Test + public void testGetClientSecret_WithoutFeatureFlag_DoesNotReadPwd() + throws DatabricksSQLException { + Properties props = new Properties(); + props.setProperty("password", "my-oauth-secret"); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(OAUTH_M2M_BASE_URL, props); + assertNull(ctx.getClientSecret()); + assertFalse(ctx.isOAuthSecretFromPwdEnabled()); + } + + @Test + public void testGetClientSecret_WithoutFeatureFlag_ReadsExplicitSecret() + throws DatabricksSQLException { + // When feature is disabled, OAuth2Secret is used as normal + Properties props = new Properties(); + props.setProperty("password", "password-value"); + props.setProperty("OAuth2Secret", "explicit-secret"); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(OAUTH_M2M_BASE_URL, props); + assertEquals("explicit-secret", ctx.getClientSecret()); + assertFalse(ctx.isOAuthSecretFromPwdEnabled()); + } + + @Test + public void testGetClientSecret_WithOAuthSecretFromPwd_ReadsFromPwdParam() + throws DatabricksSQLException { + String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1"; + Properties props = new Properties(); + props.setProperty("pwd", "pwd-value"); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props); + assertEquals("pwd-value", ctx.getClientSecret()); + } + + @Test + public void testGetClientSecret_WithOAuthSecretFromPwd_NoPwdProvided_ThrowsError() + throws DatabricksSQLException { + // When feature is enabled but no PWD/password is provided, should throw error + String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1"; + Properties props = new Properties(); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props); + DatabricksDriverException ex = + assertThrows(DatabricksDriverException.class, ctx::getClientSecret); + assertTrue(ex.getMessage().contains("EnableOAuthSecretFromPwd is enabled")); + assertTrue(ex.getMessage().contains("PWD or password")); + } + + @Test + public void testGetClientSecret_WithOAuthSecretFromPwd_ExplicitSecretOnly_NoPwd_ThrowsError() + throws DatabricksSQLException { + // When feature is enabled, OAuth2Secret provided but no PWD — should throw error + // because the feature mandates reading from PWD + String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1"; + Properties props = new Properties(); + props.setProperty("OAuth2Secret", "explicit-secret"); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props); + DatabricksDriverException ex = + assertThrows(DatabricksDriverException.class, ctx::getClientSecret); + assertTrue(ex.getMessage().contains("EnableOAuthSecretFromPwd is enabled")); + } + + @Test + public void testGetClientSecret_FeatureDisabledExplicitly_DoesNotReadPwd() + throws DatabricksSQLException { + // Explicitly set EnableOAuthSecretFromPwd=0 + String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=0"; + Properties props = new Properties(); + props.setProperty("password", "my-oauth-secret"); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props); + assertNull(ctx.getClientSecret()); + assertFalse(ctx.isOAuthSecretFromPwdEnabled()); + } + + @Test + public void testGetClientSecret_FeatureEnabled_PasswordInUrl() throws DatabricksSQLException { + // Password provided in the JDBC URL itself (not via Properties) + String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1;pwd=url-secret"; + Properties props = new Properties(); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props); + assertEquals("url-secret", ctx.getClientSecret()); + } + + @Test + public void testGetClientSecret_FeatureEnabled_BrowserBasedAuth() throws DatabricksSQLException { + // Browser-based auth (Auth_Flow=2) with EnableOAuthSecretFromPwd + String url = + "jdbc:databricks://sample-host.cloud.databricks.com:9999/default;AuthMech=11;Auth_Flow=2;" + + "httpPath=/sql/1.0/warehouses/9999999999999999;EnableOAuthSecretFromPwd=1"; + Properties props = new Properties(); + props.setProperty("password", "browser-secret"); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props); + assertEquals("browser-secret", ctx.getClientSecret()); + } + + @Test + public void testGetClientSecret_FeatureEnabled_RefreshTokenFlow() throws DatabricksSQLException { + // Refresh token flow (Auth_Flow=0) with EnableOAuthSecretFromPwd + String url = + "jdbc:databricks://sample-host.cloud.databricks.com:9999/default;AuthMech=11;Auth_Flow=0;" + + "httpPath=/sql/1.0/warehouses/9999999999999999;EnableOAuthSecretFromPwd=1"; + Properties props = new Properties(); + props.setProperty("password", "refresh-secret"); + + DatabricksConnectionContext ctx = + (DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props); + assertEquals("refresh-secret", ctx.getClientSecret()); + } } diff --git a/src/test/java/com/databricks/jdbc/integration/IntegrationTestUtil.java b/src/test/java/com/databricks/jdbc/integration/IntegrationTestUtil.java index f0ba7dd48..ce701b17e 100644 --- a/src/test/java/com/databricks/jdbc/integration/IntegrationTestUtil.java +++ b/src/test/java/com/databricks/jdbc/integration/IntegrationTestUtil.java @@ -140,6 +140,14 @@ public static Connection getValidM2MConnection() throws SQLException { return DriverManager.getConnection(getJdbcM2MUrl(), createM2MConnectionProperties()); } + public static Connection getValidM2MConnectionWithSecretFromPwd() throws SQLException { + String url = getJdbcM2MUrl() + ";EnableOAuthSecretFromPwd=1"; + Properties connProps = new Properties(); + connProps.put("OAuth2ClientId", System.getenv("DATABRICKS_JDBC_M2M_CLIENT_ID")); + connProps.put("password", System.getenv("DATABRICKS_JDBC_M2M_CLIENT_SECRET")); + return DriverManager.getConnection(url, connProps); + } + public static Connection getValidSPTokenFedConnection() throws SQLException { return DriverManager.getConnection(getSPTokenFedUrl(), createSPTokenFedConnectionProperties()); } diff --git a/src/test/java/com/databricks/jdbc/integration/e2e/OAuthTests.java b/src/test/java/com/databricks/jdbc/integration/e2e/OAuthTests.java index dd2fbaaec..7b0ff9370 100644 --- a/src/test/java/com/databricks/jdbc/integration/e2e/OAuthTests.java +++ b/src/test/java/com/databricks/jdbc/integration/e2e/OAuthTests.java @@ -16,6 +16,12 @@ void testM2M() throws SQLException { assertDoesNotThrow(() -> connection.createStatement().execute("select 1")); } + @Test + void testM2MWithSecretFromPwd() throws SQLException { + Connection connection = getValidM2MConnectionWithSecretFromPwd(); + assertDoesNotThrow(() -> connection.createStatement().execute("select 1")); + } + @Test void testPAT() throws SQLException { Properties connectionProperties = new Properties(); diff --git a/src/test/java/com/databricks/jdbc/integration/fakeservice/tests/M2MAuthIntegrationTests.java b/src/test/java/com/databricks/jdbc/integration/fakeservice/tests/M2MAuthIntegrationTests.java index 61f662dc2..bdd461e15 100644 --- a/src/test/java/com/databricks/jdbc/integration/fakeservice/tests/M2MAuthIntegrationTests.java +++ b/src/test/java/com/databricks/jdbc/integration/fakeservice/tests/M2MAuthIntegrationTests.java @@ -47,6 +47,64 @@ void testIncorrectCredentialsForM2M() { .contains("Connection failure while using the OSS Databricks JDBC driver."); } + @Test + void testSuccessfulM2MConnectionWithSecretFromPwd() throws SQLException { + String url = getFakeServiceM2MUrl() + "EnableOAuthSecretFromPwd=1;"; + Properties connProps = new Properties(); + connProps.put("OAuth2ClientId", TEST_CLIENT_ID); + connProps.put("password", TEST_CLIENT_SECRET); + connProps.put( + DatabricksJdbcUrlParams.CONN_CATALOG.getParamName(), + FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_CATALOG.getParamName())); + connProps.put( + DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName(), + FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName())); + + Connection conn = DriverManager.getConnection(url, connProps); + assertNotNull(conn); + assertFalse(conn.isClosed()); + conn.close(); + } + + @Test + void testM2MWithSecretFromPwd_PwdWinsOverExplicitSecret() throws SQLException { + // When EnableOAuthSecretFromPwd=1, PWD/password takes precedence over OAuth2Secret. + // Providing the correct secret in password and an invalid one in OAuth2Secret should succeed. + String url = getFakeServiceM2MUrl() + "EnableOAuthSecretFromPwd=1;"; + Properties connProps = new Properties(); + connProps.put("OAuth2ClientId", TEST_CLIENT_ID); + connProps.put("password", TEST_CLIENT_SECRET); + connProps.put("OAuth2Secret", "invalid-should-be-ignored"); + connProps.put( + DatabricksJdbcUrlParams.CONN_CATALOG.getParamName(), + FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_CATALOG.getParamName())); + connProps.put( + DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName(), + FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName())); + + Connection conn = DriverManager.getConnection(url, connProps); + assertNotNull(conn); + assertFalse(conn.isClosed()); + conn.close(); + } + + @Test + void testM2MWithSecretFromPwd_NoPwdProvided_ThrowsError() { + // EnableOAuthSecretFromPwd=1 but no PWD/password — should fail with validation error + String url = getFakeServiceM2MUrl() + "EnableOAuthSecretFromPwd=1;"; + Properties connProps = new Properties(); + connProps.put("OAuth2ClientId", TEST_CLIENT_ID); + connProps.put( + DatabricksJdbcUrlParams.CONN_CATALOG.getParamName(), + FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_CATALOG.getParamName())); + connProps.put( + DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName(), + FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName())); + + Exception e = assertThrows(Exception.class, () -> DriverManager.getConnection(url, connProps)); + assertTrue(e.getMessage().contains("EnableOAuthSecretFromPwd is enabled")); + } + private Connection getValidM2MConnection() throws SQLException { return DriverManager.getConnection( getFakeServiceM2MUrl(), createFakeServiceM2MConnectionProperties(TEST_CLIENT_SECRET));