feat: shared Azure OIDC auth, Azure DevOps MCP, and Azure MCP network/docs improvements - #48599
Conversation
… MCP docs Fixes #47768 - Azure and Azure DevOps MCP and CLI authentication not passed through agent. - shared/azure-auth.md: new shared component that re-authenticates Azure CLI inside the agent sandbox using OIDC federated identity. Fetches an OIDC token in pre-steps and runs 'az login --service-principal --federated-token' in pre-agent-steps, writing credentials to a writable AZURE_CONFIG_DIR (/tmp/gh-aw/agent/.azure). This crosses the runner→agent process boundary that caused PermissionError on /home/runner/.azure. - shared/mcp/azure-devops.md: new shared component for the Azure DevOps HTTP MCP server at https://mcp.dev.azure.com/<organization>. Authenticates via ADO_MCP_AUTH_TOKEN bearer header. Adds network allow-list for *.dev.azure.com, *.visualstudio.com, and *.microsoftonline.com. - shared/mcp/azure.md: adds network allow-list entries for Azure service domains, and adds a documentation section showing the OIDC/command-based authentication alternative using shared/azure-auth.md + @azure/mcp npm package. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
@copilot refactor existing smoke azure agentic workflows to use the new shared aws |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in 22ccff1. Updated |
|
/smoke-copilot-aoai-entra |
|
📰 BREAKING: Smoke Copilot - AOAI (Entra) is now investigating this issue comment. Sources say the story is developing... |
|
@copilot update documentation pages with more details using the tech-doc writer agent |
|
✅ PR Code Quality Reviewer completed the code quality review. |
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (0 additions detected in default business logic paths). |
|
✅ Test Quality Sentinel completed test quality analysis. No test files were added or modified in this PR (PR #48599: feat - shared Azure OIDC auth, Azure DevOps MCP, and Azure MCP network/docs improvements). All changes are workflow documentation and configuration. Test Quality Sentinel skipped. |
There was a problem hiding this comment.
Azure OIDC Auth & Azure DevOps MCP Review
Good approach to solving the runner->agent credential gap. Two blocking issues found:
1. OIDC token not masked (security) — .github/workflows/shared/azure-auth.md line 44
The raw OIDC token is stored in a variable and file without calling echo "::add-mask::". If any step runs with debug output or an error message includes the variable, the token will appear in plain text in workflow logs.
2. Missing Bearer prefix on ADO MCP Authorization header (correctness) — .github/workflows/shared/mcp/azure-devops.md line 19
The header is set to the raw secret value without a Bearer prefix. HTTP bearer token authentication requires Authorization: Bearer <token>. Without this, the MCP server will respond with 401.
Non-blocking observations
az account show --output tableprints subscription name/ID to the runner log. Low risk but consider--output nonefor audit-conscious environments.network.allowedinazure-auth.mdonly listslogin.microsoftonline.comandmanagement.azure.com; data-plane services will need additional entries (correctly noted in docs).azure-devops.mddocuments an OIDC token exchange approach in comments but provides no pre-steps implementation — acceptable future improvement gap.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 33.2 AIC · ⌖ 5.89 AIC · ⊞ 5K
|
|
||
| pre-agent-steps: | ||
| - name: Re-authenticate Azure CLI with OIDC | ||
| env: |
There was a problem hiding this comment.
The OIDC token is fetched and stored but never masked in runner logs. If any step enables debug output (set -x) or an error message echoes the variable, the raw token will appear in the workflow log.
Add echo "::add-mask::$OIDC_TOKEN" immediately after the token is fetched:
OIDC_TOKEN=$(curl -sS \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=(azureadtokenexchange/redacted) \
| jq -r '.value')
echo "::add-mask::$OIDC_TOKEN" # add this lineThis follows the GitHub Actions security hardening guide.
@copilot please address this.
| azure-devops: | ||
| url: "https://mcp.dev.azure.com/${{ github.aw.import-inputs.organization }}" | ||
| headers: | ||
| Authorization: "${{ secrets.ADO_MCP_AUTH_TOKEN }}" |
There was a problem hiding this comment.
The Authorization header value is "${{ secrets.ADO_MCP_AUTH_TOKEN }}" with no Bearer prefix. The Azure DevOps MCP server (and most OAuth-protected HTTP APIs) require the header to be formatted as Bearer <token>. Without the prefix, authentication will fail with 401.
Change to:
headers:
Authorization: "Bearer ${{ secrets.ADO_MCP_AUTH_TOKEN }}"@copilot please address this.
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /tdd and /grill-with-docs — commenting with actionable improvements, no blocking issues.
📋 Key Themes & Highlights
Key Themes
- Hardening: The OIDC token fetch (
curl) has no timeout; a hung endpoint stalls the runner indefinitely. - Credential hygiene: Token is read into a shell variable before being passed to
az login; adding--only-show-errorsprevents accidental log leakage. - Input validation: The
organizationinput inazure-devops.mdis unconstrained — a full URL instead of just the org name silently breaks the server URL. - Network completeness: ADO MCP server auth may require
app.vssps.visualstudio.comin addition to the listed*.microsoftonline.com.
Positive Highlights
- ✅ Clean two-step OIDC bridge (pre-steps / pre-agent-steps) is a well-designed abstraction that solves the process-boundary credential gap elegantly.
- ✅ Token file is chmod 600 and deleted immediately after use — good credential hygiene.
- ✅ Clear error messages with
::error::annotations guide users when permissions are misconfigured. - ✅ Docs in
azure.mdnow clearly present both auth paths (client-secret and OIDC), reducing configuration confusion. - ✅ Smoke test integration validates the end-to-end flow in CI.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 54.3 AIC · ⌖ 5.16 AIC · ⊞ 6.7K
Comment /matt to run again
| --tenant "$GH_AW_AZURE_TENANT_ID" \ | ||
| --federated-token "$OIDC_TOKEN" \ | ||
| --output none | ||
| rm -f "$GH_AW_AZURE_OIDC_TOKEN_FILE" |
There was a problem hiding this comment.
[/tdd] The OIDC token is read via cat into a shell variable, meaning it briefly lives in process memory as a string. There is no guard preventing it from appearing in verbose az logs if those are enabled.
💡 Suggestion
Add --only-show-errors to the az login call so stderr output is suppressed. Also consider whether process substitution or a temp-fd approach could keep the token out of the environment entirely:
az login --service-principal --username "$GH_AW_AZURE_CLIENT_ID" --tenant "$GH_AW_AZURE_TENANT_ID" --federated-token "$(cat "$GH_AW_AZURE_OIDC_TOKEN_FILE")" --output none --only-show-errors@copilot please address this.
| | jq -r '.value') | ||
| if [ -z "$OIDC_TOKEN" ] || [ "$OIDC_TOKEN" = "null" ]; then | ||
| echo "::error::Failed to obtain Azure OIDC token — ensure id-token: write permission is granted" | ||
| exit 1 |
There was a problem hiding this comment.
[/tdd] The curl command fetching the OIDC token uses -sS but has no timeout. A hung token endpoint will stall the runner indefinitely.
💡 Suggestion
Add --max-time 30 to bound the request:
OIDC_TOKEN=$(curl -sS --max-time 30 \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=(azureadtokenexchange/redacted) \
| jq -r '.value')@copilot please address this.
| type: string | ||
| required: true | ||
| description: Azure DevOps organization name (the subdomain in https://dev.azure.com/<organization>) | ||
|
|
There was a problem hiding this comment.
[/grill-with-docs] The organization input accepts any string, but if a user passes a full URL (e.g. `(dev.azure.com/redacted) instead of just the org name, the constructed MCP URL will be malformed.
💡 Suggestion
Add a note or example in the import-schema description making the format explicit, and consider a validation pattern if the schema supports it:
organization:
type: string
required: true
description: Azure DevOps organization name only (e.g. "my-org"), not a full URL@copilot please address this.
| allowed: | ||
| - "*.dev.azure.com" | ||
| - "*.visualstudio.com" | ||
| - "*.microsoftonline.com" |
There was a problem hiding this comment.
[/grill-with-docs] The network allow-list includes *.microsoftonline.com for auth but is missing app.vssps.visualstudio.com (VSTS profile/token service) and vssps.dev.azure.com (identity/auth endpoints) that the ADO MCP server may need for token validation.
💡 Suggestion
Until the exact required domains for mcp.dev.azure.com are confirmed, document the known auth endpoints. Consider adding:
network:
allowed:
- "*.dev.azure.com"
- "*.visualstudio.com"
- "*.microsoftonline.com"
- "app.vssps.visualstudio.com"@copilot please address this.
| - name: Re-authenticate Azure CLI with OIDC | ||
| env: | ||
| GH_AW_AZURE_CLIENT_ID: ${{ github.aw.import-inputs.azure-client-id }} | ||
| GH_AW_AZURE_TENANT_ID: ${{ github.aw.import-inputs.azure-tenant-id }} |
There was a problem hiding this comment.
[/tdd] GH_AW_AZURE_OIDC_TOKEN_FILE is written to $GITHUB_ENV, which means the token file path (but not the token itself) persists in the environment for later steps. If a subsequent step or action exports env vars to logs, the path is benign — but the env var name is overly broad and could conflict with other auth components.
💡 Suggestion
Prefix with a more namespace-specific var, or document that this is an internal implementation detail that importing workflows should not rely on. The current name is already prefixed with GH_AW_AZURE_ which is reasonable; just consider adding a note that it is transient.
@copilot please address this.
|
@copilot run pr-finisher skill |
There was a problem hiding this comment.
🔎 Code quality review by PR Code Quality Reviewer · sonnet50 · 88.3 AIC · ⌖ 4.32 AIC · ⊞ 7.4K
Comment /review to run again
| azure-devops: | ||
| url: "https://mcp.dev.azure.com/${{ github.aw.import-inputs.organization }}" | ||
| headers: | ||
| Authorization: "${{ secrets.ADO_MCP_AUTH_TOKEN }}" |
There was a problem hiding this comment.
Missing Bearer prefix on the Authorization header — this will send a raw token where Azure DevOps expects a bearer-scheme value, causing every request to fail with 401.
💡 Details
The docs immediately below (line 32) state "The server authenticates using a bearer token passed in the Authorization header," but the header value is "${{ secrets.ADO_MCP_AUTH_TOKEN }}" with no scheme prefix. Compare with shared/mcp/tavily.md, which correctly uses "Bearer ${{ secrets.TAVILY_API_KEY }}". Azure DevOps REST APIs require either Basic <base64(:PAT)> for PATs or Bearer <token> for OAuth/OIDC-derived tokens — a bare token value will be rejected.
Fix:
headers:
Authorization: "Bearer ${{ secrets.ADO_MCP_AUTH_TOKEN }}"If PATs are meant to be supported too (as the setup docs suggest), this needs to document that PAT users must base64-encode :PAT themselves and use Basic, since a single static header format can't handle both PAT and OIDC token schemes correctly.
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Updated documentation details in commit e4a1731. Added Azure OIDC auth bridge and Azure DevOps MCP guidance to |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
@copilot add a note that azure devops support is experimental |
|
|
||
| Consumers import it with `imports: [shared/mcp/tavily.md]`. | ||
|
|
||
| For Azure-based integrations, combine `shared/azure-auth.md` with Azure MCP |
| id-token: write | ||
|
|
||
| imports: | ||
| - uses: shared/azure-auth.md |
| with: | ||
| azure-client-id: ${{ vars.AZURE_CLIENT_ID }} | ||
| azure-tenant-id: ${{ vars.AZURE_TENANT_ID }} | ||
| - uses: shared/mcp/azure-devops.md |
|
@copilot run pr-finisher skill, add note that Azure Dev Ops support is still experimental |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Added an experimental note for Azure DevOps MCP support in |
|
🎉 This pull request is included in a new release. Release: |
Azure CLI login performed during runner setup is not available to the agent sandbox process — different user ownership (
root:rootvsrunner) and process isolation mean credentials written to/home/runner/.azureare inaccessible inside the container. Azure MCP and Azure DevOps MCP both fail silently (zero tools) as a result.Changes
shared/azure-auth.md(new)Shared component that bridges the runner→agent credential gap using OIDC workload identity federation:
pre-steps: fetches a short-lived OIDC token from the GitHub Actions token endpoint; writes it to/tmp/gh-aw/azure/oidc-token.txtwithchmod 600pre-agent-steps: runsaz login --service-principal --federated-token; writes credentials to/tmp/gh-aw/agent/.azure; deletes the token file immediately afterAZURE_CONFIG_DIR=/tmp/gh-aw/agent/.azureat job level so the agent sandbox inherits a valid, writable Azure CLI sessionRequires a federated identity credential on the app registration (issuer
https://token.actions.githubusercontent.com, audienceapi://AzureADTokenExchange). After this runs,@azure/mcpresolves credentials viaAzureCliCredentialinDefaultAzureCredential.shared/mcp/azure-devops.md(new)HTTP MCP server for Azure DevOps at
https://mcp.dev.azure.com/<organization>, bearer-authenticated viasecrets.ADO_MCP_AUTH_TOKEN. Adds network domains*.dev.azure.com,*.visualstudio.com,*.microsoftonline.com.shared/mcp/azure.md(updated)network.allowedentries forlogin.microsoftonline.com,management.azure.com,*.azure.com(previously absent, causing silent firewall blocks for the Azure CLI and SDK)npx @azure/mcp@latest) alongside the existing container + client-secret approach