Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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).
Comment thread
msrathore-db marked this conversation as resolved.
Outdated

## 1.14.0

- Add statement-level query tag support (databricks/databricks-sql-nodejs#366 by @sreekanth-db)
Expand Down
14 changes: 9 additions & 5 deletions lib/connection/auth/DatabricksOAuth/OAuthManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,10 @@ 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';
// 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`;
}

protected getAuthorizationUrl(): string {
Expand All @@ -293,17 +296,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
}
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,65 @@ class OpenIDClientStub implements BaseClient {
});
});
});

describe('AzureOAuthManager (tenant awareness)', () => {
function makeAzure(overrides: Partial<OAuthManagerOptions> = {}) {
return new AzureOAuthManager({
host: 'adb-1234567890123456.1.azuredatabricks.net',
flow: OAuthFlow.M2M,
context: new ClientContextStub(),
...overrides,
});
}

// Access protected methods for unit inspection.
const call = <T>(mgr: AzureOAuthManager, name: string, ...args: unknown[]): T =>
(mgr as unknown as Record<string, (...a: unknown[]) => T>)[name](...args);

describe('getOIDCConfigUrl', () => {
it('falls back to /organizations/ when azureTenantId is not set (baseline-compatible)', () => {
const mgr = makeAzure();
const url = call<string>(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<string>(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<string>(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', () => {
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<string[]>(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<string[]>(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<string[]>(mgr, 'getScopes', [OAuthScope.offlineAccess]);
expect(scopes).to.include(OAuthScope.offlineAccess);
});
});
});
Loading