Skip to content

Add user-assigned managed identity (UAMI) support for Azure Arc (MSAL.NET #6128 parity) - #644

Merged
Gladwin Johnson VR (gladjohn) merged 11 commits into
mainfrom
gladjohn/azure-arc-uami
Aug 10, 2026
Merged

Gladwin Johnson VR (gladjohn) merged 11 commits into
mainfrom
gladjohn/azure-arc-uami

Conversation

@gladjohn

@gladjohn Gladwin Johnson VR (gladjohn) commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

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.

  • Remove the client-side block in New() that rejected user-assigned identities on Azure Arc.
  • Forward the user-assigned selector on the Arc token request (createAzureArcAuthRequest): client_id / msi_res_id / object_id, matching the other managed-identity sources.
  • After a successful response, validate that the identity Azure Arc actually used matches the one requested (verifyAzureArcUserAssignedIdentity). A new agent echoes the used identity in the token response (client_id / object_id, and msi_res_id / mi_res_id); MSAL compares it case-insensitively to the requested selector and returns the token only on a match.
  • Fail closed when the echo is missing or different: return an error, no token. A legacy agent silently returns the system-assigned identity for a UAMI request; without this check MSAL would hand back a token for the wrong identity.
  • System-assigned is unaffected; a genuine 404 (identity not assigned to the machine) surfaces as a normal service/call error, not the fail-closed error.

Implementation note: getTokenForRequest now captures the raw response fields into TokenResponse.AdditionalFields (the typed UnmarshalJSON drops 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 as msi_res_id is accepted.
  • TestAzureArcUserAssignedNotFoundSurfacesServiceError — a 404 (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 via GetSource() (skips off-Arc). Mirrors .NET's AcquireToken_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

  • Do not merge yet: the positive path (agent echoes the used identity → token returned) is not testable against any released Connected Machine agent (latest is 1.66; none implement the echo). This mirrors the gating on MSAL.NET #6128 and should merge once a preview agent with the echo contract is available.
  • Field naming matches the agreed contract: the Arc request uses msi_res_id (a live Connected Machine agent silently ignores mi_res_id and returns the system-assigned identity); the response echo is read as both msi_res_id and mi_res_id.

…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
@gladjohn Gladwin Johnson VR (gladjohn) changed the title [Do Not Merge] Add user-assigned managed identity (UAMI) support for Azure Arc (MSAL.NET #6128 parity) Add user-assigned managed identity (UAMI) support for Azure Arc (MSAL.NET #6128 parity) Aug 2, 2026
Comment thread apps/managedidentity/managedidentity.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.AdditionalFields so 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
Comment thread apps/tests/e2e/managedidentity_arc_e2e_test.go Outdated
Comment thread apps/managedidentity/managedidentity.go Outdated
…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
@sonarqubecloud

Copy link
Copy Markdown

Comment thread apps/managedidentity/managedidentity.go
@gladjohn
Gladwin Johnson VR (gladjohn) merged commit 63dcc2d into main Aug 10, 2026
15 checks passed
Robbie-Microsoft added a commit that referenced this pull request Aug 11, 2026
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>
Robbie-Microsoft added a commit that referenced this pull request Aug 11, 2026
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>
Robbie-Microsoft added a commit that referenced this pull request Aug 11, 2026
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>
Robbie-Microsoft added a commit that referenced this pull request Aug 11, 2026
…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>
Robbie-Microsoft added a commit that referenced this pull request Aug 11, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants