Skip to content

Commit

Permalink
Release 3.1 (#12)
Browse files Browse the repository at this point in the history
* chore(clientcertauth): Implement client certificate authentication
* fix(deps): Revert main Azure App Registration and Enterprise Application Orchestrator extension .NET project to .NET 6 from .NET 8.
* chore(deps): Revert Keyfactor.Logging

---------

Co-authored-by: Keyfactor <[email protected]>
Co-authored-by: Hayden Roszell <[email protected]>
  • Loading branch information
3 people authored Jun 3, 2024
1 parent 132f967 commit 05369a7
Show file tree
Hide file tree
Showing 30 changed files with 1,217 additions and 134 deletions.
Binary file not shown.
Binary file removed .github/images/AzureApp-basic-store-type-dialog.png
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed .github/images/AzureSP-basic-store-type-dialog.png
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand Down
75 changes: 54 additions & 21 deletions AzureEnterpriseApplicationOrchestrator.Tests/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,33 @@ public AzureEnterpriseApplicationOrchestrator_Client()
ConfigureLogging();
}

[IntegrationTestingFact]
public void GraphClient_Application_AddGetRemove_ReturnSuccess()
[IntegrationTestingTheory]
[InlineData("clientcert")]
[InlineData("clientsecret")]
public void GraphClient_Application_AddGetRemove_ReturnSuccess(string testAuthMethod)
{
// Arrange
string certName = "AppTest" + Guid.NewGuid().ToString()[..6];
X509Certificate2 ssCert = GetSelfSignedCert(certName);
string b64Cert = Convert.ToBase64String(ssCert.Export(X509ContentType.Cert));

IntegrationTestingFact env = new();

IAzureGraphClient client = new GraphClient.Builder()
IAzureGraphClientBuilder clientBuilder = new GraphClient.Builder()
.WithTenantId(env.TenantId)
.WithApplicationId(env.ApplicationId)
.WithClientSecret(env.ClientSecret)
.WithTargetApplicationId(env.TargetApplicationId)
.Build();
.WithTargetApplicationId(env.TargetApplicationId);

if (testAuthMethod == "clientcert")
{
clientBuilder.WithClientSecret(env.ClientSecret);
}
else
{
var cert = X509Certificate2.CreateFromPemFile(env.ClientCertificatePath);
clientBuilder.WithClientCertificate(cert);
}

IAzureGraphClient client = clientBuilder.Build();

// Step 1 - Add the certificate to the Application

Expand Down Expand Up @@ -89,8 +100,10 @@ public void GraphClient_Application_AddGetRemove_ReturnSuccess()
Assert.False(exists);
}

[IntegrationTestingFact]
public void GraphClient_ServicePrincipal_AddGetRemove_ReturnSuccess()
[IntegrationTestingTheory]
[InlineData("clientcert")]
[InlineData("clientsecret")]
public void GraphClient_ServicePrincipal_AddGetRemove_ReturnSuccess(string testAuthMethod)
{
// Arrange
const string password = "passwordpasswordpassword";
Expand All @@ -99,13 +112,22 @@ public void GraphClient_ServicePrincipal_AddGetRemove_ReturnSuccess()
string b64PfxSslCert = Convert.ToBase64String(ssCert.Export(X509ContentType.Pfx, password));

IntegrationTestingFact env = new();

IAzureGraphClient client = new GraphClient.Builder()
IAzureGraphClientBuilder clientBuilder = new GraphClient.Builder()
.WithTenantId(env.TenantId)
.WithApplicationId(env.ApplicationId)
.WithClientSecret(env.ClientSecret)
.WithTargetApplicationId(env.TargetApplicationId)
.Build();
.WithTargetApplicationId(env.TargetApplicationId);

if (testAuthMethod == "clientcert")
{
clientBuilder.WithClientSecret(env.ClientSecret);
}
else
{
var cert = X509Certificate2.CreateFromPemFile(env.ClientCertificatePath);
clientBuilder.WithClientCertificate(cert);
}

IAzureGraphClient client = clientBuilder.Build();

// Step 1 - Add the certificate to the Service Principal (and set it as the preferred SAML signing certificate)

Expand Down Expand Up @@ -152,8 +174,10 @@ public void GraphClient_ServicePrincipal_AddGetRemove_ReturnSuccess()
Assert.False(exists);
}

[IntegrationTestingFact]
public void GraphClient_DiscoverApplicationIds_ReturnSuccess()
[IntegrationTestingTheory]
[InlineData("clientcert")]
[InlineData("clientsecret")]
public void GraphClient_DiscoverApplicationIds_ReturnSuccess(string testAuthMethod)
{
// Arrange
const string password = "passwordpasswordpassword";
Expand All @@ -162,13 +186,22 @@ public void GraphClient_DiscoverApplicationIds_ReturnSuccess()
string b64PfxSslCert = Convert.ToBase64String(ssCert.Export(X509ContentType.Pfx, password));

IntegrationTestingFact env = new();

IAzureGraphClient client = new GraphClient.Builder()
IAzureGraphClientBuilder clientBuilder = new GraphClient.Builder()
.WithTenantId(env.TenantId)
.WithApplicationId(env.ApplicationId)
.WithClientSecret(env.ClientSecret)
.WithTargetApplicationId(env.TargetApplicationId)
.Build();
.WithTargetApplicationId(env.TargetApplicationId);

if (testAuthMethod == "clientcert")
{
clientBuilder.WithClientSecret(env.ClientSecret);
}
else
{
var cert = X509Certificate2.CreateFromPemFile(env.ClientCertificatePath);
clientBuilder.WithClientCertificate(cert);
}

IAzureGraphClient client = clientBuilder.Build();

// Act
OperationResult<IEnumerable<string>> operationResult = client.DiscoverApplicationIds();
Expand Down
8 changes: 8 additions & 0 deletions AzureEnterpriseApplicationOrchestrator.Tests/FakeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Security.Cryptography.X509Certificates;
using AzureEnterpriseApplicationOrchestrator.Client;
using Keyfactor.Logging;
using Keyfactor.Orchestrators.Common.Enums;
Expand All @@ -31,6 +32,7 @@ public class FakeBuilder : IAzureGraphClientBuilder
public string? _targetApplicationId { get; set; }
public string? _applicationId { get; set; }
public string? _clientSecret { get; set; }
public X509Certificate2? _clientCertificate { get; set; }
public string? _azureCloudEndpoint { get; set; }

public IAzureGraphClientBuilder WithTenantId(string tenantId)
Expand All @@ -57,6 +59,12 @@ public IAzureGraphClientBuilder WithClientSecret(string clientSecret)
return this;
}

public IAzureGraphClientBuilder WithClientCertificate(X509Certificate2 clientCertificate)
{
_clientCertificate = clientCertificate;
return this;
}

public IAzureGraphClientBuilder WithAzureCloud(string azureCloud)
{
_azureCloudEndpoint = azureCloud;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public sealed class IntegrationTestingFact : FactAttribute
public string TenantId { get; private set; }
public string ApplicationId { get; private set; }
public string ClientSecret { get; private set; }
public string ClientCertificatePath { get; private set; }

public string TargetApplicationId { get; private set; }

Expand All @@ -27,6 +28,7 @@ public IntegrationTestingFact()
TenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID") ?? string.Empty;
ApplicationId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID") ?? string.Empty;
ClientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET") ?? string.Empty;
ClientCertificatePath = Environment.GetEnvironmentVariable("AZURE_PATH_TO_CLIENT_CERTIFICATE") ?? string.Empty;

TargetApplicationId = Environment.GetEnvironmentVariable("AZURE_TARGET_APPLICATION_ID") ?? string.Empty;

Expand All @@ -37,3 +39,27 @@ public IntegrationTestingFact()
}
}

public sealed class IntegrationTestingTheory : TheoryAttribute
{
public string TenantId { get; private set; }
public string ApplicationId { get; private set; }
public string ClientSecret { get; private set; }
public string ClientCertificatePath { get; private set; }

public string TargetApplicationId { get; private set; }

public IntegrationTestingTheory()
{
TenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID") ?? string.Empty;
ApplicationId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID") ?? string.Empty;
ClientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET") ?? string.Empty;
ClientCertificatePath = Environment.GetEnvironmentVariable("AZURE_PATH_TO_CLIENT_CERTIFICATE") ?? string.Empty;

TargetApplicationId = Environment.GetEnvironmentVariable("AZURE_TARGET_APPLICATION_ID") ?? string.Empty;

if (string.IsNullOrEmpty(TenantId) || string.IsNullOrEmpty(ApplicationId) || string.IsNullOrEmpty(ClientSecret) || string.IsNullOrEmpty(TargetApplicationId))
{
Skip = "Integration testing environment variables are not set - Skipping test. Please run `make setup` to set the environment variables.";
}
}
}
92 changes: 91 additions & 1 deletion AzureEnterpriseApplicationOrchestrator.Tests/JobClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using AzureEnterpriseApplicationOrchestrator;
using AzureEnterpriseApplicationOrchestrator.Client;
using AzureEnterpriseApplicationOrchestrator.Tests;
Expand All @@ -32,7 +35,7 @@ public AzureEnterpriseApplicationOrchestrator_JobClientBuilder()
}

[Fact]
public void GraphJobClientBuilder_ValidCertificateStoreConfig_BuildValidClient()
public void GraphJobClientBuilder_ValidCertificateStoreConfigWithClientSecret_BuildValidClient()
{
// Verify that the GraphJobClientBuilder uses the certificate store configuration
// provided by Keyfactor Command/the Universal Orchestrator correctly as required
Expand Down Expand Up @@ -68,6 +71,93 @@ public void GraphJobClientBuilder_ValidCertificateStoreConfig_BuildValidClient()

_logger.LogInformation("GraphJobClientBuilder_ValidCertificateStoreConfig_BuildValidClient - Success");
}

[IntegrationTestingTheory]
[InlineData("pkcs12")]
[InlineData("pem")]
[InlineData("encryptedPem")]
public void GraphJobClientBuilder_ValidCertificateStoreConfigWithClientCertificate_BuildValidClient(string certificateFormat)
{
// Verify that the GraphJobClientBuilder uses the certificate store configuration
// provided by Keyfactor Command/the Universal Orchestrator correctly as required
// by the IAzureGraphClientBuilder interface.

// Arrange
GraphJobClientBuilder<FakeClient.FakeBuilder> jobClientBuilderWithFakeBuilder = new();

string password = "passwordpasswordpassword";
string certName = "SPTest" + Guid.NewGuid().ToString()[..6];
X509Certificate2 ssCert = GetSelfSignedCert(certName);

string b64ClientCertificate;
if (certificateFormat == "pkcs12")
{
b64ClientCertificate = Convert.ToBase64String(ssCert.Export(X509ContentType.Pfx, password));
}
else if (certificateFormat == "pem")
{
string pemCert = ssCert.ExportCertificatePem();
string keyPem = ssCert.GetRSAPrivateKey()!.ExportPkcs8PrivateKeyPem();
b64ClientCertificate = Convert.ToBase64String(Encoding.UTF8.GetBytes(keyPem + '\n' + pemCert));
password = "";
}
else
{
PbeParameters pbeParameters = new PbeParameters(
PbeEncryptionAlgorithm.Aes256Cbc,
HashAlgorithmName.SHA384,
300_000);
string pemCert = ssCert.ExportCertificatePem();
string keyPem = ssCert.GetRSAPrivateKey()!.ExportEncryptedPkcs8PrivateKeyPem(password.ToCharArray(), pbeParameters);
b64ClientCertificate = Convert.ToBase64String(Encoding.UTF8.GetBytes(keyPem + '\n' + pemCert));
}

// Set up the certificate store with names that correspond to how we expect them to be interpreted by
// the builder
CertificateStore fakeCertificateStoreDetails = new()
{
ClientMachine = "fake-tenant-id",
StorePath = "fake-azure-target-application-id",
Properties = $@"{{""ServerUsername"": ""fake-azure-application-id"",""ServerPassword"": ""{password}"",""ClientCertificate"": ""{b64ClientCertificate}"",""AzureCloud"": ""fake-azure-cloud""}}"
};

// Act
IAzureGraphClient fakeAppGatewayClient = jobClientBuilderWithFakeBuilder
.WithCertificateStoreDetails(fakeCertificateStoreDetails)
.Build();

// Assert

// IAzureGraphClient doesn't require any of the properties set by the builder to be exposed
// since the production Build() method creates an Azure Resource Manager client.
// But, our builder is fake and exposes the properties we need to test (via the FakeBuilder class).
Assert.Equal("fake-tenant-id", jobClientBuilderWithFakeBuilder._builder._tenantId);
Assert.Equal("fake-azure-target-application-id", jobClientBuilderWithFakeBuilder._builder._targetApplicationId);
Assert.Equal("fake-azure-application-id", jobClientBuilderWithFakeBuilder._builder._applicationId);
Assert.Equal("fake-azure-cloud", jobClientBuilderWithFakeBuilder._builder._azureCloudEndpoint);
Assert.Equal(ssCert.GetCertHash(), jobClientBuilderWithFakeBuilder._builder._clientCertificate!.GetCertHash());
Assert.NotNull(jobClientBuilderWithFakeBuilder._builder._clientCertificate!.GetRSAPrivateKey());
Assert.Equal(jobClientBuilderWithFakeBuilder._builder._clientCertificate!.GetRSAPrivateKey()!.ExportRSAPrivateKeyPem(), ssCert.GetRSAPrivateKey()!.ExportRSAPrivateKeyPem());

_logger.LogInformation("GraphJobClientBuilder_ValidCertificateStoreConfig_BuildValidClient - Success");
}

