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

Implement objectId support for ManagedIdentityCredential #45605

Merged
merged 14 commits into from
Sep 4, 2024
1 change: 1 addition & 0 deletions sdk/identity/Azure.Identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 1.13.0-beta.2 (Unreleased)

### Features Added
- `ManagedIdentityCredential` now supports specifying a user-assigned identity by object ID.
christothes marked this conversation as resolved.
Show resolved Hide resolved

### Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,27 @@ 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) { }
[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<Azure.Core.AccessToken> 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() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal class AzureArcManagedIdentitySource : ManagedIdentitySource
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.";
christothes marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand All @@ -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");
}
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ internal class AzureApplicationCredentialOptions : TokenCredentialOptions
/// <summary>
/// Specifies the client id of the azure ManagedIdentity in the case of user assigned identity.
/// </summary>
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading.Tasks;
using Azure.Core.Pipeline;
using System.Linq;
using System.ComponentModel;

namespace Azure.Identity
{
Expand Down Expand Up @@ -41,8 +42,9 @@ protected ManagedIdentityCredential()
/// If not provided, a system-assigned managed identity is used.
/// </param>
/// <param name="options">Options to configure the management of the requests sent to Microsoft Entra ID.</param>
[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 }))
christothes marked this conversation as resolved.
Show resolved Hide resolved
{
_logAccountDetails = options?.Diagnostics?.IsAccountIdentifierLoggingEnabled ?? false;
}
Expand All @@ -54,21 +56,33 @@ public ManagedIdentityCredential(string clientId = null, TokenCredentialOptions
/// The resource ID to authenticate for a <see href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview#how-a-user-assigned-managed-identity-works-with-an-azure-vm">user-assigned managed identity</see>.
/// </param>
/// <param name="options">Options to configure the management of the requests sent to Microsoft Entra ID.</param>
[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();
}

/// <summary>
/// Creates an instance of <see cref="ManagedIdentityCredential"/> configured with the specified options.
/// </summary>
/// <param name="options">The options used to configure the credential.</param>
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));
christothes marked this conversation as resolved.
Show resolved Hide resolved
_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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Identity
{
/// <summary>
/// Options used to configure the <see cref="ManagedIdentityCredential"/>.
/// </summary>
public class ManagedIdentityCredentialOptions : TokenCredentialOptions
{
/// <summary>
/// Specifies the configuration for the managed identity.
/// </summary>
public ManagedIdentityId ManagedIdentityId { get; set; }
christothes marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,34 @@ public virtual TokenCredential CreateManagedIdentityCredential()
var options = Options.Clone<DefaultAzureCredentialOptions>();
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),
ExcludeTokenExchangeManagedIdentitySource = options.ExcludeWorkloadIdentityCredential,
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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -69,13 +67,16 @@ 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 => Constants.ManagedIdentityResourceId,
christothes marked this conversation as resolved.
Show resolved Hide resolved
_ => null
};

if (idQueryParam != null)
{
request.Uri.AppendQuery(Constants.ManagedIdentityResourceId, _resourceId);
request.Uri.AppendQuery(idQueryParam, _managedIdentityId._userAssignedId);
}

return request;
Expand Down
19 changes: 10 additions & 9 deletions sdk/identity/Azure.Identity/src/ImdsManagedIdentitySource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,15 @@ 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;
private bool _isChainedCredential;

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();
Expand Down Expand Up @@ -72,13 +70,16 @@ 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 => Constants.ManagedIdentityResourceId,
christothes marked this conversation as resolved.
Show resolved Hide resolved
_ => null
};

if (idQueryParam != null)
{
request.Uri.AppendQuery(Constants.ManagedIdentityResourceId, _resourceId);
request.Uri.AppendQuery(idQueryParam, _managedIdentityId._userAssignedId);
}

return request;
Expand Down
Loading