Skip to content

Commit fb3c4a7

Browse files
gopalldbclaude
andcommitted
Add EnableOAuthSecretFromPwd connection parameter (#1132)
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 <noreply@anthropic.com> Signed-off-by: Gopal Lal <gopal.lal@databricks.com>
1 parent 10c59d5 commit fb3c4a7

9 files changed

Lines changed: 274 additions & 3 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Added
66
- 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.
7+
- 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, `PWD`/`password` always takes precedence over `OAuth2Secret`.
78

89
### Updated
910

src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,25 @@ public List<String> getOAuthScopesForU2M() throws DatabricksParsingException {
340340

341341
@Override
342342
public String getClientSecret() {
343+
if (isOAuthSecretFromPwdEnabled()) {
344+
String pwdSecret =
345+
getParameter(DatabricksJdbcUrlParams.PWD, getParameter(DatabricksJdbcUrlParams.PASSWORD));
346+
if (pwdSecret == null) {
347+
throw new DatabricksDriverException(
348+
"EnableOAuthSecretFromPwd is enabled but no PWD/password property was provided."
349+
+ " Set the OAuth client secret via the PWD or password connection property.",
350+
DatabricksDriverErrorCode.INPUT_VALIDATION_ERROR);
351+
}
352+
return pwdSecret;
353+
}
343354
return getParameter(DatabricksJdbcUrlParams.CLIENT_SECRET);
344355
}
345356

357+
@Override
358+
public boolean isOAuthSecretFromPwdEnabled() {
359+
return getParameter(DatabricksJdbcUrlParams.ENABLE_OAUTH_SECRET_FROM_PWD).equals("1");
360+
}
361+
346362
@Override
347363
public String getGoogleServiceAccount() {
348364
return getParameter(DatabricksJdbcUrlParams.GOOGLE_SERVICE_ACCOUNT);

src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,4 +468,7 @@ public interface IDatabricksConnectionContext {
468468
* @return the link prefetch window size (default: 128)
469469
*/
470470
int getLinkPrefetchWindow();
471+
472+
/** Returns whether the driver should read OAuth secret from PWD/password property. */
473+
boolean isOAuthSecretFromPwdEnabled();
471474
}

src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ public enum DatabricksJdbcUrlParams {
219219
NON_ROWCOUNT_QUERY_PREFIXES(
220220
"NonRowcountQueryPrefixes",
221221
"Comma-separated list of query prefixes (like INSERT,UPDATE,DELETE) that should return result sets instead of row counts",
222-
"");
222+
""),
223+
ENABLE_OAUTH_SECRET_FROM_PWD(
224+
"EnableOAuthSecretFromPwd", "Read OAuth secret/token from PWD/password property", "0");
223225

224226
private final String paramName;
225227
private final String defaultValue;

src/main/java/com/databricks/jdbc/common/util/DatabricksDriverPropertyUtil.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ public static List<DriverPropertyInfo> buildMissingPropertiesList(
132132
case TOKEN_PASSTHROUGH:
133133
if (connectionContext.getOAuthRefreshToken() != null) {
134134
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_ID, true);
135-
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true);
135+
if (!(connectionContext.isOAuthSecretFromPwdEnabled()
136+
&& (connectionContext.isPropertyPresent(PWD)
137+
|| connectionContext.isPropertyPresent(PASSWORD)))) {
138+
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true);
139+
}
136140
handleTokenEndpointAndDiscoveryMode(missingPropertyInfos, connectionContext);
137141
} else {
138142
addMissingProperty(
@@ -149,7 +153,11 @@ public static List<DriverPropertyInfo> buildMissingPropertiesList(
149153
} else if (connectionContext.getCloud() == Cloud.AZURE) {
150154
addMissingProperty(missingPropertyInfos, connectionContext, AZURE_TENANT_ID, false);
151155
}
152-
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true);
156+
if (!(connectionContext.isOAuthSecretFromPwdEnabled()
157+
&& (connectionContext.isPropertyPresent(PWD)
158+
|| connectionContext.isPropertyPresent(PASSWORD)))) {
159+
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true);
160+
}
153161
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_ID, true);
154162

155163
if (connectionContext.isPropertyPresent(USE_JWT_ASSERTION)) {

src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,4 +1357,173 @@ public void testOAuthWebServerTimeoutCustom() throws DatabricksSQLException {
13571357
TestConstants.VALID_URL_1 + ";OAuthWebServerTimeout=300", properties);
13581358
assertEquals(300, connectionContext.getOAuthWebServerTimeout());
13591359
}
1360+
1361+
// ===== OAuth Secret from PWD Tests =====
1362+
1363+
private static final String OAUTH_M2M_BASE_URL =
1364+
"jdbc:databricks://sample-host.cloud.databricks.com:9999/default;AuthMech=11;Auth_Flow=1;"
1365+
+ "httpPath=/sql/1.0/warehouses/9999999999999999";
1366+
1367+
@Test
1368+
public void testGetClientSecret_WithOAuthSecretFromPwd_ReadsFromPassword()
1369+
throws DatabricksSQLException {
1370+
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
1371+
Properties props = new Properties();
1372+
props.setProperty("password", "my-oauth-secret");
1373+
1374+
DatabricksConnectionContext ctx =
1375+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
1376+
assertEquals("my-oauth-secret", ctx.getClientSecret());
1377+
assertTrue(ctx.isOAuthSecretFromPwdEnabled());
1378+
}
1379+
1380+
@Test
1381+
public void testGetClientSecret_WithOAuthSecretFromPwd_PwdWinsOverExplicitSecret()
1382+
throws DatabricksSQLException {
1383+
// When feature is enabled, PWD/password always takes precedence over OAuth2Secret
1384+
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
1385+
Properties props = new Properties();
1386+
props.setProperty("password", "password-value");
1387+
props.setProperty("OAuth2Secret", "explicit-secret");
1388+
1389+
DatabricksConnectionContext ctx =
1390+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
1391+
assertEquals("password-value", ctx.getClientSecret());
1392+
}
1393+
1394+
@Test
1395+
public void testGetClientSecret_WithOAuthSecretFromPwd_PwdParamWinsOverExplicitSecret()
1396+
throws DatabricksSQLException {
1397+
// Same as above but with pwd param instead of password
1398+
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
1399+
Properties props = new Properties();
1400+
props.setProperty("pwd", "pwd-value");
1401+
props.setProperty("OAuth2Secret", "explicit-secret");
1402+
1403+
DatabricksConnectionContext ctx =
1404+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
1405+
assertEquals("pwd-value", ctx.getClientSecret());
1406+
}
1407+
1408+
@Test
1409+
public void testGetClientSecret_WithoutFeatureFlag_DoesNotReadPwd()
1410+
throws DatabricksSQLException {
1411+
Properties props = new Properties();
1412+
props.setProperty("password", "my-oauth-secret");
1413+
1414+
DatabricksConnectionContext ctx =
1415+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(OAUTH_M2M_BASE_URL, props);
1416+
assertNull(ctx.getClientSecret());
1417+
assertFalse(ctx.isOAuthSecretFromPwdEnabled());
1418+
}
1419+
1420+
@Test
1421+
public void testGetClientSecret_WithoutFeatureFlag_ReadsExplicitSecret()
1422+
throws DatabricksSQLException {
1423+
// When feature is disabled, OAuth2Secret is used as normal
1424+
Properties props = new Properties();
1425+
props.setProperty("password", "password-value");
1426+
props.setProperty("OAuth2Secret", "explicit-secret");
1427+
1428+
DatabricksConnectionContext ctx =
1429+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(OAUTH_M2M_BASE_URL, props);
1430+
assertEquals("explicit-secret", ctx.getClientSecret());
1431+
assertFalse(ctx.isOAuthSecretFromPwdEnabled());
1432+
}
1433+
1434+
@Test
1435+
public void testGetClientSecret_WithOAuthSecretFromPwd_ReadsFromPwdParam()
1436+
throws DatabricksSQLException {
1437+
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
1438+
Properties props = new Properties();
1439+
props.setProperty("pwd", "pwd-value");
1440+
1441+
DatabricksConnectionContext ctx =
1442+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
1443+
assertEquals("pwd-value", ctx.getClientSecret());
1444+
}
1445+
1446+
@Test
1447+
public void testGetClientSecret_WithOAuthSecretFromPwd_NoPwdProvided_ThrowsError()
1448+
throws DatabricksSQLException {
1449+
// When feature is enabled but no PWD/password is provided, should throw error
1450+
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
1451+
Properties props = new Properties();
1452+
1453+
DatabricksConnectionContext ctx =
1454+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
1455+
DatabricksDriverException ex =
1456+
assertThrows(DatabricksDriverException.class, ctx::getClientSecret);
1457+
assertTrue(ex.getMessage().contains("EnableOAuthSecretFromPwd is enabled"));
1458+
assertTrue(ex.getMessage().contains("PWD or password"));
1459+
}
1460+
1461+
@Test
1462+
public void testGetClientSecret_WithOAuthSecretFromPwd_ExplicitSecretOnly_NoPwd_ThrowsError()
1463+
throws DatabricksSQLException {
1464+
// When feature is enabled, OAuth2Secret provided but no PWD — should throw error
1465+
// because the feature mandates reading from PWD
1466+
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
1467+
Properties props = new Properties();
1468+
props.setProperty("OAuth2Secret", "explicit-secret");
1469+
1470+
DatabricksConnectionContext ctx =
1471+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
1472+
DatabricksDriverException ex =
1473+
assertThrows(DatabricksDriverException.class, ctx::getClientSecret);
1474+
assertTrue(ex.getMessage().contains("EnableOAuthSecretFromPwd is enabled"));
1475+
}
1476+
1477+
@Test
1478+
public void testGetClientSecret_FeatureDisabledExplicitly_DoesNotReadPwd()
1479+
throws DatabricksSQLException {
1480+
// Explicitly set EnableOAuthSecretFromPwd=0
1481+
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=0";
1482+
Properties props = new Properties();
1483+
props.setProperty("password", "my-oauth-secret");
1484+
1485+
DatabricksConnectionContext ctx =
1486+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
1487+
assertNull(ctx.getClientSecret());
1488+
assertFalse(ctx.isOAuthSecretFromPwdEnabled());
1489+
}
1490+
1491+
@Test
1492+
public void testGetClientSecret_FeatureEnabled_PasswordInUrl() throws DatabricksSQLException {
1493+
// Password provided in the JDBC URL itself (not via Properties)
1494+
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1;pwd=url-secret";
1495+
Properties props = new Properties();
1496+
1497+
DatabricksConnectionContext ctx =
1498+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
1499+
assertEquals("url-secret", ctx.getClientSecret());
1500+
}
1501+
1502+
@Test
1503+
public void testGetClientSecret_FeatureEnabled_BrowserBasedAuth() throws DatabricksSQLException {
1504+
// Browser-based auth (Auth_Flow=2) with EnableOAuthSecretFromPwd
1505+
String url =
1506+
"jdbc:databricks://sample-host.cloud.databricks.com:9999/default;AuthMech=11;Auth_Flow=2;"
1507+
+ "httpPath=/sql/1.0/warehouses/9999999999999999;EnableOAuthSecretFromPwd=1";
1508+
Properties props = new Properties();
1509+
props.setProperty("password", "browser-secret");
1510+
1511+
DatabricksConnectionContext ctx =
1512+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
1513+
assertEquals("browser-secret", ctx.getClientSecret());
1514+
}
1515+
1516+
@Test
1517+
public void testGetClientSecret_FeatureEnabled_RefreshTokenFlow() throws DatabricksSQLException {
1518+
// Refresh token flow (Auth_Flow=0) with EnableOAuthSecretFromPwd
1519+
String url =
1520+
"jdbc:databricks://sample-host.cloud.databricks.com:9999/default;AuthMech=11;Auth_Flow=0;"
1521+
+ "httpPath=/sql/1.0/warehouses/9999999999999999;EnableOAuthSecretFromPwd=1";
1522+
Properties props = new Properties();
1523+
props.setProperty("password", "refresh-secret");
1524+
1525+
DatabricksConnectionContext ctx =
1526+
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
1527+
assertEquals("refresh-secret", ctx.getClientSecret());
1528+
}
13601529
}

src/test/java/com/databricks/jdbc/integration/IntegrationTestUtil.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ public static Connection getValidM2MConnection() throws SQLException {
140140
return DriverManager.getConnection(getJdbcM2MUrl(), createM2MConnectionProperties());
141141
}
142142

143+
public static Connection getValidM2MConnectionWithSecretFromPwd() throws SQLException {
144+
String url = getJdbcM2MUrl() + ";EnableOAuthSecretFromPwd=1";
145+
Properties connProps = new Properties();
146+
connProps.put("OAuth2ClientId", System.getenv("DATABRICKS_JDBC_M2M_CLIENT_ID"));
147+
connProps.put("password", System.getenv("DATABRICKS_JDBC_M2M_CLIENT_SECRET"));
148+
return DriverManager.getConnection(url, connProps);
149+
}
150+
143151
public static Connection getValidSPTokenFedConnection() throws SQLException {
144152
return DriverManager.getConnection(getSPTokenFedUrl(), createSPTokenFedConnectionProperties());
145153
}

src/test/java/com/databricks/jdbc/integration/e2e/OAuthTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ void testM2M() throws SQLException {
1616
assertDoesNotThrow(() -> connection.createStatement().execute("select 1"));
1717
}
1818

19+
@Test
20+
void testM2MWithSecretFromPwd() throws SQLException {
21+
Connection connection = getValidM2MConnectionWithSecretFromPwd();
22+
assertDoesNotThrow(() -> connection.createStatement().execute("select 1"));
23+
}
24+
1925
@Test
2026
void testPAT() throws SQLException {
2127
Properties connectionProperties = new Properties();

src/test/java/com/databricks/jdbc/integration/fakeservice/tests/M2MAuthIntegrationTests.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,64 @@ void testIncorrectCredentialsForM2M() {
4747
.contains("Connection failure while using the OSS Databricks JDBC driver.");
4848
}
4949

50+
@Test
51+
void testSuccessfulM2MConnectionWithSecretFromPwd() throws SQLException {
52+
String url = getFakeServiceM2MUrl() + "EnableOAuthSecretFromPwd=1;";
53+
Properties connProps = new Properties();
54+
connProps.put("OAuth2ClientId", TEST_CLIENT_ID);
55+
connProps.put("password", TEST_CLIENT_SECRET);
56+
connProps.put(
57+
DatabricksJdbcUrlParams.CONN_CATALOG.getParamName(),
58+
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_CATALOG.getParamName()));
59+
connProps.put(
60+
DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName(),
61+
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName()));
62+
63+
Connection conn = DriverManager.getConnection(url, connProps);
64+
assertNotNull(conn);
65+
assertFalse(conn.isClosed());
66+
conn.close();
67+
}
68+
69+
@Test
70+
void testM2MWithSecretFromPwd_PwdWinsOverExplicitSecret() throws SQLException {
71+
// When EnableOAuthSecretFromPwd=1, PWD/password takes precedence over OAuth2Secret.
72+
// Providing the correct secret in password and an invalid one in OAuth2Secret should succeed.
73+
String url = getFakeServiceM2MUrl() + "EnableOAuthSecretFromPwd=1;";
74+
Properties connProps = new Properties();
75+
connProps.put("OAuth2ClientId", TEST_CLIENT_ID);
76+
connProps.put("password", TEST_CLIENT_SECRET);
77+
connProps.put("OAuth2Secret", "invalid-should-be-ignored");
78+
connProps.put(
79+
DatabricksJdbcUrlParams.CONN_CATALOG.getParamName(),
80+
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_CATALOG.getParamName()));
81+
connProps.put(
82+
DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName(),
83+
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName()));
84+
85+
Connection conn = DriverManager.getConnection(url, connProps);
86+
assertNotNull(conn);
87+
assertFalse(conn.isClosed());
88+
conn.close();
89+
}
90+
91+
@Test
92+
void testM2MWithSecretFromPwd_NoPwdProvided_ThrowsError() {
93+
// EnableOAuthSecretFromPwd=1 but no PWD/password — should fail with validation error
94+
String url = getFakeServiceM2MUrl() + "EnableOAuthSecretFromPwd=1;";
95+
Properties connProps = new Properties();
96+
connProps.put("OAuth2ClientId", TEST_CLIENT_ID);
97+
connProps.put(
98+
DatabricksJdbcUrlParams.CONN_CATALOG.getParamName(),
99+
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_CATALOG.getParamName()));
100+
connProps.put(
101+
DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName(),
102+
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName()));
103+
104+
Exception e = assertThrows(Exception.class, () -> DriverManager.getConnection(url, connProps));
105+
assertTrue(e.getMessage().contains("EnableOAuthSecretFromPwd is enabled"));
106+
}
107+
50108
private Connection getValidM2MConnection() throws SQLException {
51109
return DriverManager.getConnection(
52110
getFakeServiceM2MUrl(), createFakeServiceM2MConnectionProperties(TEST_CLIENT_SECRET));

0 commit comments

Comments
 (0)