Skip to content

Add claimsFromClient API for client-originated claims#1039

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

Add claimsFromClient API for client-originated claims#1039
Robbie-Microsoft wants to merge 8 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 PR #5999 (WithClaimsFromClient) to msal-java as a new per-request builder method claimsFromClient(String claimsJson).

In .NET the method is added directly on the Managed Identity builder and as a generic extension on AbstractConfidentialClientAcquireTokenParameterBuilder<T>, covering every confidential-client flow. msal-java has no shared confidential base parameters class, so the method is added per builder:

  • Managed Identity (ManagedIdentityParameters)
  • Client Credentials (ClientCredentialParameters)
  • On-Behalf-Of (OnBehalfOfParameters)
  • User Federated Identity Credential (UserFederatedIdentityCredentialParameters)
  • Authorization Code (AuthorizationCodeParameters) — confidential-client web-app code redemption

Unlike server-issued claims challenges (CAE) — which bypass and refresh the cache — client-originated claims are:

  1. Forwarded on the wire as a standard OAuth claims parameter, and
  2. Cached, keyed per distinct claims value via an extended access-token cache key (extCacheKeyHash).

Behavior

  • Builder: blank/whitespace is a no-op; otherwise the value must be a single well-formed JSON object via JsonHelper.validateJsonObjectFormat, which rejects non-objects, malformed JSON, and a valid object followed by trailing tokens (e.g. {}{} or {} garbage). Validation failures throw MsalClientException(INVALID_JSON) with a constant, payload-free message — the raw claims value (which may be sensitive) is never echoed in the exception.
  • Confidential-client flows (CC / OBO / FIC / Authorization Code): client claims are merged into the claims body parameter in TokenRequestExecutor. The wire-merge and cache-write paths read apiParameters().clientClaims() / computeExtCacheKeyHash() generically, so each flow is covered once its parameter class exposes the value.
  • Managed Identity: restricted to IMDS — claims are sent as a query parameter (GET) or body parameter (POST). The source is validated before the cache read (an unsupported source can never return a cached token or reach the wire); a non-IMDS source throws INVALID_REQUEST. MSAL does not restrict the claim contents — the JSON object is forwarded to IMDS as-is, and IMDS accepts or rejects its keys.
  • Cache: write/read paths are keyed on extCacheKeyHash across all flows, including user-token isolation for the FIC and Authorization Code account paths. Distinct claims yield distinct cache entries; identical claims hit the cache. Refreshed tokens (e.g. User-FIC) are written back to the same partition as the original — a refresh-token request inherits the parent silent request's ext hash, so a refreshed token never leaks into the default partition. (Auth-code redemption always hits the network — its practical value here is wire forwarding plus cache-key isolation.)

Note: AuthorizationCodeParameters is shared between PublicClientApplication and ConfidentialClientApplication in msal-java, so claimsFromClient is also visible on public-client auth-code; this is harmless (the value is simply forwarded on the wire) and is the pragmatic way to cover the confidential use.

Public-client-only flows (username/password, interactive, integrated Windows auth, device code) are intentionally excluded, matching .NET where WithClaimsFromClient is confidential-only.

Callers should send the identical claims value on every request for a given token: because the raw value is part of the cache key, changing or omitting it routes the request to a different cache partition.

Cross-MSAL alignment

This port was reconciled against the sibling PRs (go #629, python #937, js #8686) porting dotnet #5999:

  • The MI MSIv1 xms_az_nwperimid allow-list was removed — claims are forwarded to IMDS as-is, matching the final .NET and Go behavior.
  • The MI source check was hoisted before the cache read (parity with Go), with the transport-layer check retained as defense-in-depth via a shared validateClientClaimsSource helper.
  • Refreshed tokens now stay in their client-claims cache partition (User-FIC refresh path); see the Cache note above.
  • The extended-cache-key hash format is intentionally unchanged: Java matches the merged .NET reference byte-for-byte (plain key+value concat → SHA256 → Base64Url, pinned by FmiTest.fmiPath_HashValueMatchesCrossSDK). Go's length-prefixed/lowercase variant is the cross-SDK outlier; changing Java unilaterally would break .NET parity and the golden test, so it needs a .NET-led cross-SDK decision.

Tests

  • New ClientClaimsTest — JSON validation (including trailing-token rejection and a payload-not-leaked assertion on the error message), builder no-op/invalid across all five param types, CC + OBO wire + cache isolation, and an Authorization Code wire test.
  • UserFederatedIdentityCredentialTest — FIC wire + user-token cache isolation, plus a refresh-token regression test asserting a refreshed token stays in its client-claims partition.
  • ManagedIdentityTests (nested ClientClaimsTests) — IMDS query-param transport, cache isolation, unsupported-source error path, and non-xms_az_nwperimid claim forwarding (no client-side key allow-list).

msal4j-sdk module: 0 failures, 0 errors; full reactor BUILD SUCCESS.

Ports msal-dotnet PR #5999 (WithClaimsFromClient) to msal-java as a new
per-request builder method claimsFromClient(String) on the Managed Identity,
Client Credentials, On-Behalf-Of, and User Federated Identity Credential
parameter builders.

Unlike server-issued `claims` challenges (CAE), which bypass and refresh the
cache, client-originated claims are forwarded on the wire as a standard OAuth
`claims` parameter and are cached, keyed per distinct claims value via an
extended access-token cache key (extCacheKeyHash).

- New claimsFromClient(String) builders: blank is a no-op; non-object JSON
  throws MsalClientException(INVALID_JSON) via JsonHelper.validateJsonObjectFormat.
- Wire injection in TokenRequestExecutor for confidential-client flows; IMDS
  transport (query string for GET, body for POST) in AbstractManagedIdentitySource.
- Managed Identity restricts client claims to IMDS (MSIv1), allowing only the
  xms_az_nwperimid claim; other sources or keys throw INVALID_REQUEST.
- Cache write/read paths keyed on extCacheKeyHash across CC/MI/OBO/FIC,
  including user-token isolation for the FIC flow.
- Tests: new ClientClaimsTest plus FIC and Managed Identity coverage.

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

This PR adds a new per-request claimsFromClient(String claimsJson) API to multiple MSAL4J flows (Client Credentials, OBO, Managed Identity, and User Federated Identity Credential) to support client-originated OAuth claims that are forwarded on the wire and cached with isolation via an extended cache-key hash.

Changes:

  • Adds claimsFromClient(...) builder support and clientClaims()/computeExtCacheKeyHash() plumbing across the four parameter types.
  • Merges client-originated claims into the outgoing OAuth token request claims parameter for confidential-client flows, and forwards claims to IMDS managed identity with MSIv1 key restrictions.
  • Extends TokenCache read/write paths to incorporate extCacheKeyHash so distinct client-claims values produce distinct cache entries, with new/updated tests to validate wire + cache behavior.

Reviewed changes

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

Show a summary per file
File Description
msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/UserFederatedIdentityCredentialTest.java Adds user-FIC wire forwarding + cache-isolation tests for client claims.
msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java Adds managed-identity IMDS forwarding, cache isolation, and invalid-usage tests for client claims.
msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientClaimsTest.java New test suite for JSON validation, builder behavior, wire forwarding, and cache isolation (CC/OBO).
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserFederatedIdentityCredentialParameters.java Adds client-claims storage + extended cache-key hash computation for user-FIC parameters.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java Merges client-originated claims into the outgoing OAuth claims request parameter for confidential-client flows.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java Generalizes ext cache-key hash computation and filters cache reads by extCacheKeyHash.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfParameters.java Adds client-claims + extended cache-key hash computation for OBO parameters.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityParameters.java Adds client-claims + extended cache-key hash computation for managed identity parameters.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java Adds JSON-object validation helper for claimsFromClient inputs.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IAcquireTokenParameters.java Adds default clientClaims() and computeExtCacheKeyHash() hooks to unify behavior across flows.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java Adds client-claims support and folds it into the extended cache-key hash for client credentials.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java Introduces INVALID_REQUEST for unsupported parameter combinations in this feature.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java Passes extCacheKeyHash into cache lookups so isolation works end-to-end.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByUserFederatedIdentityCredentialSupplier.java Propagates extCacheKeyHash into the silent request path for user-FIC.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByOnBehalfOfSupplier.java Propagates extCacheKeyHash into the silent request path for OBO.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByManagedIdentitySupplier.java Propagates extCacheKeyHash into the silent request path for managed identity.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractManagedIdentitySource.java Forwards client claims to IMDS and enforces MSIv1 xms_az_nwperimid top-level key restriction.

Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java
Extends client-originated claims coverage to the last remaining
confidential-client flow (Authorization Code / web-app code redemption),
matching the generic AbstractConfidentialClientAcquireTokenParameterBuilder
extension in msal-dotnet PR #5999.

AuthorizationCodeParameters now carries clientClaims with the same
cache-key isolation pattern as OnBehalfOfParameters (extCacheKeyHash).
Wire forwarding and cache writes are already generic, so the claims are
forwarded as the OAuth claims body parameter and cached per distinct value.

Adds an auth-code wire test plus auth-code cases to the builder no-op and
invalid-JSON tests in ClientClaimsTest.

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 18 out of 18 changed files in this pull request and generated 3 comments.

Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java
Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java Outdated
Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java Outdated
- JsonHelper.validateJsonObjectFormat: reject trailing tokens after the
  root object (e.g. "{}{}", "{} garbage") by requiring END_DOCUMENT, and
  stop including the parser message in the exception so a (potentially
  sensitive) claims payload is never leaked. Uses a single constant message.
- TokenRequestExecutor: correct the now-outdated comment listing the flows
  covered by the client-claims wire merge (adds authorization-code, which is
  shared with PublicClientApplication).
- ClientCredentialParameters: remove an accidentally duplicated Javadoc block
  before computeExtCacheKeyHash().
- ClientClaimsTest: add trailing-token rejection and payload-not-leaked tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientClaimsTest.java Outdated

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 18 out of 18 changed files in this pull request and generated 2 comments.

Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java Outdated
- ClientClaimsTest: add a test proving server claims() and client
  claimsFromClient() merge into a single wire "claims" param, plus a
  precedence test (client value wins on a leaf conflict, nested objects
  deep-merge). Consolidate the three JSON-validation tests and the
  trailing-token test into one, keeping the payload-not-leaked assertion,
  to focus the suite on library behavior rather than parser mechanics.
- AuthorizationCodeParameters.claimsFromClient: document the public-client
  caveat (a public-client auth-code token cached under the extended key is
  not returned by acquireTokenSilently, which refreshes without claims).
- TokenCache.computeExtCacheKeyHashForRequest: include authorization-code
  in the covered-flows Javadoc.

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 18 out of 18 changed files in this pull request and generated 1 comment.

Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java
Apply the applicable review findings from the sibling MSAL PRs (go #629,
python #937, js #8686) that port dotnet #5999:

- Managed Identity: drop the MSIv1 `xms_az_nwperimid` allow-list. MSAL no
  longer inspects/restricts claim keys; the JSON object is forwarded to IMDS
  as-is and IMDS decides what it accepts. Matches the final .NET and Go state.
- Validate the MI source (IMDS-only) *before* the cache read in
  AcquireTokenByManagedIdentitySupplier, not just on the transport path, so an
  unsupported source can never return a cached token or reach the wire. The
  shared validateClientClaimsSource helper keeps a single rule definition
  (kept at the transport layer as defense-in-depth).
- Docs: note that callers must send the identical claims value on every
  request for a given token, since the raw value is part of the cache key
  (changing/omitting it routes to a different cache partition). Applied to all
  confidential-client builders and Managed Identity.

Cache-key hash format is intentionally left unchanged: Java matches the merged
.NET reference byte-for-byte (pinned by FmiTest.fmiPath_HashValueMatchesCrossSDK).
Go's length-prefixed variant is the cross-SDK outlier; changing Java would break
.NET parity and the golden test, so it needs .NET-led coordination.

Co-authored-by: Copilot App <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 18 out of 18 changed files in this pull request and generated 1 comment.

905315a removed the MI MSIv1 claim-key allow-list, so INVALID_REQUEST is no
longer thrown for an unsupported claim key — only for an unsupported managed
identity source. Drop the stale clause from the error-code Javadoc.

Co-authored-by: Copilot App <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 18 out of 18 changed files in this pull request and generated no new comments.

A RefreshTokenRequest inherits the parent silent request's RequestContext,
whose apiParameters is a SilentParameters that carries no client-originated
claims (computeExtCacheKeyHash() returns ""). As a result, when an access
token acquired with an extended cache key was refreshed, the refreshed token
was written back to the default cache partition, breaking isolation.

This is reachable for User-FIC: its supplier builds an account-scoped silent
request and threads the ext-cache-key hash onto it, and User-FIC tokens carry
a refresh token, so an expired token hits the refresh path. (CC/MI/OBO build
account-less silent requests and never reach the refresh path.)

Fix: in TokenCache.computeExtCacheKeyHashForRequest, prefer the parent silent
request's ext hash for a RefreshTokenRequest (exposed via a new
RefreshTokenRequest.extCacheKeyHash() accessor) so the refreshed token stays in
the same partition. Adds a regression test asserting a refreshed User-FIC token
retains its client-claims partition (fails before the fix with two entries).

Co-authored-by: Copilot App <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 19 out of 19 changed files in this pull request and generated 1 comment.

The claimsFromClient Javadoc framed the silent-refresh cache caveat as a
public-client concern that "matters less" on confidential clients. In fact
acquireTokenSilently uses SilentParameters (no claimsFromClient surface) on
both PublicClientApplication and ConfidentialClientApplication, so a silent
refresh won't match the client-claims cache entry and refreshes without the
claims in either case. Reword to state the caveat plainly for both types.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

3 participants