4

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.

  • I'd suggest setting the logging level to Debug and see what comes through, as the certificate authentication handler emits log entries. Are you sure you're passing a client certificate in your request? – Mickaël Derriey Jan 07 '21 at 04:43
  • Did you ever have any luck? I'm having the same issue with OnCertificateValidated and OnAuthenticationFailed not being hit. I did discover that there is another event: OnChallenge, that you can include. That one is being hit for me. I'd suggest adding that to see if it's being hit. Though I'm not sure what to do with it if it is. Microsoft's documentation on that event is unsurprisingly not helpful. – Jason247 Sep 24 '21 at 15:11

1 Answers1

0

I had a similar issue on IIS Express where only "OnChallenge" fired but not "OnCertificateValidated" or "OnAuthenticationFailed", and kept getting 403 status responses.

This answer from a different question solved it for my case (IIS Express). Set the access sslFlags in .vs\config\applicationhost.config to use client certificate:

<access sslFlags="Ssl, SslNegotiateCert, SslRequireCert" />
David Tang
  • 167
  • 3
  • 8