-
Notifications
You must be signed in to change notification settings - Fork 353
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
base: msiv2-feature
Are you sure you want to change the base?
Conversation
|
||
//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); |
There was a problem hiding this comment.
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 ..."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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}"); |
There was a problem hiding this comment.
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}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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}"); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>(); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
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 theIMsalMtlsHttpClientFactory
interface.Refactoring and new classes:
src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
: RefactoredManagedIdentityAuthRequest
into an abstract class to support different Managed Identity sources.src/client/Microsoft.Identity.Client/Internal/Requests/CredentialBasedMsiAuthRequest.cs
: AddedCredentialManagedIdentityAuthRequest
class to handle Managed Identity authentication using certificates.src/client/Microsoft.Identity.Client/Internal/Requests/LegacyManagedIdentityAuthRequest.cs
: AddedLegacyManagedIdentityAuthRequest
class to handle legacy Managed Identity authentication.Modifications in existing methods:
src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs
: UpdatedExecuteAsync
method to determine the Managed Identity source and use the appropriate authentication request class.Visibility changes:
src/client/Microsoft.Identity.Client/AppConfig/IMsalMtlsHttpClientFactory.cs
: Changed the visibility ofIMsalMtlsHttpClientFactory
from internal to public.Testing
tests will be added in a new PR
Performance impact
none
Documentation
Have a separate task for docs