Skip to content

Preserve ManagedIdentity when converting AcquireTokenOptions to TokenAcquisitionOptions#3914

Merged
gladjohn merged 2 commits into
masterfrom
gladjohn/preserve-managedidentity-in-tokenacquirer
Jul 2, 2026
Merged

Preserve ManagedIdentity when converting AcquireTokenOptions to TokenAcquisitionOptions#3914
gladjohn merged 2 commits into
masterfrom
gladjohn/preserve-managedidentity-in-tokenacquirer

Conversation

@gladjohn

@gladjohn gladjohn commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

TokenAcquirer.GetEffectiveTokenAcquisitionOptions(AcquireTokenOptions) maps the
caller-supplied AcquireTokenOptions onto a new TokenAcquisitionOptions, but never
copies the ManagedIdentity property. As a result, any
ITokenAcquirer.GetTokenForAppAsync / GetTokenForUserAsync caller that requests a
managed identity silently loses it and falls back to the confidential-client path.

The fix is a single line that copies ManagedIdentity alongside the other properties.

The bug in detail

The ITokenAcquirer surface (GetTokenForAppAsync / GetTokenForUserAsync) funnels
through GetEffectiveTokenAcquisitionOptions, which rebuilds a TokenAcquisitionOptions
from the caller's AcquireTokenOptions:

return (tokenAcquisitionOptions == null) ? null : new TokenAcquisitionOptions
{
    AuthenticationOptionsName = authenticationScheme,
    CancellationToken = cancellationToken,
    Claims = tokenAcquisitionOptions.Claims,
    CorrelationId = tokenAcquisitionOptions.CorrelationId,
    ExtraQueryParameters = tokenAcquisitionOptions.ExtraQueryParameters,
    ForceRefresh = tokenAcquisitionOptions.ForceRefresh,
    LongRunningWebApiSessionKey = tokenAcquisitionOptions.LongRunningWebApiSessionKey,
    Tenant = tokenAcquisitionOptions.Tenant,
    UserFlow = tokenAcquisitionOptions.UserFlow,
    PopPublicKey = tokenAcquisitionOptions.PopPublicKey,
    PopClaim = tokenAcquisitionOptions.PopClaim,
    ExtraParameters = tokenAcquisitionOptions.ExtraParameters,
    // ManagedIdentity was NOT copied here  <-- the bug
    FmiPath = tokenAcquisitionOptions.FmiPath
};

Downstream, TokenAcquisition.GetAuthenticationResultForAppInternalAsync selects between
the managed-identity flow and the confidential-client flow based on whether
ManagedIdentity is set:

if (tokenAcquisitionOptions?.ManagedIdentity != null)
{
    // acquire a token via managed identity (MSAL ManagedIdentityApplication)
}
else
{
    // build a confidential client and use its configured credential
}

Because ManagedIdentity is dropped during the conversion, this check is always false
for ITokenAcquirer callers, so IdWeb builds a confidential client that has no
credential configured
. For token-bound (mTLS PoP) requests that then surfaces as:

IDW10115: Token binding requires either a signing certificate or a binding-aware
signed assertion to compute the bound authorization header. The loaded credential
provides neither.

The DownstreamApi path (CreateTokenAcquisitionOptionsFromApiOptions) already copies
ManagedIdentity, which is why this only reproduces through the ITokenAcquirer seam.

Why this fix is needed — MISE Native managed-identity mTLS PoP

The concrete motivating scenario is MISE Native (the NativeAOT mise.dll) performing
user-assigned managed identity + mTLS Proof-of-Possession against a downstream API
(e.g. Azure Key Vault).

MISE Native's authorization-header creation resolves an MTLS_POP token strategy that
calls ITokenAcquirer.GetTokenForAppAsync(scope, acquireTokenOptions) with:

  • acquireTokenOptions.ManagedIdentity = { UserAssignedClientId = <uami> }
  • ProtocolScheme = MTLS_POP

Because GetEffectiveTokenAcquisitionOptions dropped ManagedIdentity, MISE Native could
never take the managed-identity path — every call fell back to the confidential client and
failed with Cannot acquire bound authorization header (IDW10115, masked by the outer
result). This blocks managed-identity mTLS PoP for MISE Native entirely.

End-to-end validation

Rebuilding mise.dll against this fix and running the MISE Native downstream-API call:

  • Before: IdWeb built a confidential client (no credential) → IDW10115
    Cannot acquire bound authorization header.
  • After: ManagedIdentityApplication created, ApiId AcquireTokenForUserAssignedManagedIdentity,
    IsConfidentialClient: False, IsMtlsPopRequested: True,
    [Managed Identity] mTLS PoP requested, routing to IMDSv2 → the bound token is acquired
    via the managed-identity path. (Off the target host, IMDSv2 returns identity_not_found,
    which still confirms the correct managed-identity path is now taken instead of the
    confidential-client fallback.)

Tests

Adds two regression tests to TokenAcquirerTests that build a TokenAcquirer over a
substitute ITokenAcquisition, invoke the app and user token paths with an
AcquireTokenOptions.ManagedIdentity, and assert the ManagedIdentity survives into the
effective TokenAcquisitionOptions handed to ITokenAcquisition:

  • GetTokenForAppAsync_WithManagedIdentity_PreservesManagedIdentityInEffectiveOptions
  • GetTokenForUserAsync_WithManagedIdentity_PreservesManagedIdentityInEffectiveOptions

Both fail without the fix (Assert.NotNull() Failure: Value is null) and pass with it.

…AcquisitionOptions

TokenAcquirer.GetEffectiveTokenAcquisitionOptions maps the caller's
AcquireTokenOptions onto a new TokenAcquisitionOptions, but did not copy the
ManagedIdentity property. As a result, any ITokenAcquirer.GetTokenForAppAsync
or GetTokenForUserAsync call that specified a managed identity silently lost
it and fell back to the confidential-client path. For token-binding (mTLS PoP)
scenarios this surfaced as IDW10115 ("Token binding requires either a signing
certificate or a binding-aware signed assertion...").

Copy ManagedIdentity alongside the other AcquireTokenOptions properties, and
add regression tests asserting the managed identity flows through to the
effective TokenAcquisitionOptions for both the app and user token paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@gladjohn gladjohn requested a review from a team as a code owner July 2, 2026 13:21
@gladjohn gladjohn merged commit 52f6f6a into master Jul 2, 2026
4 checks passed
@gladjohn gladjohn deleted the gladjohn/preserve-managedidentity-in-tokenacquirer branch July 2, 2026 15:09
This was referenced Jul 3, 2026
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