From 42ecf2091d23a60b0ae09249ee390c54127300d4 Mon Sep 17 00:00:00 2001 From: Parth Bansal Date: Tue, 6 Oct 2026 10:58:54 +0000 Subject: [PATCH] Add explicit Azure CLI authentication --- package-lock.json | 492 +++++++++++++++++- packages/auth/NEXT_CHANGELOG.md | 4 + packages/auth/package.json | 1 + packages/auth/src/credentials/azure-cli.ts | 66 +++ packages/auth/src/credentials/errors.ts | 20 + packages/auth/src/credentials/index.ts | 4 + .../auth/tests/credentials/azure-cli.test.ts | 286 ++++++++++ packages/auth/vitest.config.browser.ts | 1 + 8 files changed, 868 insertions(+), 6 deletions(-) create mode 100644 packages/auth/src/credentials/azure-cli.ts create mode 100644 packages/auth/tests/credentials/azure-cli.test.ts diff --git a/package-lock.json b/package-lock.json index b9b1db964..d85e6cf38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,173 @@ "node": ">=6.0.0" } }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.1", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/core-client/-/core-client-1.11.1.tgz", + "integrity": "sha512-2QygG2F76ZpMP2eMztiJvAiFMu71M9rDeU7vO/QKg5Css7MgM4frUOslFjhVjRhbGaCNPtz/S8M6y46/fFKVuQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-process": { + "version": "1.0.0", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/core-process/-/core-process-1.0.0.tgz", + "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.3", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/identity/-/identity-4.13.3.tgz", + "integrity": "sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-process": "^1.0.0", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^6.0.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.23.0", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/msal-browser/-/msal-browser-5.23.0.tgz", + "integrity": "sha512-IRCwkRCK47hBXK+7bu67UZWY7HS0Jx1dZgi4C1HVbjlBum8Jy8fb2l87xFr6HxdQJWy951zRUHtwwDITHFAtaA==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.14.1", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/msal-common/-/msal-common-16.14.1.tgz", + "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "6.0.1", + "resolved": "https://npm-proxy.cloud.databricks.com/@azure/msal-node/-/msal-node-6.0.1.tgz", + "integrity": "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.1", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://npm-proxy.dev.databricks.com/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -2532,6 +2699,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.9", + "resolved": "https://npm-proxy.cloud.databricks.com/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz", + "integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.1", "resolved": "https://npm-proxy.dev.databricks.com/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", @@ -2747,6 +2928,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://npm-proxy.cloud.databricks.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://npm-proxy.dev.databricks.com/ajv/-/ajv-6.15.0.tgz", @@ -2956,6 +3146,27 @@ "node": "20 || >=22" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://npm-proxy.cloud.databricks.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://npm-proxy.cloud.databricks.com/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://npm-proxy.dev.databricks.com/cac/-/cac-6.7.14.tgz", @@ -3066,7 +3277,6 @@ "version": "4.4.3", "resolved": "https://npm-proxy.dev.databricks.com/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3097,6 +3307,46 @@ "dev": true, "license": "MIT" }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://npm-proxy.cloud.databricks.com/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://npm-proxy.cloud.databricks.com/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://npm-proxy.cloud.databricks.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defu": { "version": "6.1.7", "resolved": "https://npm-proxy.cloud.databricks.com/defu/-/defu-6.1.7.tgz", @@ -3162,6 +3412,15 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://npm-proxy.cloud.databricks.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://npm-proxy.dev.databricks.com/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3745,6 +4004,32 @@ "dev": true, "license": "MIT" }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://npm-proxy.cloud.databricks.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://npm-proxy.cloud.databricks.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://npm-proxy.dev.databricks.com/ignore/-/ignore-7.0.5.tgz", @@ -3814,6 +4099,21 @@ "dev": true, "license": "ISC" }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://npm-proxy.cloud.databricks.com/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://npm-proxy.dev.databricks.com/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3847,6 +4147,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://npm-proxy.cloud.databricks.com/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://npm-proxy.dev.databricks.com/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -3857,6 +4175,21 @@ "node": ">=8" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://npm-proxy.cloud.databricks.com/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://npm-proxy.dev.databricks.com/isexe/-/isexe-2.0.0.tgz", @@ -4013,6 +4346,49 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://npm-proxy.cloud.databricks.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://npm-proxy.cloud.databricks.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://npm-proxy.cloud.databricks.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://npm-proxy.dev.databricks.com/keyv/-/keyv-4.5.4.tgz", @@ -4073,6 +4449,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://npm-proxy.cloud.databricks.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://npm-proxy.cloud.databricks.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://npm-proxy.cloud.databricks.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://npm-proxy.cloud.databricks.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://npm-proxy.cloud.databricks.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://npm-proxy.cloud.databricks.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://npm-proxy.dev.databricks.com/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4080,6 +4492,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://npm-proxy.cloud.databricks.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://npm-proxy.dev.databricks.com/loupe/-/loupe-3.2.1.tgz", @@ -4224,7 +4642,6 @@ "version": "2.1.3", "resolved": "https://npm-proxy.dev.databricks.com/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -4277,6 +4694,24 @@ "wrappy": "1" } }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://npm-proxy.cloud.databricks.com/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://npm-proxy.dev.databricks.com/optionator/-/optionator-0.9.4.tgz", @@ -4838,6 +5273,18 @@ "dev": true, "license": "MIT" }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://npm-proxy.cloud.databricks.com/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://npm-proxy.dev.databricks.com/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4862,11 +5309,30 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://npm-proxy.cloud.databricks.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/semver": { "version": "7.8.0", "resolved": "https://npm-proxy.dev.databricks.com/semver/-/semver-7.8.0.tgz", "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5347,9 +5813,7 @@ "version": "2.8.1", "resolved": "https://npm-proxy.cloud.databricks.com/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.22.3", @@ -6683,6 +7147,21 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://npm-proxy.cloud.databricks.com/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://npm-proxy.dev.databricks.com/yaml/-/yaml-2.9.0.tgz", @@ -6856,6 +7335,7 @@ "version": "0.51.0", "license": "Apache-2.0", "dependencies": { + "@azure/identity": "^4.13.3", "@databricks/sdk-core": ">=0.51.0 <1.0.0", "zod": "^4.3.6" }, diff --git a/packages/auth/NEXT_CHANGELOG.md b/packages/auth/NEXT_CHANGELOG.md index 5e8ed133b..deafe3d4a 100644 --- a/packages/auth/NEXT_CHANGELOG.md +++ b/packages/auth/NEXT_CHANGELOG.md @@ -4,6 +4,10 @@ ### New Features and Improvements +- Added explicit Node.js Azure CLI authentication through + `newAzureCliCredentials`, with an optional tenant ID. Azure CLI authentication + is not part of the default credential chain. + ### Bug Fixes ### Documentation diff --git a/packages/auth/package.json b/packages/auth/package.json index e601266fa..f7df92dc6 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -53,6 +53,7 @@ "node": ">=22.0.0" }, "dependencies": { + "@azure/identity": "^4.13.3", "@databricks/sdk-core": ">=0.51.0 <1.0.0", "zod": "^4.3.6" } diff --git a/packages/auth/src/credentials/azure-cli.ts b/packages/auth/src/credentials/azure-cli.ts new file mode 100644 index 000000000..7a4ebe886 --- /dev/null +++ b/packages/auth/src/credentials/azure-cli.ts @@ -0,0 +1,66 @@ +import {AzureCliCredential} from '@azure/identity'; +import {z} from 'zod'; + +import type {Token, TokenCredentials} from '../auth'; +import {newTokenCredentials, tokenProviderFn} from '../auth'; + +import {AzureCliCredentialsError} from './errors'; + +const AZURE_DATABRICKS_SCOPE = '2ff814a6-3304-4ab8-85cb-cd0e6f879c1d/.default'; + +/** Configures {@link newAzureCliCredentials}. */ +export interface AzureCliCredentialsOptions { + /** Selects a tenant by ID, defaulting to the Azure CLI's current tenant. */ + tenantId?: string; +} + +/** + * Creates Node.js credentials using an existing `az login` session. + * Azure CLI authentication is not included in the default credential chain. + */ +export function newAzureCliCredentials( + options?: AzureCliCredentialsOptions +): TokenCredentials { + const credential = new AzureCliCredential( + options?.tenantId === undefined ? undefined : {tenantId: options.tenantId} + ); + const provider = tokenProviderFn(() => fetchAzureCliToken(credential)); + return newTokenCredentials('azure-cli', provider); +} + +const tokenResponseSchema = z.object({ + token: z.string().min(1), + expiresOnTimestamp: z + .number() + .transform(timestamp => new Date(timestamp)) + .pipe(z.date()), +}); + +async function fetchAzureCliToken( + credential: AzureCliCredential +): Promise { + let response: unknown; + try { + response = await credential.getToken(AZURE_DATABRICKS_SCOPE); + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e); + throw new AzureCliCredentialsError( + 'TOKEN_FETCH_FAILED', + `cannot get Azure CLI access token: ${message}`, + {cause: e} + ); + } + + const result = tokenResponseSchema.safeParse(response); + if (!result.success) { + throw new AzureCliCredentialsError( + 'INVALID_RESPONSE', + `invalid Azure CLI token response: ${result.error.message}` + ); + } + return { + value: result.data.token, + type: 'Bearer', + expiry: result.data.expiresOnTimestamp, + }; +} diff --git a/packages/auth/src/credentials/errors.ts b/packages/auth/src/credentials/errors.ts index 53618286a..c6b7570e4 100644 --- a/packages/auth/src/credentials/errors.ts +++ b/packages/auth/src/credentials/errors.ts @@ -67,3 +67,23 @@ export class U2mCredentialsError extends Error { this.code = code; } } + +/** Identifies why Azure CLI credentials failed. */ +export type AzureCliCredentialsErrorCode = + | 'TOKEN_FETCH_FAILED' + | 'INVALID_RESPONSE'; + +/** Reports a failed Azure CLI token request or an unusable token response. */ +export class AzureCliCredentialsError extends Error { + readonly code: AzureCliCredentialsErrorCode; + + constructor( + code: AzureCliCredentialsErrorCode, + message: string, + options?: ErrorOptions + ) { + super(message, options); + this.name = 'AzureCliCredentialsError'; + this.code = code; + } +} diff --git a/packages/auth/src/credentials/index.ts b/packages/auth/src/credentials/index.ts index f160e25be..74c56a319 100644 --- a/packages/auth/src/credentials/index.ts +++ b/packages/auth/src/credentials/index.ts @@ -3,15 +3,19 @@ */ export { + AzureCliCredentialsError, M2mCredentialsError, PatCredentialsError, U2mCredentialsError, } from './errors'; export type { + AzureCliCredentialsErrorCode, M2mCredentialsErrorCode, PatCredentialsErrorCode, U2mCredentialsErrorCode, } from './errors'; +export {newAzureCliCredentials} from './azure-cli'; +export type {AzureCliCredentialsOptions} from './azure-cli'; export {newM2mCredentials} from './m2m'; export type {M2mCredentialsOptions} from './m2m'; export {newPatCredentials} from './pat'; diff --git a/packages/auth/tests/credentials/azure-cli.test.ts b/packages/auth/tests/credentials/azure-cli.test.ts new file mode 100644 index 000000000..d13eac0a8 --- /dev/null +++ b/packages/auth/tests/credentials/azure-cli.test.ts @@ -0,0 +1,286 @@ +import type { + AzureCliCredential, + AzureCliCredentialOptions, +} from '@azure/identity'; +import {afterEach, describe, expect, it, vi} from 'vitest'; + +import type { + AzureCliCredentialsErrorCode, + AzureCliCredentialsOptions, + DefaultCredentialsErrorCode, +} from '../../src/credentials'; +import { + AzureCliCredentialsError, + DefaultCredentialsError, + defaultCredentials, + newAzureCliCredentials, +} from '../../src/credentials'; +import * as browserCredentials from '../../src/credentials/index.browser'; + +const {credentialMock, getTokenMock} = vi.hoisted(() => { + const getTokenMock = vi.fn(); + const credentialMock = vi.fn((_options?: AzureCliCredentialOptions) => ({ + getToken: getTokenMock, + })); + return {credentialMock, getTokenMock}; +}); + +vi.mock('@azure/identity', () => ({ + AzureCliCredential: credentialMock, +})); + +const EXPIRY = new Date('2026-12-31T00:00:00Z'); +const DATABRICKS_SCOPE = '2ff814a6-3304-4ab8-85cb-cd0e6f879c1d/.default'; + +describe('newAzureCliCredentials', () => { + afterEach(() => { + credentialMock.mockClear(); + getTokenMock.mockReset(); + }); + + const successCases: { + name: string; + options?: AzureCliCredentialsOptions; + wantAzureOptions?: AzureCliCredentialOptions; + }[] = [ + {name: 'leaves the tenant unset when options are omitted'}, + {name: 'leaves the tenant unset for empty options', options: {}}, + { + name: 'forwards an explicit tenant', + options: {tenantId: 'tenant-id'}, + wantAzureOptions: {tenantId: 'tenant-id'}, + }, + ]; + + it.each(successCases)('$name', async ({options, wantAzureOptions}) => { + getTokenMock.mockResolvedValue({ + token: 'azure-token', + expiresOnTimestamp: EXPIRY.getTime(), + }); + + const credentials = newAzureCliCredentials(options); + + expect(credentials.name()).toBe('azure-cli'); + expect(credentialMock).toHaveBeenCalledExactlyOnceWith(wantAzureOptions); + expect(getTokenMock).not.toHaveBeenCalled(); + await expect(credentials.token()).resolves.toStrictEqual({ + value: 'azure-token', + type: 'Bearer', + expiry: EXPIRY, + }); + await expect(credentials.authHeaders()).resolves.toStrictEqual([ + {key: 'Authorization', value: 'Bearer azure-token'}, + ]); + expect(getTokenMock).toHaveBeenCalledTimes(2); + expect(getTokenMock).toHaveBeenNthCalledWith(1, DATABRICKS_SCOPE); + expect(getTokenMock).toHaveBeenNthCalledWith(2, DATABRICKS_SCOPE); + }); + + it('does not forward undeclared vendor options', () => { + const vendorOptions: AzureCliCredentialOptions = { + tenantId: 'tenant-id', + subscription: 'ignored-subscription', + processTimeoutInMs: 15_000, + }; + + newAzureCliCredentials(vendorOptions); + + expect(credentialMock).toHaveBeenCalledExactlyOnceWith({ + tenantId: 'tenant-id', + }); + expect(getTokenMock).not.toHaveBeenCalled(); + }); + + it('asks the Azure CLI for a fresh token on subsequent calls', async () => { + const refreshedExpiry = new Date(EXPIRY.getTime() + 3600_000); + getTokenMock + .mockResolvedValueOnce({ + token: 'first-token', + expiresOnTimestamp: EXPIRY.getTime(), + }) + .mockResolvedValueOnce({ + token: 'refreshed-token', + expiresOnTimestamp: refreshedExpiry.getTime(), + }); + const credentials = newAzureCliCredentials(); + + await expect(credentials.token()).resolves.toStrictEqual({ + value: 'first-token', + type: 'Bearer', + expiry: EXPIRY, + }); + await expect(credentials.token()).resolves.toStrictEqual({ + value: 'refreshed-token', + type: 'Bearer', + expiry: refreshedExpiry, + }); + expect(credentialMock).toHaveBeenCalledOnce(); + expect(getTokenMock).toHaveBeenCalledTimes(2); + }); + + const errorCases: { + name: string; + setup: () => void; + wantCode: AzureCliCredentialsErrorCode; + wantMessage: RegExp; + }[] = [ + { + name: 'the Azure CLI is not installed', + setup: (): void => { + getTokenMock.mockRejectedValue( + new Error('Azure CLI could not be found.') + ); + }, + wantCode: 'TOKEN_FETCH_FAILED', + wantMessage: /Azure CLI could not be found/, + }, + { + name: 'the user is not logged in', + setup: (): void => { + getTokenMock.mockRejectedValue(new Error("Please run 'az login'.")); + }, + wantCode: 'TOKEN_FETCH_FAILED', + wantMessage: /az login/, + }, + { + name: 'the CLI command fails', + setup: (): void => { + getTokenMock.mockRejectedValue(new Error('CLI command failed')); + }, + wantCode: 'TOKEN_FETCH_FAILED', + wantMessage: /CLI command failed/, + }, + { + name: 'the CLI reports malformed JSON', + setup: (): void => { + getTokenMock.mockRejectedValue(new SyntaxError('invalid JSON')); + }, + wantCode: 'TOKEN_FETCH_FAILED', + wantMessage: /invalid JSON/, + }, + { + name: 'the token is empty', + setup: (): void => { + getTokenMock.mockResolvedValue({ + token: '', + expiresOnTimestamp: EXPIRY.getTime(), + }); + }, + wantCode: 'INVALID_RESPONSE', + wantMessage: /invalid Azure CLI token response/, + }, + { + name: 'the token expiry is NaN', + setup: (): void => { + getTokenMock.mockResolvedValue({ + token: 'azure-token', + expiresOnTimestamp: NaN, + }); + }, + wantCode: 'INVALID_RESPONSE', + wantMessage: /invalid Azure CLI token response/, + }, + { + name: 'the token expiry is infinite', + setup: (): void => { + getTokenMock.mockResolvedValue({ + token: 'azure-token', + expiresOnTimestamp: Infinity, + }); + }, + wantCode: 'INVALID_RESPONSE', + wantMessage: /invalid Azure CLI token response/, + }, + { + name: 'the token expiry exceeds the date range', + setup: (): void => { + getTokenMock.mockResolvedValue({ + token: 'azure-token', + expiresOnTimestamp: Number.MAX_VALUE, + }); + }, + wantCode: 'INVALID_RESPONSE', + wantMessage: /invalid Azure CLI token response/, + }, + ]; + + it.each(errorCases)( + 'rejects when $name', + async ({setup, wantCode, wantMessage}) => { + setup(); + const result = newAzureCliCredentials().authHeaders(); + + await expect(result).rejects.toBeInstanceOf(AzureCliCredentialsError); + await expect(result).rejects.toMatchObject({ + name: 'AzureCliCredentialsError', + code: wantCode, + message: wantMessage, + }); + } + ); + + it('preserves the Azure Identity error as the cause', async () => { + const cause = new Error('Azure authentication failed'); + getTokenMock.mockRejectedValue(cause); + + await expect(newAzureCliCredentials().token()).rejects.toMatchObject({ + cause, + }); + }); + + it('allows retrying after a failed token request', async () => { + getTokenMock + .mockRejectedValueOnce(new Error("Please run 'az login'.")) + .mockResolvedValueOnce({ + token: 'azure-token', + expiresOnTimestamp: EXPIRY.getTime(), + }); + const credentials = newAzureCliCredentials(); + + await expect(credentials.token()).rejects.toBeInstanceOf( + AzureCliCredentialsError + ); + await expect(credentials.token()).resolves.toStrictEqual({ + value: 'azure-token', + type: 'Bearer', + expiry: EXPIRY, + }); + }); + + const defaultChainCases: { + name: string; + authType?: string; + wantCode: DefaultCredentialsErrorCode; + }[] = [ + { + name: 'does not auto-detect Azure CLI authentication', + wantCode: 'NO_AUTH_CONFIGURED', + }, + { + name: 'does not select Azure CLI authentication through authType', + authType: 'azure-cli', + wantCode: 'AUTH_TYPE_NOT_FOUND', + }, + ]; + + it.each(defaultChainCases)('$name', async ({authType, wantCode}) => { + const credentials = defaultCredentials({ + profile: { + host: 'https://workspace.azuredatabricks.net', + ...(authType !== undefined && {authType}), + }, + }); + const result = credentials.authHeaders(); + + await expect(result).rejects.toBeInstanceOf(DefaultCredentialsError); + await expect(result).rejects.toMatchObject({code: wantCode}); + expect(credentialMock).not.toHaveBeenCalled(); + expect(getTokenMock).not.toHaveBeenCalled(); + }); + + it('does not export Azure CLI credentials from the browser entry point', () => { + expect(Object.keys(browserCredentials)).not.toContain( + 'newAzureCliCredentials' + ); + }); +}); diff --git a/packages/auth/vitest.config.browser.ts b/packages/auth/vitest.config.browser.ts index 67ff72bd4..4f30d603a 100644 --- a/packages/auth/vitest.config.browser.ts +++ b/packages/auth/vitest.config.browser.ts @@ -10,6 +10,7 @@ export default defineConfig({ }, include: ['tests/**/*.test.ts'], exclude: [ + 'tests/credentials/azure-cli.test.ts', 'tests/credentials/u2m.test.ts', 'tests/oidc/env.test.ts', 'tests/oidc/file.test.ts',