Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.ManagedIdentity;
using Microsoft.Identity.Client.ManagedIdentity.V2;

namespace Microsoft.Identity.Client.ApiConfig.Parameters
{
Expand All @@ -24,6 +26,13 @@ internal class AcquireTokenForManagedIdentityParameters : IAcquireTokenParameter

public bool IsMtlsPopRequested { get; set; }

// When the MI source produced / resolved an mTLS binding certificate, we attach it here
// so the request layer can apply a cache-correct IAuthenticationOperation.
public X509Certificate2 MtlsCertificate { get; set; }
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 avoid double caching. There is a component specialized in retrieving cached certificates, you should make use of that component instead of caching the cert in memory again here.


// CSR response we get back when IMDSv2 minted the certificate.
internal CertificateRequestResponse CertificateRequestResponse { get; set; }

internal Func<AttestationTokenInput, CancellationToken, Task<AttestationTokenResponse>> AttestationTokenProvider { get; set; }

public void LogParameters(ILoggerAdapter logger)
Expand All @@ -38,6 +47,10 @@ public void LogParameters(ILoggerAdapter logger)
Claims: {!string.IsNullOrEmpty(Claims)}
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.

$"[AcquireTokenForManagedIdentityParameters] IsMtlsPopRequested={IsMtlsPopRequested}, " +
$"MtlsCert={(MtlsCertificate != null ? MtlsCertificate.Thumbprint : "null")}");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: MtlsCertificate?.Thumbprint ?? "null"

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.

}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/client/Microsoft.Identity.Client/ApplicationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public static void ResetStateForTest()
OidcRetrieverWithCache.ResetCacheForTest();
AuthorityManager.ClearValidationCache();
SingletonThrottlingManager.GetInstance().ResetCache();
ManagedIdentityClient.ResetSourceForTest();
ManagedIdentityClient.ResetSourceAndBindingForTest();
AuthorityManager.ClearValidationCache();
PoPCryptoProviderFactory.Reset();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ public AuthenticationRequestParameters(

public bool IsMtlsPopRequested => _commonParameters.IsMtlsPopRequested;

// Request‑scoped override for the authentication operation.
internal IAuthenticationOperation AuthenticationOperationOverride { get; set; }

// Effective operation for this request: prefer the override, otherwise the default.
public IAuthenticationOperation AuthenticationScheme =>
Copy link
Member

Choose a reason for hiding this comment

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

Maybe you want to just make this settable?

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.

AuthenticationOperationOverride ?? _commonParameters.AuthenticationOperation;

/// <summary>
/// Indicates if the user configured claims via .WithClaims. Not affected by Client Capabilities
/// </summary>
Expand All @@ -127,8 +134,6 @@ public string Claims
}
}

public IAuthenticationOperation AuthenticationScheme => _commonParameters.AuthenticationOperation;

public IEnumerable<string> PersistedCacheParameters => _commonParameters.AdditionalCacheParameters;

public SortedList<string, string> CacheKeyComponents {get; private set; }
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ protected AbstractManagedIdentity(RequestContext requestContext, ManagedIdentity
}

