Skip to content

Commit

Permalink
New features for AppAuthentication library (#4976)
Browse files Browse the repository at this point in the history
* Adding new SqlAzureAuthProvider class in new .NET 4.7.2 targeted build, along with needed refactoring

* Adding support for user-assigned managed identities and certificate subject name/issuer authentication

* Adding new custom service functionality

* Updating documentation for AppAuth

* Updating release notes for 1.2.0-preview release

* MsiAccessTokenProvider bug fix and test updates/fixes

* Removing net472 targets
  • Loading branch information
nonik0 authored and dsgouda committed Nov 8, 2018
1 parent 65d20d5 commit 25adfed
Show file tree
Hide file tree
Showing 50 changed files with 1,689 additions and 331 deletions.
1 change: 1 addition & 0 deletions Documentation/sdk-for-net-packages.md
Expand Up @@ -74,6 +74,7 @@ Below are the packages maintained in this repository.

| Azure Common Library | Package Name | NuGet Download |
| ---------------------------------- | --------------------------------------------------- | -------------- |
| AppAuthentication | `Microsoft.Azure.Services.AppAuthentication` | [![ClientRuntime](https://img.shields.io/nuget/vpre/Microsoft.Azure.Services.AppAuthentication.svg)](https://www.nuget.org/packages/Microsoft.Azure.Services.AppAuthentication) |
| ClientRuntime | `Microsoft.Rest.ClientRuntime` | [![ClientRuntime](https://img.shields.io/nuget/vpre/Microsoft.Rest.ClientRuntime.svg)](https://www.nuget.org/packages/Microsoft.Rest.ClientRuntime) |
| ClientRuntime.Azure | `Microsoft.Rest.ClientRuntime.Azure` | [![ClientRuntime.Azure](https://img.shields.io/nuget/vpre/Microsoft.Rest.ClientRuntime.Azure.svg)](https://www.nuget.org/packages/Microsoft.Rest.ClientRuntime.Azure) |
| ClientRuntime.Azure.Authentication | `Microsoft.Rest.ClientRuntime.Azure.Authentication` | [![ClientRuntime.Azure.Authentication](https://img.shields.io/nuget/vpre/Microsoft.Rest.ClientRuntime.Azure.Authentication.svg)](https://www.nuget.org/packages/Microsoft.Rest.ClientRuntime.Azure.Authentication) |
Expand Down
Expand Up @@ -11,6 +11,10 @@
using Microsoft.Azure.Services.AppAuthentication.IntegrationTests.Helpers;
using Microsoft.Azure.Services.AppAuthentication.IntegrationTests.Models;
using Microsoft.Azure.Services.AppAuthentication.TestCommon;
#if net472
using System.Data;
using System.Data.SqlClient;
#endif

namespace Microsoft.Azure.Services.AppAuthentication.IntegrationTests
{
Expand Down Expand Up @@ -76,11 +80,11 @@ private async Task GetTokenUsingDeveloperTool(bool isAzureCli)
{
foreach (var resourceId in resourceIdList)
{
string token =
var authResult =
await
astp.GetAccessTokenAsync(resourceId);
astp.GetAuthenticationResultAsync(resourceId);

Validator.ValidateToken(token, astp.PrincipalUsed, Constants.UserType, _tenantId);
Validator.ValidateToken(authResult.AccessToken, astp.PrincipalUsed, Constants.UserType, _tenantId, expiresOn: authResult.ExpiresOn);
}

var callback = astp.KeyVaultTokenCallback;
Expand All @@ -106,7 +110,7 @@ public async Task GetTokenUsingVisualStudio()
#if FullNetFx
/// <summary>
/// Test case where token is fetched using Integrated Windows Authentication.
/// Person runnning the test must be using domain joined machine, where domain is federated with Azure AD.
/// Person running the test must be using domain joined machine, where domain is federated with Azure AD.
/// </summary>
/// <returns></returns>
[Fact]
Expand All @@ -117,32 +121,42 @@ public async Task GetTokenUsingIntegratedWindowsAuthenticationTest()
for (int i = 0; i < 100; i++)
{
// Get token using active directory integrated authentication
string token =
var authResult =
await
astp.GetAccessTokenAsync(Constants.GraphResourceId);
astp.GetAuthenticationResultAsync(Constants.GraphResourceId);

// This will not cause IWA. Token for SQL will be used to get token for Graph API (MRRT)
AzureServiceTokenProvider astp1 = new AzureServiceTokenProvider(Constants.IntegratedAuthConnectionString);
await astp1.GetAccessTokenAsync(Constants.SqlAzureResourceId);

Validator.ValidateToken(token, astp.PrincipalUsed, Constants.UserType, astp1.PrincipalUsed.TenantId);
Validator.ValidateToken(authResult.AccessToken, astp.PrincipalUsed, Constants.UserType, astp1.PrincipalUsed.TenantId, expiresOn: authResult.ExpiresOn);
}
}
#endif

private enum CertIdentifierType
{
SubjectName,
Thumbprint,
KeyVaultCertificateSecretIdentifier
}

/// <summary>
/// One must be logged in using Azure CLI and set AppAuthenticationTestCertUrl to a secret URL for a certificate in Key Vault before running this test.
/// The test creates a new Azure AD application and service principal, uses the cert as the credential, and then uses the library to authenticate to it, using the cert.
/// After the cert, the Azure AD application is deleted. Cert is removed from current user store on the machine.
/// </summary>
/// <param name="isThumbprint"></param>
/// <param name="certIdentifierType"></param>
/// <returns></returns>
private async Task GetTokenUsingServicePrincipalWithCertTestImpl(bool isThumbprint)
private async Task GetTokenUsingServicePrincipalWithCertTestImpl(CertIdentifierType certIdentifierType)
{
string testCertUrl = Environment.GetEnvironmentVariable(Constants.TestCertUrlEnv);

// Get a certificate from key vault.
// For security, this certificate is not hard coded in the test case, since it gets added as app credential in Azure AD, and may not get removed.
AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider(Constants.AzureCliConnectionString);
KeyVaultHelper keyVaultHelper = new KeyVaultHelper(new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback)));
string certAsString = await keyVaultHelper.ExportCertificateAsBlob(Environment.GetEnvironmentVariable(Constants.TestCertUrlEnv)).ConfigureAwait(false);
string certAsString = await keyVaultHelper.ExportCertificateAsBlob(testCertUrl).ConfigureAwait(false);

X509Certificate2 cert = new X509Certificate2(Convert.FromBase64String(certAsString), string.Empty);

Expand All @@ -151,27 +165,42 @@ private async Task GetTokenUsingServicePrincipalWithCertTestImpl(bool isThumbpri
Application app = await graphHelper.CreateApplicationAsync(cert).ConfigureAwait(false);

// Get token using service principal, this will cache the cert
string token = string.Empty;
AppAuthenticationResult authResult = null;
int count = 5;

string thumbprint = cert.Thumbprint?.ToLower();

// Construct connection string using client id and cert name
string thumbprintOrSubjectName = isThumbprint ? $"CertificateThumbprint={thumbprint}" : $"CertificateSubjectName={cert.Subject}";
string connectionString = $"RunAs=App;AppId={app.AppId};TenantId={_tenantId};{thumbprintOrSubjectName};CertificateStoreLocation={Constants.CurrentUserStore};";
// Construct connection string using client id and cert info
string connectionString = null;
switch (certIdentifierType)
{
case CertIdentifierType.SubjectName:
case CertIdentifierType.Thumbprint:
string thumbprintOrSubjectName = (certIdentifierType == CertIdentifierType.Thumbprint) ? $"CertificateThumbprint={thumbprint}" : $"CertificateSubjectName={cert.Subject}";
connectionString = $"RunAs=App;AppId={app.AppId};TenantId={_tenantId};{thumbprintOrSubjectName};CertificateStoreLocation={Constants.CurrentUserStore};";
break;
case CertIdentifierType.KeyVaultCertificateSecretIdentifier:
connectionString = $"RunAs=App;AppId={app.AppId};TenantId={_tenantId};CertificateKeyVaultCertificateSecretIdentifier={testCertUrl};";
break;
}

AzureServiceTokenProvider astp =
new AzureServiceTokenProvider(connectionString);

// Import the certificate
CertUtil.ImportCertificate(cert);
if (certIdentifierType == CertIdentifierType.SubjectName ||
certIdentifierType == CertIdentifierType.Thumbprint)
{
// Import the certificate
CertUtil.ImportCertificate(cert);
}

while (string.IsNullOrEmpty(token) && count > 0)
while (authResult == null && count > 0)
{
try
{
await astp.GetAccessTokenAsync(Constants.KeyVaultResourceId);

token = await astp.GetAccessTokenAsync(Constants.SqlAzureResourceId, _tenantId);
authResult = await astp.GetAuthenticationResultAsync(Constants.SqlAzureResourceId, _tenantId);
}
catch
{
Expand All @@ -182,33 +211,44 @@ private async Task GetTokenUsingServicePrincipalWithCertTestImpl(bool isThumbpri
}
}

// Delete the cert
CertUtil.DeleteCertificate(cert.Thumbprint);
if (certIdentifierType == CertIdentifierType.SubjectName ||
certIdentifierType == CertIdentifierType.Thumbprint)
{
// Delete the cert
CertUtil.DeleteCertificate(cert.Thumbprint);

var deletedCert = CertUtil.GetCertificate(cert.Thumbprint);
Assert.Equal(null, deletedCert);
var deletedCert = CertUtil.GetCertificate(cert.Thumbprint);
Assert.Null(deletedCert);
}

// Get token again using a cert which is deleted, but in the cache
await astp.GetAccessTokenAsync(Constants.SqlAzureResourceId, _tenantId);

// Delete the application and service principal
await graphHelper.DeleteApplicationAsync(app);

Validator.ValidateToken(token, astp.PrincipalUsed, Constants.AppType, _tenantId, app.AppId, cert.Thumbprint);
Validator.ValidateToken(authResult.AccessToken, astp.PrincipalUsed, Constants.AppType, _tenantId,
app.AppId, cert.Thumbprint, expiresOn: authResult.ExpiresOn);
}

[Fact]
public async Task GetTokenThumbprintTest()
{
await GetTokenUsingServicePrincipalWithCertTestImpl(false);
await GetTokenUsingServicePrincipalWithCertTestImpl(CertIdentifierType.Thumbprint);
}

[Fact]
public async Task GetTokenSubjectNameTest()
{
await GetTokenUsingServicePrincipalWithCertTestImpl(true);
await GetTokenUsingServicePrincipalWithCertTestImpl(CertIdentifierType.SubjectName);
}


[Fact]
public async Task GetTokenKeyVaultIdentifierTest()
{
await GetTokenUsingServicePrincipalWithCertTestImpl(CertIdentifierType.KeyVaultCertificateSecretIdentifier);
}

[Fact]
public async Task GetTokenUsingServicePrincipalWithClientSecretTest()
{
Expand All @@ -217,17 +257,17 @@ public async Task GetTokenUsingServicePrincipalWithClientSecretTest()
Application app = await graphHelper.CreateApplicationAsync(secret);

// Get token using service principal
string token = string.Empty;
AppAuthenticationResult authResult = null;
int count = 5;

Environment.SetEnvironmentVariable(Constants.ConnectionStringEnvironmentVariableName, $"RunAs=App;TenantId={_tenantId};AppId={app.AppId};AppKey={secret}");
AzureServiceTokenProvider astp = new AzureServiceTokenProvider();

while (string.IsNullOrEmpty(token) && count > 0)
while (authResult == null && count > 0)
{
try
{
token = await astp.GetAccessTokenAsync(Constants.SqlAzureResourceId, _tenantId);
authResult = await astp.GetAuthenticationResultAsync(Constants.SqlAzureResourceId, _tenantId);

await astp.GetAccessTokenAsync(Constants.KeyVaultResourceId);

Expand All @@ -247,7 +287,31 @@ public async Task GetTokenUsingServicePrincipalWithClientSecretTest()

Environment.SetEnvironmentVariable(Constants.ConnectionStringEnvironmentVariableName, null);

Validator.ValidateToken(token, astp.PrincipalUsed, Constants.AppType, _tenantId, app.AppId);
Validator.ValidateToken(authResult.AccessToken, astp.PrincipalUsed, Constants.AppType, _tenantId, app.AppId, expiresOn: authResult.ExpiresOn);
}

#if net472
/// <summary>
/// One must be logged in using Azure CLI and set AppAuthenticationTestSqlDbEndpoint to a SQL Azure database endpoint before running this test.
/// The test validates that it can open a connection with the SQL database using SqlAzureAuthProvider, which implements the SqlAuthenticationProvider interface.
/// </summary>
/// <returns></returns>
[Fact]
public async Task ConnectToSqlDbWithSqlAppAuthProvider()
{
// register SqlAzureAppAuthProvider and create the connection string
SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, new SqlAppAuthenticationProvider());
string connectionString = $"server={Environment.GetEnvironmentVariable(Constants.TestSqlDbEndpoint)};Authentication=Active Directory Interactive;UID=";

// verify the connection can be successfully opened
var sqlConnection = new SqlConnection(connectionString);
await sqlConnection.OpenAsync();
Assert.Equal(ConnectionState.Open, sqlConnection.State);

// verify connection can be successfully closed
sqlConnection.Close();
Assert.Equal(ConnectionState.Closed, sqlConnection.State);
}
#endif
}
}
Expand Up @@ -30,7 +30,6 @@ public async Task<string> ExportCertificateAsBlob(string secretUrl)
if (0 == string.CompareOrdinal(certContentSecret.ContentType, CertificateContentType.Pfx))
{
return certContentSecret.Value;

}

throw new Exception($"Certificate not found at {secretUrl}");
Expand Down
@@ -1,8 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$([MSBuild]::GetPathOfFileAbove('test.props'))" />
<PropertyGroup>
<TargetFrameworks>net452;netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>netcoreapp2.0;net452</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net472'">
<DefineConstants>net472;FullNetFx</DefineConstants>
<OutputPath>bin\$(Configuration)\</OutputPath>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.KeyVault" Version="2.3.2" />
</ItemGroup>
Expand Down

0 comments on commit 25adfed

Please sign in to comment.