You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(kernel): JWT private-key M2M auth on use_kernel=True (#921)
* feat(kernel): support JWT private-key M2M auth on use_kernel=True
Route JWT private-key client-assertion auth (RFC 7523) through the kernel
backend. When the caller passes `oauth_jwt_key_file` (+ `oauth_client_id`
and `oauth_jwt_kid`, optional `oauth_jwt_passphrase` / `oauth_jwt_algorithm`
/ `oauth_scopes` / `token_url`), the bridge forwards them to the kernel's
`auth_type="oauth-m2m-jwt"`, which signs a short-lived assertion with the
private key instead of sending a client secret and owns the token lifecycle.
- auth_bridge.py: new JWT branch (checked before shared-secret M2M and PAT,
since a private-key file is unambiguous JWT M2M intent); mutually exclusive
with oauth_client_secret / credentials_provider; requires client_id + kid.
- session.py: forward the new oauth_jwt_* / token_url kwargs into the kernel
auth options.
- tests: 9 unit tests covering routing, precedence, validation, and
ambiguity guards.
Verified end-to-end: `SELECT 1` via use_kernel=True against an Azure
Databricks warehouse, authenticated by Entra ID against the service
principal's registered public certificate.
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>
* fix(kernel): annotate kwargs at first assignment to satisfy mypy
The JWT branch introduced an earlier untyped `kwargs =`, so mypy flagged
the M2M branch's `kwargs: Dict[str, Any]` as a redefinition. Move the
annotation to the first (JWT) assignment.
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>
* fix(kernel): guard JWT M2M + databricks-oauth auth_type; renumber comments
Address peco-review-bot review on #921:
- Medium: add the missing ambiguity guard for oauth_jwt_key_file +
auth_type="databricks-oauth" (U2M intent), mirroring the existing
shared-secret M2M + U2M guard. Fails loudly rather than silently
resolving to one flow. Covered by a new unit test.
- Low: renumber the inline resolution-order comments (PAT→3, U2M→4,
creds→5, else→6) to match the docstring after the JWT branch insert.
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>
* docs: document JWT private-key M2M params in CONNECTION_PARAMETERS.md
Address Eric's review on #921: add rows for oauth_jwt_key_file /
oauth_jwt_kid / oauth_jwt_passphrase / oauth_jwt_algorithm / token_url
to the connection-parameter reference (all kernel-only).
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>
* security: suppress unreachable cryptography PKCS#7 advisory (osv-scanner)
Security Scan flagged cryptography@49.0.0 CVE-2026-69247
(GHSA-g6cj-pr64-35w5 / PYSEC-2026-3552, CVSS 8.2) — a Bleichenbacher-style
oracle in PKCS#7 EnvelopedData / S-MIME decryption. `cryptography` is a
transitive dep (PyJWT[crypto] / oauthlib) used only for OAuth token
signing/verification; the connector never decrypts PKCS#7 / S-MIME, so the
vulnerable path is never invoked. Pre-existing on main (transitive, not
introduced by this PR). Suppress both OSV ids with justification; a clean
bump awaits PyJWT/oauthlib floors pulling cryptography>=50.
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>
* fix(kernel): forward token_url on shared-secret M2M too (JDBC parity)
Address peco-review-bot: the CONNECTION_PARAMETERS.md row said token_url
"applies to shared-secret M2M too", but the bridge only forwarded it on
the JWT branch — so an oauth_client_id + oauth_client_secret + token_url
connection silently dropped token_url.
token_url is an auth-method-agnostic token-endpoint override: JDBC's
OAuth2ConnAuthTokenEndpoint is consumed by the client-secret M2M provider,
the JWT provider, and the refresh provider alike; the Node driver also
forwards it on both branches; and the kernel's oauth-m2m auth_type accepts
it (pyo3). Forward it on the shared-secret M2M branch so code matches the
doc and the other drivers. Adds a unit test.
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>
---------
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,7 @@
1
1
# Release History
2
2
3
3
# Unreleased
4
+
- Kernel backend (`use_kernel=True`): OAuth **M2M with a JWT private-key client assertion** (RFC 7523) is now supported. Pass `oauth_client_id` + `oauth_jwt_key_file` + `oauth_jwt_kid` (with optional `oauth_jwt_passphrase` for an encrypted PKCS#8 key, `oauth_jwt_algorithm` defaulting to `RS256`, `oauth_scopes`, and `token_url` for the IdP token endpoint) and the connector routes them to the kernel's `auth_type="oauth-m2m-jwt"`, which signs a short-lived assertion with the private key instead of sending a client secret. The kernel owns the token lifecycle. A private-key file is treated as unambiguous JWT M2M intent and is mutually exclusive with `oauth_client_secret` / `credentials_provider` (both raise `NotSupportedError`). Verified end-to-end against an Azure Databricks workspace with the service principal's public certificate registered on its Entra ID app registration. Requires `databricks-sql-kernel >= 0.2.0` with JWT support.
4
5
- Kernel backend (`use_kernel=True`): OAuth U2M with `auth_type="databricks-oauth"` now forwards the connector's `databricks-sql-python` OAuth-app bundle (`client_id` + `sql offline_access` scopes + redirect port) into the kernel, so a bare U2M connection authenticates as `databricks-sql-python` — parity with the Thrift path — instead of inheriting the kernel's own `databricks-sql-connector` default. A caller-supplied `oauth_client_id` (with its coupled `oauth_redirect_port`) is honored, as is a caller-supplied `oauth_scopes`; absent one, the connector default (`sql offline_access`) is forwarded. Note: the kernel binds a single U2M redirect port, so unlike the Thrift path (which tries the full `8020..8024` range) the kernel path uses only one port and does not fall back to the next port if it is already bound — pass `oauth_redirect_port` (with `oauth_client_id`) to pick a free one on a port collision. `auth_type="azure-oauth"` (Azure AD) is not yet supported on the kernel path and raises `NotSupportedError` — use the Thrift backend for it (PECOBLR-4040; Azure tracked by PECOBLR-4120)
Copy file name to clipboardExpand all lines: CONNECTION_PARAMETERS.md
+5Lines changed: 5 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -73,6 +73,11 @@ to change without notice.
73
73
|`oauth_client_id` (U2M) |`str`| ✅ | ✅ | built-in client id | Custom U2M client id. Forwarded on both; when absent, each path applies its own built-in default. |
74
74
|`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`.) |
75
75
|`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. |
76
+
|`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`. |
77
+
|`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.) |
78
+
|`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. |
|`token_url` (OAuth M2M) |`str`| ❌ | ✅ |`None` ⇒ OIDC discovery |**Kernel-only.** OAuth IdP token endpoint override. Required for JWT M2M against an external-IdP-backed workspace (e.g. Entra ID for Azure Databricks), since Databricks-native OIDC does not advertise the `private_key_jwt` method. Applies to shared-secret M2M too. |
76
81
|`oauth_scopes`|`List[str]`| ❌ | ✅ |`["sql","offline_access"]`|**Thrift ignores custom scopes** — it always uses the built-in scope set. Only the kernel honors a custom `oauth_scopes`. |
77
82
|`credentials_provider`|`CredentialsProvider`| ✅ | ❌ |`None`| Custom external credentials provider. **Rejected on the kernel path** (`NotSupportedError`) — it is an opaque token source, so the kernel cannot own the token lifecycle; use `oauth_client_id` + `oauth_client_secret` for M2M, or the Thrift backend. |
78
83
|`identity_federation_client_id`|`str`| ✅ | ✅ |`None`| Workload identity / token-federation client id (kernel support added in #910). |
0 commit comments