public virtual async Task<ManagedIdentityResponse> AuthenticateAsync(
AcquireTokenForManagedIdentityParameters parameters,
AcquireTokenForManagedIdentityParameters parameters,
CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
Expand All @@ -54,14 +54,25 @@ public virtual async Task<ManagedIdentityResponse> AuthenticateAsync(

HttpResponse response;

// Convert the scopes to a resource string.
string resource = parameters.Resource;

_isMtlsPopRequested = parameters.IsMtlsPopRequested;

ManagedIdentityRequest request = await CreateRequestAsync(resource).ConfigureAwait(false);

// Automatically add claims / capabilities if this MI source supports them
// Bubble cert + CSR to parameters so upper layers can apply mtls_pop before caching
if (request != null)
{
if (request.MtlsCertificate != null)
{
parameters.MtlsCertificate = request.MtlsCertificate;
}

if (request.CertificateRequestResponse != null)
{
parameters.CertificateRequestResponse = request.CertificateRequestResponse;
}
}

if (_sourceType.SupportsClaimsAndCapabilities())
{
request.AddClaimsAndCapabilities(
Expand Down Expand Up @@ -110,7 +121,6 @@ public virtual async Task<ManagedIdentityResponse> AuthenticateAsync(
cancellationToken: cancellationToken,
retryPolicy: retryPolicy)
.ConfigureAwait(false);

}

return await HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Concurrent;
using Microsoft.Identity.Client.ManagedIdentity.V2;

namespace Microsoft.Identity.Client.ManagedIdentity
{
/// <summary>
/// Stores and manages certificate binding metadata for Azure managed identities using IMDSv2.
/// This class caches certificate information and STS endpoints per identity (MSI client ID),
/// maintaining separate mappings for different token types to ensure proper security isolation.
/// </summary>
/// <remarks>
/// Each managed identity can have separate certificate bindings for different authentication methods:
/// - Bearer tokens: Standard OAuth2 bearer tokens
/// - PoP (Proof of Possession) tokens: Enhanced security tokens bound to a specific certificate
///
/// The Subject is set once (first-wins pattern) while thumbprints can rotate during certificate renewal.
/// This design allows proper certificate rotation while maintaining stable subject identities.
/// </remarks>
internal class ImdsV2BindingMetadata
Copy link
Member

Choose a reason for hiding this comment

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

nit: you could have a key that includes the token type, so as to avoid storing {tokenType}Cert and {tokenType}Thumbprint

{
/// <summary>
/// The X.509 certificate subject distinguished name used for this identity.
/// This value is set once (first-wins) and persists across certificate rotations.
/// </summary>
public string Subject { get; set; }

/// <summary>
/// Response data for Bearer token certificate authentication, including
/// certificate data and STS endpoint information.
/// </summary>
public CertificateRequestResponse BearerResponse { get; set; }

/// <summary>
/// Thumbprint of the certificate used for Bearer token authentication.
/// Updated during certificate rotation.
/// </summary>
public string BearerThumbprint { get; set; }

/// <summary>
/// Response data for PoP (Proof of Possession) token certificate authentication,
/// including certificate data and STS endpoint information.
/// </summary>
public CertificateRequestResponse PopResponse { get; set; }

/// <summary>
/// Thumbprint of the certificate used for PoP token authentication.
/// Updated during certificate rotation.
/// </summary>
public string PopThumbprint { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
// Licensed under the MIT License.

using System;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using Microsoft.Identity.Client.Internal;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Parameters;
using Microsoft.Identity.Client.PlatformsCommon.Shared;
using System.IO;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.ManagedIdentity.V2;
using Microsoft.Identity.Client.PlatformsCommon.Shared;

namespace Microsoft.Identity.Client.ManagedIdentity
{
Expand All @@ -22,9 +24,14 @@ internal class ManagedIdentityClient
private const string LinuxHimdsFilePath = "/opt/azcmagent/bin/himds";
internal static ManagedIdentitySource s_sourceName = ManagedIdentitySource.None;

internal static void ResetSourceForTest()
internal static readonly ConcurrentDictionary<string, ImdsV2BindingMetadata> s_identityToBindingMetadataMap
= new ConcurrentDictionary<string, ImdsV2BindingMetadata>(StringComparer.Ordinal);

internal static void ResetSourceAndBindingForTest()
{
s_sourceName = ManagedIdentitySource.None;
s_identityToBindingMetadataMap.Clear();
RemoveAllTestBindingCertsFromUserStoreForTest();
}

internal async Task<ManagedIdentityResponse> SendTokenRequestForManagedIdentityAsync(
Expand Down Expand Up @@ -68,7 +75,7 @@ internal async Task<ManagedIdentitySource> GetManagedIdentitySourceAsync(Request
return source;
}

// probe IMDSv2
// probe IMDSv2
var response = await ImdsV2ManagedIdentitySource.GetCsrMetadataAsync(requestContext, probeMode: true).ConfigureAwait(false);
if (response != null)
{
Expand Down Expand Up @@ -157,5 +164,11 @@ private static bool ValidateAzureArcEnvironment(string identityEndpoint, string
logger?.Verbose(() => "[Managed Identity] Azure Arc managed identity is not available.");
return false;
}

// 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?

{
MtlsBindingStore.RemoveAllBySubject("CN=Test");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Security.Cryptography.X509Certificates;
using Microsoft.Identity.Client.ApiConfig.Parameters;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.ManagedIdentity.V2;
using Microsoft.Identity.Client.OAuth2;
using Microsoft.Identity.Client.Utils;

Expand All @@ -29,6 +30,8 @@ internal class ManagedIdentityRequest

public X509Certificate2 MtlsCertificate { get; set; }

internal CertificateRequestResponse CertificateRequestResponse { get; set; }

public ManagedIdentityRequest(
HttpMethod method,
Uri endpoint,
Expand Down
Loading