Skip to content

Add User Federated Identity Credential (user_fic) grant type support#918

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

Add User Federated Identity Credential (user_fic) grant type support#918
Avery-Dunn wants to merge 2 commits into
devfrom
avdunn/fic-fmi-support

Conversation

@Avery-Dunn
Copy link
Copy Markdown
Contributor

This PR adds the user_fic (User Federated Identity Credential) grant type and related APIs to MSAL Go's confidential client, matching what was done in other MSALs.

In addition, this PR also adds a new piece for FMI required by an (eventual) support for agent identity scenarios: passing fmi_path and other context to assertion callbacks so they can acquire the correct credentials.


What's included

FMI completion — assertion callback context:

  • Callable client assertions now receive an optional context dict when they accept one argument
  • Context contains client_id, token_endpoint, and fmi_path (when set on the request)
  • Backward-compatible: zero-arg callables continue to work unchanged (detection via inspect.signature)
  • BaseClient._accepts_context() / _invoke_assertion_callable() — new internal methods for dispatch

FIC — new public API:

  • ConfidentialClientApplication.acquire_token_by_user_federated_identity_credential(scopes, assertion, username=None, user_object_id=None, claims_challenge=None) — acquires a user-scoped token using the user_fic grant type

FIC — token request behavior:

  • Sends grant_type=user_fic in the POST body
  • Sends user_federated_identity_credential=<assertion> (the instance token from Leg 2)
  • Sends either user_id=<OID> or username=<UPN> (mutually exclusive, validated at call time)
  • Augments scopes with openid, profile, offline_access via existing _decorate_scope()
  • Sends client_info=1 to receive account information in the response
  • Includes client credential (secret or assertion) for confidential client authentication

FIC — cache behavior:

  • FIC always hits the network (no built-in silent lookup)
  • Tokens are stored in the standard user token cache with full account information
  • FIC tokens use standard AccessToken credential type (not atext) — no ext_cache_key
  • Developers use acquire_token_silent() with the returned account for subsequent cached access
  • This matches MSAL .NET and MSAL Go's pattern and keeps the implementation simple

Supporting changes:

  • user_federated_identity_credential, user_id, and client_info added to _EXT_CACHE_KEY_EXCLUDED_FIELDS in token_cache.py, ensuring FIC body parameters don't pollute the extended cache key hash
  • New telemetry API ID ACQUIRE_TOKEN_BY_USER_FIC_ID = "950"

Internal changes:

  • BaseClient._accepts_context() — inspects callable signature to determine if it accepts context
  • BaseClient._invoke_assertion_callable() — dispatches to context-aware or zero-arg callable
  • Client.obtain_token_by_user_fic() — new oauth2cli method that builds the user_fic POST body
  • BaseClient._obtain_token() — updated to use _invoke_assertion_callable() for assertion dispatch

Tests (16 total):

FIC protocol tests (4):

  • test_sends_correct_grant_type_and_params — verifies grant_type=user_fic, credential param, client_info, client_id
  • test_scope_includes_oidc_scopes — verifies openid/offline_access/profile augmentation
  • test_with_username_sends_username_not_user_id — username param sent, user_id absent
  • test_with_oid_sends_user_id_not_username — user_id param sent, username absent

FIC cache behavior tests (3):

  • test_token_stored_in_user_cache_with_account — token stored with populated account (home_account_id)
  • test_token_not_stored_as_atext — FIC tokens use standard AccessToken type, no ext_cache_key
  • test_acquire_token_silent_returns_cached_fic_tokenacquire_token_silent returns cached FIC token

FIC validation tests (5):

  • test_empty_assertion_raises — empty assertion rejected
  • test_none_assertion_raises — None assertion rejected
  • test_no_user_identifier_raises — missing user identification rejected
  • test_both_user_identifiers_raises — both OID and UPN rejected (mutually exclusive)
  • test_reserved_scopes_rejected — reserved OIDC scopes in input rejected (consistent with other APIs)

Assertion callback context tests (4):

  • test_context_aware_callback_receives_fmi_path — one-arg callback receives client_id, token_endpoint, fmi_path
  • test_context_aware_callback_omits_fmi_path_when_not_set — fmi_path absent from context when not on request
  • test_legacy_zero_arg_callback_still_works — zero-arg callable invoked normally
  • test_context_callback_type_error_not_swallowed — internal TypeError in one-arg callback propagates (not masked by dispatch)

Copilot AI review requested due to automatic review settings May 15, 2026 18:01
@Avery-Dunn Avery-Dunn requested a review from a team as a code owner May 15, 2026 18:01
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

This PR adds user federated identity credential (user_fic) token acquisition support for confidential clients and extends client assertion callbacks with request context for FMI scenarios.

Changes:

  • Adds acquire_token_by_user_federated_identity_credential() and low-level obtain_token_by_user_fic().
  • Adds context-aware client assertion callback dispatch.
  • Updates token cache extended-key exclusions and adds protocol/cache/validation tests.

Reviewed changes

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

File Description
msal/application.py Adds public confidential-client API and telemetry ID for user FIC acquisition.
msal/oauth2cli/oauth2.py Adds assertion callback context handling and low-level user_fic grant request construction.
msal/token_cache.py Excludes user FIC request fields from extended cache key calculation.
tests/test_application.py Adds unit tests for user FIC protocol behavior, cache behavior, validation, and assertion callback context.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread msal/oauth2cli/oauth2.py
Comment on lines +825 to +828
data.update(
scope=scope,
user_federated_identity_credential=assertion,
client_info="1",
Comment thread msal/oauth2cli/oauth2.py Outdated
Comment on lines +179 to +192
"""Check if a callable accepts at least one positional argument."""
try:
sig = inspect.signature(func)
params = [
p for p in sig.parameters.values()
if p.kind in (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
)
]
return len(params) >= 1
except (ValueError, TypeError):
return False # Signature not inspectable; treat as zero-arg

Comment thread tests/test_application.py Outdated
})


import base64
Comment thread msal/token_cache.py
Comment on lines +70 to +72
"user_federated_identity_credential",
"user_id",
"client_info",
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.

2 participants