Skip to content

Add Federated Managed Identity (FMI) support for client credentials flow#1025

Merged
Avery-Dunn merged 13 commits into
devfrom
avdunn/fmi-support
May 20, 2026
Merged

Add Federated Managed Identity (FMI) support for client credentials flow#1025
Avery-Dunn merged 13 commits into
devfrom
avdunn/fmi-support

Conversation

@Avery-Dunn
Copy link
Copy Markdown
Contributor

@Avery-Dunn Avery-Dunn commented Apr 15, 2026

This PR adds Federated Managed Identity (FMI) support to MSAL Java's client credentials flow, enabling Leg 1 and Leg 2 of the agent identity protocol. FMI allows a blueprint application to acquire tokens scoped to a specific agent identity by providing an fmi_path parameter in the token request.

What's included

New public APIs:

  • ClientCredentialParameters.fmiPath(String) — sets the fmi_path body parameter sent in the client credentials POST request. Rejects null, empty, and whitespace values (matching MSAL .NET's WithFmiPath validation).
  • ClientCredentialFactory.createFromCallback(Function<AssertionRequestOptions, String>) — creates a client assertion from a context-aware callback function (needed for Leg 2, where the assertion callback must know the FMI path to acquire the correct credential)
  • AssertionRequestOptions — immutable context object passed to assertion callbacks, providing clientId(), tokenEndpoint(), and clientAssertionFmiPath()

Cache key isolation (ext_cache_key):

  • Tokens acquired with different fmi_path values are isolated in the cache using an extended cache key hash
  • Hash algorithm matches other MSALs: SHA-256 of sorted key+value concatenation, Base64URL-encoded
  • Hash is memoized on the ClientCredentialParameters instance (immutable after construction), matching MSAL .NET's approach of computing once at cache item creation
  • Credential type changes from "AccessToken" to "AText" (AccessToken_Extended) for tokens with additional cache key components
  • Cache lookups correctly filter bidirectionally: FMI-tagged entries are not returned for non-FMI requests, and vice versa

Internal changes:

  • ClientCredentialRequest injects fmi_path into the OAuth2 POST body when set
  • TokenRequestExecutor passes clientAssertionFmiPath to the assertion callback via AssertionRequestOptions
  • ClientAssertion extended to support Function<AssertionRequestOptions, String> callbacks (in addition to existing static String and Callable<String> modes). Dispatch priority: context-aware Function > Callable > static string.
  • TokenCache extended with extCacheKeyHashMatches() bidirectional filter logic
  • AccessTokenCacheEntity extended with extCacheKeyHash field
  • StringHelper.computeExtCacheKeyHash() implements the SHA-256 hash algorithm
  • AcquireTokenByClientCredentialSupplier propagates extCacheKeyHash through the silent request path

Tests:

  • FmiTest.java — 19 unit tests covering: input validation (null/empty/blank fmiPath rejection), cache key computation and cross-SDK hash verification, assertion callback context propagation (clientAssertionFmiPath passed correctly), bidirectional cache isolation (FMI vs non-FMI, different FMI paths), and legacy Callable fallback behavior
  • FmiIT.java — 7 integration tests covering FMI credential acquisition (from cert, from RMA, chained FMI credentials, token from FMI credential, multi-FMI-path cache isolation)
  • AgenticIT.java — 3 integration tests covering FMI scenarios using RMA (FMI credential acquisition from RMA, token cache hit for repeated requests)

Alignment with MSAL .NET

This implementation matches the FMI behavior available on MSAL .NET's main branch. Key equivalences:

.NET Java
WithFmiPath(string) ClientCredentialParameters.fmiPath(String)
WithClientAssertion(Func<AssertionRequestOptions, Task<string>>) ClientCredentialFactory.createFromCallback(Function<AssertionRequestOptions, String>)
AssertionRequestOptions (ClientId, TokenEndpoint, FmiPath) AssertionRequestOptions (clientId, tokenEndpoint, clientAssertionFmiPath)
CoreHelpers.ComputeAccessTokenExtCacheKey() StringHelper.computeExtCacheKeyHash()
"ATExt" credential type "AText" credential type (case differs but cache key is lowercased for lookups in both SDKs)
Hash computed once at cache item creation (InitCacheKey) Hash memoized on ClientCredentialParameters
string.IsNullOrWhiteSpace validation on WithFmiPath ParameterValidationUtils.validateNotBlank on fmiPath() builder setter

Known differences from .NET (intentional)

  • Java's AssertionRequestOptions exposes clientId, tokenEndpoint, clientAssertionFmiPath. .NET additionally exposes TenantId, Authority, Claims, ClientCapabilities, CorrelationId. The additional fields can be added in a future PR if needed.
  • Java does not yet have WithFmiPathForClientAssertion (a separate FMI path passed only to the assertion callback, distinct from the body parameter). This is infrastructure for the composite AcquireTokenForAgent API which will be added in a subsequent PR.
  • Java does not have CancellationToken semantics. The callback is Function<AssertionRequestOptions, String> (synchronous) rather than .NET's async Func<..., CancellationToken, Task<string>>.
  • Java does not yet have AssertionResponse / ClientSignedAssertion support (assertion + token-binding certificate for mTLS PoP). This can be added in a future PR when jwt-pop scenarios are needed.

Comment thread msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/FmiTest.java Outdated
Comment thread msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/FmiTest.java
Comment thread msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/FmiTest.java Outdated
Comment thread msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/FmiTest.java Outdated
@Avery-Dunn Avery-Dunn changed the title First draft of FMI support Add Federated Managed Identity (FMI) support for client credentials flow May 6, 2026
@Avery-Dunn Avery-Dunn marked this pull request as ready for review May 6, 2026 20:06
@Avery-Dunn Avery-Dunn requested a review from a team as a code owner May 6, 2026 20:06
Comment thread msal4j-sdk/src/integrationtest/java/com/microsoft/aad/msal4j/AgenticIT.java Outdated
Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AssertionResponse.java Outdated
Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AssertionRequestOptions.java Outdated
Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CredentialTypeEnum.java Outdated
Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AssertionRequestOptions.java Outdated
Copy link
Copy Markdown
Contributor

@neha-bhargava neha-bhargava left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor comments otherwise looks good

* This is the single source of truth for the fmi_path cache key hash computation,
* used by both cache writes (TokenCache) and cache reads (silent lookup).
*/
String computeFmiCacheKeyHash() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be abstracted to some method in case some other additional key needs to be added

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain what you mean a bit more? This is an internal method which validates the FMI path isn't blank and calls the more generic computeExtCacheKeyHash that actually makes the hash for the cache key, so there's not a whole lot that can be abstracted.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically can compute cache key with any other additional parameter that requires to be included in cache key

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean now, good catch: the agent definitely made this part too specific.

In the latest commit I've refactored it to be more generic and expandable.

Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java Outdated
Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/StringHelper.java
Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Federated Managed Identity (FMI) support to MSAL Java's client credentials flow. A new fmiPath parameter on ClientCredentialParameters is injected as the fmi_path body parameter on the token POST, and cached tokens are isolated by an ext_cache_key_hash (SHA-256 over sorted key/value components, Base64URL-encoded) under a new ACCESS_TOKEN_EXTENDED credential type. A new AssertionRequestOptions context object and a Function<AssertionRequestOptions, String> overload of ClientCredentialFactory.createFromCallback let assertion callbacks see the per-request fmiPath, clientId, and tokenEndpoint.

Changes:

  • New public APIs: ClientCredentialParameters.fmiPath(...), AssertionRequestOptions, ClientCredentialFactory.createFromCallback(Function<AssertionRequestOptions,String>).
  • Cache isolation via ext_cache_key_hash and ACCESS_TOKEN_EXTENDED credential type plumbed through AccessTokenCacheEntity, TokenCache, SilentRequest, and AcquireTokenByClientCredentialSupplier.
  • New unit tests (FmiTest.java) and integration tests (FmiIT.java, AgenticIT.java) plus a TestHelper.getInstanceDiscoveryResponse() helper.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
ClientCredentialParameters.java Adds fmiPath field/builder and computeFmiCacheKeyHash() helper.
ClientCredentialRequest.java Injects fmi_path into the OAuth POST body.
ClientCredentialFactory.java New createFromCallback(Function<AssertionRequestOptions,String>) overload.
ClientAssertion.java New context-aware constructor, assertion(options), isContextAware().
AssertionRequestOptions.java New context object with clientId, tokenEndpoint, clientAssertionFmiPath.
TokenRequestExecutor.java Passes AssertionRequestOptions to context-aware assertions.
CredentialTypeEnum.java Adds ACCESS_TOKEN_EXTENDED("AText").
AccessTokenCacheEntity.java Adds extCacheKeyHash field, getter/setter, key-suffix, JSON round-trip.
TokenCache.java Computes/persists extCacheKeyHash on write; filters on read.
SilentRequest.java Carries extCacheKeyHash through silent-lookup path.
AcquireTokenSilentSupplier.java Forwards extCacheKeyHash to cache lookup.
AcquireTokenByClientCredentialSupplier.java Propagates FMI hash to the silent request.
StringHelper.java New computeExtCacheKeyHash(SortedMap) (SHA-256 + Base64URL).
test/.../FmiTest.java Unit tests for fmi_path, cache key isolation, hash, callback context.
test/.../TestHelper.java Adds getInstanceDiscoveryResponse() helper.
integrationtest/.../FmiIT.java Real-Entra integration tests for FMI flows.
integrationtest/.../AgenticIT.java Agent-identity callback-context and cache-isolation IT.

Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java Outdated
Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.

@Avery-Dunn Avery-Dunn merged commit 6062d6d into dev May 20, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants