I want to implement certificate authentication on my .NET Core 3.1 API. I followed the steps outlined by Microsoft here:https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-5.0
But, I don't think the events "OnCertificateValidated" and "OnAuthenticationFailed" even fire. Breakpoints are never hit, and I even tried adding code that should break in there just to test, but it never fails (most likely because it never reaches there)
I'm trying to validate the Client Certificate CN and I want to only allow certain CNs to be able to call my API.
You can find my code below. What am I doing wrong?
Startup
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService =
context.HttpContext.RequestServices.GetRequiredService<ICertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate))
{
context.Success();
}
else
{
context.Fail($"Unrecognized client certificate: {context.ClientCertificate.GetNameInfo(X509NameType.SimpleName, false)}");
}
int test = Convert.ToInt32("test");
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
int test = Convert.ToInt32("test");
context.Fail($"Invalid certificate");
return Task.CompletedTask;
}
};
});
services.AddHealthChecks();
services.AddControllers(setupAction =>
{
setupAction.Filters.Add(new ProducesResponseTypeAttribute(StatusCodes.Status406NotAcceptable));
setupAction.Filters.Add(new ProducesResponseTypeAttribute(StatusCodes.Status500InternalServerError));
setupAction.ReturnHttpNotAcceptable = true;
}).AddXmlDataContractSerializerFormatters();
services.AddScoped<IJobServiceRepository, JobServiceRepository>();
services.AddScoped<ICaptureServiceRepository, CaptureServiceRepository>();
services.Configure<KTAEndpointConfig>(Configuration.GetSection("KTAEndpointConfig"));
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
services.AddVersionedApiExplorer(setupAction =>
{
setupAction.GroupNameFormat = "'v'VV";
});
services.AddApiVersioning(setupAction =>
{
setupAction.AssumeDefaultVersionWhenUnspecified = true;
setupAction.DefaultApiVersion = new ApiVersion(1, 0);
setupAction.ReportApiVersions = true;
});
var apiVersionDecriptionProvider = services.BuildServiceProvider().GetService<IApiVersionDescriptionProvider>();
services.AddSwaggerGen(setupAction =>
{
foreach (var description in apiVersionDecriptionProvider.ApiVersionDescriptions)
{
setupAction.SwaggerDoc($"TotalAgilityOpenAPISpecification{description.GroupName}", new Microsoft.OpenApi.Models.OpenApiInfo()
{
Title = "TotalAgility API",
Version = description.ApiVersion.ToString(),
Description = "Kofax TotalAgility wrapper API to allow creating KTA jobs and uploading documents",
Contact = new Microsoft.OpenApi.Models.OpenApiContact()
{
Email = "shivam.sharma@rbc.com",
Name = "Shivam Sharma"
}
});
setupAction.OperationFilter<AddRequiredHeaderParameter>();
var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlCommentsFullPath = Path.Combine(AppContext.BaseDirectory, xmlCommentsFile);
setupAction.IncludeXmlComments(xmlCommentsFullPath);
}
setupAction.DocInclusionPredicate((documentName, apiDescription) =>
{
var actionApiVersionModel = apiDescription.ActionDescriptor
.GetApiVersionModel(ApiVersionMapping.Explicit | ApiVersionMapping.Implicit);
if (actionApiVersionModel == null)
{
return true;
}
if (actionApiVersionModel.DeclaredApiVersions.Any())
{
return actionApiVersionModel.DeclaredApiVersions.Any(v =>
$"TotalAgilityOpenAPISpecificationv{v.ToString()}" == documentName);
}
return actionApiVersionModel.ImplementedApiVersions.Any(v =>
$"TotalAgilityOpenAPISpecificationv{v.ToString()}" == documentName);
});
});
services.AddHttpsRedirection((httpsOpts) =>
{
//httpsOpts.HttpsPort = 443;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider apiVersionDescriptionProvider)
{
app.UseApiExceptionHandler();
app.UseHeaderLogContextMiddleware();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSwagger(setupAction =>
{
setupAction.SerializeAsV2 = true;
});
app.UseSwaggerUI(setupAction =>
{
foreach (var decription in apiVersionDescriptionProvider.ApiVersionDescriptions)
{
setupAction.SwaggerEndpoint($"/swagger/TotalAgilityOpenAPISpecification{decription.GroupName}/swagger.json",
decription.GroupName.ToUpperInvariant());
}
//setupAction.SwaggerEndpoint("/swagger/TotalAgilityOpenAPISpecification/swagger.json",
//"TotalAgility API");
setupAction.RoutePrefix = "";
});
app.UseSerilogRequestLogging();
app.UseHeaderValidation();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers().RequireAuthorization();
endpoints.MapHealthChecks("/health");
});
}
}
CertificateValidationService
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
namespace TotalAgility_API.Services
{
public class CertificateValidationService : ICertificateValidationService
{
private readonly IConfiguration _configuration;
private readonly ILogger<CertificateValidationService> _logger;
public CertificateValidationService(IConfiguration configuration, ILogger<CertificateValidationService> logger)
{
_logger = logger;
_configuration = configuration;
}
public bool ValidateCertificate(X509Certificate2 clientCert)
{
List<string> allowedCNs = new List<string>();
_configuration.GetSection("AllowedClientCertCNList").Bind(allowedCNs);
string cn = clientCert.GetNameInfo(X509NameType.SimpleName, false);
if (allowedCNs.Contains(cn))
{
return true;
}
else
{
_logger.LogWarning("Invalid Cert CN: {CN}", cn);
return false;
}
}
}
}
appsettings.json
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
}
},
"AllowedHosts": "*",
"AllowedClientCertCNList": [
"1",
"2",
"3",
"4"
]
}
Any help would be appreciated here. I'm new to this and I'm don't know how to proceed.