Skip to content

Conversation

@4gust
Copy link
Contributor

@4gust 4gust commented Jan 15, 2026

Fix Certificate Reload Infinite Recursion Bug

Description

Certificate reload logic was triggering for all invalid_client errors, not just certificate-related ones. This caused unnecessary retries for unrelated authentication failures like wrong passwords or missing app registrations.
Additionally, the shared _retryClientCertificate boolean flag had thread-safety issues in concurrent scenarios.
Solution
Restored specific error checking - Only reload certificates for actual cert errors:
• AADSTS700027 - Invalid key
• AADSTS700024 - Invalid time range
• AADSTS7000214 - Certificate revoked
• AADSTS1000502 - Certificate expired Replaced shared flag with per-call counter - Each call tracks its own retry count (max 1 retry),
preventing infinite loops and other conditions.
Fixes issues :
#3653 and #3654

Solution

• Removed: private bool _retryClientCertificate (shared state)
• Added: private const int MaxCertificateRetryAttempts = 1
• Added int retryCount parameter to private overloads of: AddAccountToCacheFromAuthorizationCodeAsync(AuthCodeRedemptionParameters) and GetAuthenticationResultForUserAsync(IEnumerable<string>, string?, string?, string?, ClaimsPrincipal?, TokenAcquisitionOptions?)
• GetAuthenticationResultForAppAsync(string, string?, string?, TokenAcquisitionOptions?)
• Updated IsInvalidClientCertificateOrSignedAssertionError(MsalServiceException, int) to check retryCount < MaxCertificateRetryAttempts

Others

  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

@4gust 4gust requested a review from a team as a code owner January 15, 2026 11:08
, StringComparison.OrdinalIgnoreCase
return retryCount < MaxCertificateRetryAttempts &&
#if !NETSTANDARD2_0 && !NET462 && !NET472
(exMsal.Message.Contains(Constants.InvalidKeyError, StringComparison.OrdinalIgnoreCase)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a dictionary of these error codes to avoid the if / else and duplication?

Copy link
Collaborator

@jmprieur jmprieur Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should merge this one first: #3653, @bgavrilMS , @4gust

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough @4gust - these fixes can be separated.

/// This prevents infinite retry loops.
/// </summary>
[Fact]
public void IsInvalidClientCertificateError_RetryCountAtMax_ReturnsFalse()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a lot of these tests can consolidated into 1

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only test test really matters would be the e2e with agentid and wrong cred, same with agent user identity

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to update, @4gust and I looked at the Agentic tests, and when a bad credential is used, the tests indeed have infinite recursion.

public async Task<AcquireTokenResult> AddAccountToCacheFromAuthorizationCodeAsync(
AuthCodeRedemptionParameters authCodeRedemptionParameters)
{
return await AddAccountToCacheFromAuthorizationCodeAsync(authCodeRedemptionParameters, retryCount: 0).ConfigureAwait(false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auth code flow is not used by agent identities ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the retry logic applies to all flows. GetAuthenticationResultForUserAsync is also covered and that calls ROPC / OBO or AuthCode. ROPC is used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants