Add Federated Managed Identity (FMI) and User Federated Identity Credential (user_fic) grant type support to MSAL Node#8614
Add Federated Managed Identity (FMI) and User Federated Identity Credential (user_fic) grant type support to MSAL Node#8614Avery-Dunn wants to merge 10 commits into
user_fic) grant type support to MSAL Node#8614Conversation
There was a problem hiding this comment.
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
fmiPathrequest support, request-body injection, and extended cache key handling for FMI client-credential tokens. - Adds
UserFederatedIdentityCredentialRequest,UserFederatedIdentityCredentialClient, andConfidentialClientApplication.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. |
| ]; | ||
|
|
||
| // Append extCacheKeyHash for extended cache key isolation (e.g., fmi_path) | ||
| if (credential.extCacheKeyHash) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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"; |
There was a problem hiding this comment.
Please make sure the error doc gets regenerated with these new additions
There was a problem hiding this comment.
Ran npm run apiExtractor -- --local on both msal-node and msal-common again in the latest commit.
There was a problem hiding this comment.
ApiExtractor is different. This is the error doc that needs updating
This PR adds FMI support and the
user_ficgrant 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
ConfidentialClientApplicationonly, NOTIConfidentialClientApplication, to avoid breaking existing implementations.FMI (Leg 1) — body injection and cache isolation
CommonClientCredentialRequest.fmiPath?: string— injectsfmi_pathinto the POST bodyextCacheKeyHash = Base64URL(SHA256("fmi_path" + value))viaICrypto.hashString(), computed once inacquireToken()and threaded togetCachedAuthenticationResult()andexecuteTokenRequest()to avoid duplicate computation"atext"(ACCESS_TOKEN_EXTENDED);extCacheKeyHashblock runs after auth scheme block so it takes prioritygenerateCredentialKey()appendsextCacheKeyHash; cache lookups filter bidirectionally byextCacheKeyHashpresenceextCacheKeyHashpersisted through msal-node'sSerializer/Deserializerfor file-based cache round-tripClientAssertionConfig.fmiPath?: string— assertion callbacks receive FMI contextisAccessTokenEntity()andmatchTarget()updated to recognizeACCESS_TOKEN_EXTENDEDClientCredentialClientwas passingrequest.resourceRequestUriinstead ofthis.authority.tokenEndpointto assertion callbacks; corrected to match other callersFIC (Leg 3) — new
user_ficgrant typeacquireTokenByUserFederatedIdentityCredential(request)onConfidentialClientApplicationUserFederatedIdentityCredentialRequest— public type with JSDoc example, usingPartial<Omit<...>>pattern with discriminated union:CommonUserFederatedIdentityCredentialRequest— internal type extendingBaseAuthRequestassertionat call time, with distinct error codes (emptyFicAssertion,conflictingUserIdentifiers,missingUserIdentifier)clientAssertion(string or callback) resolved in CCA before internal clientgrant_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)acquireTokenSilentfor subsequent accessDocumentation and telemetry
lib/msal-node/docs/request.md— FMI and FIC usage sectionslib/msal-node/docs/initialize-confidential-client-application.md— assertion callback context updatedApiId.acquireTokenByUserFederatedIdentityCredential = 774Tests (31 total across 3 spec files)
ClientCredentialClientFmi.spec.tsUserFederatedIdentityCredentialClient.spec.tsConfidentialClientApplicationFic.spec.tsCross-SDK alignment
WithFmiPath(string)fmiPath(String)WithFMIPath(string)fmi_path=...fmiPathon requestAcquireTokenByUserFederatedIdentityCredential(...)acquireToken(UserFederatedIdentityCredentialParameters)AcquireTokenByUserFederatedIdentityCredential(...)acquire_token_by_user_federated_identity_credential(...)acquireTokenByUserFederatedIdentityCredential(request)user_fic+ scope augmentation +client_info=1AcquireTokenSilentacquireTokenSilentIntentional differences from other MSALs:
userObjectIdXORusername)Partial<Omit<...>>+Commoninternal typeextCacheKeyHashcredential type"AccessToken_Extended"; Java:"AText""atext"(lowercase)_extraKeyPartsarray)credentialTypefield;removeAccessTokenbinding key cleanup gates onACCESS_TOKEN_WITH_AUTH_SCHEME. Can be relaxed later without breaking changes.Files changed
msal-common (additive only):
src/utils/Constants.tsUSER_FICgrant type;ACCESS_TOKEN_EXTENDED: "atext"credential typesrc/constants/AADServerParamKeys.tsUSER_FEDERATED_IDENTITY_CREDENTIAL,USERNAME,USER_ID,FMI_PATHsrc/account/ClientCredentials.tsfmiPath?: stringonClientAssertionConfigsrc/utils/ClientAssertionUtils.tsfmiPathparam ongetClientAssertion()src/cache/entities/CredentialEntity.tsextCacheKeyHash?: stringfieldsrc/cache/utils/CacheTypes.tsextCacheKeyHash?: stringonCredentialFiltersrc/cache/CacheManager.tsextCacheKeyHashfiltering;ACCESS_TOKEN_EXTENDEDinmatchTarget()src/cache/utils/CacheHelpers.tsextCacheKeyHashoncreateAccessTokenEntity();ACCESS_TOKEN_EXTENDEDinisAccessTokenEntity()src/response/ResponseHandler.tsextCacheKeyHashthrough response handlingapiReview/msal-common.api.mdmsal-node:
src/request/CommonClientCredentialRequest.tsfmiPath?: stringsrc/client/ClientCredentialClient.tstokenEndpointfix, FMI+PoP/SSH rejectionsrc/cache/CacheHelpers.tsextCacheKeyHashto credential keysrc/cache/serializer/SerializerTypes.tsextCacheKeyHashonSerializedAccessTokenEntitysrc/cache/serializer/Serializer.tsextCacheKeyHashinserializeAccessTokens()src/cache/serializer/Deserializer.tsextCacheKeyHashindeserializeAccessTokens()src/client/ConfidentialClientApplication.tsacquireTokenByUserFederatedIdentityCredential()with validation (distinct error codes) and assertion resolutionsrc/client/UserFederatedIdentityCredentialClient.tsuser_ficclient with unified scope augmentationsrc/request/CommonUserFederatedIdentityCredentialRequest.tssrc/request/UserFederatedIdentityCredentialRequest.tssrc/error/ClientAuthErrorCodes.tsemptyFicAssertion,conflictingUserIdentifiers,missingUserIdentifier,fmiWithNonBearerSchemesrc/utils/Constants.tsApiId.acquireTokenByUserFederatedIdentityCredential = 774src/index.tsUserFederatedIdentityCredentialRequestdocs/request.mddocs/initialize-confidential-client-application.mdapiReview/msal-node.api.md