Skip to content

Add forwarded_client_claims support to confidential client flows#937

Open
Robbie-Microsoft wants to merge 11 commits into
devfrom
rginsburg/client_claims
Open

Add forwarded_client_claims support to confidential client flows#937
Robbie-Microsoft wants to merge 11 commits into
devfrom
rginsburg/client_claims

Conversation

@Robbie-Microsoft

@Robbie-Microsoft Robbie-Microsoft commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports msal-dotnet's WithClaimsFromClient as a per-request forwarded_client_claims keyword — a JSON string of client-originated claims forwarded to ESTS. Scoped to confidential client, matching .NET (where it lives on the CCA builder base).

Flows (msal/application.py): acquire_token_for_client, acquire_token_on_behalf_of, acquire_token_by_user_federated_identity_credential, acquire_token_by_authorization_code, acquire_token_silent(_with_error).

An earlier revision also added this to managed identity; that has been removed — this PR is confidential-client only.

Behavior

  • Cached, unlike claims_challenge. claims_challenge is server-issued and bypasses the cache; forwarded_client_claims is cached, with the entry keyed on the claims value (reuses the existing _compute_ext_cache_key / fmi_path mechanism). Send the same value on every request that should share a token; a different or absent value is a cache miss. Isolation is bidirectional.
  • Merged with client capabilities + claims_challenge into the single OAuth claims parameter; the client value wins a direct leaf conflict.
  • Non-string input raises ValueError. Raw value never appears in errors or DEBUG logs. No-op when absent.

Naming

Renamed from client_claimsforwarded_client_claims to avoid colliding with the constructor's client_claims (a dict signed into the client-assertion JWT). The internal request-data key and wire behavior are unchanged.

Files

File Change
msal/application.py Shared _stash_client_claims() helper + wiring into the 5 flows.
msal/token_cache.py Claims parse/merge helpers; _compute_ext_cache_key matches MSAL .NET byte-for-byte.
msal/oauth2cli/oauth2.py Strips the cache-key-only client_claims pseudo-param from the wire body.

Testing

python -m pytest tests/test_token_cache.py tests/test_application.py tests/test_mi.py
# 190 passed

… flows

Port of msal-dotnet PR 5999 (WithClaimsFromClient). Adds a `client_claims`
keyword argument to `ConfidentialClientApplication.acquire_token_for_client`
and `ManagedIdentityClient.acquire_token_for_client` for forwarding
client-originated claims (e.g. the network security perimeter
`xms_az_nwperimid` claim) to ESTS/IMDS.

Unlike `claims_challenge` (server-issued, bypasses the cache), `client_claims`
tokens are cached and the cache entry is keyed on the claims value, reusing the
existing `_compute_ext_cache_key` mechanism (the `fmi_path` precedent).

- token_cache: add `_parse_claims_or_raise`, `_deep_merge_dict`, `_merge_claims`
  helpers; `claims` stays excluded from the ext cache key while `client_claims`
  participates in it.
- oauth2: strip the cache-key-only `client_claims` pseudo-parameter from the
  wire body while preserving it for the cache-add event.
- application: validate `client_claims`, merge it into the OAuth `claims`
  body parameter, and isolate the cache by claims value.
- managed_identity: support `client_claims` on the IMDS (Azure VM) source only
  (sent as the `claims` query parameter); other sources raise; MSIv1 restricts
  the claims JSON to only the `xms_az_nwperimid` key.

Adds unit tests covering cache isolation, wire shape, claim merging, source
restrictions, and MSIv1 validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 26, 2026 18:24
@Robbie-Microsoft Robbie-Microsoft requested a review from a team as a code owner June 26, 2026 18:24

Copilot AI left a comment

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.

Pull request overview

Adds support for forwarding client-originated claims via a new client_claims argument in confidential-client and managed-identity client-credentials flows, while ensuring tokens remain cacheable and isolated by the claims value (distinct from server-issued claims_challenge behavior).

Changes:

  • Introduces shared claims parsing/merging helpers and uses client_claims to participate in the extended cache key (while keeping claims excluded).
  • Ensures client_claims is merged into the OAuth claims parameter but stripped from the actual HTTP request body (cache-key-only pseudo-parameter).
  • Adds managed identity support for client_claims on IMDS/Azure VM only (including MSIv1 validation), with unit tests covering caching and wire-shape behaviors.

Reviewed changes

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

Show a summary per file
File Description
msal/token_cache.py Adds claims helpers and leverages existing ext-cache-key mechanism to isolate cached tokens by client_claims.
msal/oauth2cli/oauth2.py Prevents client_claims from being sent on the wire while preserving it for cache add events.
msal/application.py Adds client_claims to CCA acquire-token-for-client flow, validates/merges claims, and isolates cache entries by claims value.
msal/managed_identity.py Adds client_claims support for IMDS/Azure VM only, isolates cache by claims, and validates MSIv1 claim constraints.
tests/test_token_cache.py Adds unit tests for cache-key isolation and claims helper behavior.
tests/test_mi.py Adds managed-identity unit tests for IMDS forwarding, cache isolation, and unsupported source errors.
tests/test_application.py Adds CCA unit tests for wire shape, merging behavior, and cache isolation with client_claims.

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

Comment thread msal/managed_identity.py Outdated
Comment thread msal/application.py Outdated
… code, silent)

Phase 1 added client_claims to acquire_token_for_client. This extends it to the
remaining confidential client flows so client-originated claims are forwarded
and cache-isolated consistently, mirroring msal-dotnet PR 5999's
WithClaimsFromClient (which applies to all confidential client builders):

- acquire_token_on_behalf_of (OBO)
- acquire_token_by_user_federated_identity_credential (FIC)
- acquire_token_by_authorization_code
- acquire_token_silent / acquire_token_silent_with_error (cache-read isolation
  plus refresh-token request merge)

A shared _stash_client_claims() helper validates the value and stashes it into
the request data, so it (a) contributes to the extended cache key and (b) is
merged into the OAuth "claims" parameter while being stripped from the wire
body. Adds unit tests for each flow (wire shape, validation, cache isolation).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

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.

Pull request overview

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

Comment thread msal/application.py Outdated
Comment thread msal/application.py Outdated
Comment thread msal/managed_identity.py Outdated
Comment thread msal/token_cache.py
- token_cache._parse_claims_or_raise now also catches TypeError (raised when
  the input is not a str/bytes) and surfaces the same friendly ValueError, so
  every caller behaves consistently regardless of the bad input's type.
- ManagedIdentityClient.acquire_token_for_client now rejects non-string
  client_claims with a ValueError (mirroring the confidential-client flows),
  preventing a raw TypeError leak and inconsistent extended-cache-key hashing.
- ConfidentialClientApplication.acquire_token_for_client now reuses the shared
  _stash_client_claims() helper instead of duplicating the validate-and-stash
  logic, removing the risk of the two paths diverging.
- Add cross-referencing docstring notes disambiguating the per-request
  client_claims (a JSON string forwarded in the request) from the pre-existing
  constructor client_claims (a dict of claims signed into the client-assertion
  JWT).
- Add unit tests for non-string client_claims on managed identity and for
  non-string inputs to _parse_claims_or_raise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread msal/application.py Outdated
Comment thread tests/test_application.py
Address reviewer feedback on PR #937:

- Rename the new per-request parameter `client_claims` ->
  `forwarded_client_claims` across all confidential-client flows
  (acquire_token_for_client, on_behalf_of, user FIC, auth code, silent)
  and the Managed Identity acquire_token_for_client. This removes the
  naming collision with the pre-existing `client_claims` *constructor*
  parameter (a dict signed into the client-assertion JWT), which a second
  reviewer also flagged as confusing.

  The public keyword is the only thing renamed. The internal request-data
  key "client_claims" (used by the oauth2 wire-strip, _compute_ext_cache_key
  cache isolation, and _merge_claims) and the private Managed Identity
  plumbing keep their existing names, so cache keying and wire behavior are
  unchanged.

- Add test_forwarded_client_claims_merged_with_claims_challenge, covering
  the previously untested three-way merge of server-issued claims_challenge
  + client capabilities + forwarded_client_claims into the single OAuth
  "claims" wire parameter.

200 passed across test_token_cache.py, test_mi.py, test_application.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 30, 2026 21:47

Copilot AI left a comment

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.

Pull request overview

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

Comment thread msal/application.py
Comment thread msal/application.py
Robbie-Microsoft and others added 2 commits June 30, 2026 20:00
Apply six fixes derived from the sibling MSAL PRs (go #629, java #1039,
js #8686) to the forwarded_client_claims port:

1. Make _compute_ext_cache_key injective. Switch from separator-less
   key+value concatenation to length-prefixed pairs ("{len(k)}:{k}{len(v)}:{v}"),
   matching Go's post-collision-fix CacheExtKeyGenerator. Without this,
   fmi_path + client_claims (which now co-occur in acquire_token_for_client)
   could collide and return the wrong cached token. Adds boundary-collision
   regression tests. NOTE: hashes are now intentionally not byte-identical to
   MSAL .NET (which still uses unprefixed concat); caches are not shared across
   languages, so within-process injectivity is what matters.

2. Remove the MSIv1 client-side allow-list (_validate_msiv1_claims). Forward
   any JSON-object claims value as-is and let IMDS decide which keys it accepts,
   matching go/java.

3. Validate the managed-identity source before the cache read. Reject
   unsupported sources (Service Fabric, App Service, Machine Learning, Azure Arc)
   up front so an unsupported source never returns a cached client-claims token.
   _obtain_token keeps its per-source guards as a backstop.

4. Add merge-conflict precedence tests: on a direct leaf conflict the
   client-originated value wins (merged last); disjoint claims are preserved.

5. Drop the first-party xms_az_nwperimid example from public docstrings; use
   generic "client-originated claims" wording.

6. Document that the same forwarded_client_claims value must be sent on every
   request that should share the cached token (it is part of the cache key).

204 tests pass across test_token_cache.py, test_mi.py, test_application.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restore _compute_ext_cache_key to MSAL .NET's ComputeAccessTokenExtCacheKey
encoding: sorted, separator-less key+value concatenation -> SHA-256 -> base64url.
This makes the ext cache key byte-identical to MSAL .NET again.

The earlier hardening commit (4fc3639) had switched to Go's post-#629
length-prefixed encoding to make the key injective. Per maintainer decision,
msal-python should match MSAL .NET, not current Go, so that change is reverted:

- token_cache.py: restore plain key+value concatenation; docstring now notes the
  .NET match and the deliberate divergence from Go's #629 length-prefixed form.
- test_token_cache.py: restore the .NET parity hashes (bns2ytmx..., 3-rg6_wy...,
  rn_gkpxx...) and rename the parity tests to *_matches_dotnet; remove the two
  length-prefix boundary-collision regression tests (they asserted the injective
  property that .NET's encoding does not provide).

Hardening fixes #2-#6 (MI allow-list removal, MI source pre-validation, merge
conflict-precedence tests, generic docs, send-on-every-request docs) are
unchanged. 202 tests pass across test_token_cache.py, test_mi.py,
test_application.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 1, 2026 16:41

Copilot AI left a comment

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.

Pull request overview

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

Comment thread msal/application.py
Comment thread msal/application.py

Copilot AI left a comment

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.

Pull request overview

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

Comment thread msal/oauth2cli/oauth2.py
Comment thread msal/managed_identity.py
Use kwargs.get("data", {}) instead of the truthiness form in acquire_token_silent and acquire_token_silent_with_error so a caller-provided empty mapping is not replaced with a fresh dict. Matches the fmi_path / for_client sibling sites. Per Copilot review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Robbie-Microsoft Robbie-Microsoft changed the title Add client_claims support to confidential client and managed identity flows Add forwarded_client_claims support to confidential client and managed identity flows Jul 1, 2026
The forwarded client_claims value (and the merged "claims") are kept in the cache-add event data only for ext_cache_key computation, but TokenCache.add() debug-logs event["data"] and previously masked only password/client_secret/refresh_token/assertion/user_fic. Add "client_claims" and "claims" to the masked fields so DEBUG logging cannot emit raw claim contents. Per Copilot review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 1, 2026 17:06

Copilot AI left a comment

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.

Pull request overview

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

Comment thread msal/token_cache.py
…te_ext_cache_key

Clarify that pseudo-parameters like client_claims intentionally feed the extended cache key hash while being stripped from the wire, so different client-originated claims route to separate cache entries. Doc-only. Per Copilot review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…entity

Revert msal/managed_identity.py and tests/test_mi.py to their pre-PR (dev) state so forwarded_client_claims is supported only on the confidential client flows. The shared _parse_claims_or_raise / _compute_ext_cache_key helpers remain in use by the confidential client path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 10, 2026 19:22
@Robbie-Microsoft Robbie-Microsoft changed the title Add forwarded_client_claims support to confidential client and managed identity flows Add forwarded_client_claims support to confidential client flows Jul 10, 2026

Copilot AI left a comment

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.

Pull request overview

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

Comment thread msal/application.py Outdated
Comment thread tests/test_application.py
- Rewrap the _stash_client_claims docstring so no sentence dangles mid-line.
- Split the same-value cache-hit assertions out of the leaf-conflict test into test_same_forwarded_client_claims_hits_cache, so each test covers one behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 10, 2026 19:52

Copilot AI left a comment

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.

Pull request overview

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

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