Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cfca1d5
Made initial list of possible certificate identification criteria.
jesshill99 Nov 29, 2023
2dc1b16
Creating ClientCertificateConfiguration class and specs.
jesshill99 Dec 6, 2023
815986a
Implementing test for checking subject name of certificate
jesshill99 Dec 14, 2023
c8fb829
Moved certificate support into a new library
jesshill99 Jan 10, 2024
a6bbf0a
Continue to flesh out specs
jesshill99 Jan 17, 2024
e740b33
Implement step to create a cert
jesshill99 Jan 24, 2024
162bc61
Added steps to validate certificate is in store location. Added code …
jesshill99 Jan 31, 2024
e0d9c63
Finalised ClientCertificateConfiguration specs so all are now passing
jesshill99 Feb 7, 2024
c6c9cb0
Started adding specs for cert auth.
jesshill99 Feb 7, 2024
7e24b1f
Additional specs
jesshill99 Feb 14, 2024
6ce1819
Implemented validation of ClientIdWithCertificate
jesshill99 Feb 21, 2024
a56140f
Added code that uses the authentication using a certificate code and …
jesshill99 Feb 28, 2024
b02fd33
Implemented the spec for successfully getting a certificate based Cli…
jesshill99 Mar 6, 2024
51971fa
Merge branch 'main' into feature/205-client-cert-auth
jesshill99 Mar 6, 2024
18ebb00
Update the $InvokeBuildModuleVersion parameter default value
jesshill99 Mar 6, 2024
86d7dce
Updated pipeline to use the 8.0 netSdkVersion
jesshill99 Mar 13, 2024
4c9efaa
Added additionalNetSdkVersions 6.0
jesshill99 Mar 13, 2024
5fe26c6
Update dependencies
jesshill99 Mar 13, 2024
4e11ffa
Removed notes and added to an issue
jesshill99 Mar 13, 2024
5b6fcb5
Added package liicense
jesshill99 Mar 13, 2024
57a7c55
Updated the package reference, the git version and the release notes.
jesshill99 Mar 27, 2024
479a3f3
Update project configurations and dependencies
HowardvanRooijen Jun 9, 2025
8be361e
Update dependencies and target framework for .NET 8.0
HowardvanRooijen Jun 9, 2025
c151ff4
Update Microsoft.Extensions package version formats
HowardvanRooijen Jun 13, 2025
ad1558a
Hard code the v8.* package versions
HowardvanRooijen Jun 13, 2025
4c9e065
Add release notes for Corvus.Identity v4.0 with client certificate au…
HowardvanRooijen Jun 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions Documentation/ReleaseNotes/Corvus.Identity.v4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Release notes for Corvus.Identity v4.0

## v4.1.0

### New Features

#### Client Certificate Authentication Support
* **NEW**: Added [`Corvus.Identity.Certificates`](../Solutions/Corvus.Identity.Certificates/Corvus.Identity.Certificates.csproj:1) NuGet package for certificate-based authentication
* **NEW**: [`ClientCertificateConfiguration`](../Solutions/Corvus.Identity.Certificates/Corvus/Identity/Certificates/ClientCertificateConfiguration.cs:1) class for configuring certificate authentication
* **NEW**: [`ICertificateFromConfiguration`](../Solutions/Corvus.Identity.Certificates/Corvus/Identity/Certificates/ICertificateFromConfiguration.cs:1) interface for certificate resolution
* **NEW**: [`CertificateNotFoundException`](../Solutions/Corvus.Identity.Certificates/Corvus/Identity/Certificates/CertificateNotFoundException.cs:1) for certificate error handling
* **NEW**: [`IdentityCertificateServiceCollectionExtensions`](../Solutions/Corvus.Identity.Certificates/Microsoft/Extensions/DependencyInjection/IdentityCertificateServiceCollectionExtensions.cs:1) for DI registration

#### Enhanced Azure Identity Support
* **NEW**: [`ClientIdentityConfiguration.AzureAdAppClientCertificate`](../Solutions/Corvus.Identity.Azure/Corvus/Identity/ClientAuthentication/Azure/ClientIdentityConfiguration.cs:1) property for certificate-based Azure AD authentication
* **NEW**: [`TestableClientCertificateCredential`](../Solutions/Corvus.Identity.Azure/Corvus/Identity/ClientAuthentication/Azure/Internal/TestableClientCertificateCredential.cs:1) for improved testing support
* **ENHANCED**: Enhanced [`ClientIdentityConfigurationValidation`](../Solutions/Corvus.Identity.Azure/Corvus/Identity/ClientAuthentication/Azure/ClientIdentityConfigurationValidation.cs:1) with certificate validation support

### Framework and Infrastructure Updates

#### .NET 8.0 Migration
* **BREAKING**: All projects now target .NET 8.0 exclusively
* **BREAKING**: Removed .NET 6.0 support
* **UPDATED**: All project files updated to use .NET 8.0 target framework

#### Build and CI/CD Improvements
* **NEW**: Added [`.github/workflows/build.yml`](../.github/workflows/build.yml:1) GitHub Actions workflow
* **UPDATED**: Enhanced [`build.ps1`](../build.ps1:1) script with latest tooling (v1.5.4)
* **REMOVED**: Deprecated [`azure-pipelines.yml`](../azure-pipelines.yml:1) and [`azure-pipelines.release.yml`](../azure-pipelines.release.yml:1)
* **NEW**: Added [`.zf/config.ps1`](../.zf/config.ps1:1) configuration file
* **UPDATED**: Enhanced [`.github/workflows/auto_release.yml`](../.github/workflows/auto_release.yml:1) and [`.github/workflows/dependabot_approve_and_label.yml`](../.github/workflows/dependabot_approve_and_label.yml:1)

#### Testing Infrastructure Modernization
* **BREAKING**: Migrated from SpecFlow to Reqnroll
* **NEW**: [`reqnroll.json`](../Solutions/Corvus.Identity.Specs/reqnroll.json:1) configuration
* **REMOVED**: [`specflow.json`](../Solutions/Corvus.Identity.Specs/specflow.json:1) configuration
* **NEW**: [`AsyncTestTaskExtensions`](../Solutions/Corvus.Identity.Specs/Idg/AsyncTest/TaskExtensions/AsyncTestTaskExtensions.cs:1) for improved async testing support
* **NEW**: Comprehensive certificate authentication test scenarios in [`AdAppWithClientCertificate.feature`](../Solutions/Corvus.Identity.Specs/Corvus/Identity/Azure/TokenCredentialSourceFromDynamicConfiguration/AdAppWithClientCertificate.feature:1)

### Breaking Changes

#### Microsoft.Rest Support Removal
* **BREAKING**: Removed entire [`Corvus.Identity.MicrosoftRest`](../Solutions/Corvus.Identity.MicrosoftRest/Corvus.Identity.MicrosoftRest.csproj:1) package and all related components:
* [`IMicrosoftRestTokenProviderSource`](../Solutions/Corvus.Identity.MicrosoftRest/Corvus/Identity/ClientAuthentication/MicrosoftRest/IMicrosoftRestTokenProviderSource.cs:1)
* [`IMicrosoftRestTokenProviderSourceFromDynamicConfiguration`](../Solutions/Corvus.Identity.MicrosoftRest/Corvus/Identity/ClientAuthentication/MicrosoftRest/IMicrosoftRestTokenProviderSourceFromDynamicConfiguration.cs:1)
* [`IServiceIdentityMicrosoftRestTokenProviderSource`](../Solutions/Corvus.Identity.MicrosoftRest/Corvus/Identity/ClientAuthentication/MicrosoftRest/IServiceIdentityMicrosoftRestTokenProviderSource.cs:1)
* [`MicrosoftRestTokenProvider`](../Solutions/Corvus.Identity.MicrosoftRest/Corvus/Identity/ClientAuthentication/MicrosoftRest/MicrosoftRestTokenProvider.cs:1)
* [`MicrosoftRestIdentityServiceCollectionExtensions`](../Solutions/Corvus.Identity.MicrosoftRest/Microsoft/Extensions/DependencyInjection/MicrosoftRestIdentityServiceCollectionExtensions.cs:1)
* **BREAKING**: Removed all Microsoft.Rest related examples:
* [`Corvus.Identity.Examples.UsingMicrosoftRest`](../Solutions/Corvus.Identity.Examples.UsingMicrosoftRest/Corvus.Identity.Examples.UsingMicrosoftRest.csproj:1) project
* [`UseMicrosoftRestFunction`](../Solutions/Corvus.Identity.Examples.AzureFunctions/UseMicrosoftRestFunction.cs:1) from Azure Functions examples

