Certificate Authentication in ASP.NET Core 3.1

This article shows how Certificate Authentication can be implemented in ASP.NET Core 3.0. In this example, a shared self signed certificate is used to authenticate one application calling an API on a second ASP.NET Core application.

Code https://github.com/damienbod/AspNetCoreCertificateAuth

Posts in this series

History

2019-12-06: Updated Nuget packages, .NET Core 3.1

2019-09-06: Updated Nuget packages, .NET Core 3 preview 9

Setting up the Server

Add the Certificate Authentication using the Microsoft.AspNetCore.Authentication.Certificate NuGet package to the server ASP.NET Core application.

This can also be added directly in the csproj file.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" 
      Version="3.1.0" />
  </ItemGroup>

  <ItemGroup>
    <None Update="sts_dev_cert.pfx">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>

The authentication can be added in the ConfigureServices method in the Startup class. This example was built using the ASP.NET Core documentation. The AddAuthentication extension method is used to define the default scheme as “Certificate” using the CertificateAuthenticationDefaults.AuthenticationScheme string. The AddCertificate method then adds the configuration for the certificate authentication. At present, all certificates are excepted which is not good and the MyCertificateValidationService class is used to do extra validation of the client certificate. If the validation fails, the request is failed and the request for the resource will be rejected.

public void ConfigureServices(IServiceCollection services)
{
	services.AddSingleton<MyCertificateValidationService>();

	services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
		.AddCertificate(options => // code from ASP.NET Core sample
		{
			options.AllowedCertificateTypes = CertificateTypes.All;
			options.Events = new CertificateAuthenticationEvents
			{
				OnCertificateValidated = context =>
				{
					var validationService =
						context.HttpContext.RequestServices.GetService<MyCertificateValidationService>();

					if (validationService.ValidateCertificate(context.ClientCertificate))
					{
						var claims = new[]
						{
							new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
							new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
						};

						context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
						context.Success();
					}
					else
					{
						context.Fail("invalid cert");
					}

					return Task.CompletedTask;
				}
			};
		});

	services.AddAuthorization();

	services.AddControllers();
}

The AddCertificateForwarding method is used so that the client header can be specified and how the certificate is to be loaded using the HeaderConverter option. When sending the certificate with the HttpClient using the default settings, the ClientCertificate was always be null. The X-ARR-ClientCert header is used to pass the client certificate, and the cert is passed as a string to work around this.

services.AddCertificateForwarding(options =>
{
	options.CertificateHeader = "X-ARR-ClientCert";
	options.HeaderConverter = (headerValue) =>
	{
		X509Certificate2 clientCertificate = null;
		if(!string.IsNullOrWhiteSpace(headerValue))
		{
			byte[] bytes = StringToByteArray(headerValue);
			clientCertificate = new X509Certificate2(bytes);
		}

		return clientCertificate;
	};
});

The Configure method then adds the middleware. UseCertificateForwarding is added before the UseAuthentication and the UseAuthorization.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	...
	
	app.UseRouting();

	app.UseCertificateForwarding();
	app.UseAuthentication();
	app.UseAuthorization();

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapControllers();
	});
}

The MyCertificateValidationService is used to implement validation logic. Because we are using self signed certificates, we need to ensure that only our certificate can be used. We validate that the thumbprints of the client certificate and also the server one match, otherwise any certificate can be used and will be be enough to authenticate.

using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            var cert = new X509Certificate2(Path.Combine("sts_dev_cert.pfx"), "1234");
            if (clientCertificate.Thumbprint == cert.Thumbprint)
            {
                return true;
            }

            return false;
        }
    }
}

The API ValuesController is then secured using the Authorize attribute.

[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ValuesController : ControllerBase
{

...

The ASP.NET Core server project is deployed in this example as an out of process application using kestrel. To use the service, a certificate is required. This is defined using the ClientCertificateMode.RequireCertificate option.

public static IWebHost BuildWebHost(string[] args)
  => WebHost.CreateDefaultBuilder(args)
  .UseStartup<Startup>()
  .ConfigureKestrel(options =>
  {
	var cert = new X509Certificate2(Path.Combine("sts_dev_cert.pfx"), "1234");
	options.ConfigureHttpsDefaults(o =>
	{
		o.ServerCertificate = cert;
		o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
	});
  })
  .Build();

Implementing the HttpClient

The client of the API uses a HttpClient which was create using an instance of the IHttpClientFactory. This does not provide a way to define a handler for the HttpClient and so we use a HttpRequestMessage to add the Certificate to the “X-ARR-ClientCert” request header. The cert is added as a string using the GetRawCertDataString method.

private async Task<JArray> GetApiDataAsync()
{
	try
	{
		var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

		var client = _clientFactory.CreateClient();

		var request = new HttpRequestMessage()
		{
			RequestUri = new Uri("https://localhost:44379/api/values"),
			Method = HttpMethod.Get,
		};

		request.Headers.Add("X-ARR-ClientCert", cert.GetRawCertDataString());
		var response = await client.SendAsync(request);

		if (response.IsSuccessStatusCode)
		{
			var responseContent = await response.Content.ReadAsStringAsync();
			var data = JArray.Parse(responseContent);

			return data;
		}

		throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
	}
	catch (Exception e)
	{
		throw new ApplicationException($"Exception {e}");
	}
}

If the correct certificate is sent to the server, the data will be returned. If no certificate is sent, or the wrong certificate, then a 403 will be returned. It would be nice if the IHttpClientFactory would have a way of defining a handler for the HttpClient. I also believe a non valid certificates should fail per default and not require extra validation for this. The AddCertificateForwarding should also not be required to use for a default HTTPClient client calling the service.

Certificate Authentication is great, and helps add another security layer which can be used together with other solutions. See the code and ASP.NET Core src code for further documentation and examples. Links underneath.

Links

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.0

https://github.com/aspnet/AspNetCore/tree/master/src/Security/Authentication/Certificate/src

https://tools.ietf.org/html/rfc5246#section-7.4.4

21 comments

  1. […] Certificate Authentication in ASP.NET Core 3.0 – Damien Bowden […]

  2. […] Certificate Authentication in ASP.NET Core 3.0 (Damien Bowden) […]

  3. […] Certificate Authentication in ASP.NET Core 3.0 (Self Signed) […]

  4. Hi Damien, thank you for all these guides into the wonderful world of .Net core and authentication. Can you email me at av@validators.com – if you can help me (paid work) with some certificates in dotnet core. Thanks.

  5. Michael Lüthi · · Reply

    Hi
    I’ve got downloaded the sample and it starts fine when using IIS Express. Switching to kestrel standalone (command line) let the browser reject the connection because of invalid cn. Ignoring this on browser level let the browser ask vor any client certificate but even if i choose the right one handlers never get reached. Do you have any idea why?

    Thanks.
    PS: the client application therminates the http-contection imediatly by throwing an Exception

  6. Hi Michael

    I never tested this, will have a look at it, and get back to you.

    Greetings Damien

  7. Frederic Thibault · · Reply

    Cannot make it works. I followed all the steps, also restarted everything from scratch and It is always the same. I tried with the same Microsoft.AspNetCore.Authentication.Certificate version and also the current 3.0.0.

    SocketException: An established connection was aborted by the software in your host machine.

    If I remove the ClientCertificateMode.. from Program and also comments [Authorize] in controller I can get the response (just for saying that my environment works)

    Someone managed to make it works?

    1. Hi Frederic, this sounds like the cert is not trusted on your host env. Is the cert added to the host?

      Also in the logs, low down, in the debug logs, you should see the reason why the client app or the host app rejects the certificate.

      Hope this helps

      Also if you clone the git repo, and register the certs, it should work. maybe this might help you as well

      greetings Damien

      1. Frederic Thibault · ·

        Hi thanks for the quick reply.

        I publish the poc (based on yours) in github https://github.com/B-Temia/poc-certificate

        Yes, I added the “root certificate” on the local computer “Trusted Root Certification Authorities”. I also added the intermediate later… and the child later later… and I always have the same error.

        When I comment ClientCertificateMode = ClientCertificateMode.RequireCertificate; the certificate is validated(see it in the console) but I receive a 403 and CertificateAuthenticationEvents -> OnCertificateValidated is never called…

        Thanks,

  8. Frederic Thibault · · Reply

    Hi,
    When I run Visual Studio in administrator mode I dont have the socket problem anymore. I can now see in the console that the certificate is evaluated

    info: Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationHandler[12]
    AuthenticationScheme: Certificate was challenged.

    But I always receive a 403 and CertificateAuthenticationEvents.OnCertificateValidated is never called. I have the [Authorize] Attribute on the controller…

    Thanks,

    1. Exactly the same issue.

      Presumably cert is ok otherwise it wouldn’t get through IIS, no? I have requre client certificate turned on there, and if I don’t send a certificate then I get a detailed 403.7

      1. My fault, I missed out the app.UseCertificateForwarding();

  9. Karl Waclawek · · Reply

    I have the same problem, OnCertificateValidated never gets called, but I am sure the certiciate works, because I can see its details in ServerCallContext when I remove the Authorize attribue from the controller. I am using Grpc.AspNetCore 2.25.0.

  10. Me again.

    I’ve followed this guide and the certificate check is coming back as valid. However, in the AddCertificate ext method, I am also setting the RevocationMode to Online and RevocationFlag to EntireChain. I am not seeing any requests being made to check the crl. I’m using Fiddler to view the traffic, and clearing the crl cache using certutil beforehand (certutil -urlcache crl delete). I’ve also tried disabling my network adapter, but the cert is still valid.

    When I check the certificate using “certutil -f -urlfetch -verfy cert.crt” then I can see the crl requests being made.

    Do you have a specific example which enforces the crl check? Is there anything else I need to include to get it to run? Are there any logs I can check to see if it’s being attempted.

    1. Ok, I’ve figured this out now. It is not enough to just clear the crl cache, you also need to reset IIS. I guess IIS is caching the cache somehow???

      I can see the crl requests being made now. Phew.

  11. Hi, me again.

    I’ve implemented extra validation within the OnCertificateValidated event handler, as you’ve suggested. I’m calling context.Fail(“failure reason”).

    However, I am not seeing the failure reason anywhere in the response. What is the best way to return this to the client?

    1. Frederic Thibault · · Reply

      Hi,

      As far has I know, you don’t have to do any certificate validation if OnCertificateValidated is called it is because the certificate is valid, it is done by the OS. Has an example, if you remove the intermediate certificate from the store, OnCertificateValidated will not be call. If you add the child certificate in the “Untrusted Certificates” OnCertificateValidated will not be called.

      1. Hi Frederic,

        I’m referring to the first code section in the article after the text ” the MyCertificateValidationService class is used to do extra validation of the client certificate. If the validation fails, the request is failed and the request for the resource will be rejected.”

        The event handler for OnCertificateValided calls “context.Fail(“invalid cert”);”

        How can this be message be included into the response?

    2. Frederic Thibault · · Reply

      Instead of setting context.Fail and return a Task.Completed, return Task.FromException(new Exception(“invalid cert”)); and manage your exception output at the same place than everywhere else in your application by adding something like

      app.UseExceptionHandler(configure =>
      {
      configure.Run(async context =>
      {
      context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
      // context.Response.ContentType = “application/json”;

      var contextFeature = context.Features.Get();
      if (contextFeature != null)
      {
      //await context.Response.WriteAsync(new
      //{
      // statusCode = context.Response.StatusCode,
      // message = “Internal Server Error.”
      //}.ToString());
      }
      });
      });

      In the Configure section of the Startup.The contextFeature will contains your exception where ever it is throw.

      1. Frederic Thibault · ·

        Sorry I misssed

        var contextFeature = context.Features.Get();

        To get the IExceptionHandlerFeature

      2. Frederic Thibault · ·

        The editor remove the generic part of the code to get the feature.
        so for the get you must have the type IExceptionHandlerFeature .. before the ()..
        Probably the anti xss because it is a markup in some way.

Leave a Reply to The Morning Brew - Chris Alcock » The Morning Brew #2766 Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: