From b609cf7639b5315937042c5ddbf808a68bac3630 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 6 May 2026 13:06:05 -0400 Subject: [PATCH 1/4] docs: add WithClientClaims API design doc for NSP claims support Captures the proposed approach for supporting client-originated claims (NSP/Network Security Perimeter) across MSIv1, MSIv2, cert-based, and FIC auth flows. Documents the current state of each flow, the gap in the existing WithClaims() API, and the design for a new WithClientClaims() API with proper cache keying. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/nsp_claims_design.md | 97 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/nsp_claims_design.md diff --git a/docs/nsp_claims_design.md b/docs/nsp_claims_design.md new file mode 100644 index 0000000000..0bacd1b9d5 --- /dev/null +++ b/docs/nsp_claims_design.md @@ -0,0 +1,97 @@ +# WithClientClaims API Design + +## Background + +Azure Redis Cache operates in a Backing resource VM/VMSS and uses MSAL with Managed Identity credentials to acquire tokens from ESTS. The Redis team has requested that MSAL support sending NSP (Network Security Perimeter) claims to IMDS, so that the resulting tokens contain the NSP claim required to access NSP-protected resources. + +This document proposes a new `WithClientClaims()` API to support this scenario in a consistent, safe, and harmonized way across all MSAL auth flows. + +## Problem Statement + +The existing `WithClaims()` API in MSAL is designed for **server-issued** claims challenges — situations where ESTS or a downstream web API rejects a token and asks the client to re-authenticate with specific claims (e.g., CAE, MFA step-up). Its behavior is: + +- Claims bypass the token cache on every call +- Intended to be used reactively, in response to a 401/challenge + +This is the wrong model for NSP claims, where: + +- The claims are **known upfront** by the client application +- Tokens **should be cached** per claims value to avoid hammering IMDS +- The claims are **stable** (not dynamic/time-bound) + +Additionally, the current behavior of `WithClaims()` for MSIv1 (IMDS) is broken in a subtle way: it bypasses the cache but **never actually forwards the claims value to IMDS**. The claim is silently dropped. + +## State of Each Auth Flow Today + +### Cert-based (`client_credentials`) and FIC + +These flows are in the best shape. Claims are already sent to ESTS as a body parameter via `TokenClient`. MSAL already has a `ClaimsHelper` class that JSON-merges user-supplied claims with client capabilities before sending. + +`WithExtraClientAssertionClaims` already exists as an API that puts client-originated claims inside the signed JWT assertion and correctly includes them in the cache key. The main gap is that `WithClaims()` still bypasses the cache, so for NSP-style client-originated claims that should be cached, callers have no clean option today. + +### MSIv1 (IMDS) + +This is the biggest gap. `WithClaims()` bypasses the cache, but it **never actually sends the claims value to IMDS** as a query parameter. Claims forwarding in the MSI pipeline is currently only enabled for Service Fabric — IMDS is not wired up. The current behavior for MSIv1 is: cache bypassed, claims silently dropped. + +For the NSP scenario, claims need to be sent to IMDS as a query parameter **and** the resulting token needs to be cached (not bypassed). + +### MSIv2 (IMDS v2) + +MSIv2 uses a different protocol from MSIv1. It acquires an mTLS binding certificate from IMDS, then makes a POST directly to an ESTS token endpoint (`/oauth2/v2.0/token`). Claims are not forwarded in that POST request today. + +> **Open question for IMDS team**: Does the MSIv2 ESTS endpoint accept a `claims` body parameter the same way the standard ESTS token endpoint does? + +## Proposed API: `WithClientClaims(string claimsJson)` + +Add `WithClientClaims(string claimsJson)` across the MSI, client credentials, and FIC request builders. + +### Distinction from `WithClaims()` + +| API | Who originates | Cache behavior | Use case | +|---|---|---|---| +| `WithClaims()` | Server (ESTS / web API challenge) | Bypasses cache | CAE, MFA step-up | +| `WithClientClaims()` | Client application | Cached, keyed on claims value | NSP, Step-Up | + +### Key Behaviors + +1. **Does not bypass the cache.** Tokens are cached and keyed on the claims value. Different claims values produce separate cache entries. + +2. **Transport-agnostic API.** MSAL routes the claims to the correct location per flow: + - MSIv1: query parameter to IMDS + - MSIv2: body parameter in the ESTS POST request + - Cert-based / FIC: body parameter sent to ESTS + +3. **MSAL owns the JSON merge.** If a server-issued claims challenge (e.g., CAE) arrives while `WithClientClaims` is set, MSAL merges the two claims objects using the existing `ClaimsHelper` infrastructure. This infrastructure already performs JSON merging for cert-based flows today. + +4. **Stable claims only.** Callers should avoid dynamic values (timestamps, nonces) in the claims string — each unique claims value creates a distinct cache entry, and frequently changing values will create an unbounded cache. + +### Handling Dynamic Claims + +If dynamic claims truly cannot be avoided, the following options are available (each with tradeoffs): + +| Option | Description | Risk | +|---|---|---| +| `IncludeInCacheKey: false` via `WithExtraQueryParameters` | Claims sent with the request but excluded from the cache key | Cached token may not satisfy the current claims requirement — incorrect for security-sensitive claims | +| `WithClaims()` (existing) | Always bypass the cache | Hits IMDS on every call; will cause throttling for high-throughput workloads like Redis | +| Disable internal cache (`CacheOptions.DisableInternalCacheOptions`) | Caller manages their own cache externally | Maximum flexibility, maximum complexity | +| Caller normalizes claims | Strip dynamic fields before passing to `WithClientClaims`; send dynamic parts separately via `WithExtraQueryParameters` with `IncludeInCacheKey: false` | Requires caller to understand the claims structure | + +For the NSP use case specifically, the claims represent a network security perimeter identifier, which is stable per workload deployment. Dynamic values are not expected to be an issue here. + +## Open Questions + +1. **API shape**: Is `WithClientClaims` the right name and signature for all teams involved? + +2. **MSIv2 protocol** *(for IMDS team)*: Does the MSIv2 ESTS endpoint accept a `claims` body parameter? This cannot be confirmed from the MSAL code alone. + +3. **MSIv1 claims param name**: Should the NSP claim be sent as `claims` (OIDC standard) or under a different query parameter name specific to IMDS? + +4. **Rollout scope**: Implement for all flows in one PR, or start with MSIv1 and extend MSIv2/cert/FIC in follow-ups? + +## Related + +- `WithClaims()` — `AcquireTokenForManagedIdentityParameterBuilder.cs` +- `WithExtraClientAssertionClaims()` — `AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs` +- `ClaimsHelper.GetMergedClaimsAndClientCapabilities()` — `ClaimsHelper.cs` +- `ManagedIdentitySourceExtensions.SupportsClaimsAndCapabilities()` — `ManagedIdentitySourceExtensions.cs` +- `CacheKeyFactory.GetAppTokenCacheItemKey()` — `CacheKeyFactory.cs` From bd2eed175dec316bfeeb4bc9e3ba49ea2cb6d2dc Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 13 May 2026 18:32:39 -0400 Subject: [PATCH 2/4] docs: add call-site snippet, naming note, and request-level rationale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clarify that the new WithClientClaims(string) request-level API coexists without conflict with the existing obsolete app-level WithClientClaims (X509Certificate2, IDictionary) — different classes, different signatures - Add NSP SDK call-site code snippet showing claims sourced at request time, making the request-level design concrete and demonstrating why app-level placement would not support dynamic enforcement-mode changes - Add explanation of why per-request placement is correct: enforcement mode changes produce new claims; request-level avoids needing to recreate the app Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/nsp_claims_design.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/nsp_claims_design.md b/docs/nsp_claims_design.md index 0bacd1b9d5..4388ee87ed 100644 --- a/docs/nsp_claims_design.md +++ b/docs/nsp_claims_design.md @@ -45,6 +45,10 @@ MSIv2 uses a different protocol from MSIv1. It acquires an mTLS binding certific Add `WithClientClaims(string claimsJson)` across the MSI, client credentials, and FIC request builders. +### Naming note: coexistence with the existing obsolete `WithClientClaims` + +`ConfidentialClientApplicationBuilder` already has an **obsolete, app-level** `WithClientClaims(X509Certificate2, IDictionary, ...)` that signs extra claims into the client assertion JWT. The new API described here is a **request-level** method on `AcquireTokenForManagedIdentityParameterBuilder` and `AcquireTokenForClientParameterBuilder` that takes a JSON string. The two APIs are on different classes with different signatures and coexist without ambiguity. The obsolete app-level overload remains for backward compatibility and is unaffected by this change. + ### Distinction from `WithClaims()` | API | Who originates | Cache behavior | Use case | @@ -78,6 +82,26 @@ If dynamic claims truly cannot be avoided, the following options are available ( For the NSP use case specifically, the claims represent a network security perimeter identifier, which is stable per workload deployment. Dynamic values are not expected to be an issue here. +### Why the API is request-level, not app-level + +`WithClientClaims` is intentionally placed on the request builder, not the application builder, to support scenarios where claims change at runtime — for example, when an admin toggles NSP enforcement mode, the NSP SDK vends updated claims and the workload needs MSAL to acquire a new token scoped to those claims. If claims were baked into the application object, the caller would have to destroy and recreate the `ManagedIdentityApplication` on every enforcement change. + +Typical NSP usage: + +```csharp +// nspContext is updated by the NSP SDK when enforcement mode changes. +// Each distinct claims value maps to its own cache entry. +string currentNspClaims = nspContext.GetCurrentClaimsJson(); + +AuthenticationResult result = await miApp + .AcquireTokenForManagedIdentity("https://management.azure.com/") + .WithClientClaims(currentNspClaims) + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); +``` + +The per-request placement means the caller doesn't need to recreate the app when claims update — a new request with new claims produces a new cache entry automatically. + ## Open Questions 1. **API shape**: Is `WithClientClaims` the right name and signature for all teams involved? From 65f59bc8287b71d6f7e707fbded4af8c23b40e32 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 22 May 2026 13:39:56 -0400 Subject: [PATCH 3/4] docs: update NSP claims design doc with resolved questions, ETAs, scope restriction, and MSIv1 claim allowlist - Clarify CCA transport: WithClientClaims sends claims as ESTS request body parameter, NOT embedded in the client assertion JWT. This resolves Bogdan's concern. - Add Scope section: MIRP-gated, Redis Cache only, delegated identities only initially. - Add MSIv1 claim restriction: only xms_az_nwperimid is accepted; MSAL validates upfront to avoid opaque HTTP 400 from IMDS. - Add ETAs table: CCA done; MSIv1 (Raghu) canary by June 30; MSIv2 blocked on IMDS design. - Add E2E testing plan: Redis Cache team's help needed for MSI; existing test tenant for CCA. - Move resolved questions from Open Questions to a Resolved Questions table. - Update remaining open questions to only the two still unresolved items. - Add link to POC implementation PR #5999. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/nsp_claims_design.md | 63 +++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/docs/nsp_claims_design.md b/docs/nsp_claims_design.md index 4388ee87ed..df7762e1fb 100644 --- a/docs/nsp_claims_design.md +++ b/docs/nsp_claims_design.md @@ -6,6 +6,14 @@ Azure Redis Cache operates in a Backing resource VM/VMSS and uses MSAL with Mana This document proposes a new `WithClientClaims()` API to support this scenario in a consistent, safe, and harmonized way across all MSAL auth flows. +## Scope and Initial Rollout + +> **Important:** This feature is initially scoped to a specific scenario. The IMDS/MIRP team will gate it in the service layer. + +- **First consumer**: Azure Redis Cache +- **Initial service-side scope**: MIRP will enable this only for **delegated identities** in Redis Cache +- **General availability**: Depends on IMDS/MIRP team's rollout plan; do not assume this is a general-purpose NSP claims mechanism at this time + ## Problem Statement The existing `WithClaims()` API in MSAL is designed for **server-issued** claims challenges — situations where ESTS or a downstream web API rejects a token and asks the client to re-authenticate with specific claims (e.g., CAE, MFA step-up). Its behavior is: @@ -37,9 +45,7 @@ For the NSP scenario, claims need to be sent to IMDS as a query parameter **and* ### MSIv2 (IMDS v2) -MSIv2 uses a different protocol from MSIv1. It acquires an mTLS binding certificate from IMDS, then makes a POST directly to an ESTS token endpoint (`/oauth2/v2.0/token`). Claims are not forwarded in that POST request today. - -> **Open question for IMDS team**: Does the MSIv2 ESTS endpoint accept a `claims` body parameter the same way the standard ESTS token endpoint does? +MSIv2 uses a different protocol from MSIv1. It acquires an mTLS binding certificate from IMDS, then makes a POST directly to an ESTS token endpoint (`/oauth2/v2.0/token`). The MSIv2 design for `WithClientClaims` is not finalized — the IMDS team is still working on it. See the **ETAs** section. ## Proposed API: `WithClientClaims(string claimsJson)` @@ -61,13 +67,21 @@ Add `WithClientClaims(string claimsJson)` across the MSI, client credentials, an 1. **Does not bypass the cache.** Tokens are cached and keyed on the claims value. Different claims values produce separate cache entries. 2. **Transport-agnostic API.** MSAL routes the claims to the correct location per flow: - - MSIv1: query parameter to IMDS - - MSIv2: body parameter in the ESTS POST request - - Cert-based / FIC: body parameter sent to ESTS + - MSIv1: percent-encoded query parameter (`claims=...`) to IMDS + - MSIv2: body parameter in the ESTS POST request *(design pending IMDS team confirmation)* + - Cert-based / FIC: `claims` body parameter sent to ESTS — **not** embedded in the client assertion JWT + +3. **CCA: claims go in the request body, not the JWT.** For confidential client flows, `WithClientClaims` sends the NSP claim as a standard ESTS `claims` body parameter. It is **not** placed inside the signed client assertion JWT. The existing `WithExtraClientAssertionClaims` API (separate, unrelated) handles the JWT-embedding path. These two APIs are distinct and serve different purposes. + +4. **MSAL owns the JSON merge.** If a server-issued claims challenge (e.g., CAE) arrives while `WithClientClaims` is set, MSAL merges the two claims objects using the existing `ClaimsHelper` infrastructure. This infrastructure already performs JSON merging for cert-based flows today. + +5. **Stable claims only.** Callers should avoid dynamic values (timestamps, nonces) in the claims string — each unique claims value creates a distinct cache entry, and frequently changing values will create an unbounded cache. + +### MSIv1 claim restriction -3. **MSAL owns the JSON merge.** If a server-issued claims challenge (e.g., CAE) arrives while `WithClientClaims` is set, MSAL merges the two claims objects using the existing `ClaimsHelper` infrastructure. This infrastructure already performs JSON merging for cert-based flows today. +MSIv1 (IMDS v1) only accepts a single custom claim: `xms_az_nwperimid`. Any other claim key in the JSON causes IMDS to return HTTP 400 Bad Request with no diagnostic detail. -4. **Stable claims only.** Callers should avoid dynamic values (timestamps, nonces) in the claims string — each unique claims value creates a distinct cache entry, and frequently changing values will create an unbounded cache. +To avoid this silent failure, MSAL validates the claims JSON upfront for MSIv1 requests. If any top-level key other than `xms_az_nwperimid` is present, MSAL throws `MsalClientException(MsalError.InvalidRequest)` before making any network call. ### Handling Dynamic Claims @@ -102,15 +116,36 @@ AuthenticationResult result = await miApp The per-request placement means the caller doesn't need to recreate the app when claims update — a new request with new claims produces a new cache entry automatically. -## Open Questions +## ETAs + +| Flow | Owner | Status | ETA | +|------|-------|--------|-----| +| CCA (cert-based / FIC) | Robbie | ✅ Done — included in POC PR | — | +| MSIv1 (IMDS v1) | Raghu | In progress | Canary by June 30 | +| MSIv2 (IMDS v2) | TBD | Blocked — IMDS team design pending | Q2/Q3 | + +## E2E Testing Plan + +E2E testing requires the Redis Cache team's help because this feature is gated in MIRP for Redis Cache delegated identities only. -1. **API shape**: Is `WithClientClaims` the right name and signature for all teams involved? +- **MSI flows**: Requires a test VM with Managed Identity configured inside an NSP. The Redis Cache team will coordinate access to a suitable test environment. +- **CCA flow**: Can be tested with an existing MSAL test tenant app registration. Verify that the `claims` body parameter is forwarded to ESTS and the returned token contains `xms_az_nwperimid`. +- **Status**: Nidhi is confirming test environment availability with the internal team and the Redis Cache team. -2. **MSIv2 protocol** *(for IMDS team)*: Does the MSIv2 ESTS endpoint accept a `claims` body parameter? This cannot be confirmed from the MSAL code alone. +## Resolved Questions -3. **MSIv1 claims param name**: Should the NSP claim be sent as `claims` (OIDC standard) or under a different query parameter name specific to IMDS? +| # | Question | Resolution | +|---|----------|------------| +| 1 | Is `WithClientClaims` the right name? | Yes — agreed across teams | +| 2 | CCA: request body or client assertion JWT? | **Request body only.** Claims are sent as the ESTS `claims` body parameter. They are not embedded in the signed client assertion JWT. | +| 3 | MSIv1 claims param name | `claims` query parameter (OIDC standard), percent-encoded | +| 4 | Rollout scope | MSIv1 first; MSIv2 and CCA follow once MSIv2 design is ready from IMDS team | -4. **Rollout scope**: Implement for all flows in one PR, or start with MSIv1 and extend MSIv2/cert/FIC in follow-ups? +## Open Questions + +1. **MSIv2 protocol** *(for IMDS team)*: What additional changes are needed in the `/issuecredential` request body to signal that custom claims are in use? The IMDS team is designing this; MSAL implementation will follow once the contract is confirmed. + +2. **E2E test environment**: What test VM and tenant are available? *(Nidhi coordinating with internal team and Redis Cache)* ## Related @@ -119,3 +154,5 @@ The per-request placement means the caller doesn't need to recreate the app when - `ClaimsHelper.GetMergedClaimsAndClientCapabilities()` — `ClaimsHelper.cs` - `ManagedIdentitySourceExtensions.SupportsClaimsAndCapabilities()` — `ManagedIdentitySourceExtensions.cs` - `CacheKeyFactory.GetAppTokenCacheItemKey()` — `CacheKeyFactory.cs` +- POC implementation — PR #5999 + From 97c5ded2bdefb1bebde828a8b900db1b5de0a9cc Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 27 May 2026 14:15:20 -0400 Subject: [PATCH 4/4] docs: rename to WithClaimsFromClient and document no-normalization decision Addresses Bogdan's feedback on PR #5982 + #5999: - Rename the proposed API throughout from `WithClientClaims` to `WithClaimsFromClient` (Bogdan's suggestion). The historical reference to the unrelated obsolete `ConfidentialClientApplicationBuilder.WithClientClaims(X509Certificate2, ...)` overload is left intact in the Naming Note for clarity. - Document the no-normalization design decision in Key Behaviors and add it to the Resolved Questions table. MSAL uses the raw claims string verbatim as part of the cache key. The application is responsible for passing a consistent string. Quote: "We will not penalize the 99% who already do that for the cost of normalizing for the 1% who would not." Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/nsp_claims_design.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/nsp_claims_design.md b/docs/nsp_claims_design.md index df7762e1fb..84bad81183 100644 --- a/docs/nsp_claims_design.md +++ b/docs/nsp_claims_design.md @@ -1,10 +1,10 @@ -# WithClientClaims API Design +# WithClaimsFromClient API Design ## Background Azure Redis Cache operates in a Backing resource VM/VMSS and uses MSAL with Managed Identity credentials to acquire tokens from ESTS. The Redis team has requested that MSAL support sending NSP (Network Security Perimeter) claims to IMDS, so that the resulting tokens contain the NSP claim required to access NSP-protected resources. -This document proposes a new `WithClientClaims()` API to support this scenario in a consistent, safe, and harmonized way across all MSAL auth flows. +This document proposes a new `WithClaimsFromClient()` API to support this scenario in a consistent, safe, and harmonized way across all MSAL auth flows. ## Scope and Initial Rollout @@ -45,22 +45,22 @@ For the NSP scenario, claims need to be sent to IMDS as a query parameter **and* ### MSIv2 (IMDS v2) -MSIv2 uses a different protocol from MSIv1. It acquires an mTLS binding certificate from IMDS, then makes a POST directly to an ESTS token endpoint (`/oauth2/v2.0/token`). The MSIv2 design for `WithClientClaims` is not finalized — the IMDS team is still working on it. See the **ETAs** section. +MSIv2 uses a different protocol from MSIv1. It acquires an mTLS binding certificate from IMDS, then makes a POST directly to an ESTS token endpoint (`/oauth2/v2.0/token`). The MSIv2 design for `WithClaimsFromClient` is not finalized — the IMDS team is still working on it. See the **ETAs** section. -## Proposed API: `WithClientClaims(string claimsJson)` +## Proposed API: `WithClaimsFromClient(string claimsJson)` -Add `WithClientClaims(string claimsJson)` across the MSI, client credentials, and FIC request builders. +Add `WithClaimsFromClient(string claimsJson)` across the MSI, client credentials, and FIC request builders. ### Naming note: coexistence with the existing obsolete `WithClientClaims` -`ConfidentialClientApplicationBuilder` already has an **obsolete, app-level** `WithClientClaims(X509Certificate2, IDictionary, ...)` that signs extra claims into the client assertion JWT. The new API described here is a **request-level** method on `AcquireTokenForManagedIdentityParameterBuilder` and `AcquireTokenForClientParameterBuilder` that takes a JSON string. The two APIs are on different classes with different signatures and coexist without ambiguity. The obsolete app-level overload remains for backward compatibility and is unaffected by this change. +`ConfidentialClientApplicationBuilder` already has an **obsolete, app-level** `WithClientClaims(X509Certificate2, IDictionary, ...)` that signs extra claims into the client assertion JWT. To avoid any confusion with that existing certificate-based overload, the new API is named **`WithClaimsFromClient`** (Bogdan's suggestion, agreed across teams). It is a **request-level** method on `AcquireTokenForManagedIdentityParameterBuilder` and `AcquireTokenForClientParameterBuilder` that takes a JSON string. The two APIs are on different classes with different names and signatures and coexist without ambiguity. The obsolete app-level overload remains for backward compatibility and is unaffected by this change. ### Distinction from `WithClaims()` | API | Who originates | Cache behavior | Use case | |---|---|---|---| | `WithClaims()` | Server (ESTS / web API challenge) | Bypasses cache | CAE, MFA step-up | -| `WithClientClaims()` | Client application | Cached, keyed on claims value | NSP, Step-Up | +| `WithClaimsFromClient()` | Client application | Cached, keyed on claims value | NSP, Step-Up | ### Key Behaviors @@ -71,11 +71,11 @@ Add `WithClientClaims(string claimsJson)` across the MSI, client credentials, an - MSIv2: body parameter in the ESTS POST request *(design pending IMDS team confirmation)* - Cert-based / FIC: `claims` body parameter sent to ESTS — **not** embedded in the client assertion JWT -3. **CCA: claims go in the request body, not the JWT.** For confidential client flows, `WithClientClaims` sends the NSP claim as a standard ESTS `claims` body parameter. It is **not** placed inside the signed client assertion JWT. The existing `WithExtraClientAssertionClaims` API (separate, unrelated) handles the JWT-embedding path. These two APIs are distinct and serve different purposes. +3. **CCA: claims go in the request body, not the JWT.** For confidential client flows, `WithClaimsFromClient` sends the NSP claim as a standard ESTS `claims` body parameter. It is **not** placed inside the signed client assertion JWT. The existing `WithExtraClientAssertionClaims` API (separate, unrelated) handles the JWT-embedding path. These two APIs are distinct and serve different purposes. -4. **MSAL owns the JSON merge.** If a server-issued claims challenge (e.g., CAE) arrives while `WithClientClaims` is set, MSAL merges the two claims objects using the existing `ClaimsHelper` infrastructure. This infrastructure already performs JSON merging for cert-based flows today. +4. **MSAL owns the JSON merge.** If a server-issued claims challenge (e.g., CAE) arrives while `WithClaimsFromClient` is set, MSAL merges the two claims objects using the existing `ClaimsHelper` infrastructure. This infrastructure already performs JSON merging for cert-based flows today. -5. **Stable claims only.** Callers should avoid dynamic values (timestamps, nonces) in the claims string — each unique claims value creates a distinct cache entry, and frequently changing values will create an unbounded cache. +5. **Stable claims only — caller passes the exact same string each call.** MSAL does **not** parse, sort, or normalize the claims JSON. The raw string the caller provides is used verbatim as part of the cache key. If a caller passes `{"a":1}` on one call and `{ "a" : 1 }` on the next, those will be treated as two different cache entries. This keeps the hot path allocation-free and avoids penalizing the 99% of callers who pass a single canonical string for the cost of normalizing for the 1% who would not. Callers should also avoid dynamic values (timestamps, nonces) in the claims string — each unique value creates a distinct cache entry, and frequently changing values will create an unbounded cache. ### MSIv1 claim restriction @@ -92,13 +92,13 @@ If dynamic claims truly cannot be avoided, the following options are available ( | `IncludeInCacheKey: false` via `WithExtraQueryParameters` | Claims sent with the request but excluded from the cache key | Cached token may not satisfy the current claims requirement — incorrect for security-sensitive claims | | `WithClaims()` (existing) | Always bypass the cache | Hits IMDS on every call; will cause throttling for high-throughput workloads like Redis | | Disable internal cache (`CacheOptions.DisableInternalCacheOptions`) | Caller manages their own cache externally | Maximum flexibility, maximum complexity | -| Caller normalizes claims | Strip dynamic fields before passing to `WithClientClaims`; send dynamic parts separately via `WithExtraQueryParameters` with `IncludeInCacheKey: false` | Requires caller to understand the claims structure | +| Caller normalizes claims | Strip dynamic fields before passing to `WithClaimsFromClient`; send dynamic parts separately via `WithExtraQueryParameters` with `IncludeInCacheKey: false` | Requires caller to understand the claims structure | For the NSP use case specifically, the claims represent a network security perimeter identifier, which is stable per workload deployment. Dynamic values are not expected to be an issue here. ### Why the API is request-level, not app-level -`WithClientClaims` is intentionally placed on the request builder, not the application builder, to support scenarios where claims change at runtime — for example, when an admin toggles NSP enforcement mode, the NSP SDK vends updated claims and the workload needs MSAL to acquire a new token scoped to those claims. If claims were baked into the application object, the caller would have to destroy and recreate the `ManagedIdentityApplication` on every enforcement change. +`WithClaimsFromClient` is intentionally placed on the request builder, not the application builder, to support scenarios where claims change at runtime — for example, when an admin toggles NSP enforcement mode, the NSP SDK vends updated claims and the workload needs MSAL to acquire a new token scoped to those claims. If claims were baked into the application object, the caller would have to destroy and recreate the `ManagedIdentityApplication` on every enforcement change. Typical NSP usage: @@ -109,7 +109,7 @@ string currentNspClaims = nspContext.GetCurrentClaimsJson(); AuthenticationResult result = await miApp .AcquireTokenForManagedIdentity("https://management.azure.com/") - .WithClientClaims(currentNspClaims) + .WithClaimsFromClient(currentNspClaims) .ExecuteAsync(cancellationToken) .ConfigureAwait(false); ``` @@ -136,10 +136,11 @@ E2E testing requires the Redis Cache team's help because this feature is gated i | # | Question | Resolution | |---|----------|------------| -| 1 | Is `WithClientClaims` the right name? | Yes — agreed across teams | +| 1 | Is `WithClaimsFromClient` the right name? | Yes — agreed across teams (renamed from earlier `WithClientClaims` proposal to avoid clash with the obsolete certificate-based overload) | | 2 | CCA: request body or client assertion JWT? | **Request body only.** Claims are sent as the ESTS `claims` body parameter. They are not embedded in the signed client assertion JWT. | | 3 | MSIv1 claims param name | `claims` query parameter (OIDC standard), percent-encoded | | 4 | Rollout scope | MSIv1 first; MSIv2 and CCA follow once MSIv2 design is ready from IMDS team | +| 5 | Does MSAL normalize/canonicalize the claims JSON? | **No.** MSAL stores the raw string verbatim and uses it as part of the cache key. It is the application's responsibility to pass a consistent string on each call. We will not penalize the 99% who already do that for the cost of normalizing for the 1% who would not. | ## Open Questions