Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Feb 9, 2026

Changes proposed in this request

Client credentials flow (AcquireTokenForClient) returns tokens without ID tokens. AuthenticationResult.TenantId was only populated from the ID token's tid claim, resulting in null values even when the token was correctly cached with the tenant ID from the request authority.

Under concurrent requests for different tenants using WithTenantIdFromAuthority, applications received tokens with missing TenantId, causing tenant confusion (~1 in 1M requests in production).

Fix:

// Before
TenantId = msalIdTokenCacheItem?.IdToken?.TenantId;

// After  
TenantId = msalIdTokenCacheItem?.IdToken?.TenantId ?? msalAccessTokenCacheItem?.TenantId;

The access token's TenantId comes from TokenResponseHelper.GetTenantId() which correctly uses the request authority's tenant when ID token is null—the same value used for cache partitioning.

Key changes:

  • AuthenticationResult.cs (line 223): Add null-coalescing fallback to access token's TenantId
  • ParallelRequestsTests.cs: Add AcquireTokenForClient_WithTenantIdFromAuthority_TenantIdInResult_Test simulating 1,000 concurrent requests across 10 tenants

Testing

New test validates:

  • TenantId is populated in all concurrent scenarios
  • Correct tenant attribution under multi-tenant race conditions
  • No cache key mismatches between request and result

Existing test ClientCreds_MustFilterByTenantId_Async already validates cache items have correct TenantId.

Performance impact

None. Single null-coalescing check on an already-allocated cache item reference.

Documentation

  • All relevant documentation is updated.
Original prompt

This section details on the original issue you should resolve

<issue_title>[Bug] Rare race condition: AcquireTokenForClient returns token for wrong tenant when AuthenticationResult.TenantId is missing under concurrency (.WithTenantIdFromAuthority)</issue_title>
<issue_description>### Library version used

Latest

.NET version

Net 8

Scenario

ConfidentialClient - service to service (AcquireTokenForClient)

Is this a new or an existing app?

No response

Issue description and reproduction steps

We are seeing a rare but critical bug in our confidential client/service-to-service scenario using AcquireTokenForClient, where we set the tenant at the request level via .WithTenantIdFromAuthority(aadAuthorityUri).

About 1 in 1,000,000 requests will result in us receiving a token for a DIFFERENT tenant than the one requested. This only occurs when the other tenant is also making a request to our API at the same exact time, suggesting some sort of race condition.

Our logs indicate this happens mainly when the tenantId is missing from AuthenticationResult. Theory: a race in the MSAL cache when tid is missing during simultaneous requests for different tenants, causing the cache to return the wrong value. Very rare, and hard to consistently reproduce even in local testing.

Steps to reproduce:

  1. Two tenants (A and B) make concurrent requests to our API.
  2. For each request, AcquireTokenForClient is called with tenant set per-request using .WithTenantIdFromAuthority.
  3. Occasionally, the returned token is for the "other" tenant, not the tenant requested.

Unexpectedly, AuthenticationResult.TenantId is missing in those cases.

Additional context:

  • App type: Confidential client
  • Flow: Client Credentials (AcquireTokenForClient)
  • Tenant selection: Per-request (.WithTenantIdFromAuthority)
  • High concurrency scenario
  • Token cache is default in-memory (unless MSAL.NET default is different)
  • Unable to reliably reproduce; observed in production.

Questions:

  • When can AuthenticationResult.TenantId be missing?
  • How does MSAL cache behave under concurrent AcquireTokenForClient calls for different tenants?
  • Any diagnostics or flags to enable to confirm cache/lookup behavior?

Relevant code snippets

Relevant code snippet:


var _confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithCertificate(cert)
    .Build();

// Per-request (for each tenant)...
var aadAuthorityUri = $"https://login.microsoftonline.com/{tenantId}";
var result = await _confidentialClientApplication.AcquireTokenForClient(scopes)
                .WithTenantIdFromAuthority(aadAuthorityUri)
                .WithSendX5C(true).ExecuteAsync();

Expected behavior

Tokens should always correspond to the tenant specified in WithTenantIdFromAuthority.

Identity provider

Microsoft Entra ID (Work and School accounts and Personal Microsoft accounts)

Regression

No response

Solution and workarounds

Theory: cache race condition when tenantId is missing.</issue_description>

<agent_instructions>Follow test first approach to test the race condition.

Validate in what scenario tenant id is not returned in the response.

Check what could cause the race condition and possible solution.

Do not make changes not needed for this issue resolution. </agent_instructions>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits February 9, 2026 21:55
… is missing

Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com>
Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com>
Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix race condition in AcquireTokenForClient method Fix race condition: populate AuthenticationResult.TenantId from access token when ID token missing Feb 9, 2026
Copilot AI requested a review from neha-bhargava February 9, 2026 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants