Fix Azure AD OAuth: tenant-aware discovery URL + correct scope resource#363
Merged
Conversation
…ource
The AzureOAuthManager had two coupled bugs that prevented Azure AD OAuth
from working for tenant-specific Entra apps and any single-tenant Entra
app.
1) OIDC discovery URL was hardcoded to /organizations/, ignoring
azureTenantId. Single-tenant Entra apps cannot be resolved via
/organizations/ and return AADSTS50059. Now:
https://login.microsoftonline.com/${azureTenantId ?? 'organizations'}/v2.0/
.well-known/openid-configuration
When azureTenantId is unset, the URL is byte-identical to before
(/organizations/) — no regression for the multi-tenant default.
2) The OAuth scope was built from azureTenantId when provided:
const tenantId = this.options.azureTenantId ?? datatricksAzureApp;
azureScopes.push(`${tenantId}/.default`);
The Azure v2.0 scope format is <resource-app-id>/.default — a tenant
GUID isn't a resource, and Azure rejects with AADSTS500011. The scope
now always uses the Databricks Azure Login App ID.
Empirical verification (Prod Legacy, baseline vs. patched):
PAT: PASS / PASS (identical)
DB-M2M via DatabricksOAuthManager: PASS / PASS (identical)
AzureOAuthManager, no azureTenantId: AADSTS50059 / AADSTS50059
(identical; test cred is
single-tenant)
AzureOAuthManager + azureTenantId: AADSTS50059 / AADSTS7000215
(patched reaches Azure AD;
both fail because test env
has a Databricks-side
secret, not an Azure-Portal
secret)
AzureOAuthManager via useDatabricksOAuthInAzure: 403 / 403 (identical)
No prod path that worked on baseline fails on patched. The common
multi-tenant-app + no-azureTenantId path is byte-identical.
Unit tests cover:
- getOIDCConfigUrl fallback (no tenant) and tenant-specific
- getScopes: M2M+U2M scope always uses the Databricks Azure Login App
resource ID (never the tenant GUID); offline_access preserved
Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
c474301 to
f3fdcde
Compare
a418f44 to
f3fdcde
Compare
??-coalescing only substitutes for null/undefined, so an empty or whitespace-only azureTenantId would build `https://login.microsoftonline.com//v2.0/...` (double slash) and surface as an opaque Azure 404. Trim first and use || so any falsy/blank value falls back to /organizations/ like the unset case. Co-authored-by: Isaac
# Conflicts: # CHANGELOG.md
|
Thanks for your contribution! To satisfy the DCO policy in our contributing guide every commit message must include a sign-off message. One or more of your commits is missing this message. You can reword previous commit messages with an interactive rebase ( |
Entries are added under a versioned header in a dedicated "prepare release" PR — feature/fix PRs don't touch CHANGELOG. Co-authored-by: Isaac
|
Thanks for your contribution! To satisfy the DCO policy in our contributing guide every commit message must include a sign-off message. One or more of your commits is missing this message. You can reword previous commit messages with an interactive rebase ( |
vikrantpuppala
approved these changes
May 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AzureOAuthManagerhad two coupled bugs in how it constructed the Azure AD OAuth request. They were masked on the default path by two unrelated defaults happening to line up, but fired the moment a customer brought their own Entra app or setazureTenantId— which is the production-recommended posture (single-tenant Entra app per workspace, tenant supplied explicitly).This PR fixes both Azure-AD-side bugs, plus a third small footgun (empty/whitespace
azureTenantId) that the new template URL introduced.When this fires
Both U2M and M2M go through the same
AzureOAuthManager.getOIDCConfigUrl()andgetScopes()— Node has a single Azure code path, so any bug in those methods affects both flows. The bugs surface whenever any of the following is true:azureTenantIdoauthClientId(the security-recommended posture)The default path (no
oauthClientId, noazureTenantId, default multi-tenant Node client) works on baseline only because two unrelated defaults coincide. Any deviation from that exact default flips one or both bugs into a hard failure.Bug 1 — OIDC discovery URL is hardcoded to
/organizations/, ignoresazureTenantIdThe
/organizations/endpoint is Microsoft's "any work/school AAD tenant" multi-tenant endpoint. It only resolves for multi-tenant Entra apps. Single-tenant Entra apps fail at discovery with:The customer-supplied
azureTenantIdwas previously dropped on the floor — never used. After the fix, whenazureTenantIdis set the URL routes through that tenant; when unset the byte-identical/organizations/fallback is preserved for the default path.The
?.trim() || 'organizations'form (rather than?? 'organizations') also handles empty-string and whitespace-only inputs (e.g.process.env.AZURE_TENANT_ID ?? '') — without this, an empty value would renderhttps://login.microsoftonline.com//v2.0/...(double slash) and surface as an opaque Azure 404.Bug 2 — OAuth scope used the tenant GUID where the Azure resource App ID belongs
Azure v2.0 scope grammar is
<resource-app-id>/<permission-name>— the segment before the slash is the application you're requesting a token for (the Databricks Azure Login App,2ff814a6-3304-4ab8-85cb-cd0e6f879c1d), not the user's tenant. The pre-fix variable nametenantIdwas a misnomer; the fallback value (datatricksAzureApp) happened to be the correct resource ID, so the default path produced a working scope by coincidence. The moment a caller setazureTenantId, the scope became<tenant-GUID>/...and Azure rejected with:The fix separates the two concepts cleanly:
scope=parameter.Empirical verification — pecotesting production workspace
Same SPN credential, same workspace, same tenant — only the SDK code differs.
AzureOAuthManager.getToken()directly, M2M flow with a customer Entra app (d154b9ed-...) and Azure-Portal client secret, againstadb-6436897454825492.12.azuredatabricks.net, tenant9f37a392-f0ae-4280-9796-f1864a10effc:mainhttps://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration9f37a392-.../.default(tenant GUID — Bug 2)AADSTS50059— auth dies at discovery (Bug 1)https://login.microsoftonline.com/9f37a392-.../v2.0/.well-known/openid-configuration2ff814a6-.../.default(resource App ID — correct)access_tokenissued ✅End-to-end through
DBSQLClient.connect()with the same credentials:mainAADSTS50059— driver cannot acquire an Azure token; never reaches the workspaceHTTP 403from workspace ACL on2f03dd43e35e2aa0(separate authorization layer — the test SPN doesn't have warehouse permissions). With a permissioned identity, this row is a clean PASS.The 403 in the post-fix row is the same workspace-level ACL check every Azure-authenticated client hits; it's downstream of any auth code. On
mainyou cannot reach that check at all because Azure-AD authentication itself fails.Backward compatibility
When
azureTenantIdis unset, both methods produce byte-identical output to baseline:…/organizations/v2.0/…— character-for-character unchanged.2ff814a6-3304-4ab8-85cb-cd0e6f879c1d/.default(M2M) or…/user_impersonation(U2M) — same value the pre-fix fallback produced.Verified by reading both code paths and by running the default-path test against production (identical request, identical Azure response). No regression risk for the default multi-tenant Node client path that "worked by luck" before.
Test plan
getOIDCConfigUrl(fallback, tenant-specific, empty/whitespace-fallback) andgetScopes(resource-ID invariant for M2M+U2M,offline_accesspreserved). All 34AzureOAuthManager+DatabricksOAuthManagersuite tests pass locally.mainreproducesAADSTS50059; this branch produces a real Azure access token. End-to-end query blocked only by workspace ACL (separate from auth — confirmed byPERMISSION_DENIEDfrom the SQL endpoint, not from Azure).