MSAL client type
Confidential, Managed identity
Problem statement
Who is affected
Azure Resource Providers (RPs) running as backing services — e.g., Azure Redis Cache VMs using Managed Identity to acquire tokens. This is not an end-user API.
What happens today
An RP needs to include an NSP (Network Security Perimeter) claim in every token request so the resulting token is scoped to a specific network perimeter. The claim is stable — the same value is sent on every request for the lifetime of the workload.
MSAL has two APIs, and neither works for this scenario:
WithClaims(string claims) — Designed for server-initiated claims challenges (MFA, CAE). MSAL intentionally bypasses the token cache when claims are present, because challenges are one-time events. But NSP claims are stable and repeated. Using WithClaims means every call hits IMDS/ESTS — a performance disaster at scale, and a throttling risk.
WithExtraQueryParameters() — Exists on ManagedIdentityApplicationBuilder (hidden, [EditorBrowsable(Never)]). Passes the parameter to IMDS, but MSAL's cache ignores it — the parameter is not part of the cache key. Result: first call gets a token with the NSP claim, second call returns the cached token even if different claims are passed. Wrong token served silently.
The gap
There is no MSAL API that says: "Here is a claim from the client. Include it in the token request AND in the cache key so tokens are correctly partitioned by claim value."
Impact
- High-severity ICMs due to incorrect NSP claim handling
- RPs forced to choose between: cache miss on every call (
WithClaims) or wrong token reuse (WithExtraQueryParameters)
- No safe production path without building a custom token cache layer outside MSAL
Solution
New API: WithClientClaims(string claimsJson)
Note: The name WithClientClaims already exists on ConfidentialClientApplicationBuilder for a different purpose (signing claims into the JWT client assertion). The new API must use a distinct name to avoid collision. Options:
WithClaimsFromClient(string claimsJson)
WithRequestClaims(string claimsJson)
WithAdditionalClaims(string claimsJson)
Decision needed — using WithClientClaims as placeholder below.
Add a method to the token acquisition builders that:
- Forwards the claims JSON to the token endpoint (IMDS or ESTS)
- Includes the claims value in the MSAL cache key
- JSON-merges with any existing claims challenges (from
WithClaims())
// Managed Identity — primary scenario (Azure Redis Cache RP)
var result = await miApp
.AcquireTokenForManagedIdentity(resource)
.WithClientClaims(nspClaimsJson)
.ExecuteAsync();
// Client Credentials — harmonized API
var result = await ccApp
.AcquireTokenForClient(scopes)
.WithClientClaims(nspClaimsJson)
.ExecuteAsync();
Claims format
The claims JSON must follow the OIDC claims request format defined in Section 5.5 of OpenID Connect Core 1.0.
The claims parameter is a JSON object with top-level members (access_token, id_token, userinfo) each containing individual claim requests:
{
"access_token": {
"xms_nsp_id": {
"essential": true,
"value": "<network-perimeter-id>"
}
}
}
Each individual claim request supports (per Section 5.5.1):
| Property |
Type |
Meaning |
null |
— |
Voluntary claim, default manner |
essential |
boolean |
If true, the claim is essential to the request |
value |
string |
Request a specific claim value |
values |
string[] |
Request one of a set of values, in preference order |
Cache behavior
| Behavior |
Detail |
| Tokens are cached |
Yes — unlike WithClaims() which bypasses cache |
| Claims value in cache key |
Yes — different claims produce different cache entries |
| Same claims = cache hit |
Yes — stable NSP claims reuse cached tokens |
| Dynamic data risk |
If claims contain timestamps or nonces, each unique value creates a new cache entry. Callers must ensure claims are stable. MSAL should log a warning if claim values change frequently. |
Claims merging
Because claims is a reserved OIDC parameter, MSAL cannot send two separate claims values. When both WithClaims() (server challenge) and WithClientClaims() (client-originated) are used, MSAL must JSON-merge them into a single OIDC-compliant claims object:
Final merged claims = claims challenges (WithClaims) ∪ client claims (WithClientClaims)
MSAL already performs a similar merge today: ClaimsHelper.GetMergedClaimsAndClientCapabilities() merges claims challenges with client capabilities. The WithClientClaims path extends this merge to include a third source.
Scope
This issue:
| Flow |
Builder |
Priority |
| MSI v1 (IMDS) |
AcquireTokenForManagedIdentity |
Primary — NSP scenario |
| Client Credentials |
AcquireTokenForClient |
Harmonize |
Future (separate issues):
| Flow |
Builder |
Notes |
| MSI v2 (IMDS V2 / mTLS) |
AcquireTokenForManagedIdentity |
IMDS v2 doesn't support NSP claims yet |
| FIC |
AcquireTokenForClient + MI assertion |
Needs investigation |
Open Questions
-
IMDS parameter format — Does IMDS accept the standard OIDC claims parameter format? Or does it expect a distinct parameter name/format? If IMDS uses a distinct parameter (e.g., nsp_claims), JSON merging is not required. Action: Confirm with IMDS team (Nidhi / Qi).
-
API naming — WithClientClaims already exists on ConfidentialClientApplicationBuilder for JWT assertion claims (line 185-202). Need a distinct name for the new API. See options above.
-
Existing client_credentials NSP support — MSAL already supports NSP claims for client_credentials as an extra param in the client_assertion with cache keying. How does this new API relate? Should it replace the existing approach?
References
- OIDC Claims Request Parameter: OpenID Connect Core Section 5.5
- OIDC Individual Claims Requests: Section 5.5.1
- Existing MSAL claims merge:
src/client/Microsoft.Identity.Client/Internal/ClaimsHelper.cs — GetMergedClaimsAndClientCapabilities()
- Existing
WithClientClaims (assertion signing): ConfidentialClientApplicationBuilder.cs lines 185-202
- Design doc: IMDS Support for NSP-Scoped Delegated Identity Tokens (Azure MSI team)
MSAL client type
Confidential, Managed identity
Problem statement
Who is affected
Azure Resource Providers (RPs) running as backing services — e.g., Azure Redis Cache VMs using Managed Identity to acquire tokens. This is not an end-user API.
What happens today
An RP needs to include an NSP (Network Security Perimeter) claim in every token request so the resulting token is scoped to a specific network perimeter. The claim is stable — the same value is sent on every request for the lifetime of the workload.
MSAL has two APIs, and neither works for this scenario:
WithClaims(string claims)— Designed for server-initiated claims challenges (MFA, CAE). MSAL intentionally bypasses the token cache when claims are present, because challenges are one-time events. But NSP claims are stable and repeated. UsingWithClaimsmeans every call hits IMDS/ESTS — a performance disaster at scale, and a throttling risk.WithExtraQueryParameters()— Exists onManagedIdentityApplicationBuilder(hidden,[EditorBrowsable(Never)]). Passes the parameter to IMDS, but MSAL's cache ignores it — the parameter is not part of the cache key. Result: first call gets a token with the NSP claim, second call returns the cached token even if different claims are passed. Wrong token served silently.The gap
There is no MSAL API that says: "Here is a claim from the client. Include it in the token request AND in the cache key so tokens are correctly partitioned by claim value."
Impact
WithClaims) or wrong token reuse (WithExtraQueryParameters)Solution
New API:
WithClientClaims(string claimsJson)Add a method to the token acquisition builders that:
WithClaims())Claims format
The claims JSON must follow the OIDC claims request format defined in Section 5.5 of OpenID Connect Core 1.0.
The
claimsparameter is a JSON object with top-level members (access_token,id_token,userinfo) each containing individual claim requests:{ "access_token": { "xms_nsp_id": { "essential": true, "value": "<network-perimeter-id>" } } }Each individual claim request supports (per Section 5.5.1):
nullessentialtrue, the claim is essential to the requestvaluevaluesCache behavior
WithClaims()which bypasses cacheClaims merging
Because
claimsis a reserved OIDC parameter, MSAL cannot send two separateclaimsvalues. When bothWithClaims()(server challenge) andWithClientClaims()(client-originated) are used, MSAL must JSON-merge them into a single OIDC-compliant claims object:MSAL already performs a similar merge today:
ClaimsHelper.GetMergedClaimsAndClientCapabilities()merges claims challenges with client capabilities. TheWithClientClaimspath extends this merge to include a third source.Scope
This issue:
AcquireTokenForManagedIdentityAcquireTokenForClientFuture (separate issues):
AcquireTokenForManagedIdentityAcquireTokenForClient+ MI assertionOpen Questions
IMDS parameter format — Does IMDS accept the standard OIDC
claimsparameter format? Or does it expect a distinct parameter name/format? If IMDS uses a distinct parameter (e.g.,nsp_claims), JSON merging is not required. Action: Confirm with IMDS team (Nidhi / Qi).API naming —
WithClientClaimsalready exists onConfidentialClientApplicationBuilderfor JWT assertion claims (line 185-202). Need a distinct name for the new API. See options above.Existing client_credentials NSP support — MSAL already supports NSP claims for client_credentials as an extra param in the client_assertion with cache keying. How does this new API relate? Should it replace the existing approach?
References
src/client/Microsoft.Identity.Client/Internal/ClaimsHelper.cs—GetMergedClaimsAndClientCapabilities()WithClientClaims(assertion signing):ConfidentialClientApplicationBuilder.cslines 185-202