Skip to content

Design Proposal: Helper Methods for FederatedCredentialProvider for Managed Identity and Confidential Client Assertion Providers #5816

@neha-bhargava

Description

@neha-bhargava

Summary

Propose the addition of helper factory methods to FederatedCredentialProvider that simplify acquiring a FIC assertion using either a Managed Identity or a Confidential Client Application (CCA), for use with IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential.

Motivation and goals

  • Developers using the user_fic grant type must currently build their own assertion provider delegate, which requires boilerplate setup of a Managed Identity or CCA app and manual token acquisition.
  • Helper methods would remove this friction and make the correct usage pattern the default.
  • PR Implement UserFIC: IByUserFederatedIdentityCredential interface and user_fic grant type #5802 introduced the IByUserFederatedIdentityCredential interface but deferred the helper methods pending design discussion (see discussion comment).
  • The exact API shape needs to be agreed upon with the team, specifically to account for bound FIC and whether to express bound/bearer as a parameter or as separate methods.

In scope

  • FederatedCredentialProvider.FromManagedIdentity(...) — builds an assertion provider using Managed Identity, accepting:
    • ManagedIdentityId managedIdentityId
    • string audience (defaulted to "api://AzureADTokenExchange/.default")
  • FederatedCredentialProvider.FromConfidentialClient(...) — builds an assertion provider using a caller-provided Confidential Client Application, accepting:
    • IConfidentialClientApplication cca
    • string audience (defaulted to "api://AzureADTokenExchange/.default")
  • Both methods return Func<AssertionRequestOptions, Task<string>> (async). A sync overload is a potential discussion point with the team.
  • Design discussion and decision on API shape for bound vs. bearer FIC. Bound FIC can pass additional claims via AssertionRequestOptions. Two options being considered:
    • Option 1 — single method with isBound parameter:
      Func<AssertionRequestOptions, Task<string>> FromManagedIdentity(ManagedIdentityId id, string audience = "api://AzureADTokenExchange/.default", bool isBound = false)
    • Option 2 — separate methods per scenario:
      Func<AssertionRequestOptions, Task<string>> FromManagedIdentityBearer(ManagedIdentityId id, string audience = "api://AzureADTokenExchange/.default")
      Func<AssertionRequestOptions, Task<string>> FromManagedIdentityBound(ManagedIdentityId id, string audience = "api://AzureADTokenExchange/.default")
  • The team (@bgavrilMS, @gladjohn) to align on which approach best covers current and future scenarios before implementation.

Out of scope

  • Protocol-level changes to the bound FIC flow itself.
  • Changes to IByUserFederatedIdentityCredential or AcquireTokenByUserFederatedIdentityCredential signatures.
  • Custom or third-party assertion provider implementations (the helpers are opt-in convenience wrappers).
  • Final async vs. sync design decision is a follow-up discussion item, not a blocker.

Risks / unknowns

Token caching behavior:

  • FromManagedIdentity: The Managed Identity application uses a static token cache, so repeated calls to the returned delegate will always benefit from cache reuse automatically. No additional guidance or configuration is needed from the caller — cached tokens will be returned if still valid, avoiding unnecessary network calls.
  • FromConfidentialClient: The caller provides an IConfidentialClientApplication instance. Since the CCA instance and its token cache are owned by the caller, the guidance is:
    • Preferred: Reuse the same CCA instance across calls so the token cache is shared and repeated calls return cached tokens.
    • Alternative: Enable static token caching on the CCA so that multiple CCA instances share a single cache.
    • ⚠️ If neither approach is followed and a new CCA is created per call, a fresh token will be acquired on every assertion request, resulting in unnecessary network calls and risk of throttling.

Other risks:

  • Risk: If both async and sync overloads are provided, the sync variant must be carefully evaluated to avoid deadlocks in environments that do not support synchronous blocking (e.g., ASP.NET classic).
  • Helpers must propagate AssertionRequestOptions.CancellationToken to inner ExecuteAsync calls for proper cancellation support.

Examples

Using FromManagedIdentity (system-assigned):

// Static cache is used automatically — no extra configuration needed
var assertionProvider = FederatedCredentialProvider.FromManagedIdentity(
    ManagedIdentityId.SystemAssigned);

var result = await (app as IByUserFederatedIdentityCredential)
    .AcquireTokenByUserFederatedIdentityCredential(scopes, "user@contoso.com", assertionProvider)
    .ExecuteAsync();

Using FromManagedIdentity (user-assigned, with explicit audience):

var assertionProvider = FederatedCredentialProvider.FromManagedIdentity(
    ManagedIdentityId.WithUserAssignedClientId("mi-client-id"),
    audience: "api://AzureADTokenExchange/.default");

var result = await (app as IByUserFederatedIdentityCredential)
    .AcquireTokenByUserFederatedIdentityCredential(scopes, "user@contoso.com", assertionProvider)
    .ExecuteAsync();

Using FromConfidentialClient — reuse the same CCA instance (recommended):

// Build once and reuse — token cache is shared across all calls
var ccaForAssertion = ConfidentialClientApplicationBuilder
    .Create(clientId)
    .WithCertificate(cert)
    .WithAuthority(authority)
    .Build();

var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(
    ccaForAssertion,
    audience: "api://AzureADTokenExchange/.default");

var result = await (app as IByUserFederatedIdentityCredential)
    .AcquireTokenByUserFederatedIdentityCredential(scopes, "user@contoso.com", assertionProvider)
    .ExecuteAsync();

⚠️ Anti-pattern — do NOT create a new CCA per call (defeats token caching):

// Bad: a new CCA is created on every call, token cache is never reused
var result = await (app as IByUserFederatedIdentityCredential)
    .AcquireTokenByUserFederatedIdentityCredential(
        scopes, "user@contoso.com",
        FederatedCredentialProvider.FromConfidentialClient(
            ConfidentialClientApplicationBuilder.Create(clientId)
                .WithCertificate(cert)
                .WithAuthority(authority)
                .Build()))
    .ExecuteAsync();

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions