diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs index ca9ab69f92..32646eaa62 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)) @@ -37,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/AuthenticationRequestParameters.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs index 3619c73864..4e8e66f2ef 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; + internal set => _requestOverrideScheme = value; + } 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..955adab0f4 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,20 @@ 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) + { + if (_managedIdentityClient.RuntimeMtlsBindingCertificate != null) + { + 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: {_managedIdentityClient.RuntimeMtlsBindingCertificate.Thumbprint}", + () => "[ManagedIdentity][PII] Prior mTLS cert thumbprint: ***"); + } + } // 1. FIRST, handle ForceRefresh if (_managedIdentityParameters.ForceRefresh) @@ -209,6 +225,20 @@ await _managedIdentityClient .SendTokenRequestForManagedIdentityAsync(AuthenticationRequestParameters.RequestContext, _managedIdentityParameters, cancellationToken) .ConfigureAwait(false); + if (AuthenticationRequestParameters.IsMtlsPopRequested && _managedIdentityParameters.MtlsCertificate != null) + { + // Remember the cert... + _managedIdentityClient.SetRuntimeMtlsBindingCertificate(_managedIdentityParameters.MtlsCertificate); + + // Apply mTLS scheme BEFORE caching... + AuthenticationRequestParameters.AuthenticationScheme = + new MtlsPopAuthenticationOperation(_managedIdentityParameters.MtlsCertificate); + + _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..52ef40dbad 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs @@ -61,6 +61,14 @@ 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; + } + // 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..ff8fa4e9b0 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,10 @@ 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. + private X509Certificate2 _runtimeMtlsBindingCertificate; + internal X509Certificate2 RuntimeMtlsBindingCertificate => Volatile.Read(ref _runtimeMtlsBindingCertificate); + internal static void ResetSourceForTest() { s_sourceName = ManagedIdentitySource.None; @@ -157,5 +162,20 @@ private static bool ValidateAzureArcEnvironment(string identityEndpoint, string logger?.Verbose(() => "[Managed Identity] Azure Arc managed identity is not available."); return false; } + + /// + /// 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). + /// + /// + /// 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) + { + Volatile.Write(ref _runtimeMtlsBindingCertificate, cert); + } } } 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