Skip to content
Open
Show file tree
Hide file tree
Changes from all 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).

## 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)
Expand Down
12 changes: 7 additions & 5 deletions lib/connection/auth/DatabricksOAuth/OAuthManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
54 changes: 54 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,57 @@ 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`);
});
});

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