Skip to content

Commit

Permalink
Support AZURE_AUTH_LOCATION
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ellismg committed Jan 8, 2020
1 parent bd5bd73 commit c2317e2
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 11 deletions.
2 changes: 2 additions & 0 deletions sdk/identity/Azure.Identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Azure.Core.AccessToken> 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<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public partial class SharedTokenCacheCredential : Azure.Core.TokenCredential
{
public SharedTokenCacheCredential() { }
Expand Down
16 changes: 16 additions & 0 deletions sdk/identity/Azure.Identity/src/CredentialPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -53,5 +60,14 @@ public CredentialDiagnosticScope StartGetTokenScope(string fullyQualifiedMethod,

return scope;
}

/// <summary>
/// Creates a new CredentialPipeline from an existing pipeline while replacing the AuthorityHost with a new value.
/// </summary>
/// <returns></returns>
public CredentialPipeline WithAuthorityHost(Uri authorityHost)
{
return new CredentialPipeline(authorityHost, HttpPipeline, Diagnostics);
}
}
}
35 changes: 25 additions & 10 deletions sdk/identity/Azure.Identity/src/EnvironmentCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ namespace Azure.Identity
/// <item><term>AZURE_CLIENT_SECRET</term><description>A client secret that was generated for the App Registration.</description></item>
/// <item><term>AZURE_USERNAME</term><description>The username, also known as upn, of an Azure Active Directory user account.</description></item>
/// <item><term>AZURE_PASSWORD</term><description>The password of the Azure Active Directory user account. Note this does not support accounts with MFA enabled.</description></item>
/// <item><term>AZURE_AUTH_LOCATION</term><description>The path to an SDK Auth file which contains configuration information.</description></item>
/// </list>
/// This credential ultimately uses a <see cref="ClientSecretCredential"/> or <see cref="UsernamePasswordCredential"/> to
/// perform the authentication using these details. Please consult the
/// documentation of that class for more details.
/// This credential ultimately uses a <see cref="ClientSecretCredential"/>, <see cref="UsernamePasswordCredential"/> or <see cref="SdkAuthFileCredential"/>
/// perform the authentication using these details. Please consult the documentation of that class for more details.
/// </summary>
public class EnvironmentCredential : TokenCredential, IExtendedTokenCredential
{
Expand All @@ -32,7 +32,7 @@ public class EnvironmentCredential : TokenCredential, IExtendedTokenCredential

/// <summary>
/// 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 <see cref="AccessToken"/> when invoked.
/// If the expected environment variables are not found at this time, the GetToken method will throw <see cref="CredentialUnavailableException"/>.
/// </summary>
public EnvironmentCredential()
: this(CredentialPipeline.GetInstance(null))
Expand All @@ -41,7 +41,7 @@ public EnvironmentCredential()

/// <summary>
/// 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 <see cref="AccessToken"/> when invoked.
/// If the expected environment variables are not found at this time, the GetToken method will throw <see cref="CredentialUnavailableException"/>.
/// </summary>
/// <param name="options">Options that allow to configure the management of the requests sent to the Azure Active Directory service.</param>
public EnvironmentCredential(TokenCredentialOptions options)
Expand All @@ -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)
{
Expand All @@ -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)
{
Expand All @@ -100,6 +106,11 @@ internal EnvironmentCredential(CredentialPipeline pipeline)
builder.Append(" AZURE_PASSWORD");
}

if (sdkAuthLocation != null)
{
builder.Append(" AZURE_AUTH_LOCATION");
}

_unavailbleErrorMessage = builder.Append(" ]").ToString();
}
}
Expand All @@ -113,11 +124,13 @@ internal EnvironmentCredential(CredentialPipeline pipeline, TokenCredential cred

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// If the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET are not specified, the default <see cref="AccessToken"/>
/// If the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET or AZURE_AUTH_LOCATION are not specified,
/// this method throws <see cref="CredentialUnavailableException"/>.
/// </remarks>
/// <param name="requestContext">The details of the authentication request.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
Expand All @@ -129,11 +142,13 @@ public override AccessToken GetToken(TokenRequestContext requestContext, Cancell

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// If the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET are not specifeid, the default <see cref="AccessToken"/>
/// If the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET or AZURE_AUTH_LOCATION are not specified,
/// this method throws <see cref="CredentialUnavailableException"/>.
/// </remarks>
/// <param name="requestContext">The details of the authentication request.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
Expand Down
1 change: 1 addition & 0 deletions sdk/identity/Azure.Identity/src/EnvironmentVariables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
146 changes: 146 additions & 0 deletions sdk/identity/Azure.Identity/src/SdkAuthFileCredential.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Enables authentication to Azure Active Directory using configuration information stored Azure SDK Auth File.
/// </summary>
/// <remarks>
/// An Azaure SDK Auth file may be generated by passing <code>--sdk-auth</code> when generating an service principal
/// with <code>az ad sp create-for-rbac</code>. At this time <see cref="SdkAuthFileCredential"/> only supports
/// SDK Auth Files which contain a client secret, client certificates are not supported at this time.
/// </remarks>
public class SdkAuthFileCredential : TokenCredential
{
internal string FilePath { get; }

private readonly CredentialPipeline _pipeline;

// Initialized on first use by EnsureCredential
private TokenCredential _credential;

/// <summary>
/// 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, <see cref="GetToken(TokenRequestContext, CancellationToken)"/>
/// and <see cref="GetTokenAsync(TokenRequestContext, CancellationToken)"/> will throw a <see cref="AuthenticationFailedException"/>
/// with details on why the file could not be used.
/// </summary>
/// <param name="filePath">The path to the SDK Auth file.</param>
public SdkAuthFileCredential(string filePath):
this(filePath, CredentialPipeline.GetInstance(null))
{
}

/// <summary>
/// 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, <see cref="GetToken(TokenRequestContext, CancellationToken)"/>
/// and <see cref="GetTokenAsync(TokenRequestContext, CancellationToken)"/> will throw a <see cref="AuthenticationFailedException"/>
/// with details on why the file could not be used.
/// </summary>
/// <param name="pathToFile">The path to the SDK Auth file.</param>
/// <param name="options">Options that allow to configure the management of the requests sent to the Azure Active Directory service. Note that
/// <see cref="TokenCredentialOptions.AuthorityHost"/> is ignored in favor of the <code>activeDirectoryEndpointUrl</code> property
/// of the SDK Auth file.</param>
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));
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// If the SDK Auth file is missing or invalid, this method throws a <see cref="AuthenticationFailedException"/> exception.
/// </remarks>
/// <param name="requestContext">The details of the authentication request</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>An <see cref="AccessToken"/> which can be used to authenticate service client calls.</returns>
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
EnsureCredential(false, cancellationToken).GetAwaiter().GetResult();

return _credential.GetToken(requestContext, cancellationToken);
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// If the SDK Auth file is missing or invalid, this method throws a <see cref="AuthenticationFailedException"/> exception.
/// </remarks>
/// <param name="requestContext">The details of the authentication request</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>An <see cref="AccessToken"/> which can be used to authenticate service client calls.</returns>
public async override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
await EnsureCredential(true, cancellationToken).ConfigureAwait(false);

return await _credential.GetTokenAsync(requestContext, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Ensures that credential information is loaded from the SDK Auth file. This method should be called to initialize
/// <code>_credential</code> before it is used. If the SDK Auth file is not found or invalid, this method will throw
/// <see cref="AuthenticationFailedException"/>.
/// </summary>
/// <param name="isAsync">When true, the task reutrned by this method may complete asynchronously.</param>
/// <param name="cancellationToken">>A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>A task that will ensure <code>_credential</code> has been initialized</returns>
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<string, string> ParseCredentialsFile(string filePath)
{
return JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(filePath));
}

private static async Task<Dictionary<string, string>> ParseCredentialsFileAsync(string filePath, CancellationToken cancellationToken)
{
using Stream s = File.OpenRead(filePath);
return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(s, null, cancellationToken);
}

private TokenCredential BuildCredentialForCredentialsFile(Dictionary<string, string> 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)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

<ItemGroup>
<ProjectReference Include="..\src\Azure.Identity.csproj" />
<None Update="Data\authfile.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="Data\cert.pfx" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Expand Down
12 changes: 12 additions & 0 deletions sdk/identity/Azure.Identity/tests/Data/authfile.json
Original file line number Diff line number Diff line change
@@ -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/"
}
Loading

0 comments on commit c2317e2

Please sign in to comment.