From 13219f4c01fe0561f4a8401a9b60d22c5b21f7fb Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Wed, 15 Oct 2025 10:18:43 -0700 Subject: [PATCH 1/4] initial --- ...cquireTokenForManagedIdentityParameters.cs | 3 + .../AuthenticationRequestParameters.cs | 12 +++- .../Requests/ManagedIdentityAuthRequest.cs | 32 +++++++++++ .../AbstractManagedIdentity.cs | 5 ++ .../ManagedIdentity/ManagedIdentityClient.cs | 12 ++++ .../ManagedIdentityTests/ImdsV2Tests.cs | 55 ++++++++----------- 6 files changed, 87 insertions(+), 32 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs index ca9ab69f92..3f2580c347 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -26,6 +27,8 @@ internal class AcquireTokenForManagedIdentityParameters : IAcquireTokenParameter internal Func> AttestationTokenProvider { get; set; } + internal X509Certificate2 MtlsCertificate { get; set; } + public void LogParameters(ILoggerAdapter logger) { if (logger.IsLoggingEnabled(LogLevel.Info)) diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs index 3619c73864..a3ae399e62 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs @@ -127,7 +127,17 @@ public string Claims } } - public IAuthenticationOperation AuthenticationScheme => _commonParameters.AuthenticationOperation; + private IAuthenticationOperation _requestOverrideScheme; + + /// + /// Effective authentication operation (scheme) for this request. + /// Defaults to the app's configured operation unless a request-scoped override is applied. + /// + public IAuthenticationOperation AuthenticationScheme + { + get => _requestOverrideScheme ?? _commonParameters.AuthenticationOperation; // <-- correct fallback + internal set => _requestOverrideScheme = value; // internal set satisfies the “make it settable?” review + } public IEnumerable PersistedCacheParameters => _commonParameters.AdditionalCacheParameters; diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs index 3db3707a9f..53095dca80 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs @@ -2,9 +2,11 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.ApiConfig.Parameters; +using Microsoft.Identity.Client.AuthScheme.PoP; using Microsoft.Identity.Client.Cache.Items; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.ManagedIdentity; @@ -39,6 +41,18 @@ protected override async Task ExecuteAsync(CancellationTok { AuthenticationResult authResult = null; ILoggerAdapter logger = AuthenticationRequestParameters.RequestContext.Logger; + + // Prime the scheme before any cache lookup if we already have a binding cert from a prior mint + if (AuthenticationRequestParameters.IsMtlsPopRequested && + !(AuthenticationRequestParameters.AuthenticationScheme is MtlsPopAuthenticationOperation)) + { + var priorCert = _managedIdentityClient.RuntimeMtlsBindingCertificate; + if (priorCert != null) + { + AuthenticationRequestParameters.AuthenticationScheme = new MtlsPopAuthenticationOperation(priorCert); + AuthenticationRequestParameters.RequestContext.Logger.Info("[ManagedIdentity] Using prior mTLS binding certificate for cache lookup."); + } + } // 1. FIRST, handle ForceRefresh if (_managedIdentityParameters.ForceRefresh) @@ -209,6 +223,24 @@ await _managedIdentityClient .SendTokenRequestForManagedIdentityAsync(AuthenticationRequestParameters.RequestContext, _managedIdentityParameters, cancellationToken) .ConfigureAwait(false); + if (AuthenticationRequestParameters.IsMtlsPopRequested + && _managedIdentityParameters.MtlsCertificate != null + && !(AuthenticationRequestParameters.AuthenticationScheme is MtlsPopAuthenticationOperation)) + { + // Remember the cert for future requests (same app instance) BEFORE we clear it. + _managedIdentityClient.SetRuntimeMtlsBindingCertificate(_managedIdentityParameters.MtlsCertificate); + + // Apply mTLS scheme BEFORE caching so the token is stored under the mtls_pop key. + AuthenticationRequestParameters.AuthenticationScheme = + new MtlsPopAuthenticationOperation(_managedIdentityParameters.MtlsCertificate); + + // Do not hold cert past this request boundary + _managedIdentityParameters.MtlsCertificate = null; + + AuthenticationRequestParameters.RequestContext.Logger.Info( + "[ManagedIdentity] Applied mtls_pop scheme prior to caching."); + } + var msalTokenResponse = MsalTokenResponse.CreateFromManagedIdentityResponse(managedIdentityResponse); msalTokenResponse.Scope = AuthenticationRequestParameters.Scope.AsSingleString(); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs index b86d617ac7..3c73b83898 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs @@ -61,6 +61,11 @@ public virtual async Task AuthenticateAsync( ManagedIdentityRequest request = await CreateRequestAsync(resource).ConfigureAwait(false); + if (parameters.IsMtlsPopRequested && request?.MtlsCertificate != null) + { + parameters.MtlsCertificate = request.MtlsCertificate; + } + // Automatically add claims / capabilities if this MI source supports them if (_sourceType.SupportsClaimsAndCapabilities()) { diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs index 4e8e7fd8ce..f0a393b2c8 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs @@ -10,6 +10,7 @@ using System.IO; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.ManagedIdentity.V2; +using System.Security.Cryptography.X509Certificates; namespace Microsoft.Identity.Client.ManagedIdentity { @@ -22,6 +23,9 @@ internal class ManagedIdentityClient private const string LinuxHimdsFilePath = "/opt/azcmagent/bin/himds"; internal static ManagedIdentitySource s_sourceName = ManagedIdentitySource.None; + // Holds the most recently minted mTLS binding certificate for this application instance. + internal X509Certificate2 RuntimeMtlsBindingCertificate { get; private set; } + internal static void ResetSourceForTest() { s_sourceName = ManagedIdentitySource.None; @@ -157,5 +161,13 @@ private static bool ValidateAzureArcEnvironment(string identityEndpoint, string logger?.Verbose(() => "[Managed Identity] Azure Arc managed identity is not available."); return false; } + + internal void SetRuntimeMtlsBindingCertificate(X509Certificate2 cert) + { + var old = RuntimeMtlsBindingCertificate; + RuntimeMtlsBindingCertificate = cert; + //dispose prior stored cert on replacement + old?.Dispose(); + } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs index 61316f08d7..9ea9f1815d 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs @@ -314,11 +314,10 @@ public async Task mTLSPopTokenHappyPath( Assert.IsNotNull(result); Assert.IsNotNull(result.AccessToken); Assert.AreEqual(result.TokenType, MTLSPoP); - // Assert.IsNotNull(result.BindingCertificate); // TODO: implement mTLS Pop BindingCertificate + Assert.IsNotNull(result.BindingCertificate); Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); - // TODO: broken until Gladwin's PR is merged in - /*result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() .WithAttestationProviderForTests(s_fakeAttestationProvider) .ExecuteAsync().ConfigureAwait(false); @@ -326,8 +325,8 @@ public async Task mTLSPopTokenHappyPath( Assert.IsNotNull(result); Assert.IsNotNull(result.AccessToken); Assert.AreEqual(result.TokenType, MTLSPoP); - // Assert.IsNotNull(result.BindingCertificate); // TODO: implement mTLS Pop BindingCertificate - Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource);*/ + Assert.IsNotNull(result.BindingCertificate); + Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource); } } @@ -358,19 +357,18 @@ public async Task mTLSPopTokenIsPerIdentity( Assert.IsNotNull(result); Assert.IsNotNull(result.AccessToken); Assert.AreEqual(result.TokenType, MTLSPoP); - // Assert.IsNotNull(result.BindingCertificate); // TODO: implement mTLS Pop BindingCertificate + Assert.IsNotNull(result.BindingCertificate); Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); - // TODO: broken until Gladwin's PR is merged in - /*result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(result); Assert.IsNotNull(result.AccessToken); Assert.AreEqual(result.TokenType, MTLSPoP); - // Assert.IsNotNull(result.BindingCertificate); // TODO: implement mTLS Pop BindingCertificate - Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource);*/ + Assert.IsNotNull(result.BindingCertificate); + Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource); #endregion Identity 1 #region Identity 2 @@ -393,11 +391,10 @@ public async Task mTLSPopTokenIsPerIdentity( Assert.IsNotNull(result2); Assert.IsNotNull(result2.AccessToken); Assert.AreEqual(result2.TokenType, MTLSPoP); - // Assert.IsNotNull(result2.BindingCertificate); // TODO: implement mTLS Pop BindingCertificate + Assert.IsNotNull(result2.BindingCertificate); Assert.AreEqual(TokenSource.IdentityProvider, result2.AuthenticationResultMetadata.TokenSource); - // TODO: broken until Gladwin's PR is merged in - /*result2 = await managedIdentityApp2.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + result2 = await managedIdentityApp2.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() .WithAttestationProviderForTests(s_fakeAttestationProvider) .ExecuteAsync().ConfigureAwait(false); @@ -405,8 +402,8 @@ public async Task mTLSPopTokenIsPerIdentity( Assert.IsNotNull(result2); Assert.IsNotNull(result2.AccessToken); Assert.AreEqual(result2.TokenType, MTLSPoP); - // Assert.IsNotNull(result2.BindingCertificate); // TODO: implement mTLS Pop BindingCertificate - Assert.AreEqual(TokenSource.Cache, result2.AuthenticationResultMetadata.TokenSource);*/ + Assert.IsNotNull(result2.BindingCertificate); + Assert.AreEqual(TokenSource.Cache, result2.AuthenticationResultMetadata.TokenSource); #endregion Identity 2 // TODO: Assert.AreEqual(CertificateCache.Count, 2); @@ -439,26 +436,22 @@ public async Task mTLSPopTokenIsReAcquiredWhenCertificatIsExpired( Assert.IsNotNull(result); Assert.IsNotNull(result.AccessToken); Assert.AreEqual(result.TokenType, MTLSPoP); - // Assert.IsNotNull(result.BindingCertificate); // TODO: implement mTLS Pop BindingCertificate + Assert.IsNotNull(result.BindingCertificate); Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); - // TODO: Add functionality to check cert expiration in the cache - /** - AddMocksToGetEntraToken(httpManager, userAssignedIdentityId, userAssignedId, mTLSPop: true); - - result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) - .ExecuteAsync().ConfigureAwait(false); + //To-Do : Add cert expiry check functionality + //AddMocksToGetEntraToken(httpManager, userAssignedIdentityId, userAssignedId, mTLSPop: true); - Assert.IsNotNull(result); - Assert.IsNotNull(result.AccessToken); - Assert.AreEqual(result.TokenType, MTLSPoP); - // Assert.IsNotNull(result.BindingCertificate); // TODO: implement mTLS Pop BindingCertificate - Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + //result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + // .WithMtlsProofOfPossession() + // .WithAttestationProviderForTests(s_fakeAttestationProvider) + // .ExecuteAsync().ConfigureAwait(false); - Assert.AreEqual(CertificateCache.Count, 1); // expired cert was removed from the cache - */ + //Assert.IsNotNull(result); + //Assert.IsNotNull(result.AccessToken); + //Assert.AreEqual(result.TokenType, MTLSPoP); + //Assert.IsNotNull(result.BindingCertificate); + //Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); } } #endregion mTLS Pop Token Tests From 3a8dba888c2442b41a0f63e6ab0d7aff1e555631 Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:50:04 -0700 Subject: [PATCH 2/4] pr comments --- ...cquireTokenForManagedIdentityParameters.cs | 1 + .../Requests/ManagedIdentityAuthRequest.cs | 19 ++++++++-------- .../AbstractManagedIdentity.cs | 3 +++ .../ManagedIdentity/ManagedIdentityClient.cs | 22 ++++++++++++++----- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs index 3f2580c347..32646eaa62 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs @@ -40,6 +40,7 @@ public void LogParameters(ILoggerAdapter logger) Resource: {Resource} Claims: {!string.IsNullOrEmpty(Claims)} RevokedTokenHash: {!string.IsNullOrEmpty(RevokedTokenHash)} + IsMtlsPopRequested: {IsMtlsPopRequested} """); } } diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs index 53095dca80..f899b6b908 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs @@ -43,14 +43,17 @@ protected override async Task ExecuteAsync(CancellationTok ILoggerAdapter logger = AuthenticationRequestParameters.RequestContext.Logger; // Prime the scheme before any cache lookup if we already have a binding cert from a prior mint - if (AuthenticationRequestParameters.IsMtlsPopRequested && - !(AuthenticationRequestParameters.AuthenticationScheme is MtlsPopAuthenticationOperation)) + if (AuthenticationRequestParameters.IsMtlsPopRequested) { var priorCert = _managedIdentityClient.RuntimeMtlsBindingCertificate; if (priorCert != null) { AuthenticationRequestParameters.AuthenticationScheme = new MtlsPopAuthenticationOperation(priorCert); - AuthenticationRequestParameters.RequestContext.Logger.Info("[ManagedIdentity] Using prior mTLS binding certificate for cache lookup."); + + logger.Info("[ManagedIdentity] Using prior mTLS binding certificate for cache lookup."); + logger.InfoPii( + () => $"[ManagedIdentity][PII] Prior mTLS cert thumbprint: {priorCert.Thumbprint}", + () => "[ManagedIdentity][PII] Prior mTLS cert thumbprint: ***"); } } @@ -223,20 +226,16 @@ await _managedIdentityClient .SendTokenRequestForManagedIdentityAsync(AuthenticationRequestParameters.RequestContext, _managedIdentityParameters, cancellationToken) .ConfigureAwait(false); - if (AuthenticationRequestParameters.IsMtlsPopRequested - && _managedIdentityParameters.MtlsCertificate != null - && !(AuthenticationRequestParameters.AuthenticationScheme is MtlsPopAuthenticationOperation)) + if (AuthenticationRequestParameters.IsMtlsPopRequested && _managedIdentityParameters.MtlsCertificate != null) { - // Remember the cert for future requests (same app instance) BEFORE we clear it. + // Remember the cert... _managedIdentityClient.SetRuntimeMtlsBindingCertificate(_managedIdentityParameters.MtlsCertificate); - // Apply mTLS scheme BEFORE caching so the token is stored under the mtls_pop key. + // Apply mTLS scheme BEFORE caching... AuthenticationRequestParameters.AuthenticationScheme = new MtlsPopAuthenticationOperation(_managedIdentityParameters.MtlsCertificate); - // Do not hold cert past this request boundary _managedIdentityParameters.MtlsCertificate = null; - AuthenticationRequestParameters.RequestContext.Logger.Info( "[ManagedIdentity] Applied mtls_pop scheme prior to caching."); } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs index 3c73b83898..52ef40dbad 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs @@ -61,6 +61,9 @@ public virtual async Task AuthenticateAsync( ManagedIdentityRequest request = await CreateRequestAsync(resource).ConfigureAwait(false); + // When IMDSv2 mints a binding certificate during this request (via CSR), + // it's exposed via request.MtlsCertificate. Bubble it up so the request + // layer can set the mtls_pop scheme if (parameters.IsMtlsPopRequested && request?.MtlsCertificate != null) { parameters.MtlsCertificate = request.MtlsCertificate; diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs index f0a393b2c8..98af2d1b2a 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs @@ -24,7 +24,8 @@ internal class ManagedIdentityClient internal static ManagedIdentitySource s_sourceName = ManagedIdentitySource.None; // Holds the most recently minted mTLS binding certificate for this application instance. - internal X509Certificate2 RuntimeMtlsBindingCertificate { get; private set; } + private X509Certificate2 _runtimeMtlsBindingCertificate; + internal X509Certificate2 RuntimeMtlsBindingCertificate => Volatile.Read(ref _runtimeMtlsBindingCertificate); internal static void ResetSourceForTest() { @@ -162,12 +163,23 @@ private static bool ValidateAzureArcEnvironment(string identityEndpoint, string return false; } + /// + /// Sets the in-memory binding certificate used to prime the mtls_pop scheme on subsequent requests. + /// + /// + /// Disposing an releases resources for this in-memory instance; + /// it does not remove a certificate from any OS certificate store (store removal requires ). + /// internal void SetRuntimeMtlsBindingCertificate(X509Certificate2 cert) { - var old = RuntimeMtlsBindingCertificate; - RuntimeMtlsBindingCertificate = cert; - //dispose prior stored cert on replacement - old?.Dispose(); + // Atomically swap the reference and dispose the previous one (if different). + var old = System.Threading.Interlocked.Exchange(ref _runtimeMtlsBindingCertificate, cert); + + // If the same instance is passed again, do not dispose it (it is now the current value). + if (!object.ReferenceEquals(old, cert)) + { + old?.Dispose(); + } } } } From dfd6425371dc2c061b265aa23b0e55a120566603 Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:30:39 -0700 Subject: [PATCH 3/4] pr comments --- .../AuthenticationRequestParameters.cs | 4 ++-- .../ManagedIdentity/ManagedIdentityClient.cs | 18 +++++++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs index a3ae399e62..4e8e66f2ef 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs @@ -135,8 +135,8 @@ public string Claims /// public IAuthenticationOperation AuthenticationScheme { - get => _requestOverrideScheme ?? _commonParameters.AuthenticationOperation; // <-- correct fallback - internal set => _requestOverrideScheme = value; // internal set satisfies the “make it settable?” review + get => _requestOverrideScheme ?? _commonParameters.AuthenticationOperation; + internal set => _requestOverrideScheme = value; } public IEnumerable PersistedCacheParameters => _commonParameters.AdditionalCacheParameters; diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs index 98af2d1b2a..ff8fa4e9b0 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs @@ -164,22 +164,18 @@ private static bool ValidateAzureArcEnvironment(string identityEndpoint, string } /// - /// Sets the in-memory binding certificate used to prime the mtls_pop scheme on subsequent requests. + /// Sets (or replaces) the in-memory binding certificate used to prime the mtls_pop scheme on subsequent requests. + /// The certificate is intentionally NOT disposed here to avoid invalidating caller-held references (e.g., via AuthenticationResult). /// /// - /// Disposing an releases resources for this in-memory instance; - /// it does not remove a certificate from any OS certificate store (store removal requires ). + /// Lifetime considerations: + /// - The binding certificate is ephemeral and valid for the token’s binding duration. + /// - If rotation occurs, older certificates will be eligible for GC once no longer referenced. + /// - Explicit disposal can be revisited if a deterministic rotation / shutdown strategy is introduced. /// internal void SetRuntimeMtlsBindingCertificate(X509Certificate2 cert) { - // Atomically swap the reference and dispose the previous one (if different). - var old = System.Threading.Interlocked.Exchange(ref _runtimeMtlsBindingCertificate, cert); - - // If the same instance is passed again, do not dispose it (it is now the current value). - if (!object.ReferenceEquals(old, cert)) - { - old?.Dispose(); - } + Volatile.Write(ref _runtimeMtlsBindingCertificate, cert); } } } From a9b0d07e6449c7e75a4f8f8172f74a9ab3887479 Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:12:47 -0700 Subject: [PATCH 4/4] pr comments --- .../Internal/Requests/ManagedIdentityAuthRequest.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs index f899b6b908..955adab0f4 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs @@ -45,14 +45,13 @@ protected override async Task ExecuteAsync(CancellationTok // Prime the scheme before any cache lookup if we already have a binding cert from a prior mint if (AuthenticationRequestParameters.IsMtlsPopRequested) { - var priorCert = _managedIdentityClient.RuntimeMtlsBindingCertificate; - if (priorCert != null) + if (_managedIdentityClient.RuntimeMtlsBindingCertificate != null) { - AuthenticationRequestParameters.AuthenticationScheme = new MtlsPopAuthenticationOperation(priorCert); + AuthenticationRequestParameters.AuthenticationScheme = new MtlsPopAuthenticationOperation(_managedIdentityClient.RuntimeMtlsBindingCertificate); logger.Info("[ManagedIdentity] Using prior mTLS binding certificate for cache lookup."); logger.InfoPii( - () => $"[ManagedIdentity][PII] Prior mTLS cert thumbprint: {priorCert.Thumbprint}", + () => $"[ManagedIdentity][PII] Prior mTLS cert thumbprint: {_managedIdentityClient.RuntimeMtlsBindingCertificate.Thumbprint}", () => "[ManagedIdentity][PII] Prior mTLS cert thumbprint: ***"); } }