Skip to content

[Feature Request] Add MSAL .NET API for User FIC #5766

@neha-bhargava

Description

@neha-bhargava

MSAL client type

Confidential

Problem statement

Internal partners moving off passwords for test automation need a first-class MSAL API to acquire tokens for a specific user using User Federated Identity Credentials (User FIC). The underlying token endpoint flow is ROPC-like but uses a federated user credential JWT instead of a password, via grant_type=user_fic, username, and user_federated_identity_credential.

Proposed solution

Proposed API (DevEx)

Summary

Add support for User Federated Identity Credential (UserFIC) flow to IConfidentialClientApplication, enabling acquisition of user-scoped tokens using federated identity credential assertions instead of passwords.

OAuth Flow:

  • grant_type: user_fic
  • Parameters: username, user_federated_identity_credential (assertion), scope
  • Returns user-scoped access token with account information in cache

Proposed API

Core Interface

namespace Microsoft.Identity.Client
{
    /// <summary>
    /// Provides methods to acquire tokens using User Federated Identity Credential (UserFIC) flow.
    /// </summary>
    public interface IByUserFederatedIdentityCredential
    {
        /// <summary>
        /// Acquires a user-scoped token using federated identity credential assertion.
        /// </summary>
        /// <param name="scopes">Scopes requested to access a protected API</param>
        /// <param name="username">User principal name (e.g., user@contoso.com)</param>
        /// <param name="assertionCallback">
        /// Callback to provide the assertion token. Invoked when acquiring new tokens (not from cache).
        /// </param>
        /// <param name="tokenExchangeScope">
        /// Scope for assertion token. Defaults to "api://AzureADTokenExchange/.default".
        /// Use "api://AzureADTokenExchangeUSGov/.default" for US Gov,
        /// "api://AzureADTokenExchangeChina/.default" for China.
        /// </param>
        AcquireTokenByUserFederatedIdentityCredentialParameterBuilder 
            AcquireTokenByUserFederatedIdentityCredential(
                IEnumerable<string> scopes,
                string username,
                Func<Task<string>> assertionCallback,
                string tokenExchangeScope = "api://AzureADTokenExchange/.default");
    }
}

Parameter Builder

namespace Microsoft.Identity.Client
{
    public sealed class AcquireTokenByUserFederatedIdentityCredentialParameterBuilder :
        AbstractConfidentialClientAcquireTokenParameterBuilder<AcquireTokenByUserFederatedIdentityCredentialParameterBuilder>
    {
        public AcquireTokenByUserFederatedIdentityCredentialParameterBuilder WithSendX5C(bool sendX5C);
        public AcquireTokenByUserFederatedIdentityCredentialParameterBuilder WithClaims(string claims);
        public AcquireTokenByUserFederatedIdentityCredentialParameterBuilder WithCorrelationId(Guid correlationId);
        public AcquireTokenByUserFederatedIdentityCredentialParameterBuilder WithProofOfPossession(PoPAuthenticationConfiguration popConfig);
        public AcquireTokenByUserFederatedIdentityCredentialParameterBuilder WithForceRefresh(bool forceRefresh);
        public Task<AuthenticationResult> ExecuteAsync(CancellationToken cancellationToken = default);
    }
}

Usage Examples

Example 1: Managed Identity as Assertion Source

// Setup (reuse these instances)
var miApp = ManagedIdentityApplicationBuilder
    .Create(ManagedIdentityId.SystemAssigned)
    .Build();

var confidentialApp = ConfidentialClientApplicationBuilder
    .Create(clientId)
    .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
    .WithCertificate(cert)
    .Build();

// Acquire user-scoped token
var result = await confidentialApp
    .AcquireTokenByUserFederatedIdentityCredential(
        scopes: new[] { "User.Read", "Mail.Send" },
        username: "user@contoso.com",
        assertionCallback: async () =>
        {
            var miResult = await miApp
                .AcquireTokenForManagedIdentity("api://AzureADTokenExchange/.default")
                .ExecuteAsync();
            return miResult.AccessToken;
        })
    .ExecuteAsync();

// Result contains user context
Console.WriteLine($"User: {result.Account.Username}");
Console.WriteLine($"Token Source: {result.AuthenticationResultMetadata.TokenSource}"); // IdentityProvider

Example 2: Client Credentials (Certificate) as Assertion Source

// Separate app for getting assertions (using certificate)
var assertionApp = ConfidentialClientApplicationBuilder
    .Create(assertionAppId)
    .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
    .WithCertificate(assertionCert)
    .Build();

var mainApp = ConfidentialClientApplicationBuilder
    .Create(mainAppId)
    .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
    .WithCertificate(mainCert)
    .Build();

// Acquire user token using client credential assertion
var result = await mainApp
    .AcquireTokenByUserFederatedIdentityCredential(
        scopes: new[] { "User.Read" },
        username: "user@contoso.com",
        assertionCallback: async () =>
        {
            var assertionResult = await assertionApp
                .AcquireTokenForClient(new[] { "api://AzureADTokenExchange/.default" }) 
                .ExecuteAsync();
            return assertionResult.AccessToken;
        })
    .ExecuteAsync();

Example 3: Recommended Pattern - Use cache for subsequent calls

// First call
var result = await GetTokenAsync(username, scopes, null).ConfigureAwait(false);
IAccount account = await cca.GetAccountAsync(result.Account.HomeAccountId.Identifier).ConfigureAwait(false);

// Subsequent calls to get the token from the cache
result = await GetTokenAsync(username, scopes, account).ConfigureAwait(false);

public async Task<AuthenticationResult> GetTokenAsync(string username, string[] scopes, IAccount account)
{
    // Try silent first if account is not null
  
    if (account != null)
    {
        try
        {
            return await _app.AcquireTokenSilent(scopes, account).ExecuteAsync();
        }
        catch (MsalUiRequiredException)
        {
            // Expected - fall through to UserFIC
        }
    }
    
    // Acquire new token with UserFIC
    return await _app
        .AcquireTokenByUserFederatedIdentityCredential(
            scopes: scopes,
            username: username,
            assertionCallback: async () =>
            {
                var miResult = await _miApp
                    .AcquireTokenForManagedIdentity("api://AzureADTokenExchange/.default")
                    .ExecuteAsync();
                return miResult.AccessToken;
            })
        .ExecuteAsync();
}

Key Design Decisions

1. Callback-Based Approach

  • Flexibility: Supports any assertion source (Managed Identity, Client Credentials, custom)
  • Testability: Easy to mock callbacks
  • Lazy Evaluation: Callback only invoked when acquiring new tokens (not from cache)
  • Consistency: Similar pattern to AcquireTokenOnBehalfOf

2. Token Exchange Scope

  • Default: api://AzureADTokenExchange/.default (public cloud)
  • Configurable: Developer must explicitly provide scope for sovereign clouds
  • Rationale: Most common scenario is public cloud; explicit opt-in for others

3. Silent Flow

  • Developer Responsibility: Developer chooses when to try silent first
  • Pattern: Try AcquireTokenSilent, catch MsalUiRequiredException, fall back to UserFIC
  • Rationale: Gives developers full control over flow

4. Cache Behavior

  • User Token Cache: Tokens stored with user context (same as ROPC, OBO)
  • Cache Key: {clientId}_{tenantId}_{username}_{scopes}
  • Account Object: Populated with user information
  • Silent Calls: Standard AcquireTokenSilent works with returned account

Callback Behavior

Scenario Callback Invoked? Token Source
First AcquireTokenByUserFederatedIdentityCredential call ✅ Yes IdentityProvider
AcquireTokenSilent (cache hit) ❌ No Cache
AcquireTokenSilent (cache miss) ❌ No (throws) N/A
WithForceRefresh(true) ✅ Yes IdentityProvider

Implementation Impact

New Files

  • src/client/Microsoft.Identity.Client/IByUserFederatedIdentityCredential.cs
  • src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.cs
  • src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenByUserFederatedIdentityCredentialParameters.cs
  • src/client/Microsoft.Identity.Client/Internal/Requests/UserFederatedIdentityCredentialRequest.cs

Modified Files

  • src/client/Microsoft.Identity.Client/IConfidentialClientApplication.cs (add interface)
  • src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs (implement interface)
  • src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs (add executor method)
  • src/client/Microsoft.Identity.Client/OAuth2/OAuth2GrantType.cs (add UserFic constant)
  • src/client/Microsoft.Identity.Client/OAuth2/OAuth2Parameter.cs (add UserFederatedIdentityCredential constant)
  • PublicAPI files (all targets)

Testing

  • Integration tests with lab infrastructure (Managed Identity + Client Credentials scenarios)
  • Unit tests for with mocks
  • Tests to validate the cache is used to acquire the token

Alternatives

Option 2: Integrated Managed Identity or Confidential Client Application

Configure at builder level:
.WithManagedIdentityForUserFic(ManagedIdentityId.SystemAssigned, tokenExchangeUrl)
or
.WithConfidentialClientForUserFic(confidentialApp, tokenExchangeUrl)

Pro's:

  • Simpler API for common scenarios
  • Encapsulates assertion acquisition logic
  • Less boilerplate for developers
  • More explicit what the API can do

Con's:

  • Less flexible for custom assertion sources
  • Will require additional configuration to handle caching or non cache scenarios

Option 3: Provider Interface

Option 3 proposes a flexible provider-based approach for supporting various assertion sources in the UserFIC flow. Developers register an assertion provider up front, then acquire user-scoped tokens using a new, first-class API that invokes the provider as needed.

Silent authentication behavior will follow the pattern established for ROPC: The UserFIC acquisition call does not implicitly attempt cache reads. If a cache-first pattern is needed, developers should call AcquireTokenSilent themselves, then fall back to UserFIC as appropriate.


Provider Interface

A new interface supplies the assertion required for the UserFIC protocol:

public interface IUserFicAssertionProvider
{
    Task<string> GetAssertionAsync(
        string username,
        string tokenExchangeScope,
        CancellationToken cancellationToken);
}

Configuration

Developers configure the provider and the default token exchange scope once when building the Confidential Client:

var cca = ConfidentialClientApplicationBuilder
    .Create(clientId)
    .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
    .WithCertificate(cert)
    .WithUserFicAssertionProvider(
        provider: myProvider,
        tokenExchangeScope: "api://AzureADTokenExchange/.default")
    .Build();

Token Acquisition (Dev Experience)

Developers acquire tokens by username using the configured provider as follows:

var result = await cca
    .AcquireTokenByUserFederatedIdentityCredential(
        scopes: new[] { "User.Read" },
        username: "user@contoso.com")
    .ExecuteAsync(ct);
  • The configured provider supplies the assertion automatically.
  • No implicit cache lookup is performed by this call.

WithForceRefresh(true) can be supplied on the builder if needed, matching options for other MSAL APIs.


Silent Authentication Pattern (ROPC-style)

The recommended ROPC-style pattern for devs to ensure cache-first behavior:

try
{
    var account = await cca.GetAccountAsync(homeAccountId).ConfigureAwait(false);
    return await cca.AcquireTokenSilent(scopes, account).ExecuteAsync(ct);
}
catch (MsalUiRequiredException)
{
    // Silent failed, fallback to UserFIC
    return await cca
        .AcquireTokenByUserFederatedIdentityCredential(scopes, username)
        .ExecuteAsync(ct);
}

Additional Context:

  • Similar to ROPC but uses assertion instead of password
  • Tokens are user-scoped (stored in user cache partition)
  • Works with standard AcquireTokenSilent for cache retrieval
  • Callback pattern ensures fresh assertions on force refresh
  • Supports all sovereign clouds with explicit scope configuration

No response

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions