Skip to content

Commit e1764fe

Browse files
Reject credentials_provider with oauth_client_secret; document M2M options
- credentials_provider together with oauth_client_secret now raises ValueError instead of silently using the provider, matching the kernel auth bridge. azure-sp-m2m keeps ignoring oauth_* values. - The missing-oauth_client_id check no longer fires for azure-sp-m2m. - oauth_redirect_port is documented as U2M-only; oauth_client_id and oauth_client_secret docs describe the M2M shape and its exclusions. - CONNECTION_PARAMETERS.md: oauth_client_secret is supported on Thrift. - Add changelog entries. Signed-off-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
1 parent 8cf89b6 commit e1764fe

5 files changed

Lines changed: 56 additions & 12 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Release History
22

3+
# Unreleased
4+
- Support OAuth M2M (client credentials) for Databricks service principals on the default Thrift backend: `oauth_client_id` + `oauth_client_secret` now authenticate with tokens from the workspace `/oidc/v1/token` endpoint (scope `all-apis`), refreshed as they expire. Previously the secret was ignored and the connection started an interactive browser login.
5+
- Reject an unsupported `auth_type` with `ValueError` instead of falling back to the interactive browser login, and reject `oauth_client_secret` together with `credentials_provider` or a U2M `auth_type`, as the kernel backend does.
6+
- Fix: token responses with extra fields (such as `scope`) no longer fail in `ClientCredentialsTokenSource`.
7+
38
# 4.6.0 (2026-09-24)
49
- Upgrade Databricks SQL Kernel to 1.1.0; the kernel dependency is now stable and no longer experimental.
510
- Transparently auto-recover Thrift connections to Reyden / Real-Time warehouses: when a warehouse rejects the default Thrift protocol (SQLSTATE `KP001`), the session is re-opened on the kernel backend and the warehouse is remembered so later connections skip Thrift. Applies only when no backend was chosen explicitly.

‎CONNECTION_PARAMETERS.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ to change without notice.
8383
| Option | Type | Thrift | Kernel | Default Value | Note |
8484
| --------------------------------------------------- | -------------------- | :----: | :----: | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
8585
| `access_token` (PAT) | `str` | ✅ | ✅ | `None` | Personal Access Token / bearer token. When no auth signal is supplied, Thrift falls back to Databricks OAuth U2M; Kernel requires an explicit U2M `auth_type` or another supported credential flow. |
86-
| `auth_type` | `str` | ✅ | ✅ | `None` | `databricks-oauth` (U2M), `azure-oauth` (Azure AD U2M), or `azure-sp-m2m` (Azure service-principal M2M). All three work on both backends. Thrift treats an otherwise credential-less `None` as Databricks OAuth; Kernel does not implicitly select U2M. On Kernel, `azure-oauth` uses the same workspace-federated browser flow as `databricks-oauth`. |
87-
| `oauth_client_id` (OAuth) | `str` | ✅ | ✅ | built-in id for U2M | Custom U2M client id on both backends. Kernel also uses it with `oauth_client_secret` or the JWT options for M2M; those M2M flows have no built-in client-id default. |
88-
| `oauth_redirect_port` (U2M) | `int` | ✅ | ✅ | `None` | Localhost redirect port for the browser flow. On **both** backends it is only honored when a custom `oauth_client_id` is also supplied — then that single port becomes the redirect URI. With the built-in client id (or when omitted) the connector uses the full registered range 8020–8024 and binds the first free port, so a bare `oauth_redirect_port` has no effect. (Thrift: `auth.py` `oauth_redirect_port_range`; Kernel: same logic, forwarded as `redirect_ports`.) |
89-
| `oauth_client_secret` (OAuth M2M) | `str` | ❌ | ✅ | `None` | **Kernel-only in practice.** The Thrift auth path never reads `oauth_client_secret`; use `credentials_provider` or an Azure service principal for M2M on Thrift. |
86+
| `auth_type` | `str` | ✅ | ✅ | `None` | `databricks-oauth` (U2M), `azure-oauth` (Azure AD U2M), or `azure-sp-m2m` (Azure service-principal M2M). All three work on both backends. Thrift rejects any other value with `ValueError`. Thrift treats an otherwise credential-less `None` as Databricks OAuth; Kernel does not implicitly select U2M. On Kernel, `azure-oauth` uses the same workspace-federated browser flow as `databricks-oauth`. |
87+
| `oauth_client_id` (OAuth) | `str` | ✅ | ✅ | built-in id for U2M | Custom U2M client id on both backends. Both backends also use it with `oauth_client_secret` for M2M, and Kernel with the JWT options; those M2M flows have no built-in client-id default. |
88+
| `oauth_redirect_port` (U2M) | `int` | ✅ | ✅ | `None` | Localhost redirect port for the browser (U2M) flow; not used for M2M. On **both** backends it is only honored when a custom `oauth_client_id` is also supplied — then that single port becomes the redirect URI. With the built-in client id (or when omitted) the connector uses the full registered range 8020–8024 and binds the first free port, so a bare `oauth_redirect_port` has no effect. (Thrift: `auth.py` `oauth_redirect_port_range`; Kernel: same logic, forwarded as `redirect_ports`.) |
89+
| `oauth_client_secret` (OAuth M2M) | `str` | ✅ | ✅ | `None` | With `oauth_client_id`, selects OAuth M2M (client credentials) for a Databricks service principal on both backends. On Thrift the token comes from `https://<host>/oidc/v1/token` with scope `all-apis` and is refreshed as it expires. Rejected together with `credentials_provider` or `auth_type` `databricks-oauth`/`azure-oauth`; ignored with `azure-sp-m2m`. |
9090
| `oauth_jwt_key_file` (OAuth M2M, JWT private key) | `str` | ❌ | ✅ | `None` | **Kernel-only.** Path to the PEM private key for JWT private-key M2M (RFC 7523 client assertion). Supplying it selects the JWT flow: the kernel signs a short-lived assertion with the key instead of sending a client secret. Requires `oauth_client_id` + `oauth_jwt_kid`; mutually exclusive with `oauth_client_secret` / `credentials_provider`. |
9191
| `oauth_jwt_kid` (OAuth M2M, JWT private key) | `str` | ❌ | ✅ | `None` | **Kernel-only.** Key id written into the JWT header so the IdP can select the registered public key. Required with `oauth_jwt_key_file`. (For Entra ID this is the certificate's `x5t` thumbprint.) |
9292
| `oauth_jwt_passphrase` (OAuth M2M, JWT private key) | `str` | ❌ | ✅ | `None` | **Kernel-only.** Passphrase for an encrypted PKCS#8 private key; omit for an unencrypted key. |
@@ -215,9 +215,9 @@ installed kernel binding. `force_enable_telemetry` remains Python-driver-only.
215215

216216
### Supported on Kernel, missing / ignored on Thrift
217217

218-
1. Kernel-managed OAuth M2M: `oauth_client_secret`, `oauth_jwt_key_file`,
219-
`oauth_jwt_kid`, `oauth_jwt_passphrase`, `oauth_jwt_algorithm`, and
220-
`token_url`.
218+
1. Kernel-managed OAuth M2M options: `oauth_jwt_key_file`, `oauth_jwt_kid`,
219+
`oauth_jwt_passphrase`, `oauth_jwt_algorithm`, and `token_url`
220+
(`oauth_client_secret` M2M works on both backends).
221221
2. Custom `oauth_scopes`.
222222
3. Kernel U2M encrypted token storage (`oauth_token_cache_enabled`).
223223

‎src/databricks/sql/auth/auth.py‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,22 @@ def get_auth_provider(cfg: ClientContext, http_client):
2424
+ ", ".join(t.value for t in AuthType)
2525
)
2626

27+
# azure-sp-m2m is explicit and uses the azure_* credentials; oauth_* values
28+
# are ignored for it, as on the kernel path.
29+
oauth_m2m = (
30+
bool(cfg.oauth_client_secret) and cfg.auth_type != AuthType.AZURE_SP_M2M.value
31+
)
32+
if oauth_m2m and cfg.credentials_provider:
33+
# Rejected on the kernel path too; neither should silently win.
34+
raise ValueError(
35+
"Ambiguous auth: both a custom credentials_provider and "
36+
"oauth_client_secret were provided. Pass oauth_client_id + "
37+
"oauth_client_secret for OAuth M2M, or credentials_provider alone."
38+
)
39+
2740
if cfg.credentials_provider:
2841
base_provider = ExternalAuthProvider(cfg.credentials_provider)
29-
elif cfg.oauth_client_secret and cfg.auth_type != AuthType.AZURE_SP_M2M.value:
42+
elif oauth_m2m:
3043
if cfg.auth_type in [
3144
AuthType.DATABRICKS_OAUTH.value,
3245
AuthType.AZURE_OAUTH.value,
@@ -161,6 +174,11 @@ def get_python_sql_connector_auth_provider(hostname: str, http_client, **kwargs)
161174
identity_federation_client_id=kwargs.get("identity_federation_client_id"),
162175
oauth_client_secret=kwargs.get("oauth_client_secret"),
163176
)
164-
if cfg.oauth_client_secret and not kwargs.get("oauth_client_id"):
177+
if (
178+
cfg.oauth_client_secret
179+
and cfg.auth_type != AuthType.AZURE_SP_M2M.value
180+
and not cfg.credentials_provider
181+
and not kwargs.get("oauth_client_id")
182+
):
165183
raise ValueError("OAuth M2M needs oauth_client_id with oauth_client_secret")
166184
return get_auth_provider(cfg, http_client)

‎src/databricks/sql/client.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,19 @@ def __init__(
229229
230230
oauth_client_id: `str`, optional
231231
custom oauth client_id. If not specified, it will use the built-in client_id of databricks-sql-python.
232+
For OAuth M2M (with `oauth_client_secret`) this is the service principal's client ID and is required.
232233
233234
oauth_client_secret: `str`, optional
234235
OAuth secret of a service principal. Together with `oauth_client_id`
235236
(the service principal's client ID) this selects OAuth
236237
machine-to-machine authentication (client credentials, scope
237-
`all-apis`), with tokens refreshed as they expire.
238+
`all-apis`), with tokens refreshed as they expire. Cannot be
239+
combined with `credentials_provider` or with `auth_type`
240+
`databricks-oauth` / `azure-oauth`; ignored for `azure-sp-m2m`.
238241
239242
oauth_redirect_port: `int`, optional
240-
port of the oauth redirect uri (localhost). This is required when custom oauth client_id
241-
`oauth_client_id` is set
243+
port of the oauth redirect uri (localhost) for the interactive (U2M) flow. This is required when
244+
a custom `oauth_client_id` is used for U2M. Not used for OAuth M2M (`oauth_client_secret`).
242245
243246
identity_federation_client_id: `str`, optional
244247
Service-principal client ID for mandatory SP-wide workload identity

‎tests/unit/test_auth.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,24 @@ def test_get_python_sql_connector_auth_provider_oauth_m2m_errors(self):
205205
oauth_client_secret="s",
206206
)
207207

208+
def test_get_python_sql_connector_auth_provider_oauth_m2m_ambiguous(self):
209+
class MyProvider(CredentialsProvider):
210+
def auth_type(self) -> str:
211+
return "mine"
212+
213+
def __call__(self, *args, **kwargs) -> HeaderFactory:
214+
return lambda: {"foo": "bar"}
215+
216+
for client_id in ("c", None):
217+
with self.assertRaisesRegex(ValueError, "Ambiguous auth"):
218+
get_python_sql_connector_auth_provider(
219+
"example.cloud.databricks.com",
220+
MagicMock(),
221+
credentials_provider=MyProvider(),
222+
oauth_client_id=client_id,
223+
oauth_client_secret="s",
224+
)
225+
208226
def test_get_python_sql_connector_auth_provider_unknown_auth_type(self):
209227
"""An unsupported auth_type must not fall back to a browser login."""
210228
with patch(

0 commit comments

Comments
 (0)