### Dependency Updates

#### Core Dependencies
* **UPDATED**: Azure.Identity to v1.10 (from v1.8)
* **UPDATED**: Azure.Core to v1.37.0 (from v1.36.0)
* **UPDATED**: Azure.Security.KeyVault.Secrets to v4.6.0 (from v4.5.0)
* **UPDATED**: Microsoft.Extensions.* packages to v8.* versions
* **UPDATED**: Various other dependencies for .NET 8.0 compatibility

#### Package Management
* **UPDATED**: All [`packages.lock.json`](../Solutions/Corvus.Identity.Specs/packages.lock.json:1) files reflect new dependency versions
* **ENHANCED**: Improved package version management and formatting

### Code Quality and Maintenance

#### Project Structure
* **ENHANCED**: Updated all [`.csproj`](../Solutions/Corvus.Identity.Abstractions/Corvus.Identity.Abstractions.csproj:1) files with modern SDK-style project format
* **ENHANCED**: Improved code organization and namespace structure
* **UPDATED**: [`GitVersion.yml`](../GitVersion.yml:1) configuration for v4.0 release cycle

#### Configuration and Settings
* **UPDATED**: [`local.settings.template.json`](../Solutions/Corvus.Identity.Examples.AzureFunctions/local.settings.template.json:1) for Azure Functions examples
* **ENHANCED**: [`.gitignore`](../.gitignore:1) with additional exclusions
* **UPDATED**: Various configuration files for improved development experience

### Migration Guide

For users upgrading from v3.x to v4.0:

1. **Framework Migration**: Update your project to target .NET 8.0
2. **Microsoft.Rest Removal**: If using Microsoft.Rest integration, migrate to Azure.Core-based authentication
3. **Certificate Authentication**: Consider adopting the new certificate authentication features for enhanced security
4. **Package References**: Update all Corvus.Identity package references to v4.0+
5. **Testing**: If using SpecFlow-based tests, consider migrating to Reqnroll

### Known Issues

None at this time.

---

## v4.0.1

Initial patch release with dependency updates and configuration improvements for Azure Functions support.

## v4.0.0

Major version release with .NET 8.0 migration and removal of Microsoft.Rest support. See detailed changelog above.
5 changes: 3 additions & 2 deletions Solutions/Corvus.Identity.Azure/Corvus.Identity.Azure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="[8.0.*,)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[8.0.*,)" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Corvus.Identity.Abstractions\Corvus.Identity.Abstractions.csproj" />
<ProjectReference Include="..\Corvus.Identity.Certificates\Corvus.Identity.Certificates.csproj" />
</ItemGroup>

