Skip to content

How to Use FIC and FMI in Agentic Scenarios

Avery-Dunn edited this page Jun 5, 2026 · 8 revisions

Agent Identity Flow — MSAL Developer Guide

This guide explains how to implement the agent identity flow using existing shipped MSAL APIs across multiple languages. No custom or experimental APIs are required — the pattern uses standard confidential client application instances with client credentials, user federated identity credential exchange, and silent token retrieval.

If you're not already familiar with Federated Managed Identity (FMI), Federated Identity Credentials (FIC), and the three-leg agent identity flow, see the Background: Concepts & Protocol section at the end of this document.

Minimum SDK Versions

SDK Minimum Version Notes
MSAL .NET 4.83.3 OID-based UserFIC requires 4.84.2+
MSAL Java 1.25.0
MSAL Python 1.37.0
MSAL Go 1.8.0

Contents


Implementation Guide

At a high level, the recommended pattern uses separate confidential client instances to naturally isolate token caches:

  • 1 Blueprint CCA — a long-lived confidential client that owns the real certificate credential and handles Leg 1 (FMI token acquisition) for all agents. It must use Subject Name + Issuer (SN+I) authentication.
  • N Agent CCAs — one long-lived confidential client per agent app ID. Each agent CCA's assertion callback chains back to the Blueprint to obtain a T1 scoped to that agent, then uses T1 as its client assertion for downstream requests.

Because each confidential client instance has its own in-memory token cache, cache isolation happens naturally — no special cache-key configuration is needed:

Token Cache location Key components Isolation mechanism
T1 (FMI) Blueprint CCA app cache blueprintClientId + scope + fmi_path fmi_path differentiates per agent
T2 (instance) Agent CCA app cache agentAppId + scope Separate CCA instance per agent
User token Agent CCA user cache homeAccountId + agentAppId + scope Different users → different homeAccountId; different agents → different CCA

You can then perform each leg of the agent identity flow independently as needed to retrieve tokens from either the identity provider or the relevant confidential client instance's in-memory token cache.

Step 1: Create the Blueprint CCA

Start by creating the Blueprint confidential client that owns the real certificate credential and is responsible for Leg 1 token acquisition. This client is the only place that needs the certificate, and it must use Subject Name + Issuer (SN+I) authentication for FMI flows. In each SDK, that means enabling the SDK's sendX5c/WithX5C/public-certificate equivalent so the x5c chain is sent.

C# (.NET)
using Microsoft.Identity.Client;

string blueprintClientId = "YOUR_BLUEPRINT_CLIENT_ID";
string tenantId          = "YOUR_TENANT_ID";
X509Certificate2 cert    = /* load your certificate */;

IConfidentialClientApplication blueprintCca = ConfidentialClientApplicationBuilder
    .Create(blueprintClientId)
    .WithCertificate(cert, sendX5C: true)
    .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
    .Build();

Why sendX5C: true? This enables Subject Name + Issuer (SN+I) authentication, which is required for FMI flows. Without it, Entra ID will reject the Leg 1 token request.

Java
import com.microsoft.aad.msal4j.*;

String blueprintClientId = "YOUR_BLUEPRINT_CLIENT_ID";
String tenantId          = "YOUR_TENANT_ID";
String authority         = "https://login.microsoftonline.com/" + tenantId + "/";

PrivateKey privateKey       = /* load your private key */;
X509Certificate certificate = /* load your certificate */;

IClientCertificate clientCert = ClientCredentialFactory.createFromCertificate(privateKey, certificate);

ConfidentialClientApplication blueprintCca = ConfidentialClientApplication
        .builder(blueprintClientId, clientCert)
        .authority(authority)
        .sendX5c(true)  // Required for FMI flows (SN+I authentication)
        .build();

Why sendX5c(true)? This enables Subject Name + Issuer (SN+I) authentication, which is required for FMI flows. Without it, Entra ID will reject the Leg 1 token request.

Python
import msal

blueprint_client_id = "YOUR_BLUEPRINT_CLIENT_ID"
tenant_id           = "YOUR_TENANT_ID"
authority           = f"https://login.microsoftonline.com/{tenant_id}"

blueprint_app = msal.ConfidentialClientApplication(
    blueprint_client_id,
    client_credential={
        "private_key_pfx_path": "/path/to/cert.pfx",
        "public_certificate": True,   # enables SNI (send x5c chain)
    },
    authority=authority,
)

Note: The certificate dict can also use "private_key_pem" with a PEM-formatted private key string, plus "thumbprint" and optionally "public_certificate" for the x5c chain. The PFX path approach is the simplest when you have a PFX file.

