Skip to content

Add Federated Managed Identity (FMI) and User Federated Identity Credential (user_fic) grant type support to MSAL Node#8614

Open
Avery-Dunn wants to merge 10 commits into
devfrom
avdunn/fic-fmi-support
Open

Add Federated Managed Identity (FMI) and User Federated Identity Credential (user_fic) grant type support to MSAL Node#8614
Avery-Dunn wants to merge 10 commits into
devfrom
avdunn/fic-fmi-support

Conversation

@Avery-Dunn
Copy link
Copy Markdown
Contributor

@Avery-Dunn Avery-Dunn commented May 28, 2026

This PR adds FMI support and the user_fic grant type to MSAL Node's confidential client, enabling Legs 1–3 of the agent identity protocol. Both features are additive — no breaking changes.

Changes span msal-common (shared types, constants, cache infrastructure — additive optional fields only, no impact on msal-browser) and msal-node (FMI body injection + cache isolation, new FIC client, public API, docs, tests).

The new FIC API is added to ConfidentialClientApplication only, NOT IConfidentialClientApplication, to avoid breaking existing implementations.


FMI (Leg 1) — body injection and cache isolation

  • CommonClientCredentialRequest.fmiPath?: string — injects fmi_path into the POST body
  • Extended cache key isolation: extCacheKeyHash = Base64URL(SHA256("fmi_path" + value)) via ICrypto.hashString(), computed once in acquireToken() and threaded to getCachedAuthenticationResult() and executeTokenRequest() to avoid duplicate computation
  • Credential type → "atext" (ACCESS_TOKEN_EXTENDED); extCacheKeyHash block runs after auth scheme block so it takes priority
  • FMI + PoP/SSH explicitly rejected at request time (mTLS PoP is allowed, matching .NET behavior)
  • generateCredentialKey() appends extCacheKeyHash; cache lookups filter bidirectionally by extCacheKeyHash presence
  • extCacheKeyHash persisted through msal-node's Serializer/Deserializer for file-based cache round-trip
  • ClientAssertionConfig.fmiPath?: string — assertion callbacks receive FMI context
  • isAccessTokenEntity() and matchTarget() updated to recognize ACCESS_TOKEN_EXTENDED
  • Bug fix: ClientCredentialClient was passing request.resourceRequestUri instead of this.authority.tokenEndpoint to assertion callbacks; corrected to match other callers

FIC (Leg 3) — new user_fic grant type

  • acquireTokenByUserFederatedIdentityCredential(request) on ConfidentialClientApplication
  • UserFederatedIdentityCredentialRequest — public type with JSDoc example, using Partial<Omit<...>> pattern with discriminated union:
    { userObjectId: string; username?: never } | { username: string; userObjectId?: never }
  • CommonUserFederatedIdentityCredentialRequest — internal type extending BaseAuthRequest
  • Validates exactly one user identifier + non-empty assertion at call time, with distinct error codes (emptyFicAssertion, conflictingUserIdentifiers, missingUserIdentifier)
  • Per-request clientAssertion (string or callback) resolved in CCA before internal client
  • Wire protocol: grant_type=user_fic, user_federated_identity_credential=<assertion>, user_id/username, client_info=1, OIDC scope augmentation (built once and shared between thumbprint and body)
  • Always hits network; tokens cached with account info; use acquireTokenSilent for subsequent access

Documentation and telemetry

  • lib/msal-node/docs/request.md — FMI and FIC usage sections
  • lib/msal-node/docs/initialize-confidential-client-application.md — assertion callback context updated
  • ApiId.acquireTokenByUserFederatedIdentityCredential = 774

Tests (31 total across 3 spec files)

File Count Coverage
ClientCredentialClientFmi.spec.ts 14 Body injection (3), cache isolation (5), assertion context (3), auth scheme validation (3)
UserFederatedIdentityCredentialClient.spec.ts 12 Protocol correctness (6), user identification (2), scope augmentation (1), cache behavior (2), per-request assertion (1)
ConfidentialClientApplicationFic.spec.ts 5 Input validation with distinct error codes (3), CCA-level per-request assertion resolution — string and callback (2)

Cross-SDK alignment

Aspect .NET Java Go Python JS/Node (this PR)
FMI API WithFmiPath(string) fmiPath(String) WithFMIPath(string) fmi_path=... fmiPath on request
FIC API AcquireTokenByUserFederatedIdentityCredential(...) acquireToken(UserFederatedIdentityCredentialParameters) AcquireTokenByUserFederatedIdentityCredential(...) acquire_token_by_user_federated_identity_credential(...) acquireTokenByUserFederatedIdentityCredential(request)
Wire protocol user_fic + scope augmentation + client_info=1 Same Same Same Same
Cache strategy Always network; AcquireTokenSilent Built-in cache-then-network Always network Always network Always network; acquireTokenSilent

Intentional differences from other MSALs:

Aspect Other MSALs JS/Node Rationale
User identification Various (overloads, kwargs) Discriminated union (userObjectId XOR username) TypeScript-idiomatic compile-time enforcement
Request type Required base fields Partial<Omit<...>> + Common internal type Matches existing msal-node patterns
extCacheKeyHash credential type .NET: "AccessToken_Extended"; Java: "AText" "atext" (lowercase) JS cache keys are lowercased
FMI + PoP/SSH .NET allows (composes both into cache key via _extraKeyParts array) Rejected at request time JS cache key model uses a single credentialType field; removeAccessToken binding key cleanup gates on ACCESS_TOKEN_WITH_AUTH_SCHEME. Can be relaxed later without breaking changes.
CCS routing header .NET/Python send it Not sent Can be added later

Files changed

msal-common (additive only):

File Change
src/utils/Constants.ts USER_FIC grant type; ACCESS_TOKEN_EXTENDED: "atext" credential type
src/constants/AADServerParamKeys.ts USER_FEDERATED_IDENTITY_CREDENTIAL, USERNAME, USER_ID, FMI_PATH
src/account/ClientCredentials.ts fmiPath?: string on ClientAssertionConfig
src/utils/ClientAssertionUtils.ts fmiPath param on getClientAssertion()
src/cache/entities/CredentialEntity.ts extCacheKeyHash?: string field
src/cache/utils/CacheTypes.ts extCacheKeyHash?: string on CredentialFilter
src/cache/CacheManager.ts Bidirectional extCacheKeyHash filtering; ACCESS_TOKEN_EXTENDED in matchTarget()
src/cache/utils/CacheHelpers.ts extCacheKeyHash on createAccessTokenEntity(); ACCESS_TOKEN_EXTENDED in isAccessTokenEntity()
src/response/ResponseHandler.ts Thread extCacheKeyHash through response handling
apiReview/msal-common.api.md Regenerated

msal-node:

File Change
src/request/CommonClientCredentialRequest.ts fmiPath?: string
src/client/ClientCredentialClient.ts FMI body injection, hash computed once and threaded (no fallback in callees), tokenEndpoint fix, FMI+PoP/SSH rejection
src/cache/CacheHelpers.ts Appends extCacheKeyHash to credential key
src/cache/serializer/SerializerTypes.ts extCacheKeyHash on SerializedAccessTokenEntity
src/cache/serializer/Serializer.ts extCacheKeyHash in serializeAccessTokens()
src/cache/serializer/Deserializer.ts extCacheKeyHash in deserializeAccessTokens()
src/client/ConfidentialClientApplication.ts acquireTokenByUserFederatedIdentityCredential() with validation (distinct error codes) and assertion resolution
src/client/UserFederatedIdentityCredentialClient.ts New — internal user_fic client with unified scope augmentation
src/request/CommonUserFederatedIdentityCredentialRequest.ts New — internal request type
src/request/UserFederatedIdentityCredentialRequest.ts New — public request type with JSDoc example
src/error/ClientAuthErrorCodes.ts emptyFicAssertion, conflictingUserIdentifiers, missingUserIdentifier, fmiWithNonBearerScheme
src/utils/Constants.ts ApiId.acquireTokenByUserFederatedIdentityCredential = 774
src/index.ts Export UserFederatedIdentityCredentialRequest
docs/request.md FMI and FIC usage sections
docs/initialize-confidential-client-application.md Assertion callback context
apiReview/msal-node.api.md Regenerated

Copilot AI review requested due to automatic review settings May 28, 2026 16:56
@Avery-Dunn Avery-Dunn requested review from a team as code owners May 28, 2026 16:56
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 FMI support for client credentials and a new user_fic token acquisition path for MSAL Node, with shared msal-common cache/type plumbing to support the new token request and cache isolation behavior.

Changes:

  • Adds fmiPath request support, request-body injection, and extended cache key handling for FMI client-credential tokens.
  • Adds UserFederatedIdentityCredentialRequest, UserFederatedIdentityCredentialClient, and ConfidentialClientApplication.acquireTokenByUserFederatedIdentityCredential.
  • Adds shared constants/types and unit coverage for FMI/FIC request construction, cache behavior, telemetry ID, and validation.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
lib/msal-node/src/client/ClientCredentialClient.ts Adds FMI hashing, cache filtering, request-body fmi_path, and assertion callback context changes.
lib/msal-node/src/client/ConfidentialClientApplication.ts Adds public FIC acquisition method and request validation.
lib/msal-node/src/client/UserFederatedIdentityCredentialClient.ts Implements the new user_fic grant client.
lib/msal-node/src/request/CommonClientCredentialRequest.ts Adds optional fmiPath.
lib/msal-node/src/request/UserFederatedIdentityCredentialRequest.ts Adds the public FIC request type.
lib/msal-node/src/index.ts Exports the new public FIC request type.
lib/msal-node/src/cache/CacheHelpers.ts Extends credential key generation with extCacheKeyHash.
lib/msal-node/src/utils/Constants.ts Adds telemetry API ID for FIC.
lib/msal-common/src/utils/Constants.ts Adds user_fic and extended access-token credential constants.
lib/msal-common/src/utils/ClientAssertionUtils.ts Threads fmiPath into assertion callback config.
lib/msal-common/src/account/ClientCredentials.ts Adds fmiPath to assertion callback context type.
lib/msal-common/src/constants/AADServerParamKeys.ts Adds protocol parameter constants for FIC/FMI.
lib/msal-common/src/cache/entities/CredentialEntity.ts Adds optional extended cache key hash field.
lib/msal-common/src/cache/utils/CacheTypes.ts Adds extCacheKeyHash to credential filters.
lib/msal-common/src/cache/utils/CacheHelpers.ts Creates/access-token entity support for extended tokens.
lib/msal-common/src/cache/CacheManager.ts Adds extended-token cache matching and target matching support.
lib/msal-common/src/response/ResponseHandler.ts Threads extended cache key hash into cache record generation.
lib/msal-node/test/client/ClientCredentialClientFmi.spec.ts Adds FMI unit coverage.
lib/msal-node/test/client/UserFederatedIdentityCredentialClient.spec.ts Adds FIC client unit coverage.
lib/msal-node/test/client/ConfidentialClientApplicationFic.spec.ts Adds FIC CCA validation coverage.

Comment thread lib/msal-common/src/cache/entities/CredentialEntity.ts
Comment thread lib/msal-common/src/cache/utils/CacheHelpers.ts Outdated
Comment thread lib/msal-node/src/index.ts
Comment thread lib/msal-node/src/client/ConfidentialClientApplication.ts
Comment thread lib/msal-node/src/request/CommonClientCredentialRequest.ts
Comment thread lib/msal-common/src/utils/Constants.ts
Comment thread lib/msal-node/src/request/UserFederatedIdentityCredentialRequest.ts
Comment thread lib/msal-node/test/client/ConfidentialClientApplicationFic.spec.ts
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 20 out of 21 changed files in this pull request and generated 8 comments.

Comment thread lib/msal-node/src/request/UserFederatedIdentityCredentialRequest.ts Outdated
Comment thread lib/msal-node/src/client/ConfidentialClientApplication.ts
Comment thread lib/msal-common/src/cache/CacheManager.ts
Comment thread lib/msal-node/src/client/ClientCredentialClient.ts
Comment thread lib/msal-node/src/index.ts
Comment thread lib/msal-common/src/account/ClientCredentials.ts
Comment thread lib/msal-node/src/client/ConfidentialClientApplication.ts
Comment thread lib/msal-node/src/utils/Constants.ts
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 30 out of 31 changed files in this pull request and generated 2 comments.

Comment thread lib/msal-node/src/client/ClientCredentialClient.ts Outdated
Comment thread change/@azure-msal-node-4a84d075-694c-4e18-8494-11731d4b5b96.json Outdated
Comment thread lib/msal-common/src/cache/utils/CacheHelpers.ts
Comment thread lib/msal-node/src/client/ClientCredentialClient.ts Outdated
Comment thread lib/msal-node/src/client/ConfidentialClientApplication.ts Outdated
Comment thread lib/msal-node/src/client/UserFederatedIdentityCredentialClient.ts Outdated
Comment thread lib/msal-node/src/request/UserFederatedIdentityCredentialRequest.ts
Comment thread lib/msal-node/src/client/ClientCredentialClient.ts Outdated
Comment thread lib/msal-node/src/client/ClientCredentialClient.ts Outdated
Comment thread lib/msal-node/test/client/ClientCredentialClientFmi.spec.ts
Comment thread lib/msal-node/src/error/ClientAuthErrorCodes.ts Outdated
Comment thread lib/msal-node/src/client/ClientCredentialClient.ts Outdated
];

