Added AI Generate Azure DevOps Task.
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
*.vsix
|
||||||
|
task/AzureFederatedAuth/dist/
|
||||||
|
build/
|
||||||
|
AGENTS.md
|
||||||
65
README.md
Normal file
65
README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Azure DevOps Azure Federated Auth Task
|
||||||
|
|
||||||
|
Private Azure DevOps extension with a single task: `AzureFederatedAuth@1`.
|
||||||
|
|
||||||
|
The task requests an OIDC token for a selected AzureRM service connection and exports:
|
||||||
|
|
||||||
|
- `ARM_OIDC_TOKEN` (secret)
|
||||||
|
- `ARM_TENANT_ID`
|
||||||
|
- `ARM_CLIENT_ID`
|
||||||
|
- `GIT_ACCESS_TOKEN` (secret, optional)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Linux agents (YAML pipelines)
|
||||||
|
- Job setting that exposes OAuth token (`System.AccessToken`)
|
||||||
|
- AzureRM service connection with workload identity federation
|
||||||
|
- Visual Studio Marketplace publisher account (required to publish/share this extension, even for private org-only usage)
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds the TypeScript task and creates a `.vsix` extension package in `build/`.
|
||||||
|
|
||||||
|
## Publish privately
|
||||||
|
|
||||||
|
Publishing (CLI or Web UI) uses the same model:
|
||||||
|
- Upload extension version under a Visual Studio Marketplace publisher
|
||||||
|
- Share that published extension with your Azure DevOps organization(s)
|
||||||
|
|
||||||
|
There is no direct local `.vsix` install path to an org that bypasses the publisher model.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZDO_PAT='<your-pat>' ./scripts/publish.sh <vsix-path> <publisher-id> <org1> <org2> <org3>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZDO_PAT="$AZDO_PAT" ./scripts/publish.sh ./build/private.azuredevops-get-oidc-token-task-1.0.0.vsix private org-a org-b org-c
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual publish (Web UI)
|
||||||
|
|
||||||
|
You can publish the generated `.vsix` manually in the Visual Studio Marketplace publisher portal:
|
||||||
|
|
||||||
|
1. Build/package first (`./scripts/build.sh`) and note the `.vsix` path.
|
||||||
|
2. Open your publisher in Visual Studio Marketplace.
|
||||||
|
3. Upload the `.vsix` as a new extension version.
|
||||||
|
4. Share the published extension with the target Azure DevOps organization(s).
|
||||||
|
|
||||||
|
## YAML usage
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- task: AzureFederatedAuth@1
|
||||||
|
inputs:
|
||||||
|
serviceConnectionARM: 'my-arm-service-connection'
|
||||||
|
setGitAccessToken: true
|
||||||
|
```
|
||||||
|
|
||||||
|
See `examples/azure-pipelines-smoke.yml` for a full smoke validation pipeline.
|
||||||
|
|
||||||
|
When `setGitAccessToken: true`, the task exchanges the OIDC assertion against Entra ID and requests scope `499b84ac-1321-427f-aa17-267ca6975798/.default`, then sets `GIT_ACCESS_TOKEN`.
|
||||||
41
examples/azure-pipelines-smoke.yml
Normal file
41
examples/azure-pipelines-smoke.yml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
trigger: none
|
||||||
|
pr: none
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout: self
|
||||||
|
|
||||||
|
- task: AzureFederatedAuth@1
|
||||||
|
displayName: Get ARM OIDC token
|
||||||
|
inputs:
|
||||||
|
serviceConnectionARM: 'my-arm-service-connection'
|
||||||
|
setGitAccessToken: true
|
||||||
|
|
||||||
|
- bash: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ -z "${ARM_OIDC_TOKEN:-}" ]]; then
|
||||||
|
echo "ARM_OIDC_TOKEN is missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${ARM_TENANT_ID:-}" ]]; then
|
||||||
|
echo "ARM_TENANT_ID is missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${ARM_CLIENT_ID:-}" ]]; then
|
||||||
|
echo "ARM_CLIENT_ID is missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${GIT_ACCESS_TOKEN:-}" ]]; then
|
||||||
|
echo "GIT_ACCESS_TOKEN is missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ARM variables are populated."
|
||||||
|
echo "Tenant: $ARM_TENANT_ID"
|
||||||
|
echo "Client: $ARM_CLIENT_ID"
|
||||||
|
displayName: Validate exported variables
|
||||||
BIN
images/icon.png
Normal file
BIN
images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
39
images/icon.svg
Normal file
39
images/icon.svg
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" role="img" aria-label="Azure Git Federation">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0078D4"/>
|
||||||
|
<stop offset="100%" stop-color="#004E8C"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="azureFacet" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#9AD7FF"/>
|
||||||
|
<stop offset="100%" stop-color="#59B9FF"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect x="8" y="8" width="240" height="240" rx="40" fill="url(#bg)"/>
|
||||||
|
<rect x="8" y="8" width="240" height="240" rx="40" fill="none" stroke="#9ED8FF" stroke-opacity="0.35" stroke-width="2"/>
|
||||||
|
|
||||||
|
<path d="M48 196 L100 56 L130 56 L78 196 Z" fill="url(#azureFacet)"/>
|
||||||
|
<path d="M114 196 L164 76 L208 196 Z" fill="#D9F1FF" fill-opacity="0.92"/>
|
||||||
|
|
||||||
|
<!-- Federation links moved up to avoid overlap with Git branches -->
|
||||||
|
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M84 128 C84 114 96 104 110 104 L120 104" stroke="#FFD9A3" stroke-width="12"/>
|
||||||
|
<path d="M172 128 C172 114 160 104 146 104 L136 104" stroke="#FFD9A3" stroke-width="12"/>
|
||||||
|
<path d="M84 128 C84 142 96 152 110 152 L120 152" stroke="#FFC266" stroke-width="12"/>
|
||||||
|
<path d="M172 128 C172 142 160 152 146 152 L136 152" stroke="#FFC266" stroke-width="12"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g stroke="#FFFFFF" stroke-width="10" stroke-linecap="round" fill="none">
|
||||||
|
<path d="M70 208 V170 H102"/>
|
||||||
|
<path d="M186 208 V170 H154"/>
|
||||||
|
<path d="M128 208 V178"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g fill="#FFB347" stroke="#FFFFFF" stroke-width="5">
|
||||||
|
<circle cx="70" cy="208" r="10"/>
|
||||||
|
<circle cx="128" cy="208" r="10"/>
|
||||||
|
<circle cx="186" cy="208" r="10"/>
|
||||||
|
<circle cx="128" cy="178" r="10"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
17
scripts/build.sh
Executable file
17
scripts/build.sh
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
TASK_DIR="$ROOT_DIR/task/AzureFederatedAuth"
|
||||||
|
BUILD_DIR="$ROOT_DIR/build"
|
||||||
|
|
||||||
|
cd "$TASK_DIR"
|
||||||
|
rm -rf dist node_modules
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
npx tfx-cli extension create \
|
||||||
|
--manifest-globs vss-extension.json \
|
||||||
|
--output-path "$BUILD_DIR"
|
||||||
26
scripts/publish.sh
Executable file
26
scripts/publish.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -lt 3 ]]; then
|
||||||
|
echo "Usage: $0 <vsix-path> <publisher-id> <org1> [org2] [org3] ..."
|
||||||
|
echo "Requires environment variable AZDO_PAT to be set."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${AZDO_PAT:-}" ]]; then
|
||||||
|
echo "AZDO_PAT is not set."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VSIX_PATH="$1"
|
||||||
|
PUBLISHER_ID="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
for ORG in "$@"; do
|
||||||
|
echo "Publishing to organization: $ORG"
|
||||||
|
npx tfx-cli extension publish \
|
||||||
|
--vsix "$VSIX_PATH" \
|
||||||
|
--publisher "$PUBLISHER_ID" \
|
||||||
|
--token "$AZDO_PAT" \
|
||||||
|
--share-with "$ORG"
|
||||||
|
done
|
||||||
454
task/AzureFederatedAuth/package-lock.json
generated
Normal file
454
task/AzureFederatedAuth/package-lock.json
generated
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
{
|
||||||
|
"name": "azure-federated-auth-task",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "azure-federated-auth-task",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"azure-pipelines-task-lib": "^4.10.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.30",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
|
||||||
|
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/adm-zip": {
|
||||||
|
"version": "0.5.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
|
||||||
|
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/azure-pipelines-task-lib": {
|
||||||
|
"version": "4.17.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-4.17.3.tgz",
|
||||||
|
"integrity": "sha512-UxfH5pk3uOHTi9TtLtdDyugQVkFES5A836ZEePjcs3jYyxm3EJ6IlFYq6gbfd6mNBhrM9fxG2u/MFYIJ+Z0cxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"adm-zip": "^0.5.10",
|
||||||
|
"minimatch": "3.0.5",
|
||||||
|
"nodejs-file-downloader": "^4.11.1",
|
||||||
|
"q": "^1.5.1",
|
||||||
|
"semver": "^5.7.2",
|
||||||
|
"shelljs": "^0.8.5",
|
||||||
|
"uuid": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs.realpath": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/glob/node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "6",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||||
|
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/interpret": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-core-module": {
|
||||||
|
"version": "2.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
|
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/nodejs-file-downloader": {
|
||||||
|
"version": "4.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodejs-file-downloader/-/nodejs-file-downloader-4.13.0.tgz",
|
||||||
|
"integrity": "sha512-nI2fKnmJWWFZF6SgMPe1iBodKhfpztLKJTtCtNYGhm/9QXmWa/Pk9Sv00qHgzEvNLe1x7hjGDRor7gcm/ChaIQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"https-proxy-agent": "^5.0.0",
|
||||||
|
"mime-types": "^2.1.27",
|
||||||
|
"sanitize-filename": "^1.6.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-parse": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/q": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
|
||||||
|
"deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.0",
|
||||||
|
"teleport": ">=0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rechoir": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve": "^1.1.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve": {
|
||||||
|
"version": "1.22.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
|
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-core-module": "^2.16.1",
|
||||||
|
"path-parse": "^1.0.7",
|
||||||
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"resolve": "bin/resolve"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-filename": {
|
||||||
|
"version": "1.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
|
||||||
|
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
|
||||||
|
"license": "WTFPL OR ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"truncate-utf8-bytes": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "5.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||||
|
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shelljs": {
|
||||||
|
"version": "0.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
|
||||||
|
"integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^7.0.0",
|
||||||
|
"interpret": "^1.0.0",
|
||||||
|
"rechoir": "^0.6.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"shjs": "bin/shjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/truncate-utf8-bytes": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
|
||||||
|
"license": "WTFPL",
|
||||||
|
"dependencies": {
|
||||||
|
"utf8-byte-length": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/utf8-byte-length": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
|
||||||
|
"license": "(WTFPL OR MIT)"
|
||||||
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||||
|
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
task/AzureFederatedAuth/package.json
Normal file
18
task/AzureFederatedAuth/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "azure-federated-auth-task",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Azure DevOps private task to fetch OIDC token for AzureRM service connection.",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"azure-pipelines-task-lib": "^4.10.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.30",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
182
task/AzureFederatedAuth/src/index.ts
Normal file
182
task/AzureFederatedAuth/src/index.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import * as crypto from 'crypto';
|
||||||
|
import * as tl from 'azure-pipelines-task-lib/task';
|
||||||
|
|
||||||
|
type OidcResponse = {
|
||||||
|
oidcToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EntraTokenResponse = {
|
||||||
|
access_token?: string;
|
||||||
|
error?: string;
|
||||||
|
error_description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AZDO_APP_SCOPE = '499b84ac-1321-427f-aa17-267ca6975798/.default';
|
||||||
|
const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
|
||||||
|
|
||||||
|
function requireVariable(name: string): string {
|
||||||
|
const value = tl.getVariable(name);
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Missing required pipeline variable: ${name}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServiceConnectionMetadata(endpointId: string): { tenantId: string; clientId: string } {
|
||||||
|
const tenantId =
|
||||||
|
tl.getEndpointAuthorizationParameter(endpointId, 'tenantid', true) ||
|
||||||
|
tl.getEndpointDataParameter(endpointId, 'tenantid', true);
|
||||||
|
|
||||||
|
const clientId =
|
||||||
|
tl.getEndpointAuthorizationParameter(endpointId, 'serviceprincipalid', true) ||
|
||||||
|
tl.getEndpointAuthorizationParameter(endpointId, 'clientid', true) ||
|
||||||
|
tl.getEndpointDataParameter(endpointId, 'serviceprincipalid', true);
|
||||||
|
|
||||||
|
if (!tenantId) {
|
||||||
|
throw new Error('Could not resolve tenant ID from the selected AzureRM service connection.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientId) {
|
||||||
|
throw new Error('Could not resolve client ID from the selected AzureRM service connection.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tenantId, clientId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOidcUrl(baseUrl: string, serviceConnectionId: string): string {
|
||||||
|
const url = new URL(baseUrl);
|
||||||
|
url.searchParams.set('api-version', '7.1');
|
||||||
|
url.searchParams.set('serviceConnectionId', serviceConnectionId);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isJwtLike(value: string): boolean {
|
||||||
|
const parts = value.split('.');
|
||||||
|
return parts.length === 3 && parts.every((part) => part.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestOidcToken(requestUrl: string, accessToken: string): Promise<string> {
|
||||||
|
const response = await fetch(requestUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': '0'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const responseBody = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`OIDC request failed with status ${response.status} ${response.statusText}. Response: ${responseBody}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as OidcResponse;
|
||||||
|
const token = data.oidcToken?.trim();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('OIDC response did not include a non-empty oidcToken field.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isJwtLike(token)) {
|
||||||
|
throw new Error('OIDC token format is invalid (expected JWT).');
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exchangeOidcForAzureDevOpsToken(
|
||||||
|
tenantId: string,
|
||||||
|
clientId: string,
|
||||||
|
oidcToken: string
|
||||||
|
): Promise<string> {
|
||||||
|
const tokenUrl = `https://login.microsoftonline.com/${encodeURIComponent(tenantId)}/oauth2/v2.0/token`;
|
||||||
|
const body = new URLSearchParams({
|
||||||
|
grant_type: 'client_credentials',
|
||||||
|
client_id: clientId,
|
||||||
|
scope: AZDO_APP_SCOPE,
|
||||||
|
client_assertion_type: CLIENT_ASSERTION_TYPE,
|
||||||
|
client_assertion: oidcToken
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(tokenUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: body.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const rawBody = await response.text();
|
||||||
|
let data: EntraTokenResponse = {};
|
||||||
|
|
||||||
|
if (rawBody.trim().length > 0) {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(rawBody) as EntraTokenResponse;
|
||||||
|
} catch {
|
||||||
|
// Keep rawBody for error details when the response is not JSON.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = data.access_token?.trim();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorDetails =
|
||||||
|
data.error_description || data.error || rawBody.trim() || 'Unknown token exchange error.';
|
||||||
|
throw new Error(
|
||||||
|
`Failed to exchange OIDC token for Azure DevOps Git token (${response.status} ${response.statusText}): ${errorDetails}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Token exchange succeeded but no access_token was returned.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const endpointId = tl.getInput('serviceConnectionARM', true);
|
||||||
|
const setGitAccessToken = tl.getBoolInput('setGitAccessToken', false);
|
||||||
|
if (!endpointId) {
|
||||||
|
throw new Error('Task input serviceConnectionARM is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oidcBaseUrl = requireVariable('System.OidcRequestUri');
|
||||||
|
const accessToken = requireVariable('System.AccessToken');
|
||||||
|
|
||||||
|
console.log('Requesting OIDC token for ARM authentication...');
|
||||||
|
|
||||||
|
const requestUrl = buildOidcUrl(oidcBaseUrl, endpointId);
|
||||||
|
const token = await requestOidcToken(requestUrl, accessToken);
|
||||||
|
const metadata = getServiceConnectionMetadata(endpointId);
|
||||||
|
|
||||||
|
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
|
||||||
|
|
||||||
|
tl.setVariable('ARM_OIDC_TOKEN', token, true);
|
||||||
|
tl.setVariable('ARM_TENANT_ID', metadata.tenantId);
|
||||||
|
tl.setVariable('ARM_CLIENT_ID', metadata.clientId);
|
||||||
|
|
||||||
|
console.log('Successfully retrieved OIDC token.');
|
||||||
|
console.log(`OIDC Token SHA256: ${tokenHash}`);
|
||||||
|
|
||||||
|
if (setGitAccessToken) {
|
||||||
|
console.log('Exchanging OIDC token for Azure DevOps scoped Git access token...');
|
||||||
|
const gitToken = await exchangeOidcForAzureDevOpsToken(metadata.tenantId, metadata.clientId, token);
|
||||||
|
const gitTokenHash = crypto.createHash('sha256').update(gitToken).digest('hex');
|
||||||
|
tl.setVariable('GIT_ACCESS_TOKEN', gitToken, true);
|
||||||
|
console.log(`GIT Access Token SHA256: ${gitTokenHash}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
tl.setResult(tl.TaskResult.Succeeded, 'ARM OIDC variables configured.');
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
tl.error(message);
|
||||||
|
tl.setResult(tl.TaskResult.Failed, `Failed to configure ARM OIDC variables: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void run();
|
||||||
40
task/AzureFederatedAuth/task.json
Normal file
40
task/AzureFederatedAuth/task.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json",
|
||||||
|
"id": "11c532b8-f2bd-45f8-ac94-3b7e44608cc4",
|
||||||
|
"name": "AzureFederatedAuth",
|
||||||
|
"friendlyName": "Azure Federated Auth",
|
||||||
|
"description": "Requests an OIDC token for an AzureRM service connection and exports ARM_OIDC_TOKEN, ARM_TENANT_ID, ARM_CLIENT_ID, and optionally GIT_ACCESS_TOKEN for Azure DevOps Git HTTPS auth.",
|
||||||
|
"helpMarkDown": "Private task for YAML pipelines on Linux agents.",
|
||||||
|
"category": "Deploy",
|
||||||
|
"author": "private",
|
||||||
|
"version": {
|
||||||
|
"Major": 1,
|
||||||
|
"Minor": 0,
|
||||||
|
"Patch": 0
|
||||||
|
},
|
||||||
|
"instanceNameFormat": "Configure Azure federated auth for $(serviceConnectionARM)",
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "serviceConnectionARM",
|
||||||
|
"type": "connectedService:AzureRM",
|
||||||
|
"label": "Azure Resource Manager service connection",
|
||||||
|
"defaultValue": "",
|
||||||
|
"required": true,
|
||||||
|
"helpMarkDown": "AzureRM service connection used to request the OIDC token."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "setGitAccessToken",
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Also set GIT_ACCESS_TOKEN for Azure DevOps Git HTTPS auth",
|
||||||
|
"defaultValue": "false",
|
||||||
|
"required": false,
|
||||||
|
"helpMarkDown": "When enabled, exchanges the OIDC token for an Entra access token scoped to Azure DevOps (499b84ac-1321-427f-aa17-267ca6975798/.default) and sets secret variable GIT_ACCESS_TOKEN."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execution": {
|
||||||
|
"Node20_1": {
|
||||||
|
"target": "dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimumAgentVersion": "3.225.0"
|
||||||
|
}
|
||||||
14
task/AzureFederatedAuth/tsconfig.json
Normal file
14
task/AzureFederatedAuth/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
41
vss-extension.json
Normal file
41
vss-extension.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"manifestVersion": 1,
|
||||||
|
"id": "azuredevops-get-oidc-token-task",
|
||||||
|
"name": "Azure DevOps AzureFederatedAuth Task",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publisher": "private",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"id": "Microsoft.VisualStudio.Services"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Private Azure DevOps task to request an OIDC token for an AzureRM service connection and expose Terraform ARM variables.",
|
||||||
|
"categories": [
|
||||||
|
"Azure Pipelines"
|
||||||
|
],
|
||||||
|
"public": false,
|
||||||
|
"icons": {
|
||||||
|
"default": "images/icon.png"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "task/AzureFederatedAuth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "images",
|
||||||
|
"addressable": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"contributions": [
|
||||||
|
{
|
||||||
|
"id": "c5832ba7-adfc-4723-8bc6-ab6287c78b3d",
|
||||||
|
"type": "ms.vss-distributed-task.task",
|
||||||
|
"targets": [
|
||||||
|
"ms.vss-distributed-task.tasks"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": "task/AzureFederatedAuth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user