Go
import (
    "crypto"
    "crypto/x509"
    "os"
    "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)

blueprintClientID := "YOUR_BLUEPRINT_CLIENT_ID"
tenantID          := "YOUR_TENANT_ID"
authority         := "https://login.microsoftonline.com/" + tenantID

// Load certificate from PEM file
pemData, _ := os.ReadFile("/path/to/cert.pem")
certs, key, _ := confidential.CertFromPEM(pemData, "")

cred, _ := confidential.NewCredFromCert(certs, key)

// WithX5C() enables SNI (send x5c certificate chain)
blueprintApp, _ := confidential.New(authority, blueprintClientID, cred, confidential.WithX5C())

Note: CertFromPEM accepts PEM-encoded data containing both the certificate chain and private key. The second parameter is the PEM encryption password (empty string for unencrypted keys).

Step 2: Create an Agent CCA (one per agent)

Create one agent confidential client per agent app ID. Each agent client uses an assertion callback that transparently performs Leg 1 by asking the Blueprint for a T1 scoped to that agent, and because the Blueprint's client-credentials flow is cached, repeated callback invocations reuse cached T1 until it expires.

C# (.NET)
string agentAppId = "YOUR_AGENT_APP_ID";

IConfidentialClientApplication agentCca = ConfidentialClientApplicationBuilder
    .Create(agentAppId)
    .WithClientAssertion(async (AssertionRequestOptions options) =>
    {
        // Leg 1: Get or refresh the FMI token (T1) for this agent.
        // AcquireTokenForClient caches T1 automatically — this only
        // hits the network on the first call or when T1 expires.
        AuthenticationResult leg1 = await blueprintCca
            .AcquireTokenForClient(new[] { "api://AzureADTokenExchange/.default" })
            .WithFmiPath(agentAppId)
            .ExecuteAsync(options.CancellationToken)
            .ConfigureAwait(false);

        return leg1.AccessToken;
    })
    .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
    .Build();

Multiple agents? Create one agent CCA per agent app ID. Each can share the same blueprintCca — the fmi_path parameter ensures Leg 1 tokens are cached independently per agent.

Java
String agentAppId = "YOUR_AGENT_APP_ID";

Function<AssertionRequestOptions, String> assertionProvider = options -> {
    try {
        // Leg 1: Get or refresh the FMI token (T1) for this agent.
        // acquireToken caches T1 automatically — this only hits
        // the network on the first call or when T1 expires.
        IAuthenticationResult leg1 = blueprintCca.acquireToken(
                ClientCredentialParameters
                        .builder(Collections.singleton("api://AzureADTokenExchange/.default"))
                        .fmiPath(agentAppId)
                        .build()
        ).get();
        return leg1.accessToken();
    } catch (Exception e) {
        throw new RuntimeException("Failed to acquire FMI credential", e);
    }
};

IClientCredential credential = ClientCredentialFactory.createFromCallback(assertionProvider);

ConfidentialClientApplication agentCca = ConfidentialClientApplication
        .builder(agentAppId, credential)
        .authority(authority)
        .build();

Multiple agents? Create one agent CCA per agent app ID. Each can share the same blueprintCca — the fmiPath parameter ensures Leg 1 tokens are cached independently per agent.

Assertion callback context: The AssertionRequestOptions parameter provides clientId(), tokenEndpoint(), and clientAssertionFmiPath(). While the simple example above ignores options, production code can use options.clientAssertionFmiPath() to dynamically route Leg 1 requests.

Note: The assertion callback is synchronous (Function, not an async callback) — it blocks the calling thread during Leg 1. In practice this is acceptable because the blueprint CCA caches T1, so only the first call (or an expired T1) incurs a network round-trip. If your application requires fully non-blocking behavior, consider pre-warming the blueprint CCA's cache at startup.

Python
def create_agent_app(agent_app_id):
    """Build an Agent CCA whose assertion callback chains to the Blueprint."""
    def assertion_provider(context=None):
        # Leg 1: Blueprint acquires FMI credential (T1) for this agent
        result = blueprint_app.acquire_token_for_client(
            ["api://AzureADTokenExchange/.default"],
            fmi_path=agent_app_id,
        )
        if "access_token" not in result:
            raise RuntimeError(f"Leg 1 failed: {result.get('error')}")
        return result["access_token"]

    return msal.ConfidentialClientApplication(
        agent_app_id,
        client_credential={"client_assertion": assertion_provider},
        authority=authority,
    )

How assertion callbacks work in Python:

  • Pass a callable (function or lambda) as "client_assertion" — MSAL will call it whenever a fresh client assertion is needed
  • If the callable accepts a parameter, MSAL passes a context dict containing client_id, token_endpoint, and fmi_path (when applicable)
  • If the callable takes no parameters (zero-arg), MSAL calls it without arguments — both patterns are supported via inspect.signature detection
  • Do not pass a static string for client_assertion when building long-lived CCA instances — static JWTs expire and cannot be refreshed. Use a callable instead
Go
func createAgentApp(agentAppID string, blueprintApp confidential.Client) (confidential.Client, error) {
    cred := confidential.NewCredFromAssertionCallback(
        func(ctx context.Context, aro confidential.AssertionRequestOptions) (string, error) {
            // Leg 1: Blueprint acquires FMI credential (T1) for this agent.
            // AcquireTokenByCredential checks cache first, so only the first
            // call or an expired T1 hits the network.
            result, err := blueprintApp.AcquireTokenByCredential(
                ctx,
                []string{"api://AzureADTokenExchange/.default"},
                confidential.WithFMIPath(agentAppID),
            )
            if err != nil {
                return "", fmt.Errorf("Leg 1 failed: %w", err)
            }
            return result.AccessToken, nil
        },
    )

    return confidential.New(authority, agentAppID, cred)
}

How assertion callbacks work in Go:

  • NewCredFromAssertionCallback takes func(context.Context, AssertionRequestOptions) (string, error) and returns Credential directly (no error return)
  • The AssertionRequestOptions struct exposes FMIPath string — populated when WithFMIPath is set on the request
  • The callback receives the request's context.Context, enabling cancellation and timeout propagation
  • Unlike .NET and Python, the callback always receives the full AssertionRequestOptions — no signature detection is needed

Step 3: Acquire an App-Only Token (No User Context)

For the app-only scenario, call AcquireTokenForClient (or the SDK equivalent) on the agent confidential client with the downstream resource scopes. The assertion callback handles Leg 1 automatically, so the app-only flow stays a single call from your code and does not require an explicit T2 or user token exchange.

C# (.NET)
// The assertion callback acquires/refreshes T1 automatically.
// The AcquireTokenForClient call exchanges T1 for an app token
// scoped to the downstream API.
AuthenticationResult appToken = await agentCca
    .AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
    .ExecuteAsync();

// Use appToken.AccessToken for downstream API calls

This is the simplest agent identity scenario — no instance token (T2) or user token is involved. The agent proves its identity to the downstream API directly.

Java
// The assertion callback acquires/refreshes T1 automatically.
// The acquireToken call exchanges T1 for an app token
// scoped to the downstream API.
IAuthenticationResult appToken = agentCca.acquireToken(
        ClientCredentialParameters
                .builder(Collections.singleton("https://graph.microsoft.com/.default"))
                .build()
).get();

// Use appToken.accessToken() for downstream API calls

This is the simplest agent identity scenario — no instance token (T2) or user token is involved.

Python
agent_app = create_agent_app("YOUR_AGENT_APP_ID")

result = agent_app.acquire_token_for_client(
    ["https://graph.microsoft.com/.default"]
)
if "access_token" in result:
    print(f"App token: {result['access_token'][:40]}...")
else:
    print(f"Failed: {result.get('error')}: {result.get('error_description')}")

Under the hood, acquire_token_for_client calls the assertion callback, which performs Leg 1 (blueprint → T1). The T1 is then used as the client assertion for the agent's own token request. The blueprint CCA caches T1, so subsequent calls skip the Leg 1 network round-trip.

Go
agentApp, _ := createAgentApp("YOUR_AGENT_APP_ID", blueprintApp)

result, err := agentApp.AcquireTokenByCredential(
    ctx,
    []string{"https://graph.microsoft.com/.default"},
)
if err != nil {
    log.Fatalf("App token failed: %v", err)
}
fmt.Printf("App token: %.40s...\n", result.AccessToken)

Under the hood, AcquireTokenByCredential calls the assertion callback (Leg 1), then uses the resulting T1 to authenticate the agent. The blueprint client caches T1, so subsequent calls skip the network round-trip for Leg 1.

Step 4: Acquire a User-Scoped Token (With User Context)

For the user-scoped scenario, use a two-call pattern on the agent confidential client. First acquire the agent's instance token (T2) using the token-exchange scope, then exchange T2 for the downstream user-scoped token using the SDK's UserFIC API; the assertion callback still handles Leg 1 transparently. Depending on the SDK and the user information you have available, the user can be identified by UPN, OID, or the SDK-specific supported options.

C# (.NET)
string[] scopes = new[] { "https://graph.microsoft.com/.default" };

// Leg 2: Get the agent's instance token (T2).
// The assertion callback handles Leg 1 (T1) automatically.
AuthenticationResult leg2 = await agentCca
    .AcquireTokenForClient(new[] { "api://AzureADTokenExchange/.default" })
    .ExecuteAsync();

// Leg 3 (UPN): Exchange T2 + UPN for a user-scoped token
string userUpn = "user@contoso.com";
AuthenticationResult userToken = await ((IByUserFederatedIdentityCredential)agentCca)
    .AcquireTokenByUserFederatedIdentityCredential(scopes, userUpn, leg2.AccessToken)
    .ExecuteAsync();

// Leg 3 (OID): Alternatively, identify the user by Object ID (Guid)
Guid userObjectId = new Guid("11111111-2222-3333-4444-555555555555");
AuthenticationResult userToken2 = await ((IByUserFederatedIdentityCredential)agentCca)
    .AcquireTokenByUserFederatedIdentityCredential(scopes, userObjectId, leg2.AccessToken)
    .ExecuteAsync();

User identification: MSAL .NET supports both UPN and OID for user identification via separate overloads. Use string username for UPN-based identification, or Guid userObjectId for OID-based identification. The Guid type provides compile-time disambiguation — Guid.Empty is rejected at call time.

Important: AcquireTokenByUserFederatedIdentityCredential always contacts the identity provider — it does not check the cache before making a network request. Call it once per user to populate the cache, then use AcquireTokenSilent for all subsequent requests.

Java
String userUpn = "user@contoso.com";
Set<String> scopes = Collections.singleton("https://graph.microsoft.com/.default");

// Leg 2: Get the agent's instance token (T2).
// The assertion callback handles Leg 1 (T1) automatically.
IAuthenticationResult leg2 = agentCca.acquireToken(
        ClientCredentialParameters
                .builder(Collections.singleton("api://AzureADTokenExchange/.default"))
                .build()
).get();

// Leg 3: Exchange T2 + user identity for a user-scoped token
IAuthenticationResult userToken = agentCca.acquireToken(
        UserFederatedIdentityCredentialParameters
                .builder(scopes, userUpn, leg2.accessToken())
                .build()
).get();

// Use userToken.accessToken() for downstream API calls on behalf of the user

User identification: MSAL Java supports both UPN and OID for user identification. Use the builder(scopes, username, assertion) overload for UPN, or builder(scopes, userObjectId, assertion) with a UUID for OID-based identification.

Cache behavior: Unlike MSAL .NET, Java's acquireToken(UserFederatedIdentityCredentialParameters) includes built-in cache-then-network — a second call for the same user returns the cached token automatically without hitting the network. You can still use acquireTokenSilently for explicit cache-only retrieval, or use .forceRefresh(true) to bypass the cache.

Python
scopes = ["https://graph.microsoft.com/.default"]

# Leg 2: Get the agent's instance token (T2).
# The assertion callback handles Leg 1 (T1) transparently.
t2_result = agent_app.acquire_token_for_client(
    ["api://AzureADTokenExchange/.default"]
)
t2 = t2_result["access_token"]

# Leg 3: Exchange T2 + user identity for a user-scoped token
user_result = agent_app.acquire_token_by_user_federated_identity_credential(
    scopes,
    assertion=t2,
    username="user@contoso.com",
)
if "access_token" in user_result:
    print(f"User token: {user_result['access_token'][:40]}...")

Parameters:

  • scopes — list of scopes for the downstream resource
  • assertion — the instance token (T2)
  • username — user's UPN (e.g., "user@contoso.com")
  • user_object_id — alternative: user's OID as a string (use instead of username if you only have the OID)

Note: MSAL Python supports both username (UPN) and user_object_id (OID) for identifying the user in the user_fic exchange.

Go
// Leg 2: Get instance token (T2) via agent client.
// The assertion callback handles Leg 1 (T1) transparently.
t2Result, err := agentApp.AcquireTokenByCredential(ctx,
    []string{"api://AzureADTokenExchange/.default"})
if err != nil {
    log.Fatalf("Leg 2 failed: %v", err)
}

// Leg 3: Exchange T2 + user identity for a user-scoped token
userResult, err := agentApp.AcquireTokenByUserFederatedIdentityCredential(
    ctx,
    []string{"https://graph.microsoft.com/.default"},
    t2Result.AccessToken,
    confidential.WithUserFICUsername("user@contoso.com"),
)

User identification options (exactly one required):

  • WithUserFICUsername(upn) — identify by UPN (e.g., "user@contoso.com")
  • WithUserObjectID(oid) — identify by Object ID (e.g., "00000000-0000-0000-0000-000000000001")
  • These are mutually exclusive — specifying both returns an error at call time

Step 5: Retrieve Cached Tokens (Silent Calls)

After the initial requests complete, each token type has a different silent-retrieval pattern. T1 is handled automatically by the Blueprint inside the assertion callback, T2 is retrieved by calling the agent's client-credentials API again, and user tokens are retrieved via the SDK's silent API using the cached account context.

When a cached access token expires, silent retrieval uses the refresh token and the agent CCA's assertion callback (which chains back to the blueprint) to obtain a fresh access token — this works automatically in all four SDKs.

Token How to retrieve silently
T1 (FMI token) Automatic — the assertion callback calls the Blueprint's client-credentials API, which checks cache first
T2 (instance token) Call the agent CCA's client-credentials API again — it checks cache first
User token Use the SDK's silent API with the cached account; in Java, repeating the UserFIC API call also benefits from built-in cache-then-network
C# (.NET)
// For user tokens: retrieve from cache using the account identifier
IAccount account = await agentCca
    .GetAccountAsync(userToken.Account.HomeAccountId.Identifier);

AuthenticationResult cachedUserToken = await agentCca
    .AcquireTokenSilent(scopes, account)
    .ExecuteAsync();

Tip: Prefer GetAccountAsync(identifier) over filtering GetAccountsAsync() by UPN — it avoids case-sensitivity issues and is more reliable in multi-account scenarios. Store userToken.Account.HomeAccountId.Identifier after the initial AcquireTokenByUserFederatedIdentityCredential call for later retrieval.

Removing a user: Call RemoveAsync(account) on the agent CCA to remove that user's tokens from the cache. Other users' tokens and app-level tokens (T2) are unaffected.

Java
// For user tokens: retrieve from cache using the account
Set<IAccount> accounts = agentCca.getAccounts().get();
IAccount account = accounts.iterator().next();

IAuthenticationResult cachedUserToken = agentCca.acquireTokenSilently(
        SilentParameters.builder(scopes, account).build()
).get();

Tip: Store the IAccount from the initial acquireToken(UserFederatedIdentityCredentialParameters) result for later silent retrieval. In multi-user scenarios, match accounts by homeAccountId().

Removing a user: Call removeAccount(account) on the agent CCA to remove that user's tokens from the cache. Other users' tokens and app-level tokens (T2) are unaffected.

Python
# Get accounts from the agent CCA's cache
accounts = agent_app.get_accounts()

if accounts:
    # Try silent retrieval first
    result = agent_app.acquire_token_silent(
        ["https://graph.microsoft.com/.default"],
        account=accounts[0],
    )
    if result and "access_token" in result:
        print(f"Cached token: {result['access_token'][:40]}...")
    else:
        # Cache miss or expired — re-acquire via user_fic
        t2 = agent_app.acquire_token_for_client(
            ["api://AzureADTokenExchange/.default"]
        )["access_token"]
        result = agent_app.acquire_token_by_user_federated_identity_credential(
            ["https://graph.microsoft.com/.default"],
            assertion=t2,
            username="user@contoso.com",
        )

acquire_token_silent behavior:

  • Returns None if no cached token is found (unlike .NET which throws)
  • Returns a dict with "access_token" and "token_source": "cache" on a cache hit
  • Automatically refreshes expired access tokens using the refresh token + assertion callback
  • Check the return value for None before accessing dict keys

Removing a user: Call remove_account(account) on the agent CCA to remove that user's tokens from the cache. Other users' tokens and app-level tokens (T2) are unaffected.

Go
// Use the Account from the user_fic result
account := userResult.Account

silentResult, err := agentApp.AcquireTokenSilent(
    ctx,
    []string{"https://graph.microsoft.com/.default"},
    confidential.WithSilentAccount(account),
)
if err != nil {
    // Cache miss — re-acquire via user_fic
    t2, _ := agentApp.AcquireTokenByCredential(ctx,
        []string{"api://AzureADTokenExchange/.default"})
    silentResult, err = agentApp.AcquireTokenByUserFederatedIdentityCredential(
        ctx,
        []string{"https://graph.microsoft.com/.default"},
        t2.AccessToken,
        confidential.WithUserFICUsername("user@contoso.com"),
    )
}

AcquireTokenSilent behavior:

  • Returns (AuthResult, error) — errors indicate a cache miss or token refresh failure
  • WithSilentAccount(account) is required — Go returns an error if omitted
  • Automatically refreshes expired access tokens using the refresh token + assertion callback
  • Check result.Metadata.TokenSource for TokenSourceCache vs TokenSourceIdentityProvider

Removing a user: Call RemoveAccount(ctx, account) on the agent client to remove that user's tokens from the cache. Other users' tokens and app-level tokens (T2) are unaffected.


Complete Examples

Each example shows a reusable AgentTokenService class or struct that manages the Blueprint and agent confidential client instances, with methods for app-only tokens, user-scoped tokens, and silent retrieval.

C# (.NET) — AgentTokenService

This example shows a reusable AgentTokenService class that manages the blueprint CCA and any number of agent CCAs, with methods for one-time setup, per-agent CCA creation, and per-request token acquisition:

using System;
using System.Collections.Concurrent;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client;

/// <summary>
/// Manages the blueprint-to-agent token flow using the multi-CCA pattern.
/// Create one instance per blueprint application and reuse it for the
/// lifetime of your service — CCA instances and their caches are long-lived.
/// </summary>
public class AgentTokenService
{
    private static readonly string[] FicScopes = { "api://AzureADTokenExchange/.default" };

    private readonly IConfidentialClientApplication _blueprintCca;
    private readonly string _authority;
    private readonly ConcurrentDictionary<string, IConfidentialClientApplication> _agentCcas = new();

    /// <summary>
    /// One-time setup: creates the blueprint CCA that owns the real credential
    /// and will handle Leg 1 for all agents.
    /// </summary>
    public AgentTokenService(string blueprintClientId, string tenantId, X509Certificate2 cert)
    {
        _authority = $"https://login.microsoftonline.com/{tenantId}";

        _blueprintCca = ConfidentialClientApplicationBuilder
            .Create(blueprintClientId)
            .WithCertificate(cert, sendX5C: true)
            .WithAuthority(_authority)
            .Build();
    }

    /// <summary>
    /// Gets or creates an agent CCA for the given agent app ID.
    /// Each agent CCA's assertion callback chains back to the blueprint
    /// CCA for Leg 1, and caches are isolated per agent automatically.
    /// </summary>
    public IConfidentialClientApplication GetOrCreateAgentCca(string agentAppId)
    {
        return _agentCcas.GetOrAdd(agentAppId, id =>
            ConfidentialClientApplicationBuilder
                .Create(id)
                .WithClientAssertion(async (AssertionRequestOptions options) =>
                {
                    var leg1 = await _blueprintCca
                        .AcquireTokenForClient(FicScopes)
                        .WithFmiPath(id)
                        .ExecuteAsync(options.CancellationToken)
                        .ConfigureAwait(false);
                    return leg1.AccessToken;
                })
                .WithAuthority(_authority)
                .Build());
    }

    /// <summary>
    /// Acquires an app-only token for the given agent (no user context).
    /// The assertion callback handles Leg 1 transparently.
    /// </summary>
    public async Task<AuthenticationResult> AcquireAppTokenAsync(
        string agentAppId,
        string[] scopes,
        CancellationToken cancellationToken = default)
    {
        var agentCca = GetOrCreateAgentCca(agentAppId);

        return await agentCca
            .AcquireTokenForClient(scopes)
            .ExecuteAsync(cancellationToken)
            .ConfigureAwait(false);
    }

    /// <summary>
    /// Acquires a user-scoped token for the given agent and user (Legs 1-3).
    /// Returns the token and stores the account in the agent CCA's cache
    /// for later silent retrieval.
    /// </summary>
    public async Task<AuthenticationResult> AcquireUserTokenAsync(
        string agentAppId,
        string[] scopes,
        string userUpn,
        CancellationToken cancellationToken = default)
    {
        var agentCca = GetOrCreateAgentCca(agentAppId);

        // Leg 2: instance token (T2) — cached on repeat calls
        var leg2 = await agentCca
            .AcquireTokenForClient(FicScopes)
            .ExecuteAsync(cancellationToken)
            .ConfigureAwait(false);

        // Leg 3: user-scoped token (UPN)
        return await ((IByUserFederatedIdentityCredential)agentCca)
            .AcquireTokenByUserFederatedIdentityCredential(scopes, userUpn, leg2.AccessToken)
            .ExecuteAsync(cancellationToken)
            .ConfigureAwait(false);
    }

    /// <summary>
    /// Acquires a user-scoped token using OID-based identification (Legs 1-3).
    /// Use this overload when you have the user's Object ID instead of their UPN.
    /// </summary>
    public async Task<AuthenticationResult> AcquireUserTokenAsync(
        string agentAppId,
        string[] scopes,
        Guid userObjectId,
        CancellationToken cancellationToken = default)
    {
        var agentCca = GetOrCreateAgentCca(agentAppId);

        var leg2 = await agentCca
            .AcquireTokenForClient(FicScopes)
            .ExecuteAsync(cancellationToken)
            .ConfigureAwait(false);

        // Leg 3: user-scoped token (OID)
        return await ((IByUserFederatedIdentityCredential)agentCca)
            .AcquireTokenByUserFederatedIdentityCredential(scopes, userObjectId, leg2.AccessToken)
            .ExecuteAsync(cancellationToken)
            .ConfigureAwait(false);
    }

    /// <summary>
    /// Silently retrieves a cached user token. Call this for all requests
    /// after the initial AcquireUserTokenAsync call for a given user.
    /// </summary>
    public async Task<AuthenticationResult> AcquireUserTokenSilentAsync(
        string agentAppId,
        string[] scopes,
        string accountIdentifier,
        CancellationToken cancellationToken = default)
    {
        var agentCca = GetOrCreateAgentCca(agentAppId);

        var account = await agentCca
            .GetAccountAsync(accountIdentifier)
            .ConfigureAwait(false);

        if (account == null)
            throw new InvalidOperationException(
                $"No cached account found for identifier '{accountIdentifier}'. " +
                "Call AcquireUserTokenAsync first to populate the cache.");

        return await agentCca
            .AcquireTokenSilent(scopes, account)
            .ExecuteAsync(cancellationToken)
            .ConfigureAwait(false);
    }
}

Usage

// --- One-time setup (application startup) ---
X509Certificate2 cert = /* load your certificate */;

var tokenService = new AgentTokenService(
    blueprintClientId: "YOUR_BLUEPRINT_CLIENT_ID",
    tenantId:          "YOUR_TENANT_ID",
    cert);

string[] graphScopes = { "https://graph.microsoft.com/.default" };

// --- App-only: agent gets a Graph token with no user context ---
var appToken = await tokenService.AcquireAppTokenAsync("AGENT_APP_ID", graphScopes);
// Use appToken.AccessToken for API calls

// --- User flow (UPN): agent acts on behalf of a user ---
var userToken = await tokenService.AcquireUserTokenAsync(
    "AGENT_APP_ID", graphScopes, "user@contoso.com");
// Use userToken.AccessToken for user-scoped API calls

// --- User flow (OID): identify user by Object ID instead ---
Guid userOid = new Guid("11111111-2222-3333-4444-555555555555");
var userTokenByOid = await tokenService.AcquireUserTokenAsync(
    "AGENT_APP_ID", graphScopes, userOid);

// Store the identifier for later silent calls
string accountId = userToken.Account.HomeAccountId.Identifier;

// --- Subsequent requests: silent retrieval from cache ---
var cached = await tokenService.AcquireUserTokenSilentAsync(
    "AGENT_APP_ID", graphScopes, accountId);
Java — AgentTokenService

This example shows a reusable AgentTokenService class that manages the blueprint CCA and any number of agent CCAs:

import com.microsoft.aad.msal4j.*;

import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

/**
 * Manages the blueprint-to-agent token flow using the multi-CCA pattern.
 * Create one instance per blueprint application and reuse it for the
 * lifetime of your service — CCA instances and their caches are long-lived.
 */
public class AgentTokenService {

    private static final Set<String> FIC_SCOPES =
            Collections.singleton("api://AzureADTokenExchange/.default");

    private final ConfidentialClientApplication blueprintCca;
    private final String authority;
    private final ConcurrentHashMap<String, ConfidentialClientApplication> agentCcas =
            new ConcurrentHashMap<>();

    /**
     * One-time setup: creates the Blueprint CCA that owns the real credential
     * and will handle Leg 1 for all agents.
     */
    public AgentTokenService(String blueprintClientId, String tenantId,
                             PrivateKey privateKey, X509Certificate certificate)
            throws Exception {
        this.authority = "https://login.microsoftonline.com/" + tenantId + "/";

        IClientCertificate clientCert =
                ClientCredentialFactory.createFromCertificate(privateKey, certificate);

        this.blueprintCca = ConfidentialClientApplication
                .builder(blueprintClientId, clientCert)
                .authority(authority)
                .sendX5c(true)
                .build();
    }

    /**
     * Gets or creates an agent CCA for the given agent app ID.
     * Each agent CCA's assertion callback chains back to the Blueprint CCA
     * for Leg 1, and caches are isolated per agent automatically.
     */
    public ConfidentialClientApplication getOrCreateAgentCca(String agentAppId) {
        return agentCcas.computeIfAbsent(agentAppId, id -> {
            try {
                Function<AssertionRequestOptions, String> assertionProvider = options -> {
                    try {
                        IAuthenticationResult leg1 = blueprintCca.acquireToken(
                                ClientCredentialParameters.builder(FIC_SCOPES)
                                        .fmiPath(id)
                                        .build()
                        ).get();
                        return leg1.accessToken();
                    } catch (Exception e) {
                        throw new RuntimeException(
                                "Failed to acquire FMI credential for agent: " + id, e);
                    }
                };

                return ConfidentialClientApplication
                        .builder(id, ClientCredentialFactory.createFromCallback(assertionProvider))
                        .authority(authority)
                        .build();
            } catch (Exception e) {
                throw new RuntimeException("Failed to create Agent CCA for: " + id, e);
            }
        });
    }

    /** Acquires an app-only token for the given agent (no user context). */
    public IAuthenticationResult acquireAppToken(String agentAppId, Set<String> scopes)
            throws Exception {
        return getOrCreateAgentCca(agentAppId)
                .acquireToken(ClientCredentialParameters.builder(scopes).build())
                .get();
    }

    /** Acquires a user-scoped token for the given agent and user (Legs 1-3). */
    public IAuthenticationResult acquireUserToken(String agentAppId, Set<String> scopes,
                                                  String userUpn) throws Exception {
        ConfidentialClientApplication agentCca = getOrCreateAgentCca(agentAppId);

        // Leg 2: instance token (T2) — cached on repeat calls
        IAuthenticationResult leg2 = agentCca.acquireToken(
                ClientCredentialParameters.builder(FIC_SCOPES).build()
        ).get();

        // Leg 3: user-scoped token
        return agentCca.acquireToken(
                UserFederatedIdentityCredentialParameters
                        .builder(scopes, userUpn, leg2.accessToken())
                        .build()
        ).get();
    }

    /** Silently retrieves a cached user token by account home ID. */
    public IAuthenticationResult acquireUserTokenSilent(String agentAppId, Set<String> scopes,
                                                        String accountHomeId) throws Exception {
        ConfidentialClientApplication agentCca = getOrCreateAgentCca(agentAppId);

        IAccount account = agentCca.getAccounts().get().stream()
                .filter(a -> a.homeAccountId().equalsIgnoreCase(accountHomeId))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException(
                        "No cached account for '" + accountHomeId + "'. " +
                        "Call acquireUserToken first."));

        return agentCca.acquireTokenSilently(
                SilentParameters.builder(scopes, account).build()
        ).get();
    }
}

Usage

// --- One-time setup (application startup) ---
PrivateKey privateKey       = /* load your private key */;
X509Certificate certificate = /* load your certificate */;

AgentTokenService tokenService = new AgentTokenService(
        "YOUR_BLUEPRINT_CLIENT_ID", "YOUR_TENANT_ID",
        privateKey, certificate);

Set<String> graphScopes = Collections.singleton("https://graph.microsoft.com/.default");

// --- App-only: agent gets a Graph token with no user context ---
IAuthenticationResult appToken = tokenService.acquireAppToken("AGENT_APP_ID", graphScopes);
// Use appToken.accessToken() for API calls

// --- User flow: agent acts on behalf of a user ---
IAuthenticationResult userToken = tokenService.acquireUserToken(
        "AGENT_APP_ID", graphScopes, "user@contoso.com");
// Use userToken.accessToken() for user-scoped API calls

// Store the identifier for later silent calls
String accountId = userToken.account().homeAccountId();

// --- Subsequent requests: silent retrieval from cache ---
IAuthenticationResult cached = tokenService.acquireUserTokenSilent(
        "AGENT_APP_ID", graphScopes, accountId);
Python — AgentTokenService

Below is a simplified AgentTokenService class and usage example:

import msal


class AgentTokenService:
    """Manages Blueprint + Agent CCA instances for the agent identity flow."""

    def __init__(self, blueprint_client_id, tenant_id, cert_credential):
        self._authority = f"https://login.microsoftonline.com/{tenant_id}"
        self._exchange_scope = ["api://AzureADTokenExchange/.default"]

        # Blueprint CCA — owns the certificate
        self._blueprint = msal.ConfidentialClientApplication(
            blueprint_client_id,
            client_credential=cert_credential,
            authority=self._authority,
        )
        self._agents = {}  # agent_app_id → ConfidentialClientApplication

    def _get_agent_app(self, agent_app_id):
        """Get or create an Agent CCA with an assertion callback."""
        if agent_app_id not in self._agents:
            def assertion_cb(ctx=None):
                r = self._blueprint.acquire_token_for_client(
                    self._exchange_scope, fmi_path=agent_app_id)
                if "access_token" not in r:
                    raise RuntimeError(f"Leg 1 failed: {r.get('error')}")
                return r["access_token"]

            self._agents[agent_app_id] = msal.ConfidentialClientApplication(
                agent_app_id,
                client_credential={"client_assertion": assertion_cb},
                authority=self._authority,
            )
        return self._agents[agent_app_id]

    def acquire_app_token(self, agent_app_id, scopes):
        """Acquire an app-only token for the agent."""
        app = self._get_agent_app(agent_app_id)
        return app.acquire_token_for_client(scopes)

    def acquire_user_token(self, agent_app_id, scopes, username):
        """Full 3-leg flow: FMI → instance token → user_fic."""
        app = self._get_agent_app(agent_app_id)

        # Leg 2: Get instance token (T2) via agent CCA.
        # The assertion callback handles Leg 1 (T1) transparently.
        t2 = app.acquire_token_for_client(self._exchange_scope)
        if "access_token" not in t2:
            raise RuntimeError(f"Leg 2 failed: {t2.get('error')}")

        # Leg 3: Exchange T2 for user-scoped token
        return app.acquire_token_by_user_federated_identity_credential(
            scopes, assertion=t2["access_token"], username=username)

    def acquire_user_token_silent(self, agent_app_id, scopes, account):
        """Try to retrieve a cached user token silently."""
        app = self._get_agent_app(agent_app_id)
        return app.acquire_token_silent(scopes, account=account)

