-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement AzureApplicationCredential (#23218)
* Implement AzureApplicationCredential
- Loading branch information
1 parent
50ceced
commit efc94ac
Showing
5 changed files
with
252 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
sdk/identity/Azure.Identity/src/AzureApplicationCredential.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
sdk/identity/Azure.Identity/src/AzureApplicationCredentialOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
146
sdk/identity/Azure.Identity/tests/AzureApplicationCredentialTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>())); | ||
} | ||
} | ||
} | ||
} | ||
} |