public static X509Certificate2 GetSelfSignedCert(string hostname)
{
RSA rsa = RSA.Create(2048);
CertificateRequest req = new CertificateRequest($"CN={hostname}", rsa, HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);

SubjectAlternativeNameBuilder subjectAlternativeNameBuilder = new SubjectAlternativeNameBuilder();
subjectAlternativeNameBuilder.AddDnsName(hostname);
req.CertificateExtensions.Add(subjectAlternativeNameBuilder.Build());
req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("2.5.29.32.0"), new Oid("1.3.6.1.5.5.7.3.1") }, false));

X509Certificate2 selfSignedCert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));
Console.Write($"Created self-signed certificate for \"{hostname}\" with thumbprint {selfSignedCert.Thumbprint}\n");
return selfSignedCert;
}

static void ConfigureLogging()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
Expand All @@ -10,16 +10,16 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Core" Version="1.38.0" />
<PackageReference Include="Azure.Identity" Version="1.10.4" />
<PackageReference Include="Azure.ResourceManager" Version="1.10.2" />
<PackageReference Include="coverlet.collector" Version="6.0.1">
<PackageReference Include="Azure.Core" Version="1.39.0" />
<PackageReference Include="Azure.Identity" Version="1.11.3" />
<PackageReference Include="Azure.ResourceManager" Version="1.12.0" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Keyfactor.Logging" Version="1.1.1" />
<PackageReference Include="Keyfactor.Orchestrators.IOrchestratorJobExtensions" Version="0.7.0" />
<PackageReference Include="Microsoft.Graph" Version="5.44.0" />
<PackageReference Include="Microsoft.Graph" Version="5.54.0" />
</ItemGroup>

<ItemGroup>
Expand Down
28 changes: 25 additions & 3 deletions AzureEnterpriseApplicationOrchestrator/Client/GraphClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class Builder : IAzureGraphClientBuilder
private string _tenantId { get; set; }
private string _applicationId { get; set; }
private string _clientSecret { get; set; }
private X509Certificate2 _clientCertificate { get; set; }
private string _targetApplicationId { get; set; }
private Uri _azureCloudEndpoint { get; set; }

Expand Down Expand Up @@ -91,6 +92,12 @@ public IAzureGraphClientBuilder WithClientSecret(string clientSecret)
return this;
}

public IAzureGraphClientBuilder WithClientCertificate(X509Certificate2 clientCertificate)
{
_clientCertificate = clientCertificate;
return this;
}

public IAzureGraphClientBuilder WithAzureCloud(string azureCloud)
{
if (string.IsNullOrWhiteSpace(azureCloud))
Expand Down Expand Up @@ -129,9 +136,24 @@ public IAzureGraphClient Build()
AdditionallyAllowedTenants = { "*" }
};

TokenCredential credential = new ClientSecretCredential(
_tenantId, _applicationId, _clientSecret, credentialOptions
);
TokenCredential credential;
if (!string.IsNullOrWhiteSpace(_clientSecret))
{
credential = new ClientSecretCredential(
_tenantId, _applicationId, _clientSecret, credentialOptions
);
}
else if (_clientCertificate != null)
{
credential = new ClientCertificateCredential(
_tenantId, _applicationId, _clientCertificate, credentialOptions
);
}
else
{
throw new Exception("Client secret or client certificate must be provided.");
}


string[] scopes = { "https://graph.microsoft.com/.default" };

Expand Down
Loading

0 comments on commit 05369a7

Please sign in to comment.