Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MSI V2 Feature] Adding Credential as a Managed Identity Source #5148

Open
wants to merge 1 commit into
base: msiv2-feature
Choose a base branch
from

Conversation

gladjohn
Copy link
Contributor

Fixes #5117

Changes proposed in this request
This adds a new credential source to Managed Identity sources. We call it IMDSV2 source.

This pull request introduces significant changes to the Managed Identity authentication flow in the Microsoft Identity Client library. The most important changes include refactoring the ManagedIdentityAuthRequest class into an abstract class, adding new classes for handling different Managed Identity sources, and modifying the visibility of the IMsalMtlsHttpClientFactory interface.

Refactoring and new classes:

Modifications in existing methods:

Visibility changes:

Testing
tests will be added in a new PR

Performance impact
none

Documentation
Have a separate task for docs

@gladjohn gladjohn requested a review from a team as a code owner February 18, 2025 00:59

//allow only one call to the provider
logger.Verbose(() => "[CredentialManagedIdentityAuthRequest] Entering acquire token for managed identity credential request semaphore.");
await s_semaphoreSlim.WaitAsync(cancellationToken).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 semaphore should not block MSAL from getting tokens from the cache. Only from eSTS, and even them you should consider if it's worth using.

}
else
{
logger.Verbose(() => "[CredentialManagedIdentityAuthRequest] Getting Access token from cache ...");
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
logger.Verbose(() => "[CredentialManagedIdentityAuthRequest] Getting Access token from cache ...");
logger.Verbose(() => "[CredentialManagedIdentityAuthRequest] Returning Access token from cache ...");

!string.IsNullOrEmpty(AuthenticationRequestParameters.Claims) ||
AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo == CacheRefreshReason.ProactivelyRefreshed)
{
authResult = await GetAccessTokenFromTokenEndpointAsync(cancellationToken, logger).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.

log statement would be good here to show which condition was true.

MsalTokenResponse msalTokenResponse = await client
.GetTokenAsync(tokenUrl,
AuthenticationRequestParameters.RequestContext,
true,
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 the param name for booleans, to make the code easier to read

}
catch (MsalClientException ex)
{
logger.Verbose(() => $"[CredentialManagedIdentityAuthRequest] Caught an exception. {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.

nit: Doesn't it get logged higher up?

}
catch (MsalServiceException ex)
{
logger.Verbose(() => $"[CredentialManagedIdentityAuthRequest] Caught an exception. {ex.Message}. Error Code : {ex.ErrorCode} Status Code : {ex.StatusCode}");
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
logger.Verbose(() => $"[CredentialManagedIdentityAuthRequest] Caught an exception. {ex.Message}. Error Code : {ex.ErrorCode} Status Code : {ex.StatusCode}");
logger.Verbose(() => $"[CredentialManagedIdentityAuthRequest] A service exception occured. {ex.Message}. Error Code : {ex.ErrorCode} Status Code : {ex.StatusCode}");

}
catch (Exception e) when (e is not MsalServiceException)
{
logger.Error($"[CredentialManagedIdentityAuthRequest] Exception: {e.Message}");
Copy link
Member

Choose a reason for hiding this comment

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

Perfer to log the entire exception. The logger is responsible for outputting it.


protected override KeyValuePair<string, string>? GetCcsHeader(IDictionary<string, string> additionalBodyParameters)
{
return null;
}

// Override method to return a sorted set of scopes based on the input set.
protected override SortedSet<string> GetOverriddenScopes(ISet<string> inputScopes)
Copy link
Member

Choose a reason for hiding this comment

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

Why? There's only 1 scope possible? Why do we need to bring this whole "collection of scope" concept from user tokens to S2S tokens?

)
{
// Create a Managed Identity request dynamically using the provided resource.
string resource = GetOverriddenScopes(AuthenticationRequestParameters.Scope).AsSingleString();
Copy link
Member

Choose a reason for hiding this comment

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

There is no "overriden" scopes?


namespace Microsoft.Identity.Client.Internal.Requests
{
internal class LegacyManagedIdentityAuthRequest : ManagedIdentityAuthRequest
Copy link
Member

Choose a reason for hiding this comment

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

Do you actually use anything from this base class? Would it be more appropriate to inherit from RequestBase instead?

@@ -30,51 +26,52 @@ public ManagedIdentityAuthRequest(

protected override async Task<AuthenticationResult> ExecuteAsync(CancellationToken cancellationToken)
{
AuthenticationResult authResult = null;
if (AuthenticationRequestParameters.Scope == null || AuthenticationRequestParameters.Scope.Count == 0)
Copy link
Member

Choose a reason for hiding this comment

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

This should be thrown earlier, when the request object is "built".

private readonly CancellationToken _cancellationToken;
internal const string TimeoutError = "[Managed Identity] Authentication unavailable. The request to the managed identity endpoint timed out.";

// New constructor that accepts a ManagedIdentityRequest
Copy link
Member

Choose a reason for hiding this comment

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

How can it be new if it's the first version of this class?

/// <param name="credentialResponse">The CredentialResponse to be validated.</param>
private void ValidateCredentialResponse(ManagedIdentityCredentialResponse credentialResponse)
{
var errorMessages = new List<string>();
Copy link
Member

Choose a reason for hiding this comment

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

Use a StringBuilder instead.

string resource = GetOverriddenScopes(AuthenticationRequestParameters.Scope).AsSingleString();
ManagedIdentityRequest miRequest = CreateManagedIdentityRequest(resource);

var msiCredentialService = new ManagedIdentityCredentialService(
Copy link
Member

Choose a reason for hiding this comment

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

Generally it's a advised to not construct new objects in the main code path, but only in ctors. It will make unit testing easier.

Conceptually, there should be only 1 ManagedIdentityCredentialService which can serve all the requests? There is no need to pass around all this data through constructors.

{
private readonly ManagedIdentityRequest _request;
private readonly X509Certificate2 _bindingCertificate;
private readonly RequestContext _requestContext;
Copy link
Member

Choose a reason for hiding this comment

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

Try to avoid passing in "RequesetContext" around. It's starting to become a https://en.wikipedia.org/wiki/God_object

A better pattern is to pass in only what is needed - the logger and the httpManager in this case.

@@ -0,0 +1,169 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
Copy link
Member

Choose a reason for hiding this comment

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

Any opportunitiers to log client telemetry?

/// Not for final release. For platform certificates, this is a no-op.
/// </summary>
/// <returns>The renewed certificate.</returns>
public static X509Certificate2 ForceUpdateInMemoryCertificate()
Copy link
Member

Choose a reason for hiding this comment

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

How is Azure SDK going to call an intenal API?

/// <summary>
/// Event triggered when a new certificate is created.
/// </summary>
internal static event Action<X509Certificate2> CertificateUpdated;
Copy link
Member

@bgavrilMS bgavrilMS Feb 18, 2025

Choose a reason for hiding this comment

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

This is again internal, so Azure SDK can't use. YOu need to use a public class instead.

/// <returns>An X509Certificate2 valid for 90 days.</returns>
public static X509Certificate2 GetOrCreateCertificate(bool forceUpdate = false)
{
lock (s_lock)
Copy link
Member

Choose a reason for hiding this comment

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

Do we really need to block on reading the cert? Can you use a https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-threading-readerwriterlockslim instead?


bool isWindows = DesktopOsHelper.IsWindows();

X509KeyStorageFlags flags = isWindows
Copy link
Member

Choose a reason for hiding this comment

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

Pls add some comments on WHY you used these flags.

Copy link
Member

@bgavrilMS bgavrilMS left a comment

Choose a reason for hiding this comment

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

PRs should have tests. This will help reviewers go through your code. And the final version of the code will be different due to test hooks.

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