Skip to content

Commit d1cdd1b

Browse files
suryaiyer95claude
andcommitted
fix: acquire Azure AD tokens directly to bypass Bun browser-bundle resolution
- For `azure-active-directory-default` (CLI/default auth), acquire token ourselves instead of delegating to tedious's internal `@azure/identity` - Strategy: try `DefaultAzureCredential` first, fall back to `az` CLI subprocess - Bypasses Bun resolving `@azure/identity` to browser bundle where `DefaultAzureCredential` is a non-functional stub - Also bypasses CJS/ESM `isTokenCredential` boundary mismatch - All 31 driver unit tests pass, verified against real Fabric endpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b69a3d2 commit d1cdd1b

2 files changed

Lines changed: 72 additions & 22 deletions

File tree

packages/drivers/src/sqlserver.ts

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,60 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
5454
const authType = rawAuth ? (AUTH_SHORTHANDS[rawAuth.toLowerCase()] ?? rawAuth) : undefined
5555

5656
if (authType?.startsWith("azure-active-directory")) {
57-
// Azure AD / Entra ID — tedious handles credential creation internally.
58-
// We pass the type + options; tedious imports @azure/identity itself.
59-
// Verify @azure/identity is available before attempting Azure AD auth.
60-
try {
61-
await import("@azure/identity")
62-
} catch {
63-
throw new Error(
64-
"Azure AD authentication requires @azure/identity. Run: npm install @azure/identity",
65-
)
66-
}
6757
;(mssqlConfig.options as any).encrypt = true
6858

6959
if (authType === "azure-active-directory-default") {
60+
// Acquire a token ourselves and pass it as a raw access token string.
61+
// We avoid using @azure/identity's DefaultAzureCredential because:
62+
// 1. Bun can resolve @azure/identity to the browser bundle (inside
63+
// tedious or even our own import), where DefaultAzureCredential
64+
// is a non-functional stub that throws.
65+
// 2. Passing a credential object via type:"token-credential" hits a
66+
// CJS/ESM isTokenCredential boundary mismatch in Bun.
67+
//
68+
// Strategy: try @azure/identity first (works when module resolution
69+
// is correct), fall back to shelling out to `az account get-access-token`
70+
// (works everywhere Azure CLI is installed).
71+
let token: string | undefined
72+
73+
// Attempt 1: @azure/identity (fast, no subprocess)
74+
try {
75+
const azureIdentity = await import("@azure/identity")
76+
const credential = new azureIdentity.DefaultAzureCredential(
77+
config.azure_client_id
78+
? { managedIdentityClientId: config.azure_client_id as string }
79+
: undefined,
80+
)
81+
const tokenResponse = await credential.getToken("https://database.windows.net/.default")
82+
token = tokenResponse?.token
83+
} catch {
84+
// @azure/identity unavailable or browser bundle — fall through
85+
}
86+
87+
// Attempt 2: Azure CLI subprocess (universal fallback)
88+
if (!token) {
89+
try {
90+
const { execSync } = await import("node:child_process")
91+
const json = execSync(
92+
"az account get-access-token --resource https://database.windows.net/ --query accessToken -o tsv",
93+
{ encoding: "utf-8", timeout: 15000, stdio: ["pipe", "pipe", "pipe"] },
94+
).trim()
95+
if (json) token = json
96+
} catch {
97+
// az CLI not installed or not logged in
98+
}
99+
}
100+
101+
if (!token) {
102+
throw new Error(
103+
"Azure AD default auth failed. Either install @azure/identity (npm install @azure/identity) " +
104+
"or log in with Azure CLI (az login).",
105+
)
106+
}
107+
70108
mssqlConfig.authentication = {
71-
type: "azure-active-directory-default",
72-
options: {
73-
...(config.azure_client_id ? { clientId: config.azure_client_id as string } : {}),
74-
},
109+
type: "azure-active-directory-access-token",
110+
options: { token },
75111
}
76112
} else if (authType === "azure-active-directory-password") {
77113
mssqlConfig.authentication = {

packages/drivers/test/sqlserver-unit.test.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ mock.module("mssql", () => ({
6565
},
6666
}))
6767

68+
mock.module("@azure/identity", () => ({
69+
DefaultAzureCredential: class {
70+
_opts: any
71+
constructor(opts?: any) { this._opts = opts }
72+
async getToken(_scope: string) { return { token: "mock-azure-token-12345", expiresOnTimestamp: Date.now() + 3600000 } }
73+
},
74+
}))
75+
76+
mock.module("node:child_process", () => ({
77+
execSync: (_cmd: string) => "mock-cli-token-fallback\n",
78+
}))
79+
6880
// Import after mocking
6981
const { connect } = await import("../src/sqlserver")
7082

@@ -250,7 +262,7 @@ describe("SQL Server driver unit tests", () => {
250262
})
251263
})
252264

253-
test("azure-active-directory-default passes type to tedious (no credential object)", async () => {
265+
test("azure-active-directory-default acquires token and passes as access-token", async () => {
254266
resetMocks()
255267
const c = await connect({
256268
host: "myserver.database.windows.net",
@@ -259,11 +271,11 @@ describe("SQL Server driver unit tests", () => {
259271
})
260272
await c.connect()
261273
const cfg = mockConnectCalls[0]
262-
expect(cfg.authentication.type).toBe("azure-active-directory-default")
263-
expect(cfg.authentication.options.credential).toBeUndefined()
274+
expect(cfg.authentication.type).toBe("azure-active-directory-access-token")
275+
expect(cfg.authentication.options.token).toBe("mock-azure-token-12345")
264276
})
265277

266-
test("azure-active-directory-default with client_id passes clientId option", async () => {
278+
test("azure-active-directory-default with client_id passes managedIdentityClientId to credential", async () => {
267279
resetMocks()
268280
const c = await connect({
269281
host: "myserver.database.windows.net",
@@ -273,8 +285,9 @@ describe("SQL Server driver unit tests", () => {
273285
})
274286
await c.connect()
275287
const cfg = mockConnectCalls[0]
276-
expect(cfg.authentication.type).toBe("azure-active-directory-default")
277-
expect(cfg.authentication.options.clientId).toBe("mi-client-id")
288+
// Token is still passed as access-token regardless of client_id
289+
expect(cfg.authentication.type).toBe("azure-active-directory-access-token")
290+
expect(cfg.authentication.options.token).toBe("mock-azure-token-12345")
278291
})
279292

280293
test("encryption forced for all Azure AD connections", async () => {
@@ -299,7 +312,7 @@ describe("SQL Server driver unit tests", () => {
299312
expect(cfg.options.encrypt).toBe(false)
300313
})
301314

302-
test("'CLI' shorthand maps to azure-active-directory-default", async () => {
315+
test("'CLI' shorthand acquires token via DefaultAzureCredential", async () => {
303316
resetMocks()
304317
const c = await connect({
305318
host: "myserver.datawarehouse.fabric.microsoft.com",
@@ -308,7 +321,8 @@ describe("SQL Server driver unit tests", () => {
308321
})
309322
await c.connect()
310323
const cfg = mockConnectCalls[0]
311-
expect(cfg.authentication.type).toBe("azure-active-directory-default")
324+
expect(cfg.authentication.type).toBe("azure-active-directory-access-token")
325+
expect(cfg.authentication.options.token).toBe("mock-azure-token-12345")
312326
expect(cfg.options.encrypt).toBe(true)
313327
})
314328

0 commit comments

Comments
 (0)