-
Notifications
You must be signed in to change notification settings - Fork 406
How to Use FIC and FMI in Agentic Scenarios
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.
| 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 |
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.
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:
CertFromPEMaccepts PEM-encoded data containing both the certificate chain and private key. The second parameter is the PEM encryption password (empty string for unencrypted keys).
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— thefmi_pathparameter 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— thefmiPathparameter ensures Leg 1 tokens are cached independently per agent.
Assertion callback context: The
AssertionRequestOptionsparameter providesclientId(),tokenEndpoint(), andclientAssertionFmiPath(). While the simple example above ignoresoptions, production code can useoptions.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, andfmi_path(when applicable) - If the callable takes no parameters (zero-arg), MSAL calls it without arguments — both patterns are supported via
inspect.signaturedetection -
Do not pass a static string for
client_assertionwhen 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:
-
NewCredFromAssertionCallbacktakesfunc(context.Context, AssertionRequestOptions) (string, error)and returnsCredentialdirectly (no error return) - The
AssertionRequestOptionsstruct exposesFMIPath string— populated whenWithFMIPathis 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
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 callsThis 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 callsThis 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.
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 usernamefor UPN-based identification, orGuid userObjectIdfor OID-based identification. TheGuidtype provides compile-time disambiguation —Guid.Emptyis rejected at call time.
Important:
AcquireTokenByUserFederatedIdentityCredentialalways 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 useAcquireTokenSilentfor 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 userUser identification: MSAL Java supports both UPN and OID for user identification. Use the
builder(scopes, username, assertion)overload for UPN, orbuilder(scopes, userObjectId, assertion)with aUUIDfor 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 useacquireTokenSilentlyfor 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 ofusernameif you only have the OID)
Note: MSAL Python supports both
username(UPN) anduser_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
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 filteringGetAccountsAsync()by UPN — it avoids case-sensitivity issues and is more reliable in multi-account scenarios. StoreuserToken.Account.HomeAccountId.Identifierafter the initialAcquireTokenByUserFederatedIdentityCredentialcall 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
IAccountfrom the initialacquireToken(UserFederatedIdentityCredentialParameters)result for later silent retrieval. In multi-user scenarios, match accounts byhomeAccountId().
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
Noneif 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
Nonebefore 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.TokenSourceforTokenSourceCachevsTokenSourceIdentityProvider
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.
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);
}
}// --- 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();
}
}// --- 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)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 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.
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 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.
- Home
- Why use MSAL.NET
- Is MSAL.NET right for me
- Scenarios
- Register your app with AAD
- Client applications
- Acquiring tokens
- MSAL samples
- Known Issues
- Acquiring a token for the app
- Acquiring a token on behalf of a user in Web APIs
- Acquiring a token by authorization code in Web Apps
- [Credentials] Credentials
- AcquireTokenInteractive
- WAM - the Windows broker
- .NET Core
- Maui Docs
- Custom Browser
- Applying an AAD B2C policy
- Integrated Windows Authentication for domain or AAD joined machines
- Username / Password
- Device Code Flow for devices without a Web browser
- ADFS support
- High Availability
- Regional
- Token cache serialization
- Logging
- Exceptions in MSAL
- Provide your own Httpclient and proxy
- Extensibility Points
- Clearing the cache
- Client Credentials Multi-Tenant guidance
- Performance perspectives
- Differences between ADAL.NET and MSAL.NET Apps
- PowerShell support
- Testing apps that use MSAL
- Experimental Features
- Proof of Possession (PoP) tokens
- Using in Azure functions
- Extract info from WWW-Authenticate headers
- SPA Authorization Code