You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: docs/nsp_claims_design.md
+15-14Lines changed: 15 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,10 +1,10 @@
1
-
# WithClientClaims API Design
1
+
# WithClaimsFromClient API Design
2
2
3
3
## Background
4
4
5
5
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.
6
6
7
-
This document proposes a new `WithClientClaims()` API to support this scenario in a consistent, safe, and harmonized way across all MSAL auth flows.
7
+
This document proposes a new `WithClaimsFromClient()` API to support this scenario in a consistent, safe, and harmonized way across all MSAL auth flows.
8
8
9
9
## Scope and Initial Rollout
10
10
@@ -45,22 +45,22 @@ For the NSP scenario, claims need to be sent to IMDS as a query parameter **and*
45
45
46
46
### MSIv2 (IMDS v2)
47
47
48
-
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.
48
+
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.
Add `WithClientClaims(string claimsJson)` across the MSI, client credentials, and FIC request builders.
52
+
Add `WithClaimsFromClient(string claimsJson)` across the MSI, client credentials, and FIC request builders.
53
53
54
54
### Naming note: coexistence with the existing obsolete `WithClientClaims`
55
55
56
-
`ConfidentialClientApplicationBuilder` already has an **obsolete, app-level**`WithClientClaims(X509Certificate2, IDictionary<string,string>, ...)` 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.
56
+
`ConfidentialClientApplicationBuilder` already has an **obsolete, app-level**`WithClientClaims(X509Certificate2, IDictionary<string,string>, ...)` 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.
57
57
58
58
### Distinction from `WithClaims()`
59
59
60
60
| API | Who originates | Cache behavior | Use case |
61
61
|---|---|---|---|
62
62
|`WithClaims()`| Server (ESTS / web API challenge) | Bypasses cache | CAE, MFA step-up |
63
-
|`WithClientClaims()`| Client application | Cached, keyed on claims value | NSP, Step-Up |
63
+
|`WithClaimsFromClient()`| Client application | Cached, keyed on claims value | NSP, Step-Up |
64
64
65
65
### Key Behaviors
66
66
@@ -71,11 +71,11 @@ Add `WithClientClaims(string claimsJson)` across the MSI, client credentials, an
71
71
- MSIv2: body parameter in the ESTS POST request *(design pending IMDS team confirmation)*
72
72
- Cert-based / FIC: `claims` body parameter sent to ESTS — **not** embedded in the client assertion JWT
73
73
74
-
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.
74
+
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.
75
75
76
-
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.
76
+
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.
77
77
78
-
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.
78
+
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.
79
79
80
80
### MSIv1 claim restriction
81
81
@@ -92,13 +92,13 @@ If dynamic claims truly cannot be avoided, the following options are available (
92
92
|`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 |
93
93
|`WithClaims()` (existing) | Always bypass the cache | Hits IMDS on every call; will cause throttling for high-throughput workloads like Redis |
94
94
| Disable internal cache (`CacheOptions.DisableInternalCacheOptions`) | Caller manages their own cache externally | Maximum flexibility, maximum complexity |
95
-
| 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 |
95
+
| 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 |
96
96
97
97
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.
98
98
99
99
### Why the API is request-level, not app-level
100
100
101
-
`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.
101
+
`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.
@@ -136,10 +136,11 @@ E2E testing requires the Redis Cache team's help because this feature is gated i
136
136
137
137
| # | Question | Resolution |
138
138
|---|----------|------------|
139
-
| 1 | Is `WithClientClaims` the right name? | Yes — agreed across teams |
139
+
| 1 | Is `WithClaimsFromClient` the right name? | Yes — agreed across teams (renamed from earlier `WithClientClaims` proposal to avoid clash with the obsolete certificate-based overload)|
140
140
| 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. |
| 4 | Rollout scope | MSIv1 first; MSIv2 and CCA follow once MSIv2 design is ready from IMDS team |
143
+
| 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. |
0 commit comments