</Project>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Corvus.Identity.ClientAuthentication.Azure
{
using Corvus.Identity.Certificates;

/// <summary>
/// Configuration determining the Azure AD identity client code will use for some operation
/// (e.g., connecting to a storage service, or reading secrets from a key vault).
Expand All @@ -17,11 +19,7 @@ namespace Corvus.Identity.ClientAuthentication.Azure
/// a suitable authorized Azure AD identity.
/// </para>
/// <para>
/// TODO: certificate based auth. Microsoft actually recommends use of certificates instead of
/// client secrets for service principle authentication. How will we support these? Probably
/// a pair of properties: AzureAdAppClientCertificateSubject, for scenarios where we expect
/// the relevant client certificate to be ambiently available (e.g., because it's installed in
/// our compute environment), and AzureAdAppClientCertificateInKeyVault for when the cert lives
/// TODO: Key Vault certificate based auth. AzureAdAppClientCertificateInKeyVault for when the cert lives
/// in Key Vault; we'd need to introduce a KeyVaultCertificateConfiguration similar to the
/// existing KeyVaultSecretConfiguration to support this.
/// </para>
Expand Down Expand Up @@ -51,6 +49,12 @@ public class ClientIdentityConfiguration
/// </summary>
public KeyVaultSecretConfiguration? AzureAdAppClientSecretInKeyVault { get; set; }

/// <summary>
/// Gets or sets the configuration describing the client certificate to use when authenticating
/// to Azure AD as a service principal using certificate based authentication.
/// </summary>
public ClientCertificateConfiguration? AzureAdAppClientCertificate { get; set; }

/// <summary>
/// Gets or sets a value indicating where the identity to be used comes from (e.g., a
/// Managed Identity, or Azure CLI).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,20 @@ internal static class ClientIdentityConfigurationValidation
bool adAppClientIdPresent = !string.IsNullOrWhiteSpace(configuration.AzureAdAppClientId);
bool adAppClientSecretPlainTextPresent = !string.IsNullOrWhiteSpace(configuration.AzureAdAppClientSecretPlainText);
bool adAppClientSecretKeyVaultPresent = configuration.AzureAdAppClientSecretInKeyVault is not null;
bool adAppClientCertificatePresent = configuration.AzureAdAppClientCertificate is not null;
bool managedIdClientIdPresent = !string.IsNullOrWhiteSpace(configuration.ManagedIdentityClientId);
if (configuration.IdentitySourceType == ClientIdentitySourceTypes.ClientIdAndSecret
|| adAppTenantIdPresent
|| adAppClientIdPresent
|| adAppClientSecretPlainTextPresent
|| adAppClientSecretKeyVaultPresent)
|| (adAppClientSecretPlainTextPresent || adAppClientSecretKeyVaultPresent))
{
indicatedSourceTypes.Add(ClientIdentitySourceTypes.ClientIdAndSecret);
}

if (configuration.IdentitySourceType == ClientIdentitySourceTypes.ClientIdAndCertificate
|| adAppClientCertificatePresent)
{
indicatedSourceTypes.Add(ClientIdentitySourceTypes.ClientIdAndCertificate);
}

if (configuration.IdentitySourceType == ClientIdentitySourceTypes.UserAssignedManaged
|| managedIdClientIdPresent)
{
Expand Down Expand Up @@ -92,6 +96,18 @@ internal static class ClientIdentityConfigurationValidation

break;

case ClientIdentitySourceTypes.ClientIdAndCertificate:
if (!(adAppTenantIdPresent && adAppClientIdPresent &&
configuration.AzureAdAppClientCertificate is not null &&
(configuration.AzureAdAppClientCertificate.StoreLocation != 0) &&
!string.IsNullOrEmpty(configuration.AzureAdAppClientCertificate.StoreName) &&
!string.IsNullOrEmpty(configuration.AzureAdAppClientCertificate.SubjectName)))
{
return "ClientIdAndCertificate configuration must provide AzureAppTenantId, AzureAdAppClientId, and an AzureAppClientCertificate with a StoreLocation, StoreName, and SubjectName";
}

break;

case ClientIdentitySourceTypes.UserAssignedManaged:
if (!managedIdClientIdPresent)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
namespace Corvus.Identity.ClientAuthentication.Azure.Internal
{
using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;

using Corvus.Identity.Certificates;
using global::Azure;
using global::Azure.Core;
using global::Azure.Identity;
Expand All @@ -20,6 +21,7 @@ internal class AzureTokenCredentialSourceFromConfiguration : IAzureTokenCredenti
{
private readonly IKeyVaultSecretClientFactory secretClientFactory;
private readonly IKeyVaultSecretCache secretCache;
private readonly ICertificateFromConfiguration certificateSource;

/// <summary>
/// Creates an <see cref="AzureTokenCredentialSourceFromConfiguration"/>.
Expand All @@ -30,12 +32,17 @@ internal class AzureTokenCredentialSourceFromConfiguration : IAzureTokenCredenti
/// <param name="secretCache">
/// Provides caching to avoid repeated lookups in key vault.
/// </param>
/// <param name="certificateSource">
/// Retrieves certificates.
/// </param>
public AzureTokenCredentialSourceFromConfiguration(
IKeyVaultSecretClientFactory secretClientFactory,
IKeyVaultSecretCache secretCache)
IKeyVaultSecretCache secretCache,
ICertificateFromConfiguration certificateSource)
{
this.secretClientFactory = secretClientFactory ?? throw new ArgumentNullException(nameof(secretClientFactory));
this.secretCache = secretCache;
this.certificateSource = certificateSource;
}

/// <inheritdoc/>
Expand All @@ -59,6 +66,11 @@ public async ValueTask<IAzureTokenCredentialSource> CredentialSourceForConfigura
return await this.GetTokenCredentialSourceForAdAppWithClientSecret(configuration, cancellationToken).ConfigureAwait(false);
}

if (identitySourceType == ClientIdentitySourceTypes.ClientIdAndCertificate)
{
return await this.GetTokenCredentialSourceForAdAppWithClientCertificate(configuration, cancellationToken).ConfigureAwait(false);
}

TokenCredential tokenCredential = identitySourceType switch
{
ClientIdentitySourceTypes.SystemAssignedManaged => new ManagedIdentityCredential(),
Expand All @@ -83,6 +95,14 @@ public void InvalidateFailedAccessToken(ClientIdentityConfiguration configuratio
}
}

private async Task<IAzureTokenCredentialSource> GetTokenCredentialSourceForAdAppWithClientCertificate(ClientIdentityConfiguration configuration, CancellationToken cancellationToken)
{
X509Certificate2 certificate = await this.certificateSource.CertificateForConfigurationAsync(configuration.AzureAdAppClientCertificate!).ConfigureAwait(false);

// TO DO: Implement renewal callback.
return new AzureTokenCredentialSource(new TestableClientCertificateCredential(configuration.AzureAdAppTenantId!, configuration.AzureAdAppClientId!, certificate), null);
}

private async ValueTask<IAzureTokenCredentialSource> GetTokenCredentialSourceForAdAppWithClientSecret(
ClientIdentityConfiguration configuration, CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// <copyright file="TestableClientCertificateCredential.cs" company="Endjin Limited">
// Copyright (c) Endjin Limited. All rights reserved.
// </copyright>

namespace Corvus.Identity.ClientAuthentication.Azure.Internal
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using global::Azure.Identity;

/// <summary>
/// <see cref="ClientCertificateCredential"/> that makes it possible for tests to discover
/// what constructor arguments were used.
/// </summary>
internal class TestableClientCertificateCredential : ClientCertificateCredential
{
/// <summary>
/// Creates a <see cref="TestableClientSecretCredential"/>.
/// </summary>
/// <param name="tenantId">
/// The Azure Active Directory tenant (directory) Id of the service principal.
/// </param>
/// <param name="clientId">
/// The client (application) ID of the service principal.
/// </param>
/// <param name="clientCertificate">
/// A client certificate that was generated for the App Registration used to authenticate
/// the client.
/// </param>
public TestableClientCertificateCredential(string tenantId, string clientId, X509Certificate2 clientCertificate)
: base(tenantId, clientId, clientCertificate)
{
this.TenantId = tenantId;
this.ClientId = clientId;
this.ClientCertificate = clientCertificate;
}

/// <summary>
/// Gets the value passed as the <c>tenantId</c> constructor argument.
/// </summary>
public string TenantId { get; }

/// <summary>
/// Gets the value passed as the <c>clientId</c> constructor argument.
/// </summary>
public string ClientId { get; }

/// <summary>
/// Gets the value passed as the <c>clientCertificate</c> constructor argument.
/// </summary>
public X509Certificate2 ClientCertificate { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ public static IServiceCollection AddAzureTokenCredentialSourceFromDynamicConfigu
.AddSingleton<IAzureTokenCredentialSourceFromDynamicConfiguration, AzureTokenCredentialSourceFromConfiguration>()
.AddSingleton<IAccessTokenSourceFromDynamicConfiguration, AccessTokenSourceFromDynamicConfiguration>()
.AddSingleton<IKeyVaultSecretCache, KeyVaultSecretCache>()
.AddSingleton<IKeyVaultSecretClientFactory, KeyVaultSecretClientFactory>();
.AddSingleton<IKeyVaultSecretClientFactory, KeyVaultSecretClientFactory>()
.AddCertificateFromConfiguration();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="$(EndjinProjectPropsPath)" Condition="$(EndjinProjectPropsPath) != ''" />

<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageDescription></PackageDescription>
<PackageTags></PackageTags>
<PackageReleaseNotes></PackageReleaseNotes>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Endjin.RecommendedPractices.GitHub" Version="2.1.18">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
</ItemGroup>

</Project>
Loading
Loading