Support to automatically check for device code if all other auth sources failed. (#746)
This commit is contained in:
Родитель
9aa9e47013
Коммит
45d5ae0d46
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче