diff --git a/sdk/identity/Azure.Identity/CHANGELOG.md b/sdk/identity/Azure.Identity/CHANGELOG.md index 458f5e7160dc1..8e8bd7bed02b8 100644 --- a/sdk/identity/Azure.Identity/CHANGELOG.md +++ b/sdk/identity/Azure.Identity/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.13.0-beta.2 (Unreleased) ### Features Added +- `ManagedIdentityCredential` now supports specifying a user-assigned managed identity by object ID. ### Breaking Changes diff --git a/sdk/identity/Azure.Identity/README.md b/sdk/identity/Azure.Identity/README.md index ecfae4e78539e..8f153612a8906 100644 --- a/sdk/identity/Azure.Identity/README.md +++ b/sdk/identity/Azure.Identity/README.md @@ -160,15 +160,43 @@ These examples demonstrate authenticating `SecretClient` from the [Azure.Securit #### Authenticate with a user-assigned managed identity +To authenticate with a user-assigned managed identity, you must specify an ID for the managed identity. + +The following example demonstrates authenticating with a user-assigned managed identity using the client ID: + ```C# Snippet:AuthenticatingWithManagedIdentityCredentialUserAssigned -var credential = new ManagedIdentityCredential(clientId: userAssignedClientId); +string userAssignedClientId = "some client ID"; + +var credential = new ManagedIdentityCredential( + ManagedIdentityId.FromUserAssignedClientId(userAssignedClientId)); +var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), credential); +``` + +The following example demonstrates authenticating with a user-assigned managed identity using its resource ID: + +```C# Snippet:AuthenticatingWithManagedIdentityCredentialUserAssignedResourceId +ResourceIdentifier userAssignedResourceId = new ResourceIdentifier( + "/subscriptions//resourcegroups//providers/Microsoft.ManagedIdentity/userAssignedIdentities/"); + +var credential = new ManagedIdentityCredential( + ManagedIdentityId.FromUserAssignedResourceId(userAssignedResourceId)); +var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), credential); +``` + +The following example demonstrates authenticating with a user-assigned managed identity using its object ID: + +```C# Snippet:AuthenticatingWithManagedIdentityCredentialUserAssignedObjectId +string userAssignedObjectId = "some object ID"; + +var credential = new ManagedIdentityCredential( + ManagedIdentityId.FromUserAssignedObjectId(userAssignedObjectId)); var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), credential); ``` #### Authenticate with a system-assigned managed identity ```C# Snippet:AuthenticatingWithManagedIdentityCredentialSystemAssigned -var credential = new ManagedIdentityCredential(); +var credential = new ManagedIdentityCredential(ManagedIdentityId.SystemAssigned); var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), credential); ``` diff --git a/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs b/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs index 63ab76663a867..be5a509d66337 100644 --- a/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs +++ b/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs @@ -314,11 +314,28 @@ public InteractiveBrowserCredentialOptions() { } public partial class ManagedIdentityCredential : Azure.Core.TokenCredential { protected ManagedIdentityCredential() { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public ManagedIdentityCredential(Azure.Core.ResourceIdentifier resourceId, Azure.Identity.TokenCredentialOptions options = null) { } + public ManagedIdentityCredential(Azure.Identity.ManagedIdentityCredentialOptions options) { } + public ManagedIdentityCredential(Azure.Identity.ManagedIdentityId managedIdentityId) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public ManagedIdentityCredential(string clientId = null, Azure.Identity.TokenCredentialOptions options = null) { } public override Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override System.Threading.Tasks.ValueTask GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } + public partial class ManagedIdentityCredentialOptions : Azure.Identity.TokenCredentialOptions + { + public ManagedIdentityCredentialOptions() { } + public Azure.Identity.ManagedIdentityId ManagedIdentityId { get { throw null; } set { } } + } + public partial class ManagedIdentityId + { + internal ManagedIdentityId() { } + public static Azure.Identity.ManagedIdentityId SystemAssigned { get { throw null; } } + public static Azure.Identity.ManagedIdentityId FromUserAssignedClientId(string clientId) { throw null; } + public static Azure.Identity.ManagedIdentityId FromUserAssignedObjectId(string objectId) { throw null; } + public static Azure.Identity.ManagedIdentityId FromUserAssignedResourceId(Azure.Core.ResourceIdentifier resourceIdentifier) { throw null; } + } public partial class OnBehalfOfCredential : Azure.Core.TokenCredential { protected OnBehalfOfCredential() { } diff --git a/sdk/identity/Azure.Identity/src/AppServiceManagedIdentitySource.cs b/sdk/identity/Azure.Identity/src/AppServiceManagedIdentitySource.cs index 3ccc9e8803a94..b62ff6dcb5792 100644 --- a/sdk/identity/Azure.Identity/src/AppServiceManagedIdentitySource.cs +++ b/sdk/identity/Azure.Identity/src/AppServiceManagedIdentitySource.cs @@ -17,8 +17,7 @@ internal class AppServiceManagedIdentitySource : ManagedIdentitySource private readonly Uri _endpoint; private readonly string _secret; - private readonly string _clientId; - private readonly string _resourceId; + private readonly ManagedIdentityId _managedIdentityId; protected static bool TryValidateEnvVars(string msiEndpoint, string secret, out Uri endpointUri) { @@ -47,8 +46,7 @@ protected AppServiceManagedIdentitySource(CredentialPipeline pipeline, Uri endpo { _endpoint = endpoint; _secret = secret; - _clientId = options.ClientId; - _resourceId = options.ResourceIdentifier?.ToString(); + _managedIdentityId = options.ManagedIdentityId; } protected override Request CreateRequest(string[] scopes) @@ -64,14 +62,16 @@ protected override Request CreateRequest(string[] scopes) request.Uri.AppendQuery("api-version", AppServiceMsiApiVersion); request.Uri.AppendQuery("resource", resource); - if (!string.IsNullOrEmpty(_clientId)) + string idQueryParam = _managedIdentityId?._idType switch { - request.Uri.AppendQuery(ClientIdHeaderName, _clientId); - } + ManagedIdentityIdType.ClientId => ClientIdHeaderName, + ManagedIdentityIdType.ResourceId => Constants.ManagedIdentityResourceId, + _ => null + }; - if (!string.IsNullOrEmpty(_resourceId)) + if (idQueryParam != null) { - request.Uri.AppendQuery(Constants.ManagedIdentityResourceId, _resourceId); + request.Uri.AppendQuery(idQueryParam, _managedIdentityId._userAssignedId); } return request; diff --git a/sdk/identity/Azure.Identity/src/AzureArcManagedIdentitySource.cs b/sdk/identity/Azure.Identity/src/AzureArcManagedIdentitySource.cs index fcd1654e51c40..1c019d7e35c48 100644 --- a/sdk/identity/Azure.Identity/src/AzureArcManagedIdentitySource.cs +++ b/sdk/identity/Azure.Identity/src/AzureArcManagedIdentitySource.cs @@ -15,10 +15,10 @@ internal class AzureArcManagedIdentitySource : ManagedIdentitySource private const string IdentityEndpointInvalidUriError = "The environment variable IDENTITY_ENDPOINT contains an invalid Uri."; private const string NoChallengeErrorMessage = "Did not receive expected WWW-Authenticate header in the response from Azure Arc Managed Identity Endpoint."; private const string InvalidChallangeErrorMessage = "The WWW-Authenticate header in the response from Azure Arc Managed Identity Endpoint did not match the expected format."; - private const string UserAssignedNotSupportedErrorMessage = "User assigned identity is not supported by the Azure Arc Managed Identity Endpoint. To authenticate with the system assigned identity omit the client id when constructing the ManagedIdentityCredential, or if authenticating with the DefaultAzureCredential ensure the AZURE_CLIENT_ID environment variable is not set."; + private const string UserAssignedNotSupportedErrorMessage = "User-assigned managed identity is not supported by the Azure Arc Managed Identity Endpoint. To authenticate with the system-assigned managed identity, omit the client ID when constructing the ManagedIdentityCredential, or if authenticating with DefaultAzureCredential, ensure the AZURE_CLIENT_ID environment variable is not set."; private const string ArcApiVersion = "2019-11-01"; - private readonly string _clientId; + private readonly ManagedIdentityId _managedIdentityId; private readonly Uri _endpoint; public static ManagedIdentitySource TryCreate(ManagedIdentityClientOptions options) @@ -43,8 +43,8 @@ public static ManagedIdentitySource TryCreate(ManagedIdentityClientOptions optio internal AzureArcManagedIdentitySource(Uri endpoint, ManagedIdentityClientOptions options) : base(options.Pipeline) { _endpoint = endpoint; - _clientId = options.ClientId; - if (!string.IsNullOrEmpty(_clientId) || null != options.ResourceIdentifier) + _managedIdentityId = options.ManagedIdentityId; + if (options.ManagedIdentityId._idType != ManagedIdentityIdType.SystemAssigned) { AzureIdentityEventSource.Singleton.UserAssignedManagedIdentityNotSupported("Azure Arc"); } @@ -53,7 +53,7 @@ internal AzureArcManagedIdentitySource(Uri endpoint, ManagedIdentityClientOption protected override Request CreateRequest(string[] scopes) { // arc MI endpoint doesn't support user assigned identities so if client id was specified throw AuthenticationFailedException - if (!string.IsNullOrEmpty(_clientId)) + if (_managedIdentityId._idType != ManagedIdentityIdType.SystemAssigned) { throw new AuthenticationFailedException(UserAssignedNotSupportedErrorMessage); } diff --git a/sdk/identity/Azure.Identity/src/CloudShellManagedIdentitySource.cs b/sdk/identity/Azure.Identity/src/CloudShellManagedIdentitySource.cs index 14fd36f918adc..19bab9b1e3216 100644 --- a/sdk/identity/Azure.Identity/src/CloudShellManagedIdentitySource.cs +++ b/sdk/identity/Azure.Identity/src/CloudShellManagedIdentitySource.cs @@ -41,7 +41,7 @@ public static ManagedIdentitySource TryCreate(ManagedIdentityClientOptions optio public CloudShellManagedIdentitySource(Uri endpoint, ManagedIdentityClientOptions options) : base(options.Pipeline) { _endpoint = endpoint; - if (!string.IsNullOrEmpty(options.ClientId) || null != options.ResourceIdentifier) + if (options.ManagedIdentityId._idType != ManagedIdentityIdType.SystemAssigned) { AzureIdentityEventSource.Singleton.UserAssignedManagedIdentityNotSupported("Cloud Shell"); } diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzureApplicationCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/AzureApplicationCredential.cs index 4beab62212716..5803e52d213c6 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzureApplicationCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzureApplicationCredential.cs @@ -39,7 +39,7 @@ internal AzureApplicationCredential(AzureApplicationCredentialOptions options, E { _credential = new ChainedTokenCredential( environmentCredential ?? new EnvironmentCredential(options), - managedIdentityCredential ?? new ManagedIdentityCredential(options.ManagedIdentityClientId) + managedIdentityCredential ?? new ManagedIdentityCredential(options.ManagedIdentityId._userAssignedId) ); } diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzureApplicationCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/AzureApplicationCredentialOptions.cs index f86b1385f1a9c..f993a43da50f6 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzureApplicationCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzureApplicationCredentialOptions.cs @@ -13,10 +13,11 @@ internal class AzureApplicationCredentialOptions : TokenCredentialOptions /// /// Specifies the client id of the azure ManagedIdentity in the case of user assigned identity. /// - public string ManagedIdentityClientId { get; set; } = GetNonEmptyStringOrNull(EnvironmentVariables.ClientId); - private static string GetNonEmptyStringOrNull(string str) + public ManagedIdentityId ManagedIdentityId { get; set; } = GetManagedIdentityIdFromEnvironment(EnvironmentVariables.ClientId); + + private static ManagedIdentityId GetManagedIdentityIdFromEnvironment(string clientId) { - return !string.IsNullOrEmpty(str) ? str : null; + return !string.IsNullOrEmpty(clientId) ? ManagedIdentityId.FromUserAssignedClientId(clientId) : ManagedIdentityId.SystemAssigned; } } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/ManagedIdentityCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/ManagedIdentityCredential.cs index f287e73e58f08..b9947661f91c8 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/ManagedIdentityCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/ManagedIdentityCredential.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Azure.Core.Pipeline; using System.Linq; +using System.ComponentModel; namespace Azure.Identity { @@ -41,8 +42,9 @@ protected ManagedIdentityCredential() /// If not provided, a system-assigned managed identity is used. /// /// Options to configure the management of the requests sent to Microsoft Entra ID. + [EditorBrowsable(EditorBrowsableState.Never)] public ManagedIdentityCredential(string clientId = null, TokenCredentialOptions options = null) - : this(new ManagedIdentityClient(new ManagedIdentityClientOptions { ClientId = clientId, Pipeline = CredentialPipeline.GetInstance(options, IsManagedIdentityCredential: true), Options = options })) + : this(new ManagedIdentityClient(new ManagedIdentityClientOptions { ManagedIdentityId = string.IsNullOrEmpty(clientId) ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.FromUserAssignedClientId(clientId), Pipeline = CredentialPipeline.GetInstance(options, IsManagedIdentityCredential: true), Options = options })) { _logAccountDetails = options?.Diagnostics?.IsAccountIdentifierLoggingEnabled ?? false; } @@ -54,21 +56,41 @@ public ManagedIdentityCredential(string clientId = null, TokenCredentialOptions /// The resource ID to authenticate for a user-assigned managed identity. /// /// Options to configure the management of the requests sent to Microsoft Entra ID. + [EditorBrowsable(EditorBrowsableState.Never)] public ManagedIdentityCredential(ResourceIdentifier resourceId, TokenCredentialOptions options = null) - : this(new ManagedIdentityClient(new ManagedIdentityClientOptions { ResourceIdentifier = resourceId, Pipeline = CredentialPipeline.GetInstance(options, IsManagedIdentityCredential: true), Options = options })) + : this(new ManagedIdentityClient(new ManagedIdentityClientOptions { ManagedIdentityId = ManagedIdentityId.FromUserAssignedResourceId(resourceId), Pipeline = CredentialPipeline.GetInstance(options, IsManagedIdentityCredential: true), Options = options })) { _logAccountDetails = options?.Diagnostics?.IsAccountIdentifierLoggingEnabled ?? false; _clientId = resourceId.ToString(); } + /// + /// Creates an instance of capable of authenticating using the specified . + /// + /// + public ManagedIdentityCredential(ManagedIdentityId managedIdentityId) + : this(new ManagedIdentityClient(new ManagedIdentityClientOptions { ManagedIdentityId = managedIdentityId, Pipeline = CredentialPipeline.GetInstance(null, IsManagedIdentityCredential: true), Options = null })) + { } + + /// + /// Creates an instance of configured with the specified options. + /// + /// The options used to configure the credential. + public ManagedIdentityCredential(ManagedIdentityCredentialOptions options) + : this(new ManagedIdentityClient(new ManagedIdentityClientOptions { ManagedIdentityId = options.ManagedIdentityId, Pipeline = CredentialPipeline.GetInstance(options, IsManagedIdentityCredential: true), Options = options })) + { + Argument.AssertNotNull(options, nameof(options)); + _logAccountDetails = options?.Diagnostics?.IsAccountIdentifierLoggingEnabled ?? false; + } + internal ManagedIdentityCredential(string clientId, CredentialPipeline pipeline, TokenCredentialOptions options = null, bool preserveTransport = false) - : this(new ManagedIdentityClient(new ManagedIdentityClientOptions { Pipeline = pipeline, ClientId = clientId, PreserveTransport = preserveTransport, Options = options })) + : this(new ManagedIdentityClient(new ManagedIdentityClientOptions { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId(clientId), PreserveTransport = preserveTransport, Options = options })) { _clientId = clientId; } internal ManagedIdentityCredential(ResourceIdentifier resourceId, CredentialPipeline pipeline, TokenCredentialOptions options, bool preserveTransport = false) - : this(new ManagedIdentityClient(new ManagedIdentityClientOptions { Pipeline = pipeline, ResourceIdentifier = resourceId, PreserveTransport = preserveTransport, Options = options })) + : this(new ManagedIdentityClient(new ManagedIdentityClientOptions { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedResourceId(resourceId), PreserveTransport = preserveTransport, Options = options })) { _clientId = resourceId.ToString(); } diff --git a/sdk/identity/Azure.Identity/src/Credentials/ManagedIdentityCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/ManagedIdentityCredentialOptions.cs new file mode 100644 index 0000000000000..c4858ad6586b9 --- /dev/null +++ b/sdk/identity/Azure.Identity/src/Credentials/ManagedIdentityCredentialOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Identity +{ + /// + /// Options used to configure the . + /// + public class ManagedIdentityCredentialOptions : TokenCredentialOptions + { + /// + /// Specifies the configuration for the managed identity. + /// + public ManagedIdentityId ManagedIdentityId { get; set; } + } +} diff --git a/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs b/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs index f30a7c1818bca..9c6116f46858a 100644 --- a/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs +++ b/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs @@ -123,10 +123,14 @@ public virtual TokenCredential CreateManagedIdentityCredential() var options = Options.Clone(); options.IsChainedCredential = true; + if (options.ManagedIdentityClientId != null && options.ManagedIdentityResourceId != null) + { + throw new ArgumentException( + $"{nameof(DefaultAzureCredentialOptions)} cannot specify both {nameof(options.ManagedIdentityResourceId)} and {nameof(options.ManagedIdentityClientId)}."); + } + var miOptions = new ManagedIdentityClientOptions { - ResourceIdentifier = options.ManagedIdentityResourceId, - ClientId = options.ManagedIdentityClientId, Pipeline = CredentialPipeline.GetInstance(options, IsManagedIdentityCredential: true), Options = options, InitialImdsConnectionTimeout = TimeSpan.FromSeconds(1), @@ -134,6 +138,19 @@ public virtual TokenCredential CreateManagedIdentityCredential() IsForceRefreshEnabled = options.IsForceRefreshEnabled, }; + if (!string.IsNullOrEmpty(options.ManagedIdentityClientId)) + { + miOptions.ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId(options.ManagedIdentityClientId); + } + else if (options.ManagedIdentityResourceId != null) + { + miOptions.ManagedIdentityId = ManagedIdentityId.FromUserAssignedResourceId(options.ManagedIdentityResourceId); + } + else + { + miOptions.ManagedIdentityId = ManagedIdentityId.SystemAssigned; + } + return new ManagedIdentityCredential(new ManagedIdentityClient(miOptions)); } diff --git a/sdk/identity/Azure.Identity/src/ImdsManagedIdentityProbeSource.cs b/sdk/identity/Azure.Identity/src/ImdsManagedIdentityProbeSource.cs index bfe0a9efaf65d..632a790d9d871 100644 --- a/sdk/identity/Azure.Identity/src/ImdsManagedIdentityProbeSource.cs +++ b/sdk/identity/Azure.Identity/src/ImdsManagedIdentityProbeSource.cs @@ -23,8 +23,7 @@ internal class ImdsManagedIdentityProbeSource : ManagedIdentitySource internal const string GatewayError = "ManagedIdentityCredential authentication unavailable. The request failed due to a gateway error."; internal const string AggregateError = "ManagedIdentityCredential authentication unavailable. Multiple attempts failed to obtain a token from the managed identity endpoint."; - private readonly string _clientId; - private readonly string _resourceId; + private readonly ManagedIdentityId _managedIdentityId; private readonly Uri _imdsEndpoint; private TimeSpan? _imdsNetworkTimeout; private bool _isChainedCredential; @@ -33,8 +32,7 @@ internal class ImdsManagedIdentityProbeSource : ManagedIdentitySource internal ImdsManagedIdentityProbeSource(ManagedIdentityClientOptions options, MsalManagedIdentityClient client) : base(options.Pipeline) { _client = client; - _clientId = options.ClientId; - _resourceId = options.ResourceIdentifier?.ToString(); + _managedIdentityId = options.ManagedIdentityId; _imdsNetworkTimeout = options.InitialImdsConnectionTimeout; _isChainedCredential = options.Options?.IsChainedCredential ?? false; _imdsEndpoint = GetImdsUri(); @@ -69,13 +67,17 @@ protected override Request CreateRequest(string[] scopes) request.Uri.AppendQuery("resource", resource); - if (!string.IsNullOrEmpty(_clientId)) + string idQueryParam = _managedIdentityId?._idType switch { - request.Uri.AppendQuery(Constants.ManagedIdentityClientId, _clientId); - } - if (!string.IsNullOrEmpty(_resourceId)) + ManagedIdentityIdType.ClientId => Constants.ManagedIdentityClientId, + ManagedIdentityIdType.ResourceId => "msi-res-id", + ManagedIdentityIdType.ObjectId => "object_id", + _ => null + }; + + if (idQueryParam != null) { - request.Uri.AppendQuery(Constants.ManagedIdentityResourceId, _resourceId); + request.Uri.AppendQuery(idQueryParam, _managedIdentityId._userAssignedId); } return request; diff --git a/sdk/identity/Azure.Identity/src/ImdsManagedIdentitySource.cs b/sdk/identity/Azure.Identity/src/ImdsManagedIdentitySource.cs index 2fd0640f13689..7ff3f544d7d18 100644 --- a/sdk/identity/Azure.Identity/src/ImdsManagedIdentitySource.cs +++ b/sdk/identity/Azure.Identity/src/ImdsManagedIdentitySource.cs @@ -23,8 +23,7 @@ internal class ImdsManagedIdentitySource : ManagedIdentitySource internal const string GatewayError = "ManagedIdentityCredential authentication unavailable. The request failed due to a gateway error."; internal const string AggregateError = "ManagedIdentityCredential authentication unavailable. Multiple attempts failed to obtain a token from the managed identity endpoint."; - private readonly string _clientId; - private readonly string _resourceId; + private readonly ManagedIdentityId _managedIdentityId; private readonly Uri _imdsEndpoint; private bool _isFirstRequest = true; private TimeSpan? _imdsNetworkTimeout; @@ -32,8 +31,7 @@ internal class ImdsManagedIdentitySource : ManagedIdentitySource internal ImdsManagedIdentitySource(ManagedIdentityClientOptions options) : base(options.Pipeline) { - _clientId = options.ClientId; - _resourceId = options.ResourceIdentifier?.ToString(); + _managedIdentityId = options.ManagedIdentityId; _imdsNetworkTimeout = options.InitialImdsConnectionTimeout; _isChainedCredential = options.Options?.IsChainedCredential ?? false; _imdsEndpoint = GetImdsUri(); @@ -72,13 +70,17 @@ protected override Request CreateRequest(string[] scopes) request.Uri.AppendQuery("resource", resource); - if (!string.IsNullOrEmpty(_clientId)) + string idQueryParam = _managedIdentityId?._idType switch { - request.Uri.AppendQuery(Constants.ManagedIdentityClientId, _clientId); - } - if (!string.IsNullOrEmpty(_resourceId)) + ManagedIdentityIdType.ClientId => Constants.ManagedIdentityClientId, + ManagedIdentityIdType.ResourceId => "msi-res-id", + ManagedIdentityIdType.ObjectId => "object_id", + _ => null + }; + + if (idQueryParam != null) { - request.Uri.AppendQuery(Constants.ManagedIdentityResourceId, _resourceId); + request.Uri.AppendQuery(idQueryParam, _managedIdentityId._userAssignedId); } return request; diff --git a/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs b/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs index 23c6fdfdfa8a2..6c5a4974c2777 100644 --- a/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs +++ b/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs @@ -28,38 +28,34 @@ protected ManagedIdentityClient() } public ManagedIdentityClient(CredentialPipeline pipeline, string clientId = null) - : this(new ManagedIdentityClientOptions { Pipeline = pipeline, ClientId = clientId }) + : this(new ManagedIdentityClientOptions { Pipeline = pipeline, ManagedIdentityId = string.IsNullOrEmpty(clientId) ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.FromUserAssignedClientId(clientId) }) { } public ManagedIdentityClient(CredentialPipeline pipeline, ResourceIdentifier resourceId) - : this(new ManagedIdentityClientOptions { Pipeline = pipeline, ResourceIdentifier = resourceId }) + : this(new ManagedIdentityClientOptions { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedResourceId(resourceId) }) { } public ManagedIdentityClient(ManagedIdentityClientOptions options) { - if (options.ClientId != null && options.ResourceIdentifier != null) - { - throw new ArgumentException( - $"{nameof(ManagedIdentityClientOptions)} cannot specify both {nameof(options.ResourceIdentifier)} and {nameof(options.ClientId)}."); - } - - ClientId = string.IsNullOrEmpty(options.ClientId) ? null : options.ClientId; - ResourceIdentifier = string.IsNullOrEmpty(options.ResourceIdentifier) ? null : options.ResourceIdentifier; + ManagedIdentityId = options.ManagedIdentityId; Pipeline = options.Pipeline; _enableLegacyMI = options.EnableManagedIdentityLegacyBehavior; _isChainedCredential = options.Options?.IsChainedCredential ?? false; _msalManagedIdentityClient = new MsalManagedIdentityClient(options); _identitySource = new Lazy(() => SelectManagedIdentitySource(options, _enableLegacyMI, _msalManagedIdentityClient)); - _msalConfidentialClient = new MsalConfidentialClient(Pipeline, "MANAGED-IDENTITY-RESOURCE-TENENT", ClientId ?? "SYSTEM-ASSIGNED-MANAGED-IDENTITY", AppTokenProviderImpl, options.Options); + _msalConfidentialClient = new MsalConfidentialClient( + Pipeline, + "MANAGED-IDENTITY-RESOURCE-TENENT", + options.ManagedIdentityId._idType != ManagedIdentityIdType.SystemAssigned ? options.ManagedIdentityId._userAssignedId : "SYSTEM-ASSIGNED-MANAGED-IDENTITY", + AppTokenProviderImpl, + options.Options); } internal CredentialPipeline Pipeline { get; } - internal protected string ClientId { get; } - - internal ResourceIdentifier ResourceIdentifier { get; } + internal ManagedIdentityId ManagedIdentityId { get; } public async ValueTask AuthenticateAsync(bool async, TokenRequestContext context, CancellationToken cancellationToken) { @@ -79,7 +75,7 @@ public async ValueTask AuthenticateAsync(bool async, TokenRequestCo } // ServiceFabric does not support specifying user-assigned managed identity by client ID or resource ID. The managed identity selected is based on the resource configuration. - if (availableSource == MSAL.ManagedIdentitySource.ServiceFabric && (ResourceIdentifier != null || ClientId != null)) + if (availableSource == MSAL.ManagedIdentitySource.ServiceFabric && (ManagedIdentityId?._idType != ManagedIdentityIdType.SystemAssigned)) { throw new AuthenticationFailedException(Constants.MiSeviceFabricNoUserAssignedIdentityMessage); } diff --git a/sdk/identity/Azure.Identity/src/ManagedIdentityClientOptions.cs b/sdk/identity/Azure.Identity/src/ManagedIdentityClientOptions.cs index 36dec19a588a5..c0a1aa79f7c27 100644 --- a/sdk/identity/Azure.Identity/src/ManagedIdentityClientOptions.cs +++ b/sdk/identity/Azure.Identity/src/ManagedIdentityClientOptions.cs @@ -10,9 +10,7 @@ internal class ManagedIdentityClientOptions { public TokenCredentialOptions Options { get; set; } - public string ClientId { get; set; } - - public ResourceIdentifier ResourceIdentifier { get; set; } + public ManagedIdentityId ManagedIdentityId { get; set; } = ManagedIdentityId.SystemAssigned; public bool PreserveTransport { get; set; } diff --git a/sdk/identity/Azure.Identity/src/ManagedIdentityId.cs b/sdk/identity/Azure.Identity/src/ManagedIdentityId.cs new file mode 100644 index 0000000000000..26fe223f33117 --- /dev/null +++ b/sdk/identity/Azure.Identity/src/ManagedIdentityId.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core; + +namespace Azure.Identity +{ + /// + /// Defines the configuration for a managed identity enabled on a resource. + /// + public class ManagedIdentityId + { + internal string _userAssignedId; + internal ManagedIdentityIdType _idType; + + private ManagedIdentityId(ManagedIdentityIdType idType, string userAssignedId = null) + { + _idType = idType; + _userAssignedId = userAssignedId; + } + + /// + /// Create an instance of for a system-assigned managed identity. + /// + public static ManagedIdentityId SystemAssigned => + new ManagedIdentityId(ManagedIdentityIdType.SystemAssigned); + + /// + /// Create an instance of for a user-assigned managed identity. + /// + /// The client ID of the user-assigned managed identity. + public static ManagedIdentityId FromUserAssignedClientId(string clientId) => + new ManagedIdentityId(ManagedIdentityIdType.ClientId, clientId); + + /// + /// Create an instance of for a user-assigned managed identity. + /// + /// The resource identifier of the user-assigned managed identity. + public static ManagedIdentityId FromUserAssignedResourceId(ResourceIdentifier resourceIdentifier) => + new ManagedIdentityId(ManagedIdentityIdType.ResourceId, resourceIdentifier.ToString()); + + /// + /// Create an instance of for a user-assigned managed identity. + /// + /// The object ID of the user-assigned managed identity. + public static ManagedIdentityId FromUserAssignedObjectId(string objectId) => + new ManagedIdentityId(ManagedIdentityIdType.ObjectId, objectId); + } + + internal enum ManagedIdentityIdType + { + SystemAssigned, + ClientId, + ResourceId, + ObjectId + } +} diff --git a/sdk/identity/Azure.Identity/src/MsalManagedIdentityClient.cs b/sdk/identity/Azure.Identity/src/MsalManagedIdentityClient.cs index 046179236e2fa..4ccb6383b5c18 100644 --- a/sdk/identity/Azure.Identity/src/MsalManagedIdentityClient.cs +++ b/sdk/identity/Azure.Identity/src/MsalManagedIdentityClient.cs @@ -9,7 +9,6 @@ using Azure.Core; using Azure.Core.Pipeline; using Microsoft.Identity.Client; -using Microsoft.Identity.Client.AppConfig; namespace Azure.Identity { @@ -19,7 +18,7 @@ internal class MsalManagedIdentityClient private bool _isForceRefreshEnabled { get; } internal bool IsSupportLoggingEnabled { get; } - internal ManagedIdentityId ManagedIdentityId { get; } + internal Microsoft.Identity.Client.AppConfig.ManagedIdentityId ManagedIdentityId { get; } internal bool DisableInstanceDiscovery { get; } internal CredentialPipeline Pipeline { get; } internal Uri AuthorityHost { get; } @@ -39,18 +38,15 @@ public MsalManagedIdentityClient(ManagedIdentityClientOptions clientOptions) IsSupportLoggingEnabled = clientOptions?.Options?.IsUnsafeSupportLoggingEnabled ?? false; // select the correct managed identity Id. - if (!string.IsNullOrEmpty(clientOptions?.ClientId)) + ManagedIdentityId = clientOptions.ManagedIdentityId?._idType switch { - ManagedIdentityId = ManagedIdentityId.WithUserAssignedClientId(clientOptions.ClientId); - } - else if (clientOptions?.ResourceIdentifier != null) - { - ManagedIdentityId = ManagedIdentityId.WithUserAssignedResourceId(clientOptions.ResourceIdentifier.ToString()); - } - else - { - ManagedIdentityId = ManagedIdentityId.SystemAssigned; - } + ManagedIdentityIdType.SystemAssigned or null => Microsoft.Identity.Client.AppConfig.ManagedIdentityId.SystemAssigned, + ManagedIdentityIdType.ClientId => Microsoft.Identity.Client.AppConfig.ManagedIdentityId.WithUserAssignedClientId(clientOptions.ManagedIdentityId._userAssignedId), + ManagedIdentityIdType.ResourceId => Microsoft.Identity.Client.AppConfig.ManagedIdentityId.WithUserAssignedResourceId(clientOptions.ManagedIdentityId._userAssignedId), + ManagedIdentityIdType.ObjectId => Microsoft.Identity.Client.AppConfig.ManagedIdentityId.WithUserAssignedObjectId(clientOptions.ManagedIdentityId._userAssignedId), + _ => throw new InvalidOperationException("Invalid ManagedIdentityIdType") + }; + Pipeline = clientOptions.Pipeline; _clientAsyncLock = new AsyncLockWithValue(); _isForceRefreshEnabled = clientOptions.IsForceRefreshEnabled; diff --git a/sdk/identity/Azure.Identity/src/ServiceFabricManagedIdentitySource.cs b/sdk/identity/Azure.Identity/src/ServiceFabricManagedIdentitySource.cs index e26fbd6d8cdf7..99e090b5afb98 100644 --- a/sdk/identity/Azure.Identity/src/ServiceFabricManagedIdentitySource.cs +++ b/sdk/identity/Azure.Identity/src/ServiceFabricManagedIdentitySource.cs @@ -17,8 +17,7 @@ internal class ServiceFabricManagedIdentitySource : ManagedIdentitySource private readonly Uri _endpoint; private readonly string _identityHeaderValue; - private readonly string _clientId; - private readonly string _resourceId; + private ManagedIdentityId _managedIdentityId; public static ManagedIdentitySource TryCreate(ManagedIdentityClientOptions options) { @@ -61,9 +60,8 @@ internal ServiceFabricManagedIdentitySource(CredentialPipeline pipeline, Uri end { _endpoint = endpoint; _identityHeaderValue = identityHeaderValue; - _clientId = options.ClientId; - _resourceId = options.ResourceIdentifier?.ToString(); - if (!string.IsNullOrEmpty(options.ClientId) || null != options.ResourceIdentifier) + _managedIdentityId = options.ManagedIdentityId; + if (options.ManagedIdentityId._idType != ManagedIdentityIdType.SystemAssigned) { AzureIdentityEventSource.Singleton.ServiceFabricManagedIdentityRuntimeConfigurationNotSupported(); } @@ -82,14 +80,18 @@ protected override Request CreateRequest(string[] scopes) request.Uri.AppendQuery("api-version", ServiceFabricMsiApiVersion); request.Uri.AppendQuery("resource", resource); - if (!string.IsNullOrEmpty(_clientId)) + string idQueryParam = _managedIdentityId?._idType switch { - request.Uri.AppendQuery(Constants.ManagedIdentityClientId, _clientId); - } - if (!string.IsNullOrEmpty(_resourceId)) + ManagedIdentityIdType.ClientId => Constants.ManagedIdentityClientId, + ManagedIdentityIdType.ResourceId => Constants.ManagedIdentityResourceId, + _ => null + }; + + if (idQueryParam != null) { - request.Uri.AppendQuery(Constants.ManagedIdentityResourceId, _resourceId); + request.Uri.AppendQuery(idQueryParam, _managedIdentityId._userAssignedId); } + return request; } diff --git a/sdk/identity/Azure.Identity/src/TokenExchangeManagedIdentitySource.cs b/sdk/identity/Azure.Identity/src/TokenExchangeManagedIdentitySource.cs index f3716c0a06691..9700f4dedf394 100644 --- a/sdk/identity/Azure.Identity/src/TokenExchangeManagedIdentitySource.cs +++ b/sdk/identity/Azure.Identity/src/TokenExchangeManagedIdentitySource.cs @@ -29,7 +29,7 @@ public static ManagedIdentitySource TryCreate(ManagedIdentityClientOptions optio { string tokenFilePath = EnvironmentVariables.AzureFederatedTokenFile; string tenantId = EnvironmentVariables.TenantId; - string clientId = options.ClientId ?? EnvironmentVariables.ClientId; + string clientId = options.ManagedIdentityId?._userAssignedId ?? EnvironmentVariables.ClientId; if (options.ExcludeTokenExchangeManagedIdentitySource || string.IsNullOrEmpty(tokenFilePath) || string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(clientId)) { diff --git a/sdk/identity/Azure.Identity/tests/AzureApplicationCredentialTests.cs b/sdk/identity/Azure.Identity/tests/AzureApplicationCredentialTests.cs index c2d811a1483df..789690122be71 100644 --- a/sdk/identity/Azure.Identity/tests/AzureApplicationCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/AzureApplicationCredentialTests.cs @@ -30,7 +30,7 @@ public AzureApplicationCredentialTests(bool isAsync) : base(isAsync) [SetUp] public void TestSetup() { - options.ManagedIdentityClientId = clientId; + options.ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId(clientId); } [Test] @@ -38,7 +38,7 @@ public void CtorValidatesArgs() { new AzureApplicationCredential(null); new AzureApplicationCredential(new AzureApplicationCredentialOptions()); - new AzureApplicationCredential(new AzureApplicationCredentialOptions { ManagedIdentityClientId = "clientId" }); + new AzureApplicationCredential(new AzureApplicationCredentialOptions { ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("clientId") }); } [Test] diff --git a/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialFactoryTests.cs b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialFactoryTests.cs index 5f4d5d4e35828..738ebe4e4f9e4 100644 --- a/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialFactoryTests.cs +++ b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialFactoryTests.cs @@ -39,9 +39,14 @@ public void ValidateManagedIdentityCtorOptionsHonored([Values] bool setClientId, else { ManagedIdentityCredential cred = (ManagedIdentityCredential)factory.CreateManagedIdentityCredential(); - - Assert.AreEqual(expResourceId?.ToString(), cred.Client.ResourceIdentifier?.ToString()); - Assert.AreEqual(expClientId, cred.Client.ClientId); + if (setResourceId) + { + Assert.AreEqual(expResourceId.ToString(), cred.Client.ManagedIdentityId._userAssignedId); + } + if (setClientId) + { + Assert.AreEqual(expClientId, cred.Client.ManagedIdentityId._userAssignedId); + } } } } diff --git a/sdk/identity/Azure.Identity/tests/IdentityTestEnvironment.cs b/sdk/identity/Azure.Identity/tests/IdentityTestEnvironment.cs index e76f387ba9ff0..35b7ffa197f38 100644 --- a/sdk/identity/Azure.Identity/tests/IdentityTestEnvironment.cs +++ b/sdk/identity/Azure.Identity/tests/IdentityTestEnvironment.cs @@ -44,6 +44,7 @@ public class IdentityTestEnvironment : TestEnvironment public string ServicePrincipalSniCertificatePath => GetOptionalVariable("IDENTITY_SP_CERT_SNI") ?? Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "cert.pfx"); public string IdentityTestWebName => GetRecordedVariable("IDENTITY_WEBAPP_NAME"); public string VMUserAssignedManagedIdentityClientId => GetOptionalVariable("IDENTITY_VM_USER_ASSIGNED_MI_CLIENT_ID"); + public string VMUserAssignedManagedIdentityObjectId => GetOptionalVariable("IDENTITY_VM_USER_ASSIGNED_MI_OBJECT_ID"); public string IdentityTestAzFuncName => GetRecordedVariable("IDENTITY_FUNCTION_NAME"); } } diff --git a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialSFLiveTests.cs b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialSFLiveTests.cs index 70ec617f35aa7..3aec95cfbfeb7 100644 --- a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialSFLiveTests.cs +++ b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialSFLiveTests.cs @@ -62,7 +62,7 @@ public async Task ValidateUserAssignedIdentity() CredentialPipeline pipeline = CredentialPipeline.GetInstance(InstrumentClientOptions(new TokenCredentialOptions { Transport = ServiceFabricManagedIdentitySource.GetServiceFabricMITransport() })); - var cred = new ManagedIdentityCredential(new ManagedIdentityClient(new ManagedIdentityClientOptions { Pipeline = pipeline, ClientId = clientId, PreserveTransport = true })); + var cred = new ManagedIdentityCredential(new ManagedIdentityClient(new ManagedIdentityClientOptions { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId(clientId), PreserveTransport = true })); // Hard code service version or recorded tests will fail: https://github.com/Azure/azure-sdk-for-net/issues/10432 var kvoptions = InstrumentClientOptions(new SecretClientOptions(SecretClientOptions.ServiceVersion.V7_0)); diff --git a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs index 53bba7077e7bc..5f600c5f3748b 100644 --- a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs @@ -89,7 +89,7 @@ public async Task VerifyImdsRequestWithClientIdMockAsync() var pipeline = CredentialPipeline.GetInstance(options); ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions() { Pipeline = pipeline, ClientId = "mock-client-id", IsForceRefreshEnabled = true, Options = options }))); + new ManagedIdentityClientOptions() { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("mock-client-id"), IsForceRefreshEnabled = true, Options = options }))); AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); @@ -119,7 +119,7 @@ public async Task ImdsWithEmptyClientIdIsIgnoredMockAsync() ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions() { Pipeline = pipeline, ClientId = string.Empty, IsForceRefreshEnabled = true, Options = options }) + new ManagedIdentityClientOptions() { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.SystemAssigned, IsForceRefreshEnabled = true, Options = options }) )); AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); @@ -151,7 +151,7 @@ public async Task VerifyImdsRequestWithClientIdAndRegionalAuthorityNameMockAsync var pipeline = CredentialPipeline.GetInstance(options); ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions() { Pipeline = pipeline, ClientId = "mock-client-id", IsForceRefreshEnabled = true, Options = options }))); + new ManagedIdentityClientOptions() { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("mock-client-id"), IsForceRefreshEnabled = true, Options = options }))); AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default, tenantId: Guid.NewGuid().ToString())); @@ -171,7 +171,7 @@ public async Task VerifyImdsRequestWithClientIdAndNonPubCloudMockAsync(Uri autho var pipeline = CredentialPipeline.GetInstance(options); ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions() { Pipeline = pipeline, ClientId = "mock-client-id", IsForceRefreshEnabled = true, Options = options }))); + new ManagedIdentityClientOptions() { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("mock-client-id"), IsForceRefreshEnabled = true, Options = options }))); AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default, tenantId: Guid.NewGuid().ToString())); @@ -203,7 +203,7 @@ public async Task VerifyImdsRequestWithResourceIdMockAsync() new ManagedIdentityClientOptions() { Pipeline = CredentialPipeline.GetInstance(options), - ResourceIdentifier = new ResourceIdentifier(_expectedResourceId), + ManagedIdentityId = ManagedIdentityId.FromUserAssignedResourceId(new ResourceIdentifier(_expectedResourceId)), IsForceRefreshEnabled = true, Options = options }) @@ -224,6 +224,42 @@ public async Task VerifyImdsRequestWithResourceIdMockAsync() Assert.That(Uri.UnescapeDataString(query), Does.Contain($"{Constants.ManagedIdentityResourceId}={_expectedResourceId}")); } + [NonParallelizable] + [Test] + public async Task VerifyImdsRequestWithObjectIdMockAsync() + { + using var environment = new TestEnvVar(new() { { "MSI_ENDPOINT", null }, { "MSI_SECRET", null }, { "IDENTITY_ENDPOINT", null }, { "IDENTITY_HEADER", null }, { "AZURE_POD_IDENTITY_AUTHORITY_HOST", null } }); + + var response = CreateMockResponse(200, ExpectedToken); + var mockTransport = new MockTransport(response); + var options = new TokenCredentialOptions { Transport = mockTransport }; + var expectedObjectId = Guid.NewGuid().ToString(); + + ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( + new ManagedIdentityClient( + new ManagedIdentityClientOptions() + { + Pipeline = CredentialPipeline.GetInstance(options), + ManagedIdentityId = ManagedIdentityId.FromUserAssignedObjectId(expectedObjectId), + IsForceRefreshEnabled = true, + Options = options + }) + )); + + AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); + + Assert.AreEqual(ExpectedToken, actualToken.Token); + + MockRequest request = mockTransport.Requests[0]; + + string query = request.Uri.Query; + + Assert.AreEqual(request.Uri.Host, "169.254.169.254"); + Assert.AreEqual(request.Uri.Path, "/metadata/identity/oauth2/token"); + Assert.IsTrue(query.Contains("api-version=2018-02-01")); + Assert.That(Uri.UnescapeDataString(query), Does.Contain($"object_id={expectedObjectId}")); + } + [NonParallelizable] [Test] [TestCaseSource(nameof(ResourceAndClientIds))] @@ -255,8 +291,8 @@ public async Task VerifyServiceFabricRequestWithResourceIdMockAsync(string clien ManagedIdentityClientOptions clientOptions = (clientId, includeResourceIdentifier) switch { - (Item1: null, Item2: true) => new ManagedIdentityClientOptions() { ClientId = null, ResourceIdentifier = new ResourceIdentifier(_expectedResourceId), Pipeline = pipeline, IsForceRefreshEnabled = true }, - (Item1: not null, Item2: false) => new ManagedIdentityClientOptions() { ClientId = clientId, ResourceIdentifier = null, Pipeline = pipeline, IsForceRefreshEnabled = true }, + (Item1: null, Item2: true) => new ManagedIdentityClientOptions() { ManagedIdentityId = ManagedIdentityId.FromUserAssignedResourceId(new ResourceIdentifier(_expectedResourceId)), Pipeline = pipeline, IsForceRefreshEnabled = true }, + (Item1: not null, Item2: false) => new ManagedIdentityClientOptions() { ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId(clientId), Pipeline = pipeline, IsForceRefreshEnabled = true }, _ => null // TODO: remove null logic and uncomment the following line once MSAL is able to take a custom transport for Service Fabric MI source //_ => new ManagedIdentityClientOptions() { ClientId = null, ResourceIdentifier = null, Pipeline = pipeline, Options = options, PreserveTransport = true, IsForceRefreshEnabled = true } }; @@ -318,9 +354,9 @@ public async Task VerifyArcRequestWithResourceIdMockAsync(string clientId, bool ManagedIdentityClientOptions clientOptions = (clientId, includeResourceIdentifier) switch { - (Item1: null, Item2: true) => new ManagedIdentityClientOptions() { ClientId = null, ResourceIdentifier = new ResourceIdentifier(_expectedResourceId), Pipeline = pipeline, IsForceRefreshEnabled = true }, - (Item1: not null, Item2: false) => new ManagedIdentityClientOptions() { ClientId = clientId, ResourceIdentifier = null, Pipeline = pipeline, IsForceRefreshEnabled = true }, - _ => new ManagedIdentityClientOptions() { ClientId = null, ResourceIdentifier = null, Pipeline = pipeline, Options = options, PreserveTransport = true, IsForceRefreshEnabled = true } + (Item1: null, Item2: true) => new ManagedIdentityClientOptions() { ManagedIdentityId = ManagedIdentityId.FromUserAssignedResourceId(new ResourceIdentifier(_expectedResourceId)), Pipeline = pipeline, IsForceRefreshEnabled = true }, + (Item1: not null, Item2: false) => new ManagedIdentityClientOptions() { ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId(clientId), Pipeline = pipeline, IsForceRefreshEnabled = true }, + _ => new ManagedIdentityClientOptions() { ManagedIdentityId = ManagedIdentityId.SystemAssigned, Pipeline = pipeline, Options = options, PreserveTransport = true, IsForceRefreshEnabled = true } }; ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential(new ManagedIdentityClient(clientOptions))); @@ -360,7 +396,7 @@ public void VerifyImdsRequestFailurePopulatesExceptionMessage() ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions() { Pipeline = pipeline, ClientId = "mock-client-id", IsForceRefreshEnabled = true, Options = options }) + new ManagedIdentityClientOptions() { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("mock-client-id"), IsForceRefreshEnabled = true, Options = options }) )); var ex = Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); Assert.That(ex.Message, Does.Contain(expectedMessage)); @@ -384,7 +420,7 @@ public void VerifyImdsRequestFailureForDockerDesktopThrowsCUE(string error) ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions() { Pipeline = pipeline, ClientId = "mock-client-id", IsForceRefreshEnabled = true, Options = options }))); + new ManagedIdentityClientOptions() { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("mock-client-id"), IsForceRefreshEnabled = true, Options = options }))); var ex = Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); Assert.That(ex.InnerException.Message, Does.Contain(expectedMessage)); @@ -421,7 +457,7 @@ public void VerifyImdsRequestHandlesFailedRequestWithCredentialUnavailableExcept var pipeline = CredentialPipeline.GetInstance(options); ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions() { Pipeline = pipeline, ClientId = "mock-client-id", IsForceRefreshEnabled = true, Options = options }))); + new ManagedIdentityClientOptions() { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("mock-client-id"), IsForceRefreshEnabled = true, Options = options }))); var ex = Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); @@ -443,7 +479,7 @@ public async Task VerifyIMDSRequestWithPodIdentityEnvVarMockAsync(string clientI ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions() { ClientId = clientId, Pipeline = pipeline, IsForceRefreshEnabled = true, Options = options }) + new ManagedIdentityClientOptions() { ManagedIdentityId = clientId is null ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.FromUserAssignedClientId(clientId), Pipeline = pipeline, IsForceRefreshEnabled = true, Options = options }) )); AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); @@ -566,7 +602,7 @@ public async Task VerifyAppService2019RequestWithClientIdMockAsync() ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions { Pipeline = pipeline, ClientId = "mock-client-id", PreserveTransport = false, Options = options, IsForceRefreshEnabled = true }) + new ManagedIdentityClientOptions { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("mock-client-id"), PreserveTransport = false, Options = options, IsForceRefreshEnabled = true }) )); AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); @@ -598,7 +634,7 @@ public async Task VerifyAppService2019RequestWithResourceIdMockAsync() ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions { Pipeline = pipeline, ResourceIdentifier = new ResourceIdentifier(resourceId), PreserveTransport = false, Options = options, IsForceRefreshEnabled = true }) + new ManagedIdentityClientOptions { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedResourceId(new ResourceIdentifier(resourceId)), PreserveTransport = false, Options = options, IsForceRefreshEnabled = true }) )); AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); @@ -685,9 +721,9 @@ public async Task VerifyCloudShellMsiRequestWithClientIdMockAsync(string clientI ManagedIdentityClientOptions clientOptions = (clientId, includeResourceIdentifier) switch { - (Item1: null, Item2: true) => new ManagedIdentityClientOptions() { ClientId = null, ResourceIdentifier = new ResourceIdentifier(_expectedResourceId), Pipeline = pipeline, IsForceRefreshEnabled = true }, - (Item1: not null, Item2: false) => new ManagedIdentityClientOptions() { ClientId = clientId, ResourceIdentifier = null, Pipeline = pipeline, IsForceRefreshEnabled = true }, - _ => new ManagedIdentityClientOptions() { ClientId = null, ResourceIdentifier = null, Pipeline = pipeline, Options = options, PreserveTransport = true, IsForceRefreshEnabled = true } + (Item1: null, Item2: true) => new ManagedIdentityClientOptions() { ManagedIdentityId = ManagedIdentityId.FromUserAssignedResourceId(new ResourceIdentifier(_expectedResourceId)), Pipeline = pipeline, IsForceRefreshEnabled = true }, + (Item1: not null, Item2: false) => new ManagedIdentityClientOptions() { ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId(clientId), Pipeline = pipeline, IsForceRefreshEnabled = true }, + _ => new ManagedIdentityClientOptions() { ManagedIdentityId = ManagedIdentityId.SystemAssigned, Pipeline = pipeline, Options = options, PreserveTransport = true, IsForceRefreshEnabled = true } }; ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential(new ManagedIdentityClient(clientOptions))); @@ -736,7 +772,7 @@ public async Task VerifyMsiUnavailableOnIMDSRequestFailedExcpetion() { throw new TaskCanceledException(); }); - var options = new TokenCredentialOptions() {IsChainedCredential = true, Transport = mockTransport }; + var options = new TokenCredentialOptions() { IsChainedCredential = true, Transport = mockTransport }; ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( @@ -832,7 +868,7 @@ public async Task VerifyClientAuthenticateReturnsInvalidJsonOnFailure([Values(40 ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions() { Pipeline = pipeline, ClientId = "mock-client-id", IsForceRefreshEnabled = true, Options = options }))); + new ManagedIdentityClientOptions() { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("mock-client-id"), IsForceRefreshEnabled = true, Options = options }))); var ex = Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); await Task.CompletedTask; @@ -858,7 +894,7 @@ public async Task VerifyClientAuthenticateReturnsErrorResponse() ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions() { Pipeline = pipeline, ClientId = "mock-client-id", IsForceRefreshEnabled = true, Options = options }))); + new ManagedIdentityClientOptions() { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("mock-client-id"), IsForceRefreshEnabled = true, Options = options }))); var ex = Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); Assert.That(ex.Message, Does.Contain(errorMessage)); @@ -891,7 +927,7 @@ public async Task RetriesOnRetriableStatusCode([Values(404, 410, 500)] int statu ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential( new ManagedIdentityClient( - new ManagedIdentityClientOptions() { Pipeline = pipeline, ClientId = "mock-client-id", IsForceRefreshEnabled = true, Options = options }))); + new ManagedIdentityClientOptions() { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("mock-client-id"), IsForceRefreshEnabled = true, Options = options }))); var ex = Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); Assert.That(ex.Message, Does.Contain(errorMessage)); diff --git a/sdk/identity/Azure.Identity/tests/ManagedIdentitySourceTests.cs b/sdk/identity/Azure.Identity/tests/ManagedIdentitySourceTests.cs index 1f7d981479984..e36a2a8e35da9 100644 --- a/sdk/identity/Azure.Identity/tests/ManagedIdentitySourceTests.cs +++ b/sdk/identity/Azure.Identity/tests/ManagedIdentitySourceTests.cs @@ -35,7 +35,7 @@ public static IEnumerable ManagedIdentitySources() var miCredOptions = new ManagedIdentityClientOptions { Pipeline = pipeline }; var endpoint = new Uri("https://localhost"); - yield return new object[] { new ImdsManagedIdentitySource(new ManagedIdentityClientOptions { Pipeline = pipeline, ClientId = "mock-client-id" }) }; + yield return new object[] { new ImdsManagedIdentitySource(new ManagedIdentityClientOptions { Pipeline = pipeline, ManagedIdentityId = ManagedIdentityId.FromUserAssignedClientId("mock-client-id") }) }; yield return new object[] { new AppServiceV2017ManagedIdentitySource(pipeline, endpoint, "mysecret", miCredOptions) }; yield return new object[] { new AppServiceV2019ManagedIdentitySource(pipeline, endpoint, "mysecret", miCredOptions) }; yield return new object[] { new AzureArcManagedIdentitySource(endpoint, miCredOptions) }; diff --git a/sdk/identity/Azure.Identity/tests/ManagedIdentityVMIntegrationTests.cs b/sdk/identity/Azure.Identity/tests/ManagedIdentityVMIntegrationTests.cs index 9275d2eaf28e4..a932a40be0293 100644 --- a/sdk/identity/Azure.Identity/tests/ManagedIdentityVMIntegrationTests.cs +++ b/sdk/identity/Azure.Identity/tests/ManagedIdentityVMIntegrationTests.cs @@ -16,13 +16,35 @@ public ManagedIdentityVMIntegrationTests(bool isAsync) : base(isAsync) [LiveOnly] [RunOnlyOnPlatforms(SelfHostedAgent = true)] [Category("IdentityVM")] + [TestCase(ManagedIdentityIdType.SystemAssigned)] + [TestCase(ManagedIdentityIdType.ClientId)] + [TestCase(ManagedIdentityIdType.ObjectId)] // This test leverages the test app found in Azure.Identity\integration\WebApp // It validates that ManagedIdentityCredential can acquire a token in an actual Azure Web App environment - public async Task GetManagedIdentityToken() + public async Task GetManagedIdentityToken(ManagedIdentityIdType idType) { - var cred = new ManagedIdentityCredential(TestEnvironment.VMUserAssignedManagedIdentityClientId); + ManagedIdentityId managedIdentityId = idType switch + { + ManagedIdentityIdType.ClientId => ManagedIdentityId.FromUserAssignedClientId(TestEnvironment.VMUserAssignedManagedIdentityClientId), + ManagedIdentityIdType.ObjectId => ManagedIdentityId.FromUserAssignedObjectId(TestEnvironment.VMUserAssignedManagedIdentityObjectId), + _ => ManagedIdentityId.SystemAssigned + }; + ManagedIdentityCredentialOptions options = new ManagedIdentityCredentialOptions() { ManagedIdentityId = managedIdentityId }; + + var cred = new ManagedIdentityCredential(options); var token = await cred.GetTokenAsync(new(CredentialTestHelpers.DefaultScope)); Assert.NotNull(token.Token); + + var cred2 = new ManagedIdentityCredential(managedIdentityId); + token = await cred2.GetTokenAsync(new(CredentialTestHelpers.DefaultScope)); + Assert.NotNull(token.Token); + } + + public enum ManagedIdentityIdType + { + SystemAssigned, + ClientId, + ObjectId } } } diff --git a/sdk/identity/Azure.Identity/tests/Mock/MockManagedIdentityClient.cs b/sdk/identity/Azure.Identity/tests/Mock/MockManagedIdentityClient.cs index 8e32ee081eb13..aeee33340b4df 100644 --- a/sdk/identity/Azure.Identity/tests/Mock/MockManagedIdentityClient.cs +++ b/sdk/identity/Azure.Identity/tests/Mock/MockManagedIdentityClient.cs @@ -21,7 +21,7 @@ public MockManagedIdentityClient(CredentialPipeline pipeline) } public MockManagedIdentityClient(CredentialPipeline pipeline, string clientId) - : base(new ManagedIdentityClientOptions() { Pipeline = pipeline, ClientId = clientId, Options = new TokenCredentialOptions() { IsChainedCredential = true } }) + : base(new ManagedIdentityClientOptions() { Pipeline = pipeline, ManagedIdentityId = clientId is null ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.FromUserAssignedClientId(clientId), Options = new TokenCredentialOptions() { IsChainedCredential = true } }) { } diff --git a/sdk/identity/Azure.Identity/tests/samples/ReadmeSnippets.cs b/sdk/identity/Azure.Identity/tests/samples/ReadmeSnippets.cs index 30ee537101392..f84308f6f0cc5 100644 --- a/sdk/identity/Azure.Identity/tests/samples/ReadmeSnippets.cs +++ b/sdk/identity/Azure.Identity/tests/samples/ReadmeSnippets.cs @@ -75,11 +75,38 @@ public void AuthenticatingWithAuthorityHost() [Test] public void AuthenticatingWithManagedIdentityCredentialUserAssigned() { - string userAssignedClientId = ""; - #region Snippet:AuthenticatingWithManagedIdentityCredentialUserAssigned + string userAssignedClientId = "some client ID"; + + var credential = new ManagedIdentityCredential( + ManagedIdentityId.FromUserAssignedClientId(userAssignedClientId)); + var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), credential); + + #endregion + } + + [Test] + public void AuthenticatingWithManagedIdentityCredentialUserAssignedResourceId() + { + #region Snippet:AuthenticatingWithManagedIdentityCredentialUserAssignedResourceId + ResourceIdentifier userAssignedResourceId = new ResourceIdentifier( + "/subscriptions//resourcegroups//providers/Microsoft.ManagedIdentity/userAssignedIdentities/"); + + var credential = new ManagedIdentityCredential( + ManagedIdentityId.FromUserAssignedResourceId(userAssignedResourceId)); + var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), credential); + + #endregion + } + + [Test] + public void AuthenticatingWithManagedIdentityCredentialUserAssignedObjectId() + { + #region Snippet:AuthenticatingWithManagedIdentityCredentialUserAssignedObjectId + string userAssignedObjectId = "some object ID"; - var credential = new ManagedIdentityCredential(clientId: userAssignedClientId); + var credential = new ManagedIdentityCredential( + ManagedIdentityId.FromUserAssignedObjectId(userAssignedObjectId)); var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), credential); #endregion @@ -90,7 +117,7 @@ public void AuthenticatingWithManagedIdentityCredentialSystemAssigned() { #region Snippet:AuthenticatingWithManagedIdentityCredentialSystemAssigned - var credential = new ManagedIdentityCredential(); + var credential = new ManagedIdentityCredential(ManagedIdentityId.SystemAssigned); var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), credential); #endregion