Use MSAL's recent UserFIC API for agentic flows#3842
Conversation
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>
There was a problem hiding this comment.
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
ClaimsPrincipalis 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). |
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>
…AzureAD/microsoft-identity-web into avdunn/agentic-fic-scenario-fix
…, 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>
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>
|
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 A safer shape would be to reuse the existing
Pseudo-shape: private async Task<IConfidentialClientApplication> GetOrBuildConfidentialClientApplicationAsync(
MergedOptions mergedOptions,
bool isTokenBinding,
string? cacheKeySuffix = null,
Func<ConfidentialClientApplicationBuilder, string, Task>? configureCredentialsAsync = null)Then inside 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. |
…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>
|
@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. |
* Refactor agent CCA instance management * PR feedback * PR feedback
Replaces the internal ROPC piggybacking mechanism for agentic User FIC token acquisition with MSAL .NET's native
AcquireTokenByUserFederatedIdentityCredentialAPI. 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:ClaimsPrincipalwith oid/tid claims. In agentic scenarios,ClaimsPrincipalis typically null, so the cache was always bypassed — causing 2–4 unnecessary network calls per request (#3840).MSAL .NET's
AcquireTokenByUserFederatedIdentityCredentialis a first-class API for this scenario with built-in cache support. Version 4.84.2 added theGuid userObjectIdoverload for OID-based flows.Approach
Multi-CCA Pattern
AcquireTokenForClient+WithFmiPath(agentAppId).AcquireTokenForClient→ T2) and Leg 3 (AcquireTokenByUserFederatedIdentityCredential→ user token).Both use MSAL's shared static cache (
EnableSharedCacheOptions), providing natural cache key isolation via distinctClientIdvalues while ensuring tokens survive CCA re-creation.Three-Leg Flow
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 thatClaimsPrincipaloid/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 appliesWithTenantIdto Leg 1. This ensures multi-tenant scenarios work correctly, matching the pattern used byOidcIdpSignedAssertionProvider.Changes
Directory.Build.propsGuid userObjectIdoverload)TokenAcquisition.csTryGetAuthenticationResultForAgentUserFicAsync(new): Detects UPN/OID agentic flows, performs silent retrieval or the 3-leg flow via native MSAL APIsGetOrBuildAgentUserFicCcaAsync(new): Builds and caches agent CCAs with assertion callbacks that chain to the blueprint CCA; applies shared cache and size-threshold evictionExtractTenantFromTokenEndpointIfSameInstance(new): Extracts tenant from token endpoint URL when host matches configured instanceTryGetAuthenticationResultForConfidentialClientUsingRopcAsync: Intercepts agentic flows before the ROPC pathMsalExceptioninstead ofMsalUiRequiredExceptionTokenAcquisition.Logger.csLoggerMessage.Define):AgentUserFicFlowDetected,AgentUserFicSilentSuccess,AgentUserFicSilentFailureAgentUserFicAcquisitionComplete,AgentCcaCreated,AgentCcaEvictionLoggingEventId.csAgentIdentitiesExtension.csAddAgentIdentities()(AddOidcFic()preserved)AgentUserIdentityMsalAddIn.csTokenAcquisitionTests.csExtractTenantFromTokenEndpointIfSameInstancetests: Same/different instance, null inputs, invalid URINo Breaking Changes
All public APIs are unchanged:
WithAgentUserIdentity(options, agentAppId, username)— now uses native UPN path internallyWithAgentUserIdentity(options, agentAppId, userId)— now uses native OID path internallyAddAgentIdentities()— still registers OidcFic; no longer registers the (internal) add-in callbackThe deleted
AgentUserIdentityMsalAddInwasinternal staticwith no external consumers.Known Limitations
api://AzureADTokenExchange/.default(public cloud). National cloud support requires cloud-aware inference in MSAL itself — to be addressed in a follow-up.Resolves
ClaimsPrincipalis null