Usage:

service = AgentTokenService(
    blueprint_client_id="BLUEPRINT_CLIENT_ID",
    tenant_id="YOUR_TENANT_ID",
    cert_credential={
        "private_key_pfx_path": "/path/to/cert.pfx",
        "public_certificate": True,
    },
)

graph_scopes = ["https://graph.microsoft.com/.default"]

# --- App-only token ---
result = service.acquire_app_token("AGENT_APP_ID", graph_scopes)

# --- User-scoped token ---
result = service.acquire_user_token(
    "AGENT_APP_ID", graph_scopes, "user@contoso.com")

# --- Subsequent requests: silent retrieval from cache ---
agent_app = service._get_agent_app("AGENT_APP_ID")
accounts = agent_app.get_accounts()
if accounts:
    cached = service.acquire_user_token_silent(
        "AGENT_APP_ID", graph_scopes, accounts[0])
Go — AgentTokenService

Below is a simplified AgentTokenService struct and usage example:

package main

import (
    "context"
    "crypto"
    "crypto/x509"
    "fmt"
    "sync"

    "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)

// AgentTokenService manages Blueprint + Agent clients for the agent identity flow.
type AgentTokenService struct {
    authority     string
    exchangeScope []string
    blueprint     confidential.Client
    agents        map[string]confidential.Client
    mu            sync.Mutex
}

func NewAgentTokenService(blueprintClientID, tenantID string, certs []*x509.Certificate, key crypto.PrivateKey) (*AgentTokenService, error) {
    auth := "https://login.microsoftonline.com/" + tenantID

    cred, err := confidential.NewCredFromCert(certs, key)
    if err != nil {
        return nil, fmt.Errorf("creating cert cred: %w", err)
    }
    bp, err := confidential.New(auth, blueprintClientID, cred, confidential.WithX5C())
    if err != nil {
        return nil, fmt.Errorf("creating blueprint: %w", err)
    }

    return &AgentTokenService{
        authority:     auth,
        exchangeScope: []string{"api://AzureADTokenExchange/.default"},
        blueprint:     bp,
        agents:        make(map[string]confidential.Client),
    }, nil
}

func (s *AgentTokenService) getAgentApp(agentAppID string) (confidential.Client, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    if app, ok := s.agents[agentAppID]; ok {
        return app, nil
    }

    cred := confidential.NewCredFromAssertionCallback(
        func(ctx context.Context, aro confidential.AssertionRequestOptions) (string, error) {
            r, err := s.blueprint.AcquireTokenByCredential(ctx, s.exchangeScope,
                confidential.WithFMIPath(agentAppID))
            if err != nil {
                return "", fmt.Errorf("Leg 1 failed: %w", err)
            }
            return r.AccessToken, nil
        },
    )

    app, err := confidential.New(s.authority, agentAppID, cred)
    if err != nil {
        return confidential.Client{}, err
    }
    s.agents[agentAppID] = app
    return app, nil
}

func (s *AgentTokenService) AcquireAppToken(ctx context.Context, agentAppID string, scopes []string) (confidential.AuthResult, error) {
    app, err := s.getAgentApp(agentAppID)
    if err != nil {
        return confidential.AuthResult{}, err
    }
    return app.AcquireTokenByCredential(ctx, scopes)
}

func (s *AgentTokenService) AcquireUserToken(ctx context.Context, agentAppID string, scopes []string, username string) (confidential.AuthResult, error) {
    app, err := s.getAgentApp(agentAppID)
    if err != nil {
        return confidential.AuthResult{}, err
    }

    // Leg 2: Get instance token (T2) via agent client.
    // The assertion callback handles Leg 1 (T1) transparently.
    t2, err := app.AcquireTokenByCredential(ctx, s.exchangeScope)
    if err != nil {
        return confidential.AuthResult{}, fmt.Errorf("Leg 2: %w", err)
    }

    // Leg 3: Exchange T2 for user-scoped token
    return app.AcquireTokenByUserFederatedIdentityCredential(
        ctx, scopes, t2.AccessToken,
        confidential.WithUserFICUsername(username))
}

func (s *AgentTokenService) AcquireUserTokenSilent(ctx context.Context, agentAppID string, scopes []string, account confidential.Account) (confidential.AuthResult, error) {
    app, err := s.getAgentApp(agentAppID)
    if err != nil {
        return confidential.AuthResult{}, err
    }
    return app.AcquireTokenSilent(ctx, scopes, confidential.WithSilentAccount(account))
}

Usage:

// Load certificate
pemData, _ := os.ReadFile("/path/to/cert.pem")
certs, key, _ := confidential.CertFromPEM(pemData, "")

service := NewAgentTokenService("BLUEPRINT_CLIENT_ID", "YOUR_TENANT_ID", certs, key)
ctx := context.Background()
graphScopes := []string{"https://graph.microsoft.com/.default"}

// --- App-only token ---
result, _ := service.AcquireAppToken(ctx, "AGENT_APP_ID", graphScopes)

// --- User-scoped token ---
result, _ = service.AcquireUserToken(ctx, "AGENT_APP_ID", graphScopes, "user@contoso.com")

// --- Subsequent requests: silent retrieval from cache ---
cached, _ := service.AcquireUserTokenSilent(ctx, "AGENT_APP_ID", graphScopes, result.Account)

Background: Concepts & Protocol

This section provides background on the underlying Entra ID features and the wire-level protocol. If you're already familiar with Federated Managed Identity (FMI), Federated Identity Credentials (FIC), and the three-leg flow, you can skip this section.

Federated Managed Identity (FMI)

Federated Managed Identity is an Entra ID feature that lets a parent application (the blueprint) delegate its identity to child applications (the agents). The blueprint proves its identity to Entra ID and requests a scoped token for a specific agent. This FMI token (T1) allows the agent to act as itself — with its own client_id and permissions — without ever holding a secret of its own.

Federated Identity Credential (FIC / UserFIC)

A Federated Identity Credential is a trust relationship that allows an external token to be exchanged for an Entra ID token. In the user_fic grant type, an application presents both an assertion proving its identity and a user identifier, and Entra ID issues a token scoped to that user — without interactive authentication.

The Three-Leg Protocol

The agent identity flow consists of three token requests, each building on the previous result:

Leg 1: Blueprint → Entra ID        (FMI token acquisition)
  ├─ grant_type: client_credentials
  ├─ client_id:  blueprintClientId
  ├─ credential: blueprint's certificate
  ├─ scope:      api://AzureADTokenExchange/.default
  ├─ fmi_path:   agentAppId
  └─ Returns:    T1 (FMI token scoped to the agent)

Leg 2: Agent → Entra ID            (instance token acquisition)
  ├─ grant_type: client_credentials
  ├─ client_id:  agentAppId
  ├─ credential: T1 (from Leg 1, used as client_assertion)
  ├─ scope:      api://AzureADTokenExchange/.default
  └─ Returns:    T2 (agent instance token)

Leg 3: Agent → Entra ID            (user-scoped token acquisition)
  ├─ grant_type: user_fic
  ├─ client_id:  agentAppId
  ├─ credential: T1 (from Leg 1, used as client_assertion)
  ├─ assertion:  T2 (from Leg 2, used as user_federated_identity_credential)
  ├─ username:   userUpn
  ├─ scope:      downstream API scopes (e.g. https://graph.microsoft.com/.default)
  └─ Returns:    User-scoped access token

App-only variant: If your agent only needs app-level access (no user context), call AcquireTokenForClient on the agent CCA with the downstream API scope directly — the assertion callback handles Leg 1 transparently, and no explicit Leg 2 or Leg 3 is needed.

Getting started with MSAL.NET

Acquiring tokens

Web Apps / Web APIs / daemon apps

Desktop/Mobile apps

Advanced topics

FAQ

Other resources

Clone this wiki locally