Skip to content

Commit

Permalink
Implement AzureApplicationCredential (#23218)
Browse files Browse the repository at this point in the history
* Implement AzureApplicationCredential
  • Loading branch information
christothes authored Aug 10, 2021
1 parent 50ceced commit efc94ac
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 0 deletions.
1 change: 1 addition & 0 deletions sdk/identity/Azure.Identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Thank you to our developer community members who helped to make Azure Identity b

### Features Added

- Added `AzureApplicationCredential`
- Added `IsPIILoggingEnabled` property to `TokenCredentialOptions`, which controls whether MSAL PII logging is enabled, and other sensitive credential related logging content.

### Breaking Changes
Expand Down
12 changes: 12 additions & 0 deletions sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ public partial class AuthorizationCodeCredentialOptions : Azure.Identity.TokenCr
public AuthorizationCodeCredentialOptions() { }
public System.Uri RedirectUri { get { throw null; } set { } }
}
public partial class AzureApplicationCredential : Azure.Core.TokenCredential
{
public AzureApplicationCredential() { }
public AzureApplicationCredential(Azure.Identity.AzureApplicationCredentialOptions options) { }
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 AzureApplicationCredentialOptions : Azure.Identity.TokenCredentialOptions
{
public AzureApplicationCredentialOptions() { }
public string ManagedIdentityClientId { get { throw null; } set { } }
}
public static partial class AzureAuthorityHosts
{
public static System.Uri AzureChina { get { throw null; } }
Expand Down
71 changes: 71 additions & 0 deletions sdk/identity/Azure.Identity/src/AzureApplicationCredential.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Core.Pipeline;

namespace Azure.Identity
{
/// <summary>
/// Provides a <see cref="TokenCredential"/> implementation which chains the <see cref="EnvironmentCredential"/> and <see cref="ManagedIdentityCredential"/> implementations to be tried in order
/// until one of the getToken methods returns a non-default <see cref="AccessToken"/>.
/// </summary>
/// <remarks>
/// This credential is designed for applications deployed to Azure <see cref="DefaultAzureCredential"/> is
/// better suited to local development). It authenticates service principals and managed identities..
/// </remarks>
public class AzureApplicationCredential : TokenCredential
{
private readonly ChainedTokenCredential _credential;

/// <summary>
/// Initializes an instance of the <see cref="AzureApplicationCredential"/>.
/// </summary>
public AzureApplicationCredential() : this(new AzureApplicationCredentialOptions(), null, null)
{ }

/// <summary>
/// Initializes an instance of the <see cref="AzureApplicationCredential"/>.
/// </summary>
/// <param name="options">The <see cref="TokenCredentialOptions"/> to configure this credential.</param>
public AzureApplicationCredential(AzureApplicationCredentialOptions options) : this(options ?? new AzureApplicationCredentialOptions(), null, null)
{ }

internal AzureApplicationCredential(AzureApplicationCredentialOptions options, EnvironmentCredential environmentCredential = null, ManagedIdentityCredential managedIdentityCredential = null)
{
_credential = new ChainedTokenCredential(
environmentCredential ?? new EnvironmentCredential(options),
managedIdentityCredential ?? new ManagedIdentityCredential(options.ManagedIdentityClientId)
);
}

/// <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 a <see cref="CredentialUnavailableException"/> will be skipped.</returns>
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken = default)
=> GetTokenImplAsync(false, requestContext, cancellationToken).EnsureCompleted();

/// <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 a <see cref="CredentialUnavailableException"/> 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)
=> async ?
await _credential.GetTokenAsync(requestContext, cancellationToken).ConfigureAwait(false)
: _credential.GetToken(requestContext, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Azure.Identity
{
/// <summary>
/// Options to configure the <see cref="AzureApplicationCredential"/> authentication flow and requests made to Azure Identity services.
/// </summary>
public 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)
{
return !string.IsNullOrEmpty(str) ? str : null;
}
}
}
146 changes: 146 additions & 0 deletions sdk/identity/Azure.Identity/tests/AzureApplicationCredentialTests.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.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Core.TestFramework;
using Azure.Core.Tests;
using Moq;
using NUnit.Framework;

namespace Azure.Identity.Tests
{
public class AzureApplicationCredentialTests : ClientTestBase
{
private const string clientId = "MyClientId";
private const string envToken = "environmentToken";
private const string msiToken = "managedIdentityToken";
private DateTimeOffset expires = DateTimeOffset.Now;
private Mock<EnvironmentCredential> mockEnvCred;
private Mock<ManagedIdentityCredential> mockManagedIdCred;
private AzureApplicationCredentialOptions options = new AzureApplicationCredentialOptions();

public AzureApplicationCredentialTests(bool isAsync) : base(isAsync)
{
TestDiagnostics = false;
}

[SetUp]
public void TestSetup()
{
options.ManagedIdentityClientId = clientId;
}

[Test]
public void CtorValidatesArgs()
{
new AzureApplicationCredential(null);
new AzureApplicationCredential(new AzureApplicationCredentialOptions());
new AzureApplicationCredential(new AzureApplicationCredentialOptions { ManagedIdentityClientId = "clientId" });
}

[Test]
public async Task CredentialSequenceValid([Values(true, false)] bool envAvailable, [Values(true, false)] bool MsiAvailable)
{
ConfigureMocks(envAvailable, MsiAvailable);

var target = InstrumentClient(new AzureApplicationCredential(options, mockEnvCred.Object, mockManagedIdCred.Object));

if (!envAvailable && !MsiAvailable)
{
var ex = Assert.CatchAsync<AuthenticationFailedException>(async () => await target.GetTokenAsync(new TokenRequestContext(new string[] { "Scope" })));
}
else
{
var expectedToken = envAvailable switch
{
true => envToken,
false => msiToken
};
Assert.AreEqual(expectedToken, (await target.GetTokenAsync(new TokenRequestContext(new string[] { "scope" }))).Token);
}

VerifyMocks(envAvailable, MsiAvailable);
}

private void ConfigureMocks(bool EnvAvailable, bool MsiAvailable)
{
mockEnvCred = new Mock<EnvironmentCredential>();
mockManagedIdCred = new Mock<ManagedIdentityCredential>();

if (EnvAvailable)
{
mockEnvCred
.Setup(m => m.GetToken(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.Returns(new AccessToken(envToken, expires));

mockEnvCred
.Setup(m => m.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AccessToken(envToken, expires));
}
else
{
mockEnvCred
.Setup(m => m.GetToken(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.Throws(new CredentialUnavailableException("no cred"));

mockEnvCred
.Setup(m => m.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.Throws(new CredentialUnavailableException("no cred"));
}

if (MsiAvailable)
{
mockManagedIdCred
.Setup(m => m.GetToken(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.Returns(new AccessToken(msiToken, expires));

mockManagedIdCred
.Setup(m => m.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AccessToken(msiToken, expires));
}
else
{
mockManagedIdCred
.Setup(m => m.GetToken(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.Throws(new CredentialUnavailableException("no cred"));

mockManagedIdCred
.Setup(m => m.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.Throws(new CredentialUnavailableException("no cred"));
}
}

private void VerifyMocks(bool EnvAvailable, bool MsiAvailable)
{
if (EnvAvailable)
{
if (IsAsync)
{
mockEnvCred
.Verify(m => m.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()));
}
else
{
mockEnvCred
.Verify(m => m.GetToken(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()));
}
}
else if (MsiAvailable)
{
if (IsAsync)
{
mockManagedIdCred
.Verify(m => m.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()));
}
else
{
mockManagedIdCred
.Verify(m => m.GetToken(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()));
}
}
}
}
}

0 comments on commit efc94ac

Please sign in to comment.