// Append extCacheKeyHash for extended cache key isolation (e.g., fmi_path)
if (credential.extCacheKeyHash) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Putting this behind a conditional like this means the placement within the key may change in the future. Today it's the 8th segment of the key, if we add something to the array above it will become the 9th and keys that don't have this will now have 8 segments so anything depending on the specific placement may detect the wrong thing. On the other hand, including it unconditionally above will result in an extra separator for all future keys, including those that don't use this, making migration a bit harder. Have you thought about the tradeoffs here?

Copy link
Copy Markdown
Contributor Author

@Avery-Dunn Avery-Dunn Jun 2, 2026

Choose a reason for hiding this comment

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

This is more-or-less the same behavior as MSAL .NET, which has a list of default cache key components (some of which may be empty strings) and a list of extra components that is only added to the end of the list, and only if it's not empty: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/1346f6a8133dfab6bfdb44fd552c99d153e17405/src/client/Microsoft.Identity.Client/Cache/Items/MsalCacheKeys.cs#L43

As far as I can tell from a quick look it doesn't seem like key components are read by position in MSAL JS, but it's always possible a customer might do it.

Seems like this is another one of those caching differences we talked about between JS and other MSALs. Does JS offer any docs guaranteeing a certain order for cache key components?

request: CommonClientCredentialRequest
): Promise<AuthenticationResult | null> {
/*
* FMI is incompatible with PoP/SSH — those schemes store non-bearer tokens as
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is it incompatible because there's something inherently incompatible about FMI + POP/SSH or just incompatible because of cache design decisions? If the latter maybe we should reconsider the design.

Copy link
Copy Markdown
Contributor Author

@Avery-Dunn Avery-Dunn Jun 2, 2026

Choose a reason for hiding this comment

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

It's a cache key design issue, not an inherent incompatibility.

In JS, two places in CacheManager rely on the ACCESS_TOKEN_WITH_AUTH_SCHEME credential type to identify if something was POP or not: credentialMatchesFilter() and removeAccessToken()

If the new ACCESS_TOKEN_EXTENDED overrode ACCESS_TOKEN_WITH_AUTH_SCHEME, the PoP tokens wouldn't be identifiable.

In MSAL .NET, cache keys are created with an _extraKeyParts array for niche items, along with more standard items like credential type: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/1346f6a8133dfab6bfdb44fd552c99d153e17405/src/client/Microsoft.Identity.Client/Cache/Items/MsalAccessTokenCacheItem.cs#L24

It includes info about PoP and FMI by creating cache keys like this:
-PoP: homeId-env-at_with_auth_scheme-clientId-tenant-scopes-pop
-FMI: homeId-env-atext-clientId-tenant-scopes-<hash with FMI>
-FMI+PoP: homeId-env-atext-clientId-tenant-scopes-pop-<hash with FMI>

So when it needs to clean up binding tokens or other items in scenarios like PoP it doesn't need to rely on at_with_auth_scheme or other credential types, it uses different cache key constants.

So we could adjust JS's cache keys and CacheManager to not rely on credential type, but it would mean changing existing behavior that isn't directly related to FIC/FMI.

Comment thread lib/msal-node/src/request/UserFederatedIdentityCredentialRequest.ts Outdated
Comment thread lib/msal-node/src/request/CommonUserFederatedIdentityCredentialRequest.ts Outdated
Comment on lines +10 to +13
export const emptyFicAssertion = "empty_fic_assertion";
export const conflictingUserIdentifiers = "conflicting_user_identifiers";
export const missingUserIdentifier = "missing_user_identifier";
export const fmiWithNonBearerScheme = "fmi_with_non_bearer_scheme";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please make sure the error doc gets regenerated with these new additions

Copy link
Copy Markdown
Contributor Author

@Avery-Dunn Avery-Dunn Jun 2, 2026

Choose a reason for hiding this comment

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

Ran npm run apiExtractor -- --local on both msal-node and msal-common again in the latest commit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ApiExtractor is different. This is the error doc that needs updating

Comment thread lib/msal-node/src/client/ClientCredentialClient.ts Outdated
Comment thread lib/msal-node/src/client/ClientCredentialClient.ts Outdated
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.

4 participants