Skip to content

Conversation

gladjohn
Copy link
Contributor

@gladjohn gladjohn commented Oct 1, 2025

Fixes - adds cert cache logic to MSI V2 flow

Changes proposed in this request
This pull request introduces support for mTLS (mutual TLS) certificate binding in Managed Identity token acquisition, along with enhancements to request parameter handling and logging. The changes add new properties for managing mTLS certificates and certificate responses, update test reset logic, and refactor how authentication operations are selected for requests.

Managed Identity mTLS Support and Parameter Enhancements:

  • Added MtlsCertificate and CertificateRequestResponse properties to AcquireTokenForManagedIdentityParameters to support mTLS certificate binding and handling of IMDSv2 certificate responses.
  • Improved logging in AcquireTokenForManagedIdentityParameters to include mTLS-related information, such as whether mTLS PoP is requested and the thumbprint of the bound certificate.

Authentication Operation Handling:

  • Introduced AuthenticationOperationOverride in AuthenticationRequestParameters to allow per-request overrides of the authentication operation, and updated the AuthenticationScheme property to prefer the override if present. [1] [2]

Test and Codebase Maintenance:

  • Updated ApplicationBase.ResetStateForTest() to call ResetSourceAndBindingForTest() for proper cleanup of Managed Identity client state, supporting the new mTLS logic.

Refactoring and Dependency Updates:

  • Cleaned up unused dependencies and updated using statements in ManagedIdentityAuthRequest to align with the new mTLS and Managed Identity v2 support. [1] [2]

Testing
unit

Performance impact
none

Documentation
n/a now

