Add claimsFromClient API for client-originated claims#1039
Open
Robbie-Microsoft wants to merge 8 commits into
Open
Add claimsFromClient API for client-originated claims#1039Robbie-Microsoft wants to merge 8 commits into
Robbie-Microsoft wants to merge 8 commits into
Conversation
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>
Contributor
There was a problem hiding this comment.
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 andclientClaims()/computeExtCacheKeyHash()plumbing across the four parameter types. - Merges client-originated claims into the outgoing OAuth token request
claimsparameter for confidential-client flows, and forwards claims to IMDS managed identity with MSIv1 key restrictions. - Extends TokenCache read/write paths to incorporate
extCacheKeyHashso 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. |
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>
- 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>
Avery-Dunn
reviewed
Jun 29, 2026
Avery-Dunn
reviewed
Jun 29, 2026
Avery-Dunn
reviewed
Jun 29, 2026
- 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>
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>
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>
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>
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>
Avery-Dunn
approved these changes
Jul 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports msal-dotnet PR #5999 (
WithClaimsFromClient) to msal-java as a new per-request builder methodclaimsFromClient(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:ManagedIdentityParameters)ClientCredentialParameters)OnBehalfOfParameters)UserFederatedIdentityCredentialParameters)AuthorizationCodeParameters) — confidential-client web-app code redemptionUnlike server-issued
claimschallenges (CAE) — which bypass and refresh the cache — client-originated claims are:claimsparameter, andextCacheKeyHash).Behavior
JsonHelper.validateJsonObjectFormat, which rejects non-objects, malformed JSON, and a valid object followed by trailing tokens (e.g.{}{}or{} garbage). Validation failures throwMsalClientException(INVALID_JSON)with a constant, payload-free message — the raw claims value (which may be sensitive) is never echoed in the exception.claimsbody parameter inTokenRequestExecutor. The wire-merge and cache-write paths readapiParameters().clientClaims()/computeExtCacheKeyHash()generically, so each flow is covered once its parameter class exposes the value.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.extCacheKeyHashacross 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:
AuthorizationCodeParametersis shared betweenPublicClientApplicationandConfidentialClientApplicationin msal-java, soclaimsFromClientis 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
WithClaimsFromClientis 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:
xms_az_nwperimidallow-list was removed — claims are forwarded to IMDS as-is, matching the final .NET and Go behavior.validateClientClaimsSourcehelper.key+valueconcat → SHA256 → Base64Url, pinned byFmiTest.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
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(nestedClientClaimsTests) — IMDS query-param transport, cache isolation, unsupported-source error path, and non-xms_az_nwperimidclaim forwarding (no client-side key allow-list).msal4j-sdkmodule: 0 failures, 0 errors; full reactor BUILD SUCCESS.