Support to automatically check for device code if all other auth sources failed. (#746)

This commit is contained in:
Andres Paz 2021-06-22 00:51:00 -07:00 коммит произвёл GitHub
Родитель 9aa9e47013
Коммит 45d5ae0d46
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 172 добавлений и 50 удалений

Просмотреть файл

@ -4,6 +4,7 @@
#nullable enable
using System;
using System.Linq;
using Azure.Identity;
@ -18,7 +19,7 @@ namespace Microsoft.Azure.Quantum.Test
private const string SUBSCRIPTION = "916dfd6d-030c-4bd9-b579-7bb6d1926e97";
[DataTestMethod]
[DataRow(CredentialType.Default, typeof(DefaultAzureCredential))]
[DataRow(CredentialType.Default, typeof(DefaultQuantumCredential))]
[DataRow(CredentialType.Environment, typeof(EnvironmentCredential))]
[DataRow(CredentialType.ManagedIdentity, typeof(ManagedIdentityCredential))]
[DataRow(CredentialType.CLI, typeof(AzureCliCredential))]
@ -72,5 +73,29 @@ namespace Microsoft.Azure.Quantum.Test
var actual = CredentialFactory.ExtractTenantIdFromBearer(bearer);
Assert.AreEqual(expected, actual);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(SUBSCRIPTION)]
public void TestDefaultCredentialSources(string? subscriptionId)
{
var credential = CredentialFactory.CreateCredential(CredentialType.Default, subscriptionId) as DefaultQuantumCredential;
Assert.IsNotNull(credential);
var actual = credential?.Sources.Select(c => c.GetType()).ToArray();
var expected = new Type[]
{
typeof(EnvironmentCredential),
typeof(ManagedIdentityCredential),
typeof(AzureCliCredential),
typeof(SharedTokenCacheCredential),
typeof(VisualStudioCodeCredential),
typeof(InteractiveBrowserCredential),
typeof(DeviceCodeCredential),
};
CollectionAssert.AreEqual(expected, actual);
}
}
}

Просмотреть файл

@ -88,61 +88,28 @@ namespace Microsoft.Azure.Quantum.Authentication
// Used to catch all the TenantIds:
private static readonly Dictionary<string, string?> TenantIds = new Dictionary<string, string?>();
/// <summary>
/// Creates a TokenCredential that can be used to authenticate with Azure.
/// If a subscriptionId is provided, it will automatically try to identify the corresponding tenantId
/// and use that for authorization; otherwise it uses defaults.
/// </summary>
/// <param name="credentialType">The type of credential to create</param>
/// <param name="subscriptionId">The subscriptionId</param>
/// <returns>A TokenCredential class to use to fetch an authentication token from AAD.</returns>
public static TokenCredential CreateCredential(CredentialType credentialType, string? subscriptionId = null) => credentialType switch
{
CredentialType.SharedToken => CreateCredential(credentialType, () => SharedTokenOptions(subscriptionId)),
CredentialType.VisualStudio => CreateCredential(credentialType, () => VisualStudioOptions(subscriptionId)),
CredentialType.VisualStudioCode => CreateCredential(credentialType, () => VisualStudioCodeOptions(subscriptionId)),
CredentialType.Interactive => CreateCredential(credentialType, () => InteractiveOptions(subscriptionId)),
CredentialType.DeviceCode => CreateCredential(credentialType, () => DeviceCodeOptions(subscriptionId)),
CredentialType.Default => CreateCredential(credentialType, () => DefaultOptions(subscriptionId)),
_ => CreateCredential(credentialType, () => DefaultOptions(subscriptionId)),
};
/// <summary>
/// Creates an instance of TokenCredential that corresponds to the given <see cref="CredentialType"/>.
/// It creates an instance of the Credential Class with default parameters.
/// </summary>
/// <param name="credentialType">The type of Credential Class to create.</param>
/// <param name="options">A configuration method for the corresponding credential options (not used for Environment, ManagedIdentity or CLI credentials).</param>
/// <returns>An instance of TokenCredential for the corresponding value.</returns>
public static TokenCredential CreateCredential(CredentialType credentialType, Func<TokenCredentialOptions> options) => credentialType switch
{
CredentialType.Default => new DefaultQuantumCredential(subscriptionId),
CredentialType.Environment => new EnvironmentCredential(),
CredentialType.ManagedIdentity => new ManagedIdentityCredential(),
CredentialType.CLI => new AzureCliCredential(),
CredentialType.DeviceCode => new DeviceCodeCredential(options: options?.Invoke() as DeviceCodeCredentialOptions),
CredentialType.SharedToken => new SharedTokenCacheCredential(options: options?.Invoke() as SharedTokenCacheCredentialOptions),
CredentialType.VisualStudio => new VisualStudioCredential(options: options?.Invoke() as VisualStudioCredentialOptions),
CredentialType.VisualStudioCode => new VisualStudioCodeCredential(options: options?.Invoke() as VisualStudioCodeCredentialOptions),
CredentialType.Interactive => new InteractiveBrowserCredential(options: options?.Invoke() as InteractiveBrowserCredentialOptions),
CredentialType.Default => new DefaultAzureCredential(options: options?.Invoke() as DefaultAzureCredentialOptions),
CredentialType.DeviceCode => new DeviceCodeCredential(options: DeviceCodeOptions(subscriptionId)),
CredentialType.SharedToken => new SharedTokenCacheCredential(options: SharedTokenOptions(subscriptionId)),
CredentialType.VisualStudio => new VisualStudioCredential(options: VisualStudioOptions(subscriptionId)),
CredentialType.VisualStudioCode => new VisualStudioCodeCredential(options: VisualStudioCodeOptions(subscriptionId)),
CredentialType.Interactive => new InteractiveBrowserCredential(options: InteractiveOptions(subscriptionId)),
_ => throw new ArgumentException($"Credentials of type {credentialType} are not supported.")
};
/// <summary>
/// Returns an DefaultAzureCredentialOptions, populated with the TenantId for the given subscription.
/// We als disabilitate VisualStudio credentials, since they don't currently work with Azure Quantum.
/// </summary>
/// <param name="subscriptionid">An subscription Id.</param>
/// <returns>A new instance of InteractiveBrowserCredentialOptions with the TenantId populated</returns>
public static DefaultAzureCredentialOptions DefaultOptions(string? subscriptionid)
{
string? tenantId = GetTenantId(subscriptionid);
return new DefaultAzureCredentialOptions
{
// Disable VS credentials until https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1332071 is fixed:
ExcludeVisualStudioCredential = true,
ExcludeInteractiveBrowserCredential = false,
InteractiveBrowserTenantId = tenantId,
SharedTokenCacheTenantId = tenantId,
VisualStudioCodeTenantId = tenantId,
VisualStudioTenantId = tenantId,
};
}
/// <summary>
/// Returns an InteractiveBrowserCredentialOptions, populated with the TenantId for the given subscription.
/// </summary>
@ -208,7 +175,7 @@ namespace Microsoft.Azure.Quantum.Authentication
/// <returns>The tenantId for the given subscription; null if it can be found or for a null subscription.</returns>
public static string? GetTenantId(string? subscriptionId)
{
if (subscriptionId == null)
if (string.IsNullOrWhiteSpace(subscriptionId))
{
return null;
}
@ -273,7 +240,6 @@ namespace Microsoft.Azure.Quantum.Authentication
{
return null;
}
}
}
}

Просмотреть файл

@ -0,0 +1,131 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#nullable enable
namespace Microsoft.Azure.Quantum.Authentication
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using global::Azure.Core;
using global::Azure.Core.Pipeline;
using global::Azure.Identity;
/// <summary>
/// Provides a simplified authentication for quantum users by checking in order the following type of credentials.
/// - Environment
/// - ManagedIdentity
/// - CLI
/// - SharedToken
/// - VisualStudio
/// - VisualStudioCode
/// - Interactive
/// - DeviceCode
/// It will automatically pick the first credentials it can succesfully to use to login with Azure.
/// If not successful to use any of the credentials, it throws a CredentialUnavailableException.
/// </summary>
public class DefaultQuantumCredential : TokenCredential
{
private TokenCredential? _active = null;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultQuantumCredential"/> class.
/// </summary>
/// <param name="subscriptionId">The subscription id.</param>
public DefaultQuantumCredential(string? subscriptionId = null)
{
this.SubscriptionId = subscriptionId;
}
/// <summary>
/// The SubscriptionId this credentials are set for.
/// </summary>
public string? SubscriptionId { get; }
/// <summary>
/// The list of sources that will be used, in order, to get credentials
/// </summary>
public virtual IEnumerable<TokenCredential> Sources
{
get
{
yield return CredentialFactory.CreateCredential(CredentialType.Environment, SubscriptionId);
yield return CredentialFactory.CreateCredential(CredentialType.ManagedIdentity, SubscriptionId);
yield return CredentialFactory.CreateCredential(CredentialType.CLI, SubscriptionId);
yield return CredentialFactory.CreateCredential(CredentialType.SharedToken, SubscriptionId);
// Disable VS credentials until https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1332071 is fixed:
//yield return CredentialFactory.CreateCredential(CredentialType.VisualStudio, SubscriptionId);
yield return CredentialFactory.CreateCredential(CredentialType.VisualStudioCode, SubscriptionId);
yield return CredentialFactory.CreateCredential(CredentialType.Interactive, SubscriptionId);
yield return CredentialFactory.CreateCredential(CredentialType.DeviceCode, SubscriptionId);
}
}
/// <summary>
/// Sequentially calls <see cref="TokenCredential.GetToken"/> on all the specified sources, returning the first successfully obtained <see cref="AccessToken"/>. This method is called automatically by Azure SDK client libraries. You may call this method directly, but you must also handle token caching and token refreshing.
/// </summary>
/// <param name="requestContext">The details of the authentication request.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>The first <see cref="AccessToken"/> returned by the specified sources. Any credential which raises an Exception will be skipped.</returns>
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken = default)
=> GetTokenImplAsync(false, requestContext, cancellationToken).GetAwaiter().GetResult();
/// <summary>
/// Sequentially calls <see cref="TokenCredential.GetToken"/> on all the specified sources, returning the first successfully obtained <see cref="AccessToken"/>. This method is called automatically by Azure SDK client libraries. You may call this method directly, but you must also handle token caching and token refreshing.
/// </summary>
/// <param name="requestContext">The details of the authentication request.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>The first <see cref="AccessToken"/> returned by the specified sources. Any credential which raises an Exception will be skipped.</returns>
public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default)
=> await GetTokenImplAsync(true, requestContext, cancellationToken).ConfigureAwait(false);
private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestContext requestContext, CancellationToken cancellationToken)
{
if (_active != null)
{
return async
? await _active.GetTokenAsync(requestContext, cancellationToken).ConfigureAwait(false)
: _active.GetToken(requestContext, cancellationToken);
}
List<Exception> exceptions = new List<Exception>();
foreach (TokenCredential source in Sources)
{
try
{
AccessToken token = async
? await source.GetTokenAsync(requestContext, cancellationToken).ConfigureAwait(false)
: source.GetToken(requestContext, cancellationToken);
// TODO: find a cleaner method to report the credential used:
if (source.GetType() != typeof(InteractiveBrowserCredential) && source.GetType() != typeof(DeviceCodeCredential))
{
Console.WriteLine($"Authenticated using {source}");
}
_active = source;
return token;
}
catch (Exception e) when (cancellationToken.IsCancellationRequested)
{
throw e;
}
catch (Exception e)
{
var msg = $"{source.GetType().Name}: {e.Message}";
exceptions.Add(new CredentialUnavailableException(msg, e));
}
}
var reasons = string.Join('\n', exceptions.Select(e => e.Message));
throw new CredentialUnavailableException(
$"Unable to authenticate. Failed to acquire a token from the different credentials sources for the following reasons:\n{reasons}",
new AggregateException(exceptions));
}
}
}