@gladjohn gladjohn requested a review from a team as a code owner October 1, 2025 04:13
RevokedTokenHash: {!string.IsNullOrEmpty(RevokedTokenHash)}
""");

logger.Info(() =>
Copy link
Member

Choose a reason for hiding this comment

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

nit: add it to the prev logger.Info, it's important info.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed, next commit will include it.

{
try
{ store.Remove(existing); }
catch { }
Copy link
Member

@bgavrilMS bgavrilMS Oct 3, 2025

Choose a reason for hiding this comment

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

I think you should at least log if there are errors here?


internal static void ResetSourceForTest()
// Per-identity, process-wide. Identity key = MSAL Config.ClientId (SAMI/UAMI).
internal static readonly ConcurrentDictionary<string, ImdsV2BindingMetadata> s_imdsV2Binding =
Copy link
Member

Choose a reason for hiding this comment

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

Please stop using internal fields. Use internal properties.

Copy link
Member

Choose a reason for hiding this comment

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

Naming: IdToBindingMetadataMap ?

}

// Test only method to remove all test binding certs from user store.
internal static void RemoveAllTestBindingCertsFromUserStoreForTest()
Copy link
Member

Choose a reason for hiding this comment

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

Why is this part of the product? Does eSTS send these certificates?

{ store.Remove(c); }
catch { }
}
store.Close();
Copy link
Member

@bgavrilMS bgavrilMS Oct 3, 2025

Choose a reason for hiding this comment

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

use a finally clause to dispose of unamaged resources

@bgavrilMS bgavrilMS requested a review from Copilot October 3, 2025 12:51
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This pull request introduces comprehensive support for mTLS (mutual TLS) certificate binding in Managed Identity authentication flows for IMDS v2. The main focus is adding certificate caching capabilities and authentication operation overrides to support both bearer and proof-of-possession (PoP) token scenarios.

Key changes include:

  • Implementation of certificate caching mechanism with user store persistence for mTLS bindings
  • Addition of authentication operation override capability at the request level
  • Enhanced test coverage for certificate lifecycle management and identity isolation
  • Streamlined request handling logic with improved error handling and proactive refresh capabilities

Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/client/Microsoft.Identity.Client/ManagedIdentity/V2/MtlsCertStore.cs New certificate store utility for installing and retrieving mTLS certificates from user store
src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs Enhanced IMDS v2 source with certificate caching and binding metadata management
src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs Added certificate binding cache and test cleanup utilities
src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs Refactored request logic with improved cache handling and PoP support
src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs Added authentication operation override capability
src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs Added certificate and CSR response properties for mTLS support
tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs Comprehensive test updates with new certificate caching and lifecycle tests

Comment on lines 23 to 25
try
{ store.Remove(existing); }
catch { }
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Empty catch blocks swallow all exceptions and make debugging difficult. Consider logging the exception or catching specific exception types that are expected and safe to ignore.

Copilot uses AI. Check for mistakes.

Comment on lines 182 to 184
try
{ store.Remove(c); }
catch { }
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Empty catch blocks swallow all exceptions and make debugging difficult. Consider logging the exception or catching specific exception types that are expected and safe to ignore.

Copilot uses AI. Check for mistakes.

Comment on lines 184 to 188
catch { }
}
store.Close();
}
catch { }
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Empty catch blocks swallow all exceptions and make debugging difficult. Consider logging the exception or catching specific exception types that are expected and safe to ignore.

Suggested change
catch { }
}
store.Close();
}
catch { }
catch (Exception ex)
{
Console.WriteLine($"Exception removing certificate: {ex}");
}
store.Close();
}
catch (Exception ex)
{
Console.WriteLine($"Exception in RemoveAllTestBindingCertsFromUserStoreForTest: {ex}");
}

Copilot uses AI. Check for mistakes.


var mi = miBuilder.Build();

// IMPORTANT: Use in‑memory keys for bearer path (no attestation)
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Replace Unicode hyphen with ASCII hyphen in comment.

Suggested change
// IMPORTANT: Use inmemory keys for bearer path (no attestation)
// IMPORTANT: Use in-memory keys for bearer path (no attestation)

Copilot uses AI. Check for mistakes.


var sami = samiBuilder.Build();

// In‑memory keys for bearer path
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Replace Unicode hyphen with ASCII hyphen in comment.

Suggested change
// Inmemory keys for bearer path
// In-memory keys for bearer path

Copilot uses AI. Check for mistakes.


var uami = uamiBuilder.Build();

// In‑memory keys for bearer path
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Replace Unicode hyphen with ASCII hyphen in comment.

Suggested change
// Inmemory keys for bearer path
// In-memory keys for bearer path

Copilot uses AI. Check for mistakes.


var mi = miBuilder.Build();

// In‑memory keys for bearer path
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Replace Unicode hyphen with ASCII hyphen in comment.

Suggested change
// Inmemory keys for bearer path
// In-memory keys for bearer path

Copilot uses AI. Check for mistakes.


var mi = miBuilder.Build();

// In‑memory keys for bearer path
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Replace Unicode hyphen with ASCII hyphen in comment.

Suggested change
// Inmemory keys for bearer path
// In-memory keys for bearer path

Copilot uses AI. Check for mistakes.

requestFromCache.MtlsCertificate = certFromStore; // attach for TLS (and PoP)
requestFromCache.CertificateRequestResponse = cachedResponse;

_requestContext.Logger.Info("[IMDSv2] Using user‑store mTLS binding and cached IMDSv2 metadata for identity.");
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Replace Unicode hyphen with ASCII hyphen in log message.

Suggested change
_requestContext.Logger.Info("[IMDSv2] Using userstore mTLS binding and cached IMDSv2 metadata for identity.");
_requestContext.Logger.Info("[IMDSv2] Using user-store mTLS binding and cached IMDSv2 metadata for identity.");

Copilot uses AI. Check for mistakes.

if (cachedAccessTokenItem != null)
{
string cachedTokenHash = _cryptoManager.CreateSha256HashHex(cachedAccessTokenItem.Secret);
// Compute revoked‑token hash from the cached token
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Replace Unicode hyphen with ASCII hyphen in comment.

Suggested change
// Compute revokedtoken hash from the cached token
// Compute revoked-token hash from the cached token

Copilot uses AI. Check for mistakes.

ILoggerAdapter logger = AuthenticationRequestParameters.RequestContext.Logger;

// 1. FIRST, handle ForceRefresh
bool popRequested = _managedIdentityParameters.IsMtlsPopRequested || AuthenticationRequestParameters.IsMtlsPopRequested;
Copy link
Member

Choose a reason for hiding this comment

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

Why do we have 2 flags for this?

bool popRequested = _managedIdentityParameters.IsMtlsPopRequested || AuthenticationRequestParameters.IsMtlsPopRequested;

// If mtls_pop was requested and a binding exists for this identity, load it from the user store before cache lookup
ApplyMtlsOverrideFromUserStoreIfAvailable(popRequested, logger);
Copy link
Member

@bgavrilMS bgavrilMS Oct 3, 2025

Choose a reason for hiding this comment

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

  1. Design - do not pass the popRequested flag to this method.
  2. Naming - name is confusing. How about "SetPopAuthenticationSchemeWhenCached()` or smth ?

//log a warning if Claims are also set
if (!string.IsNullOrEmpty(AuthenticationRequestParameters.Claims))
{
logger.Warning("[ManagedIdentityRequest] Both ForceRefresh and Claims are set. Using ForceRefresh to skip cache.");
Copy link
Member

Choose a reason for hiding this comment

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

nit: should not be a warning.

// Copy claims (if any) so MI sources can add them
if (string.IsNullOrEmpty(_managedIdentityParameters.Claims) && !string.IsNullOrEmpty(AuthenticationRequestParameters.Claims))
{
_managedIdentityParameters.Claims = AuthenticationRequestParameters.Claims;
Copy link
Member

Choose a reason for hiding this comment

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

Can this copying of parameters happen somewhere else, in a centralized way? When we add a new param, do we need to remember to change smth?

Copy link
Member

Choose a reason for hiding this comment

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

Also, why do you set it here? This block of code deals with force refresh only?

MsalAccessTokenCacheItem cachedAccessTokenItem = await GetCachedAccessTokenAsync()
.ConfigureAwait(false);
// 2) Single cache lookup (do NOT count a hit here)
MsalAccessTokenCacheItem cachedAccessTokenItem = await GetCachedAccessTokenAsync().ConfigureAwait(false);
Copy link
Member

@bgavrilMS bgavrilMS Oct 3, 2025

Choose a reason for hiding this comment

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

But you haven't set the pop key here? What are you looking for in the cache? This will do a Bearer cache lookup even in the POP case.

_managedIdentityKeyProvider = serviceBundle.PlatformProxy.ManagedIdentityKeyProvider;
}

protected override async Task<AuthenticationResult> ExecuteAsync(CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

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

Consider splitting the orchestration logic between Bearer and POP. They are sufficiently different to warrant their own code paths. Use private methods to minimize code duplication.

Copy link
Member

@bgavrilMS bgavrilMS Oct 3, 2025

Choose a reason for hiding this comment

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

The cyclomatic complexity of this method is quite high - 10 before the changes, 13 after. There are so many "if pop"

MsalAccessTokenCacheItem cachedAccessTokenItem = await GetCachedAccessTokenAsync().ConfigureAwait(false);

// 3) Claims present => force network and compute revoked-token hash (original behavior)
bool hasClaims =
Copy link
Member

Choose a reason for hiding this comment

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

Pls don't use 2 flags for the same property.

_managedIdentityParameters.Claims = AuthenticationRequestParameters.Claims;
if (string.IsNullOrEmpty(_managedIdentityParameters.Claims))
{
_managedIdentityParameters.Claims = AuthenticationRequestParameters.Claims;
Copy link
Member

Choose a reason for hiding this comment

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

Duplicate code, this already exists at line 59

{
if (!popRequested)
return;
if (AuthenticationRequestParameters.AuthenticationOperationOverride != null)
Copy link
Member

Choose a reason for hiding this comment

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

Ideally this method should not have to deal with this/

// 3. If we have no ForceRefresh and no claims, we can use the cache
if (cachedAccessTokenItem != null)
// 4) If PoP requested but no binding cert has been applied yet, bypass cache and mint one
if (popRequested && AuthenticationRequestParameters.AuthenticationOperationOverride == null)
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 remove AuthenticationOperationOverride and use just AuthenticationOperation with a setter?

// 3. If we have no ForceRefresh and no claims, we can use the cache
if (cachedAccessTokenItem != null)
// 4) If PoP requested but no binding cert has been applied yet, bypass cache and mint one
if (popRequested && AuthenticationRequestParameters.AuthenticationOperationOverride == null)
Copy link
Member

Choose a reason for hiding this comment

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

Checking this property means is a convoluted way of saying "do I have a cached cert?". Can you check the cert instead?

if (cacheReason == CacheRefreshReason.NoCachedAccessToken)
{
authResult = await SendTokenRequestForManagedIdentityAsync(logger, cancellationToken).ConfigureAwait(false);
var recheck = await GetCachedAccessTokenAsync().ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need to recheck?

if (cacheReason == CacheRefreshReason.NoCachedAccessToken)
{
authResult = await SendTokenRequestForManagedIdentityAsync(logger, cancellationToken).ConfigureAwait(false);
var recheck = await GetCachedAccessTokenAsync().ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

This will fetch a bearer token even in the POP case. I think you are missing a test like:

  • get a bearer token
  • get a pop token (and expect it to get it from STS)

var recheck = await GetCachedAccessTokenAsync().ConfigureAwait(false);
if (recheck != null)
{
return CreateAuthenticationResultFromCache(recheck);
Copy link
Member

Choose a reason for hiding this comment

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

if there's a bearer token cached, you just returned a result

if (fallback != null)
{
logger.Info("[ManagedIdentityRequest] Proactive refresh canceled before send; returning cached access token.");
return CreateAuthenticationResultFromCache(fallback);
Copy link
Member

Choose a reason for hiding this comment

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

this gets a bearer token in pop case

// Propagate PoP and claims from common params to MI params
_managedIdentityParameters.IsMtlsPopRequested = popRequested;

if (string.IsNullOrEmpty(_managedIdentityParameters.Claims) &&
Copy link
Member

Choose a reason for hiding this comment

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

This is the 3rd time we deal with claims.

}

// Ensure the attestation provider reaches RequestContext for IMDSv2
AuthenticationRequestParameters.RequestContext.AttestationTokenProvider ??=
Copy link
Member

Choose a reason for hiding this comment

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

Should't this be at the Config / App level?

namespace Microsoft.Identity.Client.ManagedIdentity.V2
{
/// <summary>
/// Provides persistence mechanisms for certificate binding metadata in the Windows certificate store.
Copy link
Member

Choose a reason for hiding this comment

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

Everything that is Windows specific should be named "Windows" and should be used behind an interface, so as to be able to provide a Linux implementaiton.

/// <remarks>
/// This class uses the X509Certificate2.FriendlyName property to store encoded metadata that links
/// certificates to specific managed identities and token types. The metadata includes:
/// - Identity key (hashed for privacy)
Copy link
Member

Choose a reason for hiding this comment

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

privacy of what?

/// <param name="tokenType">The token type (Bearer or PoP)</param>
/// <param name="resp">The certificate response containing endpoint and identity information</param>
/// <returns>A formatted string for use as certificate FriendlyName</returns>
public static string BuildFriendlyName(string identityKey, string tokenType, CertificateRequestResponse resp)
Copy link
Member

Choose a reason for hiding this comment

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

private

/// The persistence mechanism enables MSAL to find previously created certificates for an identity
/// across application restarts, reducing the need to repeatedly mint new certificates.
/// </remarks>
internal static class BindingMetadataPersistence
Copy link
Member

Choose a reason for hiding this comment

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

Think about SOLID and how much power there is in using interfaces and abstractions. Avoid using static everywhere.

// Prefix that identifies certificates managed by MSAL for Managed Identities
private const string Prefix = "MSAL_MI_MTLS|v1|";
private const char Sep = '|';

Copy link
Member

Choose a reason for hiding this comment

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

This class should take in a logger and do some logging.


return string.Concat(Prefix, tokenType, Sep, hid, Sep, client, Sep, tenant, Sep, ep);
}
catch { return null; }
Copy link
Member

Choose a reason for hiding this comment

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

Please stop catching all errors. We will never know if smth is wrong. Let everything bubble up.

/// <param name="subject">Output parameter for the certificate subject</param>
/// <param name="thumbprint">Output parameter for the certificate thumbprint</param>
/// <returns>True if binding metadata was successfully recovered, false otherwise</returns>
public static bool TryRehydrateFromStore(
Copy link
Member

Choose a reason for hiding this comment

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

Consider using regular caching / persistance names like Get or TryGet

store.Open(OpenFlags.ReadOnly);

// Find all certificates with our prefix in the FriendlyName
var candidates = store.Certificates.OfType<X509Certificate2>()
Copy link
Member

Choose a reason for hiding this comment

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

As per copilot "Summary:
Due to .NET's API limitations, searching by FriendlyName requires enumerating and filtering all certificates. If you can use another property for more selective searching, do so. Otherwise, the current method is standard practice, but may not scale well with a large certificate store."

So this doesn't really scale. Have you tested the perf for this? Do you plan to add a memory cache in front of this?

foreach (var c in candidates)
{
// Parse the FriendlyName to extract the encoded metadata
if (!TryParse(c.FriendlyName, out var tType, out var h, out var clientId, out var tenantId, out var ep))
Copy link
Member

Choose a reason for hiding this comment

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

Pls stop using these cryptic short variable names, they are very hard to read.

// Find all certificates with our prefix in the FriendlyName
var candidates = store.Certificates.OfType<X509Certificate2>()
.Where(c => !string.IsNullOrEmpty(c.FriendlyName) &&
c.FriendlyName.StartsWith(Prefix, StringComparison.Ordinal))
Copy link
Member

Choose a reason for hiding this comment

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

Why can't you do a full match on the key?

logger?.Info("[IMDSv2] Rehydrated binding metadata from certificate store (FriendlyName tag).");
return true;
}
catch (Exception ex)
Copy link
Member

Choose a reason for hiding this comment

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

Why don't you let this bubble up?

}
catch (Exception ex)
{
logger?.Verbose(() => $"[IMDSv2] Store rehydration failed: {ex.GetType().Name}: {ex.Message}");
Copy link
Member

Choose a reason for hiding this comment

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

Why not logger.error? In any case, you should let it bubble up.

/// <summary>
/// Encodes binary data as base64url without padding to avoid separator conflicts.
/// </summary>
private static string Base64UrlNoPad(byte[] data)
Copy link
Member

Choose a reason for hiding this comment

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

What's wrong with Base64Url encoding which we already use eveyrwhere ?

/// <summary>
/// Decodes base64url-formatted string back to binary data.
/// </summary>
private static byte[] Base64UrlNoPadDecode(string s)
Copy link
Member

Choose a reason for hiding this comment

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

Why not use base64url encoding helpers?

statusCode);
}

/// <summary>
Copy link
Member

Choose a reason for hiding this comment

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

The default should be no comments for private methods. Comments denote smth exceptional. Otherwise the name of the method should be all it is needed

string v = null;
try
{ v = Environment.GetEnvironmentVariable(EnvImdsV2Bearer); }
catch { /* ignore */ }
Copy link
Member

Choose a reason for hiding this comment

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

no more try catch all.

/// <summary>
/// Creates a deterministic jitter delay from identity information.
/// This ensures multiple processes don't all try to rotate at exactly the same moment,
/// while maintaining stability (same input always produces same delay).
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need this stability? We use a random -5 +5 min jitter for proactive token refresh and that seems to work fine.

int seconds = val % (maxSeconds + 1);
return TimeSpan.FromSeconds(seconds);
}
catch { return TimeSpan.Zero; }
Copy link
Member

Choose a reason for hiding this comment

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

no try catch all

/// - Rotation uses a cross-process named mutex + stable jitter so only one process mints.
/// - Do NOT delete the existing valid binding (A). Only purge certs expired > 7 days.
/// </remarks>
internal sealed class MsiCertManager
Copy link
Member

Choose a reason for hiding this comment

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

@mzervos-msft - pls review this

m = null;
return false;
}
catch // Handle access denied or other mutex-related exceptions
Copy link
Member

Choose a reason for hiding this comment

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

why?


// Helper method to sanitize strings for mutex name creation
// Mutex names have character restrictions across platforms
static string Sanitize(string s)
Copy link
Member

Choose a reason for hiding this comment

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

This is base64url encoding and we have helpers for it - https://base64.guru/standards/base64url

var globalName = @"Global\MSAL_MI_mTLS_ROT_" + suffix;

if (TryOpenAndLock(globalName, out var mGlobal))
return mGlobal; // Successfully acquired Global mutex
Copy link
Member

Choose a reason for hiding this comment

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

You generally want to use a try / finally pattern here to release the mutex.

/// Proactive rotation at half-life ensures new certificates are created well before
/// expiration, reducing the risk of authentication failures during certificate transitions.
/// </remarks>
internal static bool IsBeyondHalfLife(X509Certificate2 cert, ILoggerAdapter logger = null)
Copy link
Member

Choose a reason for hiding this comment

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

I think we call this "NeedsRefresh"

/// - Foreground rotation: Remove older certificates, keep only specified thumbprint
/// - Background rotation: Only purge very stale certificates, preserve valid ones
///
/// All store operations are best-effort with appropriate error handling to accommodate
Copy link
Member

Choose a reason for hiding this comment

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

"best-effort" is exceptional. Use normal effort, i.e. no try catch

/// Proactive rotation at half-life ensures new certificates are created well before
/// expiration, reducing the risk of authentication failures during certificate transitions.
/// </remarks>
internal static bool IsBeyondHalfLife(X509Certificate2 cert, ILoggerAdapter logger = null)
Copy link
Member

Choose a reason for hiding this comment

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

I would move these helper methods elsewhere, this file is already large

string keepThumbprint,
ILoggerAdapter logger)
bool cleanupOlder,
out string resolvedThumbprint,
Copy link
Member

Choose a reason for hiding this comment

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

Don't you have X509Certiicate2.Thumbprint ?

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.

3 participants