From f3fdcde00d8de927eef779bf8adb9277bbbaca62 Mon Sep 17 00:00:00 2001 From: Madhavendra Rathore Date: Tue, 21 Apr 2026 14:34:12 +0530 Subject: [PATCH 1/3] Fix Azure AD OAuth: route discovery through tenant; correct scope resource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 /.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 --- CHANGELOG.md | 4 ++ .../auth/DatabricksOAuth/OAuthManager.ts | 12 +++-- .../auth/DatabricksOAuth/OAuthManager.test.ts | 54 +++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 849a12a1..abb4d7d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## Unreleased + +- Fix Azure AD OAuth for tenant-specific and single-tenant Entra apps, and correct the scope resource: use the Databricks Azure Login App ID (not the tenant GUID) as the OAuth scope; route OIDC discovery to `login.microsoftonline.com/${azureTenantId}/` when `azureTenantId` is provided (fallback `/organizations/` preserved). + ## 1.13.0 - Add token federation support with custom token providers (databricks/databricks-sql-nodejs#318, databricks/databricks-sql-nodejs#319, databricks/databricks-sql-nodejs#320 by @madhav-db) diff --git a/lib/connection/auth/DatabricksOAuth/OAuthManager.ts b/lib/connection/auth/DatabricksOAuth/OAuthManager.ts index db7e7c69..38c8a45e 100644 --- a/lib/connection/auth/DatabricksOAuth/OAuthManager.ts +++ b/lib/connection/auth/DatabricksOAuth/OAuthManager.ts @@ -277,7 +277,8 @@ export class AzureOAuthManager extends OAuthManager { public static datatricksAzureApp = '2ff814a6-3304-4ab8-85cb-cd0e6f879c1d'; protected getOIDCConfigUrl(): string { - return 'https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration'; + const tenantPath = this.options.azureTenantId ?? 'organizations'; + return `https://login.microsoftonline.com/${tenantPath}/v2.0/.well-known/openid-configuration`; } protected getAuthorizationUrl(): string { @@ -293,17 +294,18 @@ export class AzureOAuthManager extends OAuthManager { } protected getScopes(requestedScopes: OAuthScopes): OAuthScopes { - // There is no corresponding scopes in Azure, instead, access control will be delegated to Databricks - const tenantId = this.options.azureTenantId ?? AzureOAuthManager.datatricksAzureApp; + // There is no corresponding scopes in Azure, instead, access control will be delegated to Databricks. + // Scope must be the Azure *resource* ID (the Databricks Azure Login App), NOT the tenant ID. + const resourceId = AzureOAuthManager.datatricksAzureApp; const azureScopes = []; switch (this.options.flow) { case OAuthFlow.U2M: - azureScopes.push(`${tenantId}/user_impersonation`); + azureScopes.push(`${resourceId}/user_impersonation`); break; case OAuthFlow.M2M: - azureScopes.push(`${tenantId}/.default`); + azureScopes.push(`${resourceId}/.default`); break; // no default } diff --git a/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts b/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts index c2367971..eaee176a 100644 --- a/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts +++ b/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts @@ -519,3 +519,57 @@ class OpenIDClientStub implements BaseClient { }); }); }); + +describe('AzureOAuthManager (tenant awareness)', () => { + function makeAzure(overrides: Partial = {}) { + return new AzureOAuthManager({ + host: 'adb-1234567890123456.1.azuredatabricks.net', + flow: OAuthFlow.M2M, + context: new ClientContextStub(), + ...overrides, + }); + } + + // Access protected methods for unit inspection. + const call = (mgr: AzureOAuthManager, name: string, ...args: unknown[]): T => + (mgr as unknown as Record T>)[name](...args); + + describe('getOIDCConfigUrl', () => { + it('falls back to /organizations/ when azureTenantId is not set (baseline-compatible)', () => { + const mgr = makeAzure(); + const url = call(mgr, 'getOIDCConfigUrl'); + expect(url).to.equal('https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration'); + }); + + it('uses the caller-supplied tenant in the discovery URL when provided', () => { + const tenant = '9f37a392-f0ae-4280-9796-f1864a10effc'; + const mgr = makeAzure({ azureTenantId: tenant }); + const url = call(mgr, 'getOIDCConfigUrl'); + expect(url).to.equal(`https://login.microsoftonline.com/${tenant}/v2.0/.well-known/openid-configuration`); + }); + }); + + describe('getScopes — resource ID is always the Azure Login App, never a tenant GUID', () => { + it('M2M scope uses the Databricks Azure Login App resource ID even when azureTenantId is set', () => { + const tenant = '9f37a392-f0ae-4280-9796-f1864a10effc'; + const mgr = makeAzure({ azureTenantId: tenant, flow: OAuthFlow.M2M }); + const scopes = call(mgr, 'getScopes', []); + expect(scopes).to.include(`${AzureOAuthManager.datatricksAzureApp}/.default`); + expect(scopes).to.not.include(`${tenant}/.default`); + }); + + it('U2M scope uses the Databricks Azure Login App resource ID even when azureTenantId is set', () => { + const tenant = '9f37a392-f0ae-4280-9796-f1864a10effc'; + const mgr = makeAzure({ azureTenantId: tenant, flow: OAuthFlow.U2M }); + const scopes = call(mgr, 'getScopes', []); + expect(scopes).to.include(`${AzureOAuthManager.datatricksAzureApp}/user_impersonation`); + expect(scopes).to.not.include(`${tenant}/user_impersonation`); + }); + + it('preserves offline_access when requested alongside M2M', () => { + const mgr = makeAzure({ flow: OAuthFlow.M2M }); + const scopes = call(mgr, 'getScopes', [OAuthScope.offlineAccess]); + expect(scopes).to.include(OAuthScope.offlineAccess); + }); + }); +}); From cb264abecff917a1e775734c62740eb15d42aa99 Mon Sep 17 00:00:00 2001 From: Madhavendra Rathore Date: Wed, 13 May 2026 15:23:12 +0530 Subject: [PATCH 2/3] Fix empty/whitespace azureTenantId falling through to malformed URL ??-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 --- lib/connection/auth/DatabricksOAuth/OAuthManager.ts | 4 +++- .../connection/auth/DatabricksOAuth/OAuthManager.test.ts | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/connection/auth/DatabricksOAuth/OAuthManager.ts b/lib/connection/auth/DatabricksOAuth/OAuthManager.ts index 38c8a45e..83d26ebf 100644 --- a/lib/connection/auth/DatabricksOAuth/OAuthManager.ts +++ b/lib/connection/auth/DatabricksOAuth/OAuthManager.ts @@ -277,7 +277,9 @@ export class AzureOAuthManager extends OAuthManager { public static datatricksAzureApp = '2ff814a6-3304-4ab8-85cb-cd0e6f879c1d'; protected getOIDCConfigUrl(): string { - const tenantPath = this.options.azureTenantId ?? 'organizations'; + // Use logical OR so empty / whitespace-only azureTenantId also falls back to /organizations/ + // (`??` only substitutes for null/undefined, leaving `''` to produce a malformed `//v2.0/...` URL). + const tenantPath = this.options.azureTenantId?.trim() || 'organizations'; return `https://login.microsoftonline.com/${tenantPath}/v2.0/.well-known/openid-configuration`; } diff --git a/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts b/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts index eaee176a..bfec40dd 100644 --- a/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts +++ b/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts @@ -547,6 +547,14 @@ describe('AzureOAuthManager (tenant awareness)', () => { const url = call(mgr, 'getOIDCConfigUrl'); expect(url).to.equal(`https://login.microsoftonline.com/${tenant}/v2.0/.well-known/openid-configuration`); }); + + it('falls back to /organizations/ when azureTenantId is empty or whitespace', () => { + for (const tenant of ['', ' ']) { + const mgr = makeAzure({ azureTenantId: tenant }); + const url = call(mgr, 'getOIDCConfigUrl'); + expect(url).to.equal('https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration'); + } + }); }); describe('getScopes — resource ID is always the Azure Login App, never a tenant GUID', () => { From 43ef11695577c4f4bccec15d82050e4e9473ba77 Mon Sep 17 00:00:00 2001 From: Madhavendra Rathore Date: Fri, 15 May 2026 14:31:40 +0530 Subject: [PATCH 3/3] Drop Unreleased CHANGELOG section to match repo convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entries are added under a versioned header in a dedicated "prepare release" PR — feature/fix PRs don't touch CHANGELOG. Co-authored-by: Isaac --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4cdd350..99d84602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,5 @@ # Release History -## Unreleased - -- Fix Azure AD OAuth for tenant-specific and single-tenant Entra apps, and correct the scope resource: use the Databricks Azure Login App ID (not the tenant GUID) as the OAuth scope; route OIDC discovery to `login.microsoftonline.com/${azureTenantId}/` when `azureTenantId` is provided (fallback `/organizations/` preserved). - ## 1.14.0 - Add statement-level query tag support (databricks/databricks-sql-nodejs#366 by @sreekanth-db)