I have a library I created originally in .NET 6. I recently performed an in-place upgrade to .NET 8. Now, the library code never executes. Here is the code for the AuthenticationService.cs class (implements an interface, IAuthenticationService.cs, that has a stub for the IsValidClientCertificate method — used in dependency injection):
using System.Configuration;
using Corp.Lib.CertificateAuthentication.Services.Interfaces;
using System.Security.Cryptography.X509Certificates;
using Corp.Lib.CertificateAuthentication.Configuration;
using Microsoft.Extensions.Options;
using Corp.Lib.Logging;
namespace Corp.Lib.CertificateAuthentication.Services
{
public class AuthenticationService : IAuthenticationService
{
private readonly ClientCertificateSettings _ClientCertificateSettings;
public AuthenticationService(IOptions<ClientCertificateSettings> options)
{
_ClientCertificateSettings = options.Value;
}
public bool IsValidClientCertificate(X509Certificate2 clientCertificate)
{
if (clientCertificate == null!)
{
var error = new ConfigurationErrorsException("Missing certificate or certificate not sent by client.");
Logger.Log.Error(error, "Certificate validation failed. Missing certificate or certificate not sent by client.");
throw error;
}
if (_ClientCertificateSettings == null! || _ClientCertificateSettings.AllowedCertificates == null! || !_ClientCertificateSettings.AllowedCertificates.ToList().Any())
{
var error = new ConfigurationErrorsException("Certificate configuration missing. Check AppSettings.");
Logger.Log.Error(error, "Certificate validation failed. Certificate configuration missing. Check AppSettings.");
throw error;
}
if (_ClientCertificateSettings.AllowedCertificates.Any(cert => string.IsNullOrEmpty(cert.Subject)))
{
var error = new ConfigurationErrorsException("Certificate configuration missing Subject. Check AppSettings.");
Logger.Log.Error(error, "Certificate validation failed. Certificate configuration missing Subject. Check AppSettings.");
throw error;
}
if (_ClientCertificateSettings.AllowedCertificates.Any(cert => string.IsNullOrEmpty(cert.Issuer)))
{
var error = new ConfigurationErrorsException("Certificate configuration missing Subject. Check AppSettings.");
Logger.Log.Error(error, "Certificate validation failed. Certificate configuration missing Issuer. Check AppSettings.");
throw error;
}
// 1. Check time validity of certificate.
if (DateTime.Compare(DateTime.UtcNow, clientCertificate.NotBefore) < 0 || DateTime.Compare(DateTime.UtcNow, clientCertificate.NotAfter) > 0)
{
Logger.Log.Warning($"Certificate with thumbprint {clientCertificate.Thumbprint} is expired.");
return false;
}
// 2. Check subject name of certificate.
var foundSubject = false;
var certSubjectData = clientCertificate.Subject.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (certSubjectData.Any(certSubject => _ClientCertificateSettings.AllowedCertificates.Any(cert => cert.Subject.Equals(certSubject.Trim(), StringComparison.InvariantCultureIgnoreCase))))
{
foundSubject = true;
}
if (!foundSubject)
{
Logger.Log.Warning($"Certificate with thumbprint {clientCertificate.Thumbprint} does not have a matching Subject.");
return false;
}
// 3. Check issuer name of certificate.
var certIssuerData = clientCertificate.Issuer.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var foundIssuer = certIssuerData.Any(issuerData => _ClientCertificateSettings.AllowedCertificates.Any(cert => cert.Issuer.Equals(issuerData.Trim(), StringComparison.InvariantCultureIgnoreCase)));
if (!foundIssuer)
{
Logger.Log.Warning($"Certificate with thumbprint {clientCertificate.Thumbprint} does not have a matching Issuer.");
return false;
}
// Check if the Certificate exists in the personal cert store.
if (_ClientCertificateSettings.CheckThumbprintInCertStore)
{
var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
try
{
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
var certs = store.Certificates.Find(X509FindType.FindByThumbprint, clientCertificate.Thumbprint, true);
if (certs.Count == 0)
{
Logger.Log.Warning("Invalid client certificate. The thumbprint does not match with a certificate in the certificate store. {@clientCertificate}", clientCertificate);
return false;
}
}
catch (Exception ex)
{
Logger.Log.Error(ex, "An exception occurred searching for the client certificate in the certificate store. {@clientCertificate}", clientCertificate);
throw;
}
finally
{
store.Close();
store.Dispose();
}
}
return true;
}
}
}
Here is the code for the ServiceCollectionExtension.cs static extension class:
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Corp.Lib.CertificateAuthentication.Configuration;
using Corp.Lib.CertificateAuthentication.Services;
using Corp.Lib.CertificateAuthentication.Services.Interfaces;
using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.DependencyInjection;
namespace Corp.Lib.CertificateAuthentication.Extensions
{
public static class ServiceCollectionExtensions
{
public static void AddCertificateAuthenticationService(this WebApplicationBuilder builder)
{
builder.Services.Configure<ClientCertificateSettings>(builder.Configuration.GetSection("ClientCertificateSettings"));
// Require a certificate from any client sending a request.
builder.WebHost.ConfigureKestrel(kestrel =>
{
kestrel.ConfigureHttpsDefaults(defaults =>
{
defaults.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
});
builder.Services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "ssl-client-cert";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2? clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
clientCertificate = X509Certificate2.CreateFromPem(WebUtility.UrlDecode(headerValue));
}
return clientCertificate!;
};
});
builder.Services.AddSingleton<IAuthenticationService, AuthenticationService>();
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.Chained;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService = context.HttpContext.RequestServices.GetRequiredService<IAuthenticationService>();
if (validationService.IsValidClientCertificate(context.ClientCertificate))
{
var claims = new[]
{
new Claim(
ClaimTypes.NameIdentifier,
context.ClientCertificate.GetNameInfo(X509NameType.SimpleName, false),
ClaimValueTypes.String,
context.Options.ClaimsIssuer),
new Claim(
ClaimTypes.Name,
context.ClientCertificate.GetNameInfo(X509NameType.SimpleName, false),
ClaimValueTypes.String,
context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
else
{
context.Fail("Invalid certificate.");
}
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
}
public static void UseCertificateAuthenticationService(this WebApplication app)
{
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseAuthorization();
}
}
}
Finally, here is the Program.cs for the WebApi project referencing the Certificate Authentication library (the logging is required in my project, but not included here — please comment out references):
using Corp.Lib.CertificateAuthentication.Extensions;
using Corp.Lib.Logging.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.AddLogging(false);
builder.AddCertificateAuthenticationService();
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseLogging(false);
app.UseCertificateAuthenticationService();
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
The implementation I included in the previous step was basically verbatim of Microsoft Learn’s Prescribed Implementation found here (https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-8.0). I cannot even get the browser to prompt me for the certificate anymore. When it does in a UAT scenario with IIS as the web server, it prompts me for a certificate, but it ignores the AuthenticationService.cs IsValidClientCertificate() method, like it never even executes. I just want certificate authentication to run with my defined logic in addition to whatever Kestrel, IIS Express, or IIS does.