Skip to content

Use MSAL's recent UserFIC API for agentic flows#3842

Merged
Avery-Dunn merged 30 commits into
masterfrom
avdunn/agentic-fic-scenario-fix
Jul 13, 2026
Merged

Use MSAL's recent UserFIC API for agentic flows#3842
Avery-Dunn merged 30 commits into
masterfrom
avdunn/agentic-fic-scenario-fix

Conversation

@Avery-Dunn

@Avery-Dunn Avery-Dunn commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Replaces the internal ROPC piggybacking mechanism for agentic User FIC token acquisition with MSAL .NET's native AcquireTokenByUserFederatedIdentityCredential API. This resolves a customer-reported caching bug (#3840), modernizes the agentic flow to use purpose-built MSAL APIs, and simplifies the implementation.

Background

ID Web's agentic User FIC flow previously hijacked the ROPC (AcquireTokenByUsernamePassword) path via an internal add-in (AgentUserIdentityMsalAddIn) that rewrote HTTP request bodies at the last moment. This had several drawbacks:

  • No cache support: ROPC's silent-flow guard requires a non-null ClaimsPrincipal with oid/tid claims. In agentic scenarios, ClaimsPrincipal is typically null, so the cache was always bypassed — causing 2–4 unnecessary network calls per request (#3840).
  • Fragile HTTP body manipulation: Opaque, hard to debug, bypasses MSAL's validation and telemetry.
  • No type safety: String manipulation of grant types rather than MSAL's typed API surface.

MSAL .NET's AcquireTokenByUserFederatedIdentityCredential is a first-class API for this scenario with built-in cache support. Version 4.84.2 added the Guid userObjectId overload for OID-based flows.

Approach

Multi-CCA Pattern

Component Role
Blueprint CCA ID Web's existing CCA. Handles Leg 1: acquires FMI token (T1) via AcquireTokenForClient + WithFmiPath(agentAppId).
Agent CCA Per-agent CCA whose client assertion callback chains to the blueprint for Leg 1. Handles Leg 2 (AcquireTokenForClient → T2) and Leg 3 (AcquireTokenByUserFederatedIdentityCredential → user token).

Both use MSAL's shared static cache (EnableSharedCacheOptions), providing natural cache key isolation via distinct ClientId values while ensuring tokens survive CCA re-creation.

Three-Leg Flow

1. Caller → CreateAuthorizationHeaderForUserAsync(scopes, options, claimsPrincipal: null)
2. TryGetAuthenticationResultForAgentUserFicAsync detects agentic flow (UPN or OID)
3. Silent lookup via stored account identifier → cache HIT? Return cached token.
4. Cache MISS:
   Leg 1: Blueprint CCA → AcquireTokenForClient + WithFmiPath → T1 (FMI token)
   Leg 2: Agent CCA → AcquireTokenForClient (T1 as client assertion) → T2
   Leg 3: Agent CCA → AcquireTokenByUserFederatedIdentityCredential(scopes, UPN/OID, T2) → user token
5. Store account identifier for future silent lookups
6. Return user token

On subsequent calls for the same (agent, user, tenant) tuple, step 3 returns the cached token with zero network calls.

Account Identifier Storage

A ConcurrentDictionary<string, string> maps "{agentAppId}:{USER_IDENTIFIER}:{TENANTID}" → MSAL account identifier. This replaces the role that ClaimsPrincipal oid/tid claims serve in other ID Web flows. Entries are cleaned up when the CCA dictionary is cleared at the size threshold.

Agent CCA Eviction

As DOS protection, the agent CCA dictionary is cleared entirely when it exceeds a configurable threshold (default 10,000). Since all agent CCAs use MSAL's shared static cache, clearing the dictionary only discards lightweight CCA objects — tokens remain accessible to newly-built CCAs via AcquireTokenSilent.

Tenant Propagation

The assertion callback extracts the tenant from AssertionRequestOptions.TokenEndpoint (when the host matches the configured instance) and applies WithTenantId to Leg 1. This ensures multi-tenant scenarios work correctly, matching the pattern used by OidcIdpSignedAssertionProvider.

Changes

Directory.Build.props

  • Bump MSAL .NET from 4.84.1 → 4.84.2 (adds Guid userObjectId overload)

TokenAcquisition.cs

  • TryGetAuthenticationResultForAgentUserFicAsync (new): Detects UPN/OID agentic flows, performs silent retrieval or the 3-leg flow via native MSAL APIs
  • GetOrBuildAgentUserFicCcaAsync (new): Builds and caches agent CCAs with assertion callbacks that chain to the blueprint CCA; applies shared cache and size-threshold eviction
  • ExtractTenantFromTokenEndpointIfSameInstance (new): Extracts tenant from token endpoint URL when host matches configured instance
  • Early return in TryGetAuthenticationResultForConfidentialClientUsingRopcAsync: Intercepts agentic flows before the ROPC path
  • Silent catch broadened: MsalException instead of MsalUiRequiredException
  • Dead code removal: Agent identity extraction blocks from the ROPC method

TokenAcquisition.Logger.cs

  • 6 structured log messages (LoggerMessage.Define):
    • AgentUserFicFlowDetected, AgentUserFicSilentSuccess, AgentUserFicSilentFailure
    • AgentUserFicAcquisitionComplete, AgentCcaCreated, AgentCcaEviction

LoggingEventId.cs

  • 6 new event IDs (600–605)

AgentIdentitiesExtension.cs

  • Removed add-in callback registration from AddAgentIdentities() (AddOidcFic() preserved)

AgentUserIdentityMsalAddIn.cs

  • Deleted. Fully superseded by native MSAL API usage.

TokenAcquisitionTests.cs

  • 4 core agent User FIC tests: UPN caching, OID caching, ClaimsPrincipal independence, UPN/OID isolation
  • 3 shared cache isolation tests: Multi-agent/multi-user correctness, per-instance eviction behavior, shared cache token survival across CCA re-creation
  • 1 size-threshold eviction test: Verifies dictionary clear at threshold
  • 4 ExtractTenantFromTokenEndpointIfSameInstance tests: Same/different instance, null inputs, invalid URI

No Breaking Changes

All public APIs are unchanged:

  • WithAgentUserIdentity(options, agentAppId, username) — now uses native UPN path internally
  • WithAgentUserIdentity(options, agentAppId, userId) — now uses native OID path internally
  • AddAgentIdentities() — still registers OidcFic; no longer registers the (internal) add-in callback

The deleted AgentUserIdentityMsalAddIn was internal static with no external consumers.

Known Limitations

  • National cloud FIC scope: Hardcoded to api://AzureADTokenExchange/.default (public cloud). National cloud support requires cloud-aware inference in MSAL itself — to be addressed in a follow-up.

Resolves

Replace ROPC piggybacking with MSAL's native
AcquireTokenByUserFederatedIdentityCredential API using the multi-CCA
pattern (blueprint + per-agent CCAs with assertion callbacks).

This enables proper token caching for agentic User FIC flows when
ClaimsPrincipal is null, eliminating 2-4 unnecessary network round-trips
per bot message.

Phase 1: UPN-based flows only. OID-based flows remain on the existing
ROPC+add-in path pending MSAL .NET support for the OID overload.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Avery-Dunn Avery-Dunn requested a review from a team as a code owner June 4, 2026 16:56
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
@Avery-Dunn Avery-Dunn marked this pull request as draft June 4, 2026 20:04
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Avery-Dunn and others added 2 commits June 5, 2026 06:29
- Bump MSAL .NET from 4.84.1 to 4.84.2 (adds Guid userObjectId overload
  for AcquireTokenByUserFederatedIdentityCredential)
- Extend TryGetAuthenticationResultForAgentUserFicAsync to handle both
  UPN-based and OID-based agentic flows via native MSAL APIs
- Remove AgentUserIdentityMsalAddIn (ROPC body-rewriting workaround) and
  its registration in AddAgentIdentities — no longer needed
- Remove dead agent identity extraction code from ROPC path
- Add 3 OID-specific tests: cache on second call, fresh ClaimsPrincipal
  per call, and UPN/OID cache isolation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR modernizes the agentic “User FIC” token acquisition flow by replacing the prior ROPC piggybacking/request-rewrite add-in with MSAL’s native AcquireTokenByUserFederatedIdentityCredential API, aiming to restore proper MSAL cache usage and fix the cache-bypass reported in #3840.

Changes:

  • Bumps MSAL .NET to 4.84.2 and switches agentic user token acquisition to native UserFIC (multi-CCA / 3-leg flow).
  • Adds internal caching structures for per-agent CCA instances and MSAL account identifiers to enable silent token acquisition even when ClaimsPrincipal is null.
  • Removes the internal MSAL add-in that rewrote ROPC requests and adds new unit tests covering UPN/OID cache behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Directory.Build.props Updates MSAL .NET version to enable the needed UserFIC overload.
src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Implements native UserFIC flow, agent CCA caching, and account-id mapping for silent cache hits.
src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs Adds an internal key for overriding the token-exchange audience via ExtraParameters.
src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs Stops registering the old ROPC-rewrite callback in AddAgentIdentities().
src/Microsoft.Identity.Web.AgentIdentities/AgentUserIdentityMsalAddIn.cs Deletes the internal add-in that rewrote token requests.
tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs Adds new tests for agentic UserFIC caching behavior (UPN + OID).

Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
@Avery-Dunn Avery-Dunn marked this pull request as ready for review June 5, 2026 15:30
@Avery-Dunn Avery-Dunn changed the title Fix #3840: Use MSAL's UserFIC API for agentic UPN flows Use MSAL's UserFIC API for agentic flows Jun 5, 2026
@Avery-Dunn Avery-Dunn changed the title Use MSAL's UserFIC API for agentic flows Use MSAL's recent UserFIC API for agentic flows Jun 5, 2026
Avery-Dunn and others added 5 commits June 5, 2026 09:32
Replace ROPC piggybacking with MSAL's native
AcquireTokenByUserFederatedIdentityCredential API using the multi-CCA
pattern (blueprint + per-agent CCAs with assertion callbacks).

This enables proper token caching for agentic User FIC flows when
ClaimsPrincipal is null, eliminating 2-4 unnecessary network round-trips
per bot message.

Phase 1: UPN-based flows only. OID-based flows remain on the existing
ROPC+add-in path pending MSAL .NET support for the OID overload.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Bump MSAL .NET from 4.84.1 to 4.84.2 (adds Guid userObjectId overload
  for AcquireTokenByUserFederatedIdentityCredential)
- Extend TryGetAuthenticationResultForAgentUserFicAsync to handle both
  UPN-based and OID-based agentic flows via native MSAL APIs
- Remove AgentUserIdentityMsalAddIn (ROPC body-rewriting workaround) and
  its registration in AddAgentIdentities — no longer needed
- Remove dead agent identity extraction code from ROPC path
- Add 3 OID-specific tests: cache on second call, fresh ClaimsPrincipal
  per call, and UPN/OID cache isolation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.

Files not reviewed (1)
  • src/Microsoft.Identity.Web.UI/Microsoft.Identity.Web.UI.xml: Generated file

Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.Logger.cs Outdated
…, clear semaphores on eviction, fix stale XML docs and log message

- Normalize agentAppId to uppercase to prevent duplicate CCAs from GUID casing
- Accept Guid objects (not just strings) for OID via ToString() fallback
- Clear _agentCcaSemaphores alongside CCA/account dictionaries on threshold eviction
- Fix stale XML docs referencing timestamp-based eviction (now size-threshold)
- Add shared-cache caveat to AgentCcaMaxCount doc
- Fix _agentUserFicAccountIds doc (opportunistic cleanup, not MSAL-driven eviction)
- Update log message from 'sweep evicted' to 'cache cleared (exceeded size threshold)'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Revert agentAppId.ToUpperInvariant() — existing ID Web patterns (GetApplicationKey,
_applicationsByAuthorityClientId) do not normalize client IDs, so adding
case-normalization only in the agentic flow would be inconsistent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Avery-Dunn Avery-Dunn requested a review from bgavrilMS July 2, 2026 17:01
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
@bgavrilMS

Copy link
Copy Markdown
Member

I share the concern about introducing a second CCA cache for the agent User FIC path.

I don't think the blueprint CCA and agent CCA can be the exact same IConfidentialClientApplication instance, because the agent CCA has a different ClientId and needs a client assertion callback that chains back to the blueprint app for the FMI token. But I do think we should avoid maintaining _agentUserFicCcas as a separate CCA lifecycle/configuration path.

A safer shape would be to reuse the existing GetOrBuildConfidentialClientApplicationAsync / _applicationsByAuthorityClientId infrastructure and only parameterize the pieces that differ:

  1. Add an optional cache-key suffix / discriminator, e.g. agent-user-fic:{agentAppId}:{authenticationScheme}:{blueprintKey}, so agent CCAs do not collide with normal CCAs.
  2. Add an optional credential customization callback to BuildConfidentialClientApplicationAsync, so the common builder still applies CreateWithApplicationOptions, authority handling, logging/PII settings, Azure region, client capabilities, HTTP factory, cache options, and _tokenCacheProvider.Initialize(...).
  3. For agent User FIC, clone/derive the blueprint options, set ClientId = agentAppId, and provide only the specialized WithClientAssertion(...) callback that acquires the Leg 1 FMI token from the blueprint CCA.
  4. Detect the agent User FIC flow before building the normal user-flow CCA. Right now GetAuthenticationResultForUserInternalAsync builds application first, then detects User FIC, which can create an unused/wrong CCA before creating the real agent CCA.

Pseudo-shape:

private async Task<IConfidentialClientApplication> GetOrBuildConfidentialClientApplicationAsync(
    MergedOptions mergedOptions,
    bool isTokenBinding,
    string? cacheKeySuffix = null,
    Func<ConfidentialClientApplicationBuilder, string, Task>? configureCredentialsAsync = null)

Then inside BuildConfidentialClientApplicationAsync, keep all common configuration centralized and replace only this part conditionally:

if (configureCredentialsAsync is not null)
{
    await configureCredentialsAsync(builder, authority).ConfigureAwait(false);
}
else
{
    await builder.WithClientCredentialsAsync(...).ConfigureAwait(false);
}

This keeps the agent CCA as a distinct MSAL app where needed, but avoids a parallel cache/configuration path that can drift from the rest of Microsoft.Identity.Web. In particular, the current PR's agent CCA builder appears to bypass the normal token cache provider initialization and several normal MSAL app settings, which could make behavior inconsistent for distributed/session cache configurations and future CCA configuration changes.

@bgavrilMS

Copy link
Copy Markdown
Member

I share the concern about introducing a second CCA cache for the agent User FIC path.

I don't think the blueprint CCA and agent CCA can be the exact same IConfidentialClientApplication instance, because the agent CCA has a different ClientId and needs a client assertion callback that chains back to the blueprint app for the FMI token. But I do think we should avoid maintaining _agentUserFicCcas as a separate CCA lifecycle/configuration path.

A safer shape would be to reuse the existing GetOrBuildConfidentialClientApplicationAsync / _applicationsByAuthorityClientId infrastructure and only parameterize the pieces that differ:

  1. Add an optional cache-key suffix / discriminator, e.g. agent-user-fic:{agentAppId}:{authenticationScheme}:{blueprintKey}, so agent CCAs do not collide with normal CCAs.
  2. Add an optional credential customization callback to BuildConfidentialClientApplicationAsync, so the common builder still applies CreateWithApplicationOptions, authority handling, logging/PII settings, Azure region, client capabilities, HTTP factory, cache options, and _tokenCacheProvider.Initialize(...).
  3. For agent User FIC, clone/derive the blueprint options, set ClientId = agentAppId, and provide only the specialized WithClientAssertion(...) callback that acquires the Leg 1 FMI token from the blueprint CCA.
  4. Detect the agent User FIC flow before building the normal user-flow CCA. Right now GetAuthenticationResultForUserInternalAsync builds application first, then detects User FIC, which can create an unused/wrong CCA before creating the real agent CCA.

Pseudo-shape:

private async Task<IConfidentialClientApplication> GetOrBuildConfidentialClientApplicationAsync(
    MergedOptions mergedOptions,
    bool isTokenBinding,
    string? cacheKeySuffix = null,
    Func<ConfidentialClientApplicationBuilder, string, Task>? configureCredentialsAsync = null)

Then inside BuildConfidentialClientApplicationAsync, keep all common configuration centralized and replace only this part conditionally:

if (configureCredentialsAsync is not null)
{
    await configureCredentialsAsync(builder, authority).ConfigureAwait(false);
}
else
{
    await builder.WithClientCredentialsAsync(...).ConfigureAwait(false);
}

This keeps the agent CCA as a distinct MSAL app where needed, but avoids a parallel cache/configuration path that can drift from the rest of Microsoft.Identity.Web. In particular, the current PR's agent CCA builder appears to bypass the normal token cache provider initialization and several normal MSAL app settings, which could make behavior inconsistent for distributed/session cache configurations and future CCA configuration changes.

This is my main comment @Avery-Dunn - the AI helped me formulate it, but it captures my concern.

Avery-Dunn and others added 3 commits July 7, 2026 08:27
…emove ExtractTenant, drop WithExperimentalFeatures

- Simplify OID detection to Guid.TryParse(userIdObj?.ToString(), ...) instead
  of complex || assignment with null-forgiving operator
- Use AssertionRequestOptions.TenantId directly with WithTenantId() for Leg 1
  tenant propagation, replacing custom ExtractTenantFromTokenEndpointIfSameInstance
- Remove ExtractTenantFromTokenEndpointIfSameInstance method and its 4 tests
  (OidcIdpSignedAssertionProvider's copy in OidcFIC project is unaffected)
- Remove WithExperimentalFeatures() from agent CCA builder — none of the APIs
  used (WithClientAssertion, WithFmiPath, AcquireTokenByUserFederatedIdentityCredential)
  require it

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Avery-Dunn

Copy link
Copy Markdown
Contributor Author

@bgavrilMS Since the CCA instance management has been discussed and refactored several times I created a spinoff PR to handle it: #3930

It should address all of the issues and recommendations you brought up, and once it's reviewed and approved can be merged into this main PR.

@Avery-Dunn Avery-Dunn merged commit 01b604d into master Jul 13, 2026
8 checks passed
@Avery-Dunn Avery-Dunn deleted the avdunn/agentic-fic-scenario-fix branch July 13, 2026 16:13
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.

5 participants