Skip to content

Commit

Permalink
AAD: Handling Azure.Core.TokenCredential (#2191)
Browse files Browse the repository at this point in the history
  • Loading branch information
TimothyMothra authored May 27, 2021
1 parent e1ff87e commit ad3d952
Show file tree
Hide file tree
Showing 12 changed files with 608 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.SetAzureTokenCredential(object tokenCredential) -> void

Microsoft.ApplicationInsights.Channel.IAsyncFlushable
Microsoft.ApplicationInsights.Channel.IAsyncFlushable.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<bool>
Microsoft.ApplicationInsights.Channel.InMemoryChannel.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<bool>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.SetAzureTokenCredential(object tokenCredential) -> void

Microsoft.ApplicationInsights.Channel.IAsyncFlushable
Microsoft.ApplicationInsights.Channel.IAsyncFlushable.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<bool>
Microsoft.ApplicationInsights.Channel.InMemoryChannel.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<bool>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.SetAzureTokenCredential(object tokenCredential) -> void

Microsoft.ApplicationInsights.Channel.IAsyncFlushable
Microsoft.ApplicationInsights.Channel.IAsyncFlushable.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<bool>
Microsoft.ApplicationInsights.Channel.InMemoryChannel.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<bool>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#if NET461 || NETCOREAPP2_1 || NETCOREAPP3_1 || NET5_0
namespace Microsoft.ApplicationInsights.TestFramework.Extensibility.Implementation.Authentication
{
using System;
using System.Threading;
using System.Threading.Tasks;

using Azure.Core;


/// <remarks>
/// Copied from (https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core.TestFramework/src/MockCredential.cs).
/// </remarks>
public class MockCredential : TokenCredential
{
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new ValueTask<AccessToken>(GetToken(requestContext, cancellationToken));
}

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new AccessToken("TEST TOKEN " + string.Join(" ", requestContext.Scopes), DateTimeOffset.MaxValue);
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#if !NET452 && !NET46
namespace Microsoft.ApplicationInsights.TestFramework.Extensibility.Implementation.Authentication
{
using System;
using System.Threading;
using System.Threading.Tasks;

using Azure.Core;

using Microsoft.ApplicationInsights.Extensibility.Implementation.Authentication;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Moq;

/// <summary>
/// The <see cref="ReflectionCredentialEnvelope"/> cannot take a dependency on <see cref="Azure.Core.TokenCredential"/>.
/// We must use reflection to interact with this class.
/// These tests are to confirm that we can correctly identity classes that implement TokenCredential and address it's methods.
/// </summary>
/// <remarks>
/// These tests do not run in NET452 OR NET46.
/// In these cases, the test runner is NET452 or NET46 and Azure.Core.TokenCredential is NOT SUPPORTED in these frameworks.
/// This does not affect the end user because we REQUIRE the end user to create their own instance of TokenCredential.
/// This ensures that the end user is consuming the AI SDK in one of the newer frameworks.
/// </remarks>
[TestClass]
[TestCategory("AAD")]
public class ReflectionCredentialEnvelopeTests
{
[TestMethod]
public void VerifyCanIdentifyValidClass()
{
var testClass2 = new TestClassInheritsTokenCredential();
_ = new ReflectionCredentialEnvelope(testClass2);
// NO ASSERT. This test is valid if no exception is thrown. :)
}

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void VerifyCanIdentityInvalidClass()
{
var notTokenCredential2 = new NotTokenCredential2();
_ = new ReflectionCredentialEnvelope(notTokenCredential2);
}

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void VerifyCannotSetInvalidType()
{
_ = new ReflectionCredentialEnvelope(Guid.Empty);
}

[TestMethod]
public void VerifyCanMakeTokenRequestContext()
{
var testScope = new string[] { "test/scope" };

var requestContext = new TokenRequestContext(testScope);

var tokenRequestContextViaReflection = ReflectionCredentialEnvelope.AzureCore.MakeTokenRequestContext(testScope);
Assert.IsInstanceOfType(tokenRequestContextViaReflection, typeof(TokenRequestContext));
Assert.AreEqual(requestContext, tokenRequestContextViaReflection);
}

[TestMethod]
public void VerifyGetToken_UsingCompileTimeTypes()
{
var mockCredential = new MockCredential();
var requestContext = new TokenRequestContext(new string[] { "test/scope" });

var testResult = ReflectionCredentialEnvelope.AzureCore.InvokeGetToken(mockCredential, requestContext, CancellationToken.None);

Assert.AreEqual("TEST TOKEN test/scope", testResult);
}

[TestMethod]
public async Task VerifyGetTokenAsync_UsingCompileTimeTypes()
{
var mockCredential = new MockCredential();
var requestContext = new TokenRequestContext(new string[] { "test/scope" });

var testResult = await ReflectionCredentialEnvelope.AzureCore.InvokeGetTokenAsync(mockCredential, requestContext, CancellationToken.None);

Assert.AreEqual("TEST TOKEN test/scope", testResult);
}

/// <summary>
/// This more closely represents how this would be used in a production environment.
/// </summary>
[TestMethod]
public void VerifyGetToken_UsingDynamicTypes()
{
var mockCredential = (object)new MockCredential();
var requestContext = ReflectionCredentialEnvelope.AzureCore.MakeTokenRequestContext(new[] { "test/scope" });

var testResult = ReflectionCredentialEnvelope.AzureCore.InvokeGetToken(mockCredential, requestContext, CancellationToken.None);

Assert.AreEqual("TEST TOKEN test/scope", testResult);
}

/// <summary>
/// This more closely represents how this would be used in a production environment.
/// </summary>
[TestMethod]
public async Task VerifyGetTokenAsync_UsingDynamicTypes()
{
var mockCredential = (object)new MockCredential();
var requestContext = ReflectionCredentialEnvelope.AzureCore.MakeTokenRequestContext(new[] { "test/scope" });

var testResult = await ReflectionCredentialEnvelope.AzureCore.InvokeGetTokenAsync(mockCredential, requestContext, CancellationToken.None);

Assert.AreEqual("TEST TOKEN test/scope", testResult);
}

/// <summary>
/// This test verifies that both <see cref="Azure.Core"/> and <see cref="ReflectionCredentialEnvelope"/> return identical tokens.
/// </summary>
[TestMethod]
public void VerifyGetToken_ReturnsValidToken()
{
var requestContext = new TokenRequestContext(scopes: CredentialConstants.GetScopes());
var mockCredential = new MockCredential();
var tokenUsingTypes = mockCredential.GetToken(requestContext, CancellationToken.None);

var reflectionCredentialEnvelope = new ReflectionCredentialEnvelope(mockCredential);
var tokenUsingReflection = reflectionCredentialEnvelope.GetToken();

Assert.AreEqual(tokenUsingTypes.Token, tokenUsingReflection);
}

/// <summary>
/// This test verifies that both <see cref="Azure.Core"/> and <see cref="ReflectionCredentialEnvelope"/> return identical tokens.
/// </summary>
[TestMethod]
public async Task VerifyGetTokenAsync_ReturnsValidToken()
{
var requestContext = new TokenRequestContext(scopes: CredentialConstants.GetScopes());
var mockCredential = new MockCredential();
var tokenUsingTypes = await mockCredential.GetTokenAsync(requestContext, CancellationToken.None);

var reflectionCredentialEnvelope = new ReflectionCredentialEnvelope(mockCredential);
var tokenUsingReflection = await reflectionCredentialEnvelope.GetTokenAsync();

Assert.AreEqual(tokenUsingTypes.Token, tokenUsingReflection);
}

[TestMethod]
public void VerifyGetToken_IfCredentialThrowsException_EnvelopeReturnsNull()
{
Mock<TokenCredential> mockTokenCredential = new Mock<TokenCredential>();
mockTokenCredential.Setup(x => x.GetToken(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>())).Throws(new NotImplementedException());
var mockCredential = mockTokenCredential.Object;

var reflectionCredentialEnvelope = new ReflectionCredentialEnvelope(mockCredential);
var token = reflectionCredentialEnvelope.GetToken();
Assert.IsNull(token);
}

[TestMethod]
public async Task VerifyGetTokenAsync_IfCredentialThrowsException_EnvelopeReturnsNull()
{
Mock<TokenCredential> mockTokenCredential = new Mock<TokenCredential>();
mockTokenCredential.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>())).Throws(new NotImplementedException());
var mockCredential = mockTokenCredential.Object;

var reflectionCredentialEnvelope = new ReflectionCredentialEnvelope(mockCredential);
var token = await reflectionCredentialEnvelope.GetTokenAsync();
Assert.IsNull(token);
}

#region TestClasses

/// <summary>
/// This class inherits <see cref="MockCredential"/> which inherits <see cref="Azure.Core.TokenCredential"/>.
/// This class is used to verify that the <see cref="ReflectionCredentialEnvelope"/> can correctly identify tests that inherit <see cref="Azure.Core.TokenCredential"/>.
/// </summary>
private class TestClassInheritsTokenCredential : MockCredential { }

private abstract class NotTokenCredential
{
public abstract AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken);

public abstract ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken);
}

private class NotTokenCredential1 : NotTokenCredential
{
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}

public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}

private class NotTokenCredential2 : NotTokenCredential1 { }
#endregion
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#if !NET452 && !NET46
namespace Microsoft.ApplicationInsights.TestFramework.Extensibility.Implementation.Authentication
{
using System;
using System.Threading.Tasks;

using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Extensibility.Implementation.Authentication;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// These tests verify that <see cref="TelemetryConfiguration"/> can receive and store an instance of <see cref="Azure.Core.TokenCredential"/>.
/// </summary>
/// <remarks>
/// These tests do not run in NET452 OR NET46.
/// In these cases, the test runner is NET452 or NET46 and Azure.Core.TokenCredential is NOT SUPPORTED in these frameworks.
/// This does not affect the end user because we REQUIRE the end user to create their own instance of TokenCredential.
/// This ensures that the end user is consuming the AI SDK in one of the newer frameworks.
/// </remarks>
[TestClass]
[TestCategory("AAD")]
public class TelemetryConfigurationCredentialEnvelopeTests
{
/// <summary>
/// This tests verifies that each supported language can create and set a Credential.
/// </summary>
[TestMethod]
public void VerifyCanSetCredential()
{
var mockCredential = new MockCredential();

var telemetryConfiguration = new TelemetryConfiguration();
telemetryConfiguration.SetAzureTokenCredential(mockCredential);

Assert.IsInstanceOfType(telemetryConfiguration.CredentialEnvelope, typeof(ReflectionCredentialEnvelope));
Assert.AreEqual(mockCredential, telemetryConfiguration.CredentialEnvelope.Credential, "Credential should be the same instance that we pass in.");
}

/// <summary>
/// TelemetryConfiguration accepts an <see cref="Object"/> parameter, and uses reflection to verify the type at runtime
/// This test is to verify that we cannot set invalid types.
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void VerifyCannotSetInvalidObjectOnTelemetryConfiguration()
{
var telemetryConfiguration = new TelemetryConfiguration();
telemetryConfiguration.SetAzureTokenCredential(Guid.Empty);
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
<Reference Include="System.Xml.Linq" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net461' Or '$(TargetFramework)' == 'netcoreapp2.1' Or '$(TargetFramework)' == 'netcoreapp3.1' Or '$(TargetFramework)' == 'net5.0'">
<PackageReference Include="Azure.Identity" Version="1.3.0" /> <!-- Supports: netstandard2.0 -->
<PackageReference Include="Azure.Core" Version="1.14.0" /> <!-- Supports: net461, netstandard2.0, and net5.0 -->
</ItemGroup>

<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Microsoft.ApplicationInsights.Extensibility.Implementation.Authentication
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

internal static class CredentialConstants
{
/// <summary>
/// Source:
/// (https://docs.microsoft.com/azure/active-directory/develop/msal-acquire-cache-tokens#scopes-when-acquiring-tokens).
/// (https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope).
/// </summary>
public const string AzureMonitorScope = "https://monitor.azure.com//.default"; // TODO: THIS SCOPE IS UNVERIFIED. WAITING FOR SERVICES TEAM TO PROVIDE AN INT ENVIRONMENT FOR E2E TESTING.

/// <summary>
/// Get scopes for Azure Monitor as an array.
/// </summary>
/// <returns>An array of scopes.</returns>
public static string[] GetScopes() => new string[] { AzureMonitorScope };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Microsoft.ApplicationInsights.Extensibility.Implementation.Authentication
{
using System.Threading;
using System.Threading.Tasks;

internal interface ICredentialEnvelope
{
/// <summary>
/// Gets the TokenCredential instance held by this class.
/// </summary>
object Credential { get; }

/// <summary>
/// Gets an Azure.Core.AccessToken.
/// </summary>
/// <param name="cancellationToken">The System.Threading.CancellationToken to use.</param>
/// <returns>A valid Azure.Core.AccessToken.</returns>
string GetToken(CancellationToken cancellationToken = default);

/// <summary>
/// Gets an Azure.Core.AccessToken.
/// </summary>
/// <param name="cancellationToken">The System.Threading.CancellationToken to use.</param>
/// <returns>A valid Azure.Core.AccessToken.</returns>
Task<string> GetTokenAsync(CancellationToken cancellationToken = default);
}
}
Loading

0 comments on commit ad3d952

Please sign in to comment.