Repository navigation
Add user-assigned managed identity (UAMI) support for Azure Arc (MSAL.NET #6128 parity) - #644
Conversation
…closed support Port of MSAL.NET #6128. Remove the client-side block that rejected user-assigned managed identities on Azure Arc, forward the client_id / mi_res_id / object_id selector on the Arc token request, and validate the identity the agent echoes in the token response against the requested selector (case-insensitive; accepts both mi_res_id and msi_res_id). Fail closed (error, no token) when the echo is absent or does not match, so a legacy agent's silent auto-SAMI is never returned for a UAMI request. System-assigned is unaffected; a 404 surfaces as a normal service error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
…04 in Arc UAMI E2E - Extract the per-case bodies of TestAzureArcUserAssignedHonored and TestAzureArcUserAssignedNotHonoredFailsClosed into helpers to drop the loop/closure nesting flagged by SonarCloud (go:S3776, complexity 17/16 > 15). - Tighten TestAzureArcUserAssignedNonExistentE2E to assert a 404 / identity_not_found for a non-existent UAMI, matching MSAL.NET's AcquireToken_ForNonExistentUami_OnAzureArc_Fails. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
Move the Azure Arc non-existent user-assigned identity E2E test into the build-tagged e2e suite (apps/tests/e2e/managedidentity_arc_e2e_test.go) that runs on the MISEAZUREARC pool, following the pattern from #641, and remove the ad-hoc self-gating version from the unit test file. The test requests an unassigned UAMI and asserts a 404 / identity_not_found with no token, mirroring MSAL.NET's AcquireToken_ForNonExistentUami_OnAzureArc_Fails. Broaden the Arc E2E stage -run filter so both Arc E2E tests execute. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
Port .NET's AzureArcUserAssignedManagedIdentityCacheIsPartitionedByIdentity: assert the token cache is partitioned by the requested identity so a cache lookup never returns a token for a different identity. SAMI and each of the three user-assigned selectors get a distinct token; the first acquisition per identity is a cache miss (identity provider) and the second returns that identity's own token from cache (with no queued HTTP responses, so a miss would panic). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
Address review: in addition to the behavioral round-trip proof, assert the managed-identity token cache keys directly (via Manager.Marshal) so the test matches .NET's RecordAccess client-id check. There must be exactly one access-token entry per identity, and each identity's identifier (the system-assigned default and each user-assigned client/resource/object id) must key exactly one entry, which catches a key collision that behavior alone might mask. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
There was a problem hiding this comment.
🟡 Human review recommended
It introduces a security-sensitive behavior change (fail-closed validation) that depends on Azure Arc agent response contracts and warrants final human review before approval.
Pull request overview
This PR updates the Managed Identity (MI) Azure Arc flow to support user-assigned managed identities (UAMI) with a fail-closed safety check to prevent returning a token for the wrong identity when the Arc agent ignores the requested selector. It also adds unit + E2E coverage and adjusts the Azure DevOps pipeline to run the expanded Arc E2E set.
Changes:
- Remove construction-time rejection of UAMI on Azure Arc; instead forward the selector on the Arc token request and verify the agent echoed the used identity in the token response (fail closed if missing/mismatched).
- Capture raw token-response JSON fields into
TokenResponse.AdditionalFieldsso Arc-specific verification can read echoed identity fields not represented in the typed response. - Add unit tests covering honored/mismatch/no-echo/404 behaviors and add an Arc UAMI-not-found E2E test; update ADO test filter to include new Arc E2E coverage.
File summaries
| File | Description |
|---|---|
| apps/tests/e2e/managedidentity_arc_e2e_test.go | Adds an Azure Arc UAMI “not assigned” E2E test and updates Arc E2E documentation/comments. |
| apps/managedidentity/managedidentity.go | Implements Arc UAMI selector forwarding, response echo verification (fail closed), and captures raw response fields for validation. |
| apps/managedidentity/managedidentity_test.go | Replaces the old “Arc blocks UAMI” test with comprehensive Arc UAMI honored/fail-closed/404/cache-partitioning unit tests. |
| ado/build_test.yaml | Expands the Arc E2E -run filter to include the new Arc test(s). |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 0
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
…iQueryParameterMsiResourceId The constant holds the msi_res_id resource-id spelling and is no longer IMDS-only: verifyAzureArcUserAssignedIdentity accepts it as one of the two resource-id spellings Azure Arc may echo. Name it after the value (the msi_res_id parameter) rather than a single source, per PR review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
…esource-id UAMI Live testing against the Azure Connected Machine agent (1.67, user-assigned preview) showed the Arc token endpoint only honors the IMDS 'msi_res_id' spelling for the resource-id selector: a 'mi_res_id' request is silently ignored and returns the system-assigned identity, so a resource-id UAMI always failed closed. Send 'msi_res_id' on the Arc request (matching IMDS); the echo verification still accepts either spelling as a safety net. Update the happy-path/cache-partition tests to the msi_res_id request+echo and repurpose the echo-spelling test to cover the mi_res_id echo fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
… Arc UAMI A non-existent user-assigned identity returns no token on any Azure Arc agent, but the surfaced error differs by agent: a user-assigned-aware agent returns 404 (identity_not_found), while a legacy agent ignores the selector and returns the system-assigned identity, which MSAL rejects (fail closed). Accept either error and correct the comment that claimed every agent returns 404, per review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
…oud) Extract the shared Azure Arc happy-path setup into assertArcEchoHonored so both assertArcUserAssignedHonored and the mi_res_id echo safety-net test are thin wrappers, removing the duplicated mock-setup block flagged by the SonarCloud duplication gate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
…r (SonarCloud) Extract newArcChallengeClient so assertArcEchoHonored, assertArcUserAssignedFailsClosed, and assertArcAcquireFromIdP no longer each repeat the Azure Arc 401-challenge + 200-body mock setup. This removes the remaining duplicated block flagged by the SonarCloud duplication gate on new code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
…er (SonarCloud) The msi_res_id fix made the Azure Arc request-building switch byte-for-byte identical to the IMDS one, which SonarCloud flagged as duplicated code on new code (20.5%). Extract setUserAssignedQueryParam and route IMDS, Azure Arc, and verifyAzureArcUserAssignedIdentity through it, so the type switch over the identity kind exists exactly once. Behavior is unchanged; App Service keeps its own mi_res_id spelling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
|
Brings in #644 (user-assigned managed identity for Azure Arc) and #645 (reject legacy RFC 1423 encrypted PEM in CertFromPEM). Only conflict was README.md, where both sides added a new top-level section at the same insertion point: this branch's "mTLS Proof-of-Possession (SNI)" and main's "Loading Certificates from PEM". Both are kept. The PEM section is placed first because the mTLS PoP example calls CertFromPEM. apps/confidential/confidential.go auto-merged: #645 changes CertFromPEM, which this branch does not touch. Every CertFromPEM call site on this branch passes an empty password and unencrypted PEM, so the new rejection does not affect them. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Picks up main via the base branch: #644 (user-assigned managed identity for Azure Arc) and #645 (reject legacy RFC 1423 encrypted PEM in CertFromPEM), plus the README section for the latter. Clean auto-merge; no conflicts. Both IsMtlsPoP guards around the BindingCertificate assignments in base.go are unaffected and verified still present. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…edential Picks up main via the base branch: #644 (user-assigned managed identity for Azure Arc) and #645 (reject legacy RFC 1423 encrypted PEM in CertFromPEM), plus the README section for the latter. Clean auto-merge; no conflicts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ft-signer-backed-jwt-assertions Picks up main through the stack: #644 (user-assigned managed identity for Azure Arc) and #645 (reject legacy RFC 1423 encrypted PEM in CertFromPEM), plus the README section for the latter. Clean auto-merge; no conflicts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>



Summary
Port of MSAL.NET #6128. Azure Arc is previewing support for user-assigned managed identity (UAMI). This enables it in MSAL Go and, critically, makes the SDK fail closed on agents that don't honor the request.
New()that rejected user-assigned identities on Azure Arc.createAzureArcAuthRequest):client_id/msi_res_id/object_id, matching the other managed-identity sources.verifyAzureArcUserAssignedIdentity). A new agent echoes the used identity in the token response (client_id/object_id, andmsi_res_id/mi_res_id); MSAL compares it case-insensitively to the requested selector and returns the token only on a match.404(identity not assigned to the machine) surfaces as a normal service/call error, not the fail-closed error.Implementation note:
getTokenForRequestnow captures the raw response fields intoTokenResponse.AdditionalFields(the typedUnmarshalJSONdrops unknown fields), so the Arc verification can read the echoed identity.Unit tests (run in CI)
TestAzureArcUserAssignedAllowedAtConstruction— construction no longer rejects UAMI on Arc.TestAzureArcUserAssignedHonored— happy path for client id / resource id / object id (echo matches → token returned; asserts the selector is on the request).TestAzureArcUserAssignedNotHonoredFailsClosed— no-echo and different-identity both fail closed with no token.TestAzureArcUserAssignedResourceIDAcceptsMsiResIdEcho— a resource id echoed asmsi_res_idis accepted.TestAzureArcUserAssignedNotFoundSurfacesServiceError— a404(identity_not_found) surfaces as a service error, not "not supported".E2E (MI E2E – Azure Arc pipeline)
TestAzureArcUserAssignedNonExistentE2E— a REAL acquisition for a non-existent UAMI on an actual Azure Arc machine; asserts no token is returned. Self-gates viaGetSource()(skips off-Arc). Mirrors .NET'sAcquireToken_ForNonExistentUami_OnAzureArc_Fails.Validation
The fail-closed path was validated end-to-end for MSAL.NET on a real Azure Arc machine running a legacy Connected Machine agent (1.54): the agent returns the system-assigned identity for a UAMI request, and MSAL rejects it instead of leaking it. MSAL Go uses the same HIMDS protocol, and the unit tests above reproduce both the honored and fail-closed behaviors.
Notes
msi_res_id(a live Connected Machine agent silently ignoresmi_res_idand returns the system-assigned identity); the response echo is read as bothmsi_res_idandmi_res_id.