14

I would like to authenticate clients connecting to my ASP.NET Core Web API (.NET 5) running on Kestrel using certificate-based authentication.

In my Startup.cs I have the following in ConfigureServices:

services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.AllowedCertificateTypes = CertificateTypes.All;
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                // More code to verify certificates
            },
            OnAuthenticationFailed = context =>
            {
                // More code
            }
        };
    });

// Other services

And in Configure:

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthentication();

app.UseEndpoints(endpoints =>
{
    // Endpoints
});

And in Program.cs I have included:

webBuilder.ConfigureKestrel(o =>
{
    o.ConfigureHttpsDefaults(o =>
        o.ClientCertificateMode = ClientCertificateMode.RequireCertificate);
});

If I connect to the API in a browser, it prompts me for a certificate, but after I select a certificate, neither the OnCertificateValidated nor the OnAuthenticationFailed events are triggered. After some further testing, I realized that the entire options configuration delegate inside the AddCertificate call in Startup.cs never runs. This makes me think I am missing some kind of configuration for Kestrel, but I do not know what that is. As a note, my Web API does NOT use IIS hosting. What else do I need to do to use self-signed certificate-based authentication?

The code I have so far is based on the instructions found in the documentation here: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-5.0

  • 1
    You are using HTTPS (secure) which is using TLS for authentication. The certificate is part of the authentication. TLS the server sends a certificate block which contains the names of the possible certificates that can be used for authentication. The client then compares the names against the certificates in the stores (or ones specified in app). If the names do not match TLS will fail. Since Net 4.7.2 or later TLS is done in the operating system instead of in Net. In mobile devices the Kernel has to be able to support the version of TLS (1.2 or 1.3). Are you using HTTPS (not HTTP)? – jdweng Jun 27 '21 at 00:35
  • @jdweng I am using HTTPS. But I am not trying to prove the authenticity of the server (that part already works), I am trying to use certificates sent from the clients to the server to authenticate the clients as described in https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-5.0 – TheProgrammerNinja3.14 Jun 27 '21 at 03:06
  • You can't. The certificate has to be loaded on both client and server and not sent with message. A certificate is a private key and you do not want to send a key with a message. It is like given the key to your house to a burglar. TLS the server sends a certificate block with just the names of the certificate. Then client uses one of the named certificate to encrypt the message. – jdweng Jun 27 '21 at 08:00

2 Answers2

17

Ok, so in the end I was able to solve my own problem. There were two different parts to solving it, but ultimately it only required a few small modifications to my project code.

Recognizing client certificates

Firstly, the server was not recognizing the self-signed client certificates as valid certificates. This can be solved by either 1. adding all of the client certificates (or a root CA that signs them all) to the trusted certificate store of the operating system or 2. adding a ClientCertificateValidation callback to kestrel to determine whether or not a certificate is accepted or rejected.

Example of #2 (an adjustment to the ConfigureHttpsDefaults lambda in Program.cs) is below:

webBuilder.ConfigureKestrel(o =>
{
    o.ConfigureHttpsDefaults(opts =>
    {
        opts.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
        opts.ClientCertificateValidation = (cert, chain, policyErrors) =>
        {
            // Certificate validation logic here
            // Return true if the certificate is valid or false if it is invalid
        };
    });
});

As a side note, calling opts.AllowAnyClientCertificate() is a shorthand for adding a ClientCertificateValidation callback that always returns true, making ALL self-signed certificates valid.


Required Authorization

After applying either of these approaches, my API would accept queries from valid certificates, but my extra certificate validation logic in the OnCertificateValidated event was still not running. This is because, according to comments on ASP.NET Core issue #14033, this event's extra certificate validation will never run unless authorization is enabled for the endpoint being accessed. This makes sense because this event is often used to generate a ClaimsPrincipal from a certificate per the ASP.NET Core Docs on the subject. Setting ASP.NET to use authorization and requiring authorization for API calls (e.g., by applying the [Authorize] attribute to all controllers) causes the additional authentication checks to run for those API calls.

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthentication();

// Adding this and adding the [Authorize] attribute
// to controllers fixes the problem.
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    // Endpoints
});

After this, my OnCertificateValidated event was called for every connection and I was able to perform additional authentication logic and reject invalid certificates.

2

The accepted answer was very helpful for me (thanks!) but it did not solve my problem entirely.

I found that the ClientCertificateValidation function is not the only place the certificate is validated (and rejected). Using AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme). AddCertificate... also caused another validation to be triggered, after ClientCertificateValidation was handled.

To solve the problem for me, I had to configure a CustomTrustStore in lines to the CertificateAuthenticationOptions:

AddCertificate(options =>
{
    options.AllowedCertificateTypes = CertificateTypes.All;
    options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust;
    options.CustomTrustStore = new X509Certificate2Collection { rootCert };
    options.RevocationMode = X509RevocationMode.NoCheck;
    options.Events = new CertificateAuthenticationEvents
    {
        OnCertificateValidated = context =>
        {
            if (validationService.ValidateCertificate(context.ClientCertificate))
            {
                context.Success();
            }
            else
            {
                context.Fail("invalid cert");
            }

            return Task.CompletedTask;
        },
        OnAuthenticationFailed = context =>
        {
            context.Fail("invalid cert");
            return Task.CompletedTask;
        }
    };
});

Where rootCert is initiated with:

var rootCert = new X509Certificate2("RootCert.pfx", "1234");

And RootCert.pfx is added as file to the project.

Since we're using the built-in validation now from AddAuthentication(.).AddCertificate, we should disable the earlier validation with AllowAnyCertificate() (This validation does not know about our custom root trust):

builder.WebHost.ConfigureKestrel(kso =>
{
    kso.ConfigureHttpsDefaults(cao => {
        cao.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
        cao.AllowAnyClientCertificate();
        cao.CheckCertificateRevocation = false;
    });
});
benk
  • 377
  • 1
  • 4
  • 13
  • This was useful for me when deploying to Azure App Service. There was no way to add a trusted root certificate because of security reasons, and this is a neat work-around! – benk Dec 03 '21 at 08:32
  • How do you create a root certificate for the selfsigned certificate with dotnet? – RADU May 06 '23 at 17:30