Skip to content

Commit 173d32f

Browse files
suryaiyer95claude
andcommitted
fix: auto-acquire Azure AD token for azure-active-directory-access-token when none supplied
The `azure-active-directory-access-token` branch passed `token: config.token ?? config.access_token` to tedious. When neither field was set on a connection (e.g. a `fabric-migration` entry that declared the auth type but no token), tedious threw: TypeError: The "config.authentication.options.token" property must be of type string This blocked any Fabric/MSSQL config that relied on ambient credentials (Azure CLI / managed identity) but used the explicit `azure-active-directory-access-token` type instead of the `default` shorthand. Refactor token acquisition (`DefaultAzureCredential` → `az` CLI fallback) into a shared `acquireAzureToken()` helper used by both the `default` path and the `access-token` path when no token was supplied. Callers that pass an explicit token are unchanged. Also harden `mock.module("node:child_process", ...)` in `sqlserver-unit.test.ts` to spread the real module so sibling tests in the same `bun test` run keep access to `spawn` / `exec` / `fork`. Tests: 110 pass, 0 fail in `packages/drivers`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d1cdd1b commit 173d32f

2 files changed

Lines changed: 48 additions & 20 deletions

File tree

packages/drivers/src/sqlserver.ts

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,23 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
5656
if (authType?.startsWith("azure-active-directory")) {
5757
;(mssqlConfig.options as any).encrypt = true
5858

59-
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).
59+
// Resolve a raw Azure AD access token.
60+
// Used by both `azure-active-directory-default` and by
61+
// `azure-active-directory-access-token` when no token was provided.
62+
//
63+
// We acquire the token ourselves rather than letting tedious do it because:
64+
// 1. Bun can resolve @azure/identity to the browser bundle (inside
65+
// tedious or even our own import), where DefaultAzureCredential
66+
// is a non-functional stub that throws.
67+
// 2. Passing a credential object via type:"token-credential" hits a
68+
// CJS/ESM isTokenCredential boundary mismatch in Bun.
69+
//
70+
// Strategy: try @azure/identity first (works when module resolution
71+
// is correct), fall back to shelling out to `az account get-access-token`
72+
// (works everywhere Azure CLI is installed).
73+
const acquireAzureToken = async (): Promise<string> => {
7174
let token: string | undefined
7275

73-
// Attempt 1: @azure/identity (fast, no subprocess)
7476
try {
7577
const azureIdentity = await import("@azure/identity")
7678
const credential = new azureIdentity.DefaultAzureCredential(
@@ -84,30 +86,32 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
8486
// @azure/identity unavailable or browser bundle — fall through
8587
}
8688

87-
// Attempt 2: Azure CLI subprocess (universal fallback)
8889
if (!token) {
8990
try {
9091
const { execSync } = await import("node:child_process")
91-
const json = execSync(
92+
const out = execSync(
9293
"az account get-access-token --resource https://database.windows.net/ --query accessToken -o tsv",
9394
{ encoding: "utf-8", timeout: 15000, stdio: ["pipe", "pipe", "pipe"] },
9495
).trim()
95-
if (json) token = json
96+
if (out) token = out
9697
} catch {
9798
// az CLI not installed or not logged in
9899
}
99100
}
100101

101102
if (!token) {
102103
throw new Error(
103-
"Azure AD default auth failed. Either install @azure/identity (npm install @azure/identity) " +
104+
"Azure AD token acquisition failed. Either install @azure/identity (npm install @azure/identity) " +
104105
"or log in with Azure CLI (az login).",
105106
)
106107
}
108+
return token
109+
}
107110

111+
if (authType === "azure-active-directory-default") {
108112
mssqlConfig.authentication = {
109113
type: "azure-active-directory-access-token",
110-
options: { token },
114+
options: { token: await acquireAzureToken() },
111115
}
112116
} else if (authType === "azure-active-directory-password") {
113117
mssqlConfig.authentication = {
@@ -120,9 +124,12 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
120124
},
121125
}
122126
} else if (authType === "azure-active-directory-access-token") {
127+
// If the caller supplied a token, use it; otherwise acquire one
128+
// automatically (DefaultAzureCredential → az CLI).
129+
const suppliedToken = (config.token ?? config.access_token) as string | undefined
123130
mssqlConfig.authentication = {
124131
type: "azure-active-directory-access-token",
125-
options: { token: config.token ?? config.access_token },
132+
options: { token: suppliedToken ?? (await acquireAzureToken()) },
126133
}
127134
} else if (
128135
authType === "azure-active-directory-msi-vm" ||

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,12 @@ mock.module("@azure/identity", () => ({
7373
},
7474
}))
7575

76+
// Bun's mock.module() replaces the module for ALL test files in the same run,
77+
// so we re-export every symbol other tests might import (spawn, exec, fork, etc.)
78+
// in addition to the execSync stub used by the Azure CLI fallback path.
79+
const realChildProcess = await import("node:child_process")
7680
mock.module("node:child_process", () => ({
81+
...realChildProcess,
7782
execSync: (_cmd: string) => "mock-cli-token-fallback\n",
7883
}))
7984

@@ -193,7 +198,7 @@ describe("SQL Server driver unit tests", () => {
193198
expect(cfg.password).toBeUndefined()
194199
})
195200

196-
test("azure-active-directory-access-token passes token", async () => {
201+
test("azure-active-directory-access-token passes supplied token unchanged", async () => {
197202
resetMocks()
198203
const c = await connect({
199204
host: "myserver.database.windows.net",
@@ -209,6 +214,22 @@ describe("SQL Server driver unit tests", () => {
209214
})
210215
})
211216

217+
test("azure-active-directory-access-token with no token auto-acquires one", async () => {
218+
// Regression: prior to this, omitting `token`/`access_token` resulted in
219+
// `options.token: undefined`, which tedious rejects with
220+
// "config.authentication.options.token must be of type string".
221+
resetMocks()
222+
const c = await connect({
223+
host: "myserver.database.windows.net",
224+
database: "db",
225+
authentication: "azure-active-directory-access-token",
226+
})
227+
await c.connect()
228+
const cfg = mockConnectCalls[0]
229+
expect(cfg.authentication.type).toBe("azure-active-directory-access-token")
230+
expect(cfg.authentication.options.token).toBe("mock-azure-token-12345")
231+
})
232+
212233
test("azure-active-directory-service-principal-secret builds SP auth", async () => {
213234
resetMocks()
214235
const c = await connect({

0 commit comments

Comments
 (0)