From c2317e23fc49451a1425de47150cf0469284df6e Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Tue, 7 Jan 2020 13:35:31 -0800 Subject: [PATCH] Support `AZURE_AUTH_LOCATION` This change introduces a new type, `SdkAuthFileCredential` which can be used to support authenticating with an "SDK Auth File", which the `az` CLI can write. The file itself is just a JSON document that has information about the Tenent, Client ID and Client Secret. Under the hood, we just create a ClientSecretCredential with the information from the SDK Auth File. We also add `AZURE_AUTH_LOCATION` to the list of environment variaibles that the `EnvironmentCredential` considers. --- sdk/identity/Azure.Identity/CHANGELOG.md | 2 + .../api/Azure.Identity.netstandard2.0.cs | 7 + .../Azure.Identity/src/CredentialPipeline.cs | 16 ++ .../src/EnvironmentCredential.cs | 35 +++-- .../src/EnvironmentVariables.cs | 1 + .../src/SdkAuthFileCredential.cs | 146 ++++++++++++++++++ .../tests/Azure.Identity.Tests.csproj | 1 + .../Azure.Identity/tests/Data/authfile.json | 12 ++ .../EnvironmentCredentialProviderTests.cs | 26 +++- .../tests/SdkAuthFileCredentialTests.cs | 59 +++++++ 10 files changed, 294 insertions(+), 11 deletions(-) create mode 100644 sdk/identity/Azure.Identity/src/SdkAuthFileCredential.cs create mode 100644 sdk/identity/Azure.Identity/tests/Data/authfile.json create mode 100644 sdk/identity/Azure.Identity/tests/SdkAuthFileCredentialTests.cs diff --git a/sdk/identity/Azure.Identity/CHANGELOG.md b/sdk/identity/Azure.Identity/CHANGELOG.md index 9cbcec77515b8..03ffff815f21f 100644 --- a/sdk/identity/Azure.Identity/CHANGELOG.md +++ b/sdk/identity/Azure.Identity/CHANGELOG.md @@ -4,6 +4,8 @@ ### Fixes and improvements - Fix `UsernamePasswordCredential` constructor parameter mishandling +- Add `SdkAuthFileCredential` which allows using an auth file produced by the Azure CLI to authenticate +- Add support for `AZURE_AUTH_LOCATION` to `EnvironmentCredential`, which uses the newly added `SdkAuthFileCredential` ## 1.1.0 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 efe854112a9d2..ba7080be15fc0 100644 --- a/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs +++ b/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs @@ -102,6 +102,13 @@ public ManagedIdentityCredential(string clientId = null, Azure.Identity.TokenCre 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 SdkAuthFileCredential : Azure.Core.TokenCredential + { + public SdkAuthFileCredential(string filePath) { } + public SdkAuthFileCredential(string pathToFile, Azure.Identity.TokenCredentialOptions options) { } + public override Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) { throw null; } + public override System.Threading.Tasks.ValueTask GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) { throw null; } + } public partial class SharedTokenCacheCredential : Azure.Core.TokenCredential { public SharedTokenCacheCredential() { } diff --git a/sdk/identity/Azure.Identity/src/CredentialPipeline.cs b/sdk/identity/Azure.Identity/src/CredentialPipeline.cs index 9df535a04c65e..79beb319deae9 100644 --- a/sdk/identity/Azure.Identity/src/CredentialPipeline.cs +++ b/sdk/identity/Azure.Identity/src/CredentialPipeline.cs @@ -22,6 +22,13 @@ private CredentialPipeline(TokenCredentialOptions options) Diagnostics = new ClientDiagnostics(options); } + private CredentialPipeline(Uri authorityHost, HttpPipeline httpPipeline, ClientDiagnostics diagnostics) + { + AuthorityHost = authorityHost ?? throw new ArgumentNullException(nameof(authorityHost)); + HttpPipeline = httpPipeline ?? throw new ArgumentNullException(nameof(httpPipeline)); + Diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); + } + public static CredentialPipeline GetInstance(TokenCredentialOptions options) { return (options is null) ? s_Singleton.Value : new CredentialPipeline(options); @@ -53,5 +60,14 @@ public CredentialDiagnosticScope StartGetTokenScope(string fullyQualifiedMethod, return scope; } + + /// + /// Creates a new CredentialPipeline from an existing pipeline while replacing the AuthorityHost with a new value. + /// + /// + public CredentialPipeline WithAuthorityHost(Uri authorityHost) + { + return new CredentialPipeline(authorityHost, HttpPipeline, Diagnostics); + } } } diff --git a/sdk/identity/Azure.Identity/src/EnvironmentCredential.cs b/sdk/identity/Azure.Identity/src/EnvironmentCredential.cs index bb366bd5c9d6a..566c596364e53 100644 --- a/sdk/identity/Azure.Identity/src/EnvironmentCredential.cs +++ b/sdk/identity/Azure.Identity/src/EnvironmentCredential.cs @@ -19,10 +19,10 @@ namespace Azure.Identity /// AZURE_CLIENT_SECRETA client secret that was generated for the App Registration. /// AZURE_USERNAMEThe username, also known as upn, of an Azure Active Directory user account. /// AZURE_PASSWORDThe password of the Azure Active Directory user account. Note this does not support accounts with MFA enabled. + /// AZURE_AUTH_LOCATIONThe path to an SDK Auth file which contains configuration information. /// - /// This credential ultimately uses a or to - /// perform the authentication using these details. Please consult the - /// documentation of that class for more details. + /// This credential ultimately uses a , or + /// perform the authentication using these details. Please consult the documentation of that class for more details. /// public class EnvironmentCredential : TokenCredential, IExtendedTokenCredential { @@ -32,7 +32,7 @@ public class EnvironmentCredential : TokenCredential, IExtendedTokenCredential /// /// Creates an instance of the EnvironmentCredential class and reads client secret details from environment variables. - /// If the expected environment variables are not found at this time, the GetToken method will return the default when invoked. + /// If the expected environment variables are not found at this time, the GetToken method will throw . /// public EnvironmentCredential() : this(CredentialPipeline.GetInstance(null)) @@ -41,7 +41,7 @@ public EnvironmentCredential() /// /// Creates an instance of the EnvironmentCredential class and reads client secret details from environment variables. - /// If the expected environment variables are not found at this time, the GetToken method will return the default when invoked. + /// If the expected environment variables are not found at this time, the GetToken method will throw . /// /// Options that allow to configure the management of the requests sent to the Azure Active Directory service. public EnvironmentCredential(TokenCredentialOptions options) @@ -58,6 +58,7 @@ internal EnvironmentCredential(CredentialPipeline pipeline) string clientSecret = EnvironmentVariables.ClientSecret; string username = EnvironmentVariables.Username; string password = EnvironmentVariables.Password; + string sdkAuthLocation = EnvironmentVariables.SdkAuthLocation; if (tenantId != null && clientId != null) { @@ -71,9 +72,14 @@ internal EnvironmentCredential(CredentialPipeline pipeline) } } + if (_credential is null && sdkAuthLocation != null) + { + _credential = new SdkAuthFileCredential(sdkAuthLocation); + } + if (_credential is null) { - StringBuilder builder = new StringBuilder("Environment variables not fully configured. AZURE_TENANT_ID and AZURE_CLIENT_ID must be set, along with either AZURE_CLIENT_SECRET or AZURE_USERNAME and AZURE_PASSWORD. Currently set variables [ "); + StringBuilder builder = new StringBuilder("Environment variables not fully configured. AZURE_TENANT_ID and AZURE_CLIENT_ID must be set, along with either AZURE_CLIENT_SECRET or AZURE_USERNAME and AZURE_PASSWORD. Alternately, AZURE_AUTH_LOCATION ca be set. Currently set variables ["); if (tenantId != null) { @@ -100,6 +106,11 @@ internal EnvironmentCredential(CredentialPipeline pipeline) builder.Append(" AZURE_PASSWORD"); } + if (sdkAuthLocation != null) + { + builder.Append(" AZURE_AUTH_LOCATION"); + } + _unavailbleErrorMessage = builder.Append(" ]").ToString(); } } @@ -113,11 +124,13 @@ internal EnvironmentCredential(CredentialPipeline pipeline, TokenCredential cred /// /// Obtains a token from the Azure Active Directory service, using the specified client details specified in the environment variables - /// AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET or AZURE_USERNAME and AZURE_PASSWORD to authenticate. + /// AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET or AZURE_USERNAME and AZURE_PASSWORD to authenticate. Alternately, + /// if AZURE_AUTH_LOCATION is set, that information is used. /// This method is called by Azure SDK clients. It isn't intended for use in application code. /// /// - /// If the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET are not specified, the default + /// If the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET or AZURE_AUTH_LOCATION are not specified, + /// this method throws . /// /// The details of the authentication request. /// A controlling the request lifetime. @@ -129,11 +142,13 @@ public override AccessToken GetToken(TokenRequestContext requestContext, Cancell /// /// Obtains a token from the Azure Active Directory service, using the specified client details specified in the environment variables - /// AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET or AZURE_USERNAME and AZURE_PASSWORD to authenticate. + /// AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET or AZURE_USERNAME and AZURE_PASSWORD to authenticate. Alternately, + /// if AZURE_AUTH_LOCATION is set, that information is used. /// This method is called by Azure SDK clients. It isn't intended for use in application code. /// /// - /// If the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET are not specifeid, the default + /// If the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET or AZURE_AUTH_LOCATION are not specified, + /// this method throws . /// /// The details of the authentication request. /// A controlling the request lifetime. diff --git a/sdk/identity/Azure.Identity/src/EnvironmentVariables.cs b/sdk/identity/Azure.Identity/src/EnvironmentVariables.cs index 9f1f845aaeb93..b9b853f500b8b 100644 --- a/sdk/identity/Azure.Identity/src/EnvironmentVariables.cs +++ b/sdk/identity/Azure.Identity/src/EnvironmentVariables.cs @@ -12,6 +12,7 @@ internal class EnvironmentVariables public static string TenantId => Environment.GetEnvironmentVariable("AZURE_TENANT_ID"); public static string ClientId => Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"); public static string ClientSecret => Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET"); + public static string SdkAuthLocation => Environment.GetEnvironmentVariable("AZURE_AUTH_LOCATION"); public static string MsiEndpoint => Environment.GetEnvironmentVariable("MSI_ENDPOINT"); public static string MsiSecret => Environment.GetEnvironmentVariable("MSI_SECRET"); diff --git a/sdk/identity/Azure.Identity/src/SdkAuthFileCredential.cs b/sdk/identity/Azure.Identity/src/SdkAuthFileCredential.cs new file mode 100644 index 0000000000000..7c2e049d02d36 --- /dev/null +++ b/sdk/identity/Azure.Identity/src/SdkAuthFileCredential.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; + +namespace Azure.Identity +{ + /// + /// Enables authentication to Azure Active Directory using configuration information stored Azure SDK Auth File. + /// + /// + /// An Azaure SDK Auth file may be generated by passing --sdk-auth when generating an service principal + /// with az ad sp create-for-rbac. At this time only supports + /// SDK Auth Files which contain a client secret, client certificates are not supported at this time. + /// + public class SdkAuthFileCredential : TokenCredential + { + internal string FilePath { get; } + + private readonly CredentialPipeline _pipeline; + + // Initialized on first use by EnsureCredential + private TokenCredential _credential; + + /// + /// Creates an instance of the SdkAuthFileCredential class based on information in given SDK Auth file. + /// If the file is not found or there are errors parsing it, + /// and will throw a + /// with details on why the file could not be used. + /// + /// The path to the SDK Auth file. + public SdkAuthFileCredential(string filePath): + this(filePath, CredentialPipeline.GetInstance(null)) + { + } + + /// + /// Creates an instance of the SdkAuthFileCredential class based on information in given SDK Auth file. + /// If the file is not found or there are errors parsing it, + /// and will throw a + /// with details on why the file could not be used. + /// + /// The path to the SDK Auth file. + /// Options that allow to configure the management of the requests sent to the Azure Active Directory service. Note that + /// is ignored in favor of the activeDirectoryEndpointUrl property + /// of the SDK Auth file. + public SdkAuthFileCredential(string pathToFile, TokenCredentialOptions options) + : this(pathToFile, CredentialPipeline.GetInstance(options)) + { + } + + internal SdkAuthFileCredential(string pathToFile, CredentialPipeline pipeline) + { + FilePath = pathToFile ?? throw new ArgumentNullException(nameof(pathToFile)); + _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); + } + + /// + /// Obtains a token from the Azure Active Directory service, using the specified client detailed specified in the SDK Auth file. + /// This method is called by Azure SDK clients. It isn't intended for use in application code. + /// + /// + /// If the SDK Auth file is missing or invalid, this method throws a exception. + /// + /// The details of the authentication request + /// A controlling the request lifetime. + /// An which can be used to authenticate service client calls. + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + EnsureCredential(false, cancellationToken).GetAwaiter().GetResult(); + + return _credential.GetToken(requestContext, cancellationToken); + } + + /// + /// Obtains a token from the Azure Active Directory service, using the specified client detailed specified in the SDK Auth file. + /// This method is called by Azure SDK clients. It isn't intended for use in application code. + /// + /// + /// If the SDK Auth file is missing or invalid, this method throws a exception. + /// + /// The details of the authentication request + /// A controlling the request lifetime. + /// An which can be used to authenticate service client calls. + public async override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + await EnsureCredential(true, cancellationToken).ConfigureAwait(false); + + return await _credential.GetTokenAsync(requestContext, cancellationToken).ConfigureAwait(false); + } + + /// + /// Ensures that credential information is loaded from the SDK Auth file. This method should be called to initialize + /// _credential before it is used. If the SDK Auth file is not found or invalid, this method will throw + /// . + /// + /// When true, the task reutrned by this method may complete asynchronously. + /// >A controlling the request lifetime. + /// A task that will ensure _credential has been initialized + internal async Task EnsureCredential(bool isAsync, CancellationToken cancellationToken) + { + if (_credential == null) + { + try + { + _credential = BuildCredentialForCredentialsFile(isAsync ? await ParseCredentialsFileAsync(FilePath, cancellationToken).ConfigureAwait(false) : ParseCredentialsFile(FilePath)); + } catch (Exception e) when (!(e is OperationCanceledException)) + { + throw new AuthenticationFailedException("Error parsing SDK Auth File", e); + } + } + } + + private static Dictionary ParseCredentialsFile(string filePath) + { + return JsonSerializer.Deserialize>(File.ReadAllText(filePath)); + } + + private static async Task> ParseCredentialsFileAsync(string filePath, CancellationToken cancellationToken) + { + using Stream s = File.OpenRead(filePath); + return await JsonSerializer.DeserializeAsync>(s, null, cancellationToken); + } + + private TokenCredential BuildCredentialForCredentialsFile(Dictionary authData) + { + authData.TryGetValue("clientId", out string clientId); + authData.TryGetValue("clientSecret", out string clientSecret); + authData.TryGetValue("tenantId", out string tenantId); + authData.TryGetValue("activeDirectoryEndpointUrl", out string activeDirectoryEndpointUrl); + + if (clientId == null || clientSecret == null || tenantId == null || activeDirectoryEndpointUrl == null) + { + throw new Exception("Malformed Azure SDK Auth file. The file should contain 'clientId', 'clientSecret', 'tenentId' and 'activeDirectoryEndpointUrl' values."); + } + + return new ClientSecretCredential(tenantId, clientId, clientSecret, _pipeline.WithAuthorityHost(new Uri(activeDirectoryEndpointUrl))); + } + } +} diff --git a/sdk/identity/Azure.Identity/tests/Azure.Identity.Tests.csproj b/sdk/identity/Azure.Identity/tests/Azure.Identity.Tests.csproj index d410e28ca09de..d12d1e76481ca 100644 --- a/sdk/identity/Azure.Identity/tests/Azure.Identity.Tests.csproj +++ b/sdk/identity/Azure.Identity/tests/Azure.Identity.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/sdk/identity/Azure.Identity/tests/Data/authfile.json b/sdk/identity/Azure.Identity/tests/Data/authfile.json new file mode 100644 index 0000000000000..859677c61afc8 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/Data/authfile.json @@ -0,0 +1,12 @@ +{ + "clientId": "mockclientid", + "clientSecret": "mockclientsecret", + "subscriptionId": "mocksubscriptionid", + "tenantId": "mocktenantid", + "activeDirectoryEndpointUrl": "https://login.microsoftonline.com", + "resourceManagerEndpointUrl": "https://management.azure.com/", + "activeDirectoryGraphResourceId": "https://graph.windows.net/", + "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/", + "galleryEndpointUrl": "https://gallery.azure.com/", + "managementEndpointUrl": "https://management.core.windows.net/" +} diff --git a/sdk/identity/Azure.Identity/tests/EnvironmentCredentialProviderTests.cs b/sdk/identity/Azure.Identity/tests/EnvironmentCredentialProviderTests.cs index 188ad76e98b9a..27d4b44408b9a 100644 --- a/sdk/identity/Azure.Identity/tests/EnvironmentCredentialProviderTests.cs +++ b/sdk/identity/Azure.Identity/tests/EnvironmentCredentialProviderTests.cs @@ -10,6 +10,7 @@ using Azure.Core.Testing; using Azure.Identity.Tests.Mock; using System.Threading.Tasks; +using System.IO; namespace Azure.Identity.Tests { @@ -21,7 +22,7 @@ public EnvironmentCredentialProviderTests(bool isAsync) : base(isAsync) [NonParallelizable] [Test] - public void CredentialConstruction() + public void CredentialConstructionClientId() { string clientIdBackup = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"); string tenantIdBackup = Environment.GetEnvironmentVariable("AZURE_TENANT_ID"); @@ -56,6 +57,29 @@ public void CredentialConstruction() } } + [NonParallelizable] + [Test] + public void CredentialConstructionAuthLocation() + { + string authLocationBackup = Environment.GetEnvironmentVariable("AZURE_AUTH_LOCATION"); + string pathToFile = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "authfile.json"); + + try + { + Environment.SetEnvironmentVariable("AZURE_AUTH_LOCATION", pathToFile); + + var provider = new EnvironmentCredential(); + SdkAuthFileCredential cred = _credential(provider) as SdkAuthFileCredential; + + Assert.NotNull(cred); + Assert.AreEqual(pathToFile, cred.FilePath); + } + finally + { + Environment.SetEnvironmentVariable("AZURE_AUTH_LOCATION", authLocationBackup); + } + } + [Test] public async Task EnvironmentCredentialUnavailableException() { diff --git a/sdk/identity/Azure.Identity/tests/SdkAuthFileCredentialTests.cs b/sdk/identity/Azure.Identity/tests/SdkAuthFileCredentialTests.cs new file mode 100644 index 0000000000000..6dae0a254d3a6 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/SdkAuthFileCredentialTests.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Management; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Core.Testing; +using Azure.Identity.Tests.Mock; +using NUnit.Framework; + +namespace Azure.Identity.Tests +{ + public class SdkAuthFileCredentialTests : ClientTestBase + { + public SdkAuthFileCredentialTests(bool isAsync) : base(isAsync) + { + } + + [Test] + public async Task SdkAuthFileEnsureCredentialParsesCorrectly() + { + var credential = new SdkAuthFileCredential(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "authfile.json")); + var innerCredential = await _credential(credential); + + ClientSecretCredential cred = innerCredential as ClientSecretCredential; + Assert.NotNull(cred); + Assert.AreEqual("mockclientid", cred.ClientId); + Assert.AreEqual("mocktenantid", cred.TenantId); + Assert.AreEqual("mockclientsecret", cred.ClientSecret); + } + + [Test] + public Task BadSdkAuthFilePathThrowsDuringGetToken() + { + var credential = new SdkAuthFileCredential("Bougs*File*Path"); + + if (IsAsync) + { + Assert.ThrowsAsync(async () => await credential.GetTokenAsync(new TokenRequestContext(new string[] { "https://mock.scope/.default/" }, null), default(CancellationToken)).ConfigureAwait(false)); + } + else + { + Assert.Throws(() => credential.GetToken(new TokenRequestContext(new string[] { "https://mock.scope/.default/" }, null), default(CancellationToken))); + } + + return Task.CompletedTask; + } + + public async Task _credential(SdkAuthFileCredential provider) + { + await provider.EnsureCredential(IsAsync, default); + return (TokenCredential)typeof(SdkAuthFileCredential).GetField("_credential", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(provider); + } + } +}