Azure Web App Client Certificate Authentication with ASP.NET Core

This post will walk through securing an ASP.NET Core application deployed to an Azure Web App that is secured using client certificates.

The code is available for download at ClientCertDemo

Background

Many customers have implemented client certificates for older web applications and are looking at Azure Web Apps to move their code.  A common question is how to leverage client certificates in Azure Web Apps.  This demo shows one possible solution to validating the certificate.

The client application will add the client certificate to the HTTP request.  We will update the Azure Web App to enable client certificates, and the client certificate will be available using the X-ARR-ClientCert HTTP header.  We can then parse this as an X509Certificate2 class to read the properties of the certificate and perform validation.  Note that Azure Web Apps do not perform any validation on the certificate, this is up to the application to implement.  That’s what this post shows.

The source code for the web application is available at https://github.com/kaevans/ClientCertDemo

Create the Certificates

There’s nothing special about the certificate used in this demo, we’ll use self-signed certificates.  In fact, I am going to reuse the self-signed certificate that I used for creating a point-to-site VPN connection.  I am using makecert.exe, which is easy to find if you have Visual Studio 2015 installed… just load the “VS2015 x64 x86 Cross Tools Command Prompt”.

image

First, generate the root cert.

Generate a Root Certificate

  1. makecert -sky exchange -r -n "CN=NetworkingDemoRootCert2" -pe -a sha1 -len 2048 -ss My "NetworkingDemoRootCert2.cer"

Next, generate a client certificate that references the root certificate.

Generate Client Certificate

  1. makecert.exe -n "CN=NetworkingDemoClientCert2" -pe -sky exchange -m 96 -ss My -in "NetworkingDemoRootCert2" -is my -a sha1

Now open certmgr.msc to verify the certificates are there.

image

Double-click on the client cert that you just created and see its properties, we’ll need these properties in a moment.

image

Now that we have the certs, create the project.

Create the Web Application

Create a new ASP.NET Web Application.

image

Use the ASP.NET 5 Web API template, and choose host in Azure Web App.

image

You are prompted to create the Azure Web App.

image

Enable Configuration

Update the dependencies in the project.json file to include Microsoft.Extensions.OptionModel.

image

Update appsettings.json with the values from your certificate, taking care to remove spaces from the values (I lost a bit of time troubleshooting that one!)

appsettings.json

  1. {
  2.   "Logging":
  3.   {
  4.     "IncludeScopes": false,
  5.     "LogLevel":
  6.     {
  7.       "Default": "Debug",
  8.       "System": "Debug",
  9.       "Microsoft": "Debug"
  10.     }
  11.   },
  12.   "CertificateValidation":
  13.   {
  14.     "Subject": "CN=NetworkingDemoClientCert2a",
  15.     "IssuerCN": "CN=NetworkingDemoRootCert2",
  16.     "Thumbprint": "E8434AB532846AD2BDF20863A8789AF23E20ECBA"
  17.   }
  18. }

Add a class to hold the configuration values.

CertificateValidationConfig

  1. namespace ClientCertWeb
  2. {
  3.     public class CertificateValidationConfig
  4.     {
  5.         public string Subject { get; set; }
  6.         public string IssuerCN { get; set; }
  7.         public string Thumbprint { get; set; }
  8.     }
  9. }

Now update the ConfigureServices method in Startup.cs to use services.AddOptions and register your custom class with the dependency injection system.

ConfigureServices

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3.     services.AddOptions();
  4.     services.Configure<CertificateValidationConfig>(Configuration.GetSection("CertificateValidation"));
  5.     // Add framework services.
  6.     services.AddMvc();
  7. }

Add Some Middleware

Add a middleware class.

image

The middleware class will request the configuration class via the Options model, and will also request logging via dependency injection via its constructor.  We add an overload to the extensions class to add the options model in the constructor.  When the HTTP request is received, the Invoke method is run for every request, and the configuration and logging types are passed to our middleware.  The Invoke method will validate the certificate that is passed.  The validation code is borrowed from the article How To Configure TLS Mutual Authentication for Web App with a few slight modifications to enable using the configuration system and adding logging.

ClientCertMiddleware

  1. using Microsoft.AspNet.Builder;
  2. using Microsoft.AspNet.Http;
  3. using Microsoft.Extensions.Logging;
  4. using Microsoft.Extensions.OptionsModel;
  5. using System;
  6. using System.Security.Cryptography.X509Certificates;
  7. using System.Threading.Tasks;
  8.  
  9. namespace ClientCertWeb
  10. {
  11.     // You may need to install the Microsoft.AspNet.Http.Abstractions package into your project
  12.     public class ClientCertificateMiddleware
  13.     {
  14.         private readonly RequestDelegate _next;
  15.         private readonly CertificateValidationConfig _config;
  16.         private readonly ILogger _logger;
  17.  
  18.         public ClientCertificateMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<CertificateValidationConfig> options)
  19.         {
  20.             _next = next;
  21.             _config = options.Value;
  22.             _logger = loggerFactory.CreateLogger<ClientCertificateMiddleware>();
  23.         }
  24.  
  25.         public async Task Invoke(HttpContext context)
  26.         {
  27.             //Validate the cert here
  28.  
  29.             bool isValidCert = false;
  30.             X509Certificate2 certificate = null;
  31.  
  32.             string certHeader = context.Request.Headers["X-ARR-ClientCert"];
  33.  
  34.             if (!String.IsNullOrEmpty(certHeader))
  35.             {
  36.                 try
  37.                 {
  38.                     byte[] clientCertBytes = Convert.FromBase64String(certHeader);
  39.                     certificate = new X509Certificate2(clientCertBytes);
  40.  
  41.                     isValidCert = IsValidClientCertificate(certificate);
  42.                     if (isValidCert)
  43.                     {
  44.                         //Invoke the next middleware in the pipeline
  45.                         await _next.Invoke(context);
  46.                     }
  47.                     else
  48.                     {
  49.                         //Stop the pipeline here.
  50.                         _logger.LogError("Certificate is not valid");
  51.                         context.Response.StatusCode = 403;
  52.                     }
  53.                 }
  54.                 catch (Exception ex)
  55.                 {
  56.                     //What to do with exceptions in middleware?
  57.                     _logger.LogError(ex.Message, ex);
  58.                     await context.Response.WriteAsync(ex.Message);
  59.                     context.Response.StatusCode = 403;
  60.                 }
  61.             }
  62.             else
  63.             {
  64.                 _logger.LogError("X-ARR-ClientCert header is missing");
  65.                 context.Response.StatusCode = 403;
  66.             }
  67.         }
  68.  
  69.  
  70.         private bool IsValidClientCertificate(X509Certificate2 certificate)
  71.         {
  72.             // In this example we will only accept the certificate as a valid certificate if all the conditions below are met:
  73.             // 1. The certificate is not expired and is active for the current time on server.
  74.             // 2. The subject name of the certificate has the common name nildevecc
  75.             // 3. The issuer name of the certificate has the common name nildevecc and organization name Microsoft Corp
  76.             // 4. The thumbprint of the certificate is 30757A2E831977D8BD9C8496E4C99AB26CB9622B
  77.             //
  78.             // This example does NOT test that this certificate is chained to a Trusted Root Authority (or revoked) on the server
  79.             // and it allows for self signed certificates
  80.             //
  81.  
  82.             if (null == certificate) return false;
  83.  
  84.             // 1. Check time validity of certificate
  85.             if (DateTime.Compare(DateTime.Now, certificate.NotBefore) < 0 || DateTime.Compare(DateTime.Now, certificate.NotAfter) > 0) return false;
  86.  
  87.             // 2. Check subject name of certificate
  88.             bool foundSubject = false;
  89.             string[] certSubjectData = certificate.Subject.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
  90.             foreach (string s in certSubjectData)
  91.             {
  92.                 if (String.Compare(s.Trim(), _config.Subject) == 0)
  93.                 {
  94.                     foundSubject = true;
  95.                     break;
  96.                 }
  97.             }
  98.             if (!foundSubject) return false;
  99.  
  100.             // 3. Check issuer name of certificate
  101.             bool foundIssuerCN = false;
  102.             string[] certIssuerData = certificate.Issuer.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
  103.             foreach (string s in certIssuerData)
  104.             {
  105.                 if (String.Compare(s.Trim(), _config.IssuerCN) == 0)
  106.                 {
  107.                     foundIssuerCN = true;
  108.                     break;
  109.                 }
  110.  
  111.             }
  112.  
  113.             if (!foundIssuerCN) return false;
  114.  
  115.             // 4. Check thumprint of certificate
  116.             if (String.Compare(certificate.Thumbprint.Trim().ToUpper(), _config.Thumbprint) != 0) return false;
  117.  
  118.             // If you also want to test if the certificate chains to a Trusted Root Authority you can uncomment the code below
  119.             //
  120.             //X509Chain certChain = new X509Chain();
  121.             //certChain.Build(certificate);
  122.             //bool isValidCertChain = true;
  123.             //foreach (X509ChainElement chElement in certChain.ChainElements)
  124.             //{
  125.             // if (!chElement.Certificate.Verify())
  126.             // {
  127.             // isValidCertChain = false;
  128.             // break;
  129.             // }
  130.             //}
  131.             //if (!isValidCertChain) return false;
  132.  
  133.             return true;
  134.         }
  135.  
  136.     }
  137.  
  138.     // Extension method used to add the middleware to the HTTP request pipeline.
  139.     public static class ClientCertMiddlewareExtensions
  140.     {
  141.         public static IApplicationBuilder UseClientCertMiddleware(this IApplicationBuilder builder)
  142.         {
  143.             return builder.UseMiddleware<ClientCertificateMiddleware>();
  144.         }
  145.  
  146.         public static IApplicationBuilder UseClientCertMiddleware(this IApplicationBuilder builder, IOptions<CertificateValidationConfig> options)
  147.         {
  148.  
  149.             return builder.UseMiddleware<ClientCertificateMiddleware>(options);
  150.         }
  151.     }
  152. }

To enable logging when deployed to Azure, modify the web.config file and change the stdoutLogEnabled value to true.

image

When deployed to Azure, this will write the log files so that you can troubleshoot when things aren’t working.

image

Finally, we need to register our middleware in the Configure method in Startup.cs.

Configure

  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
  2. {
  3.     loggerFactory.AddConsole(Configuration.GetSection("Logging"));
  4.     loggerFactory.AddDebug();
  5.  
  6.     app.UseIISPlatformHandler();
  7.  
  8.     app.UseStaticFiles();
  9.  
  10.     app.UseClientCertMiddleware();
  11.  
  12.     app.UseMvc();
  13. }

That’s it!  We have now created the middleware needed to validate the certificate. 

Update the Azure Web App

The Azure Web App needs to be updated in order to allow client certificates.  There are instructions at How To Configure TLS Mutual Authentication for Web App that show how to use ARMClient to update the web app.  Note that you could do this at deployment time if you were deploying using an ARM Template with web deploy. 

The PUT request that you need looks like:

ARMClient PUT

  1. ARMClient PUT subscriptions/{Subscription Id}/resourcegroups/{Resource Group Name}/providers/Microsoft.Web/sites/{Website Name}?api-version=2015-04-01 @enableclientcert.json -verbose

The ARMClient tool requires your subscription ID, resource group name, and web app name.  An easy way to get this information is to go to the Azure portal and go to your web app’s properties, looking at the resource ID.

image

Create a local file, enableclientcert.json and use your web app’s location in the file.

enableclientcert.json

  1. { "location": "South Central US",
  2. "properties": {
  3. "clientCertEnabled": true } }

Run ARMClient.exe with the PUT URL and the output will look similar to the following.

image

Publish the Web Application

Right-click the project and choose Publish to publish the web application to Azure.

image

When the publish is complete, you’ll get an error page in the browser with a 403 error. 

image

I opened an incognito window in Chrome and changed to the HTTPS URL for my Web API controller.  I am prompted for a client certificate.

image

I provide it and now I can access the values.

image

Create a Client Application

For an extra bonus, I created a client application to test things out.  It uses the HttpClient API to make a request to Azure.  Make sure that you are using the SSL URL, else the certificate will not be sent.

Client

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Net.Http;
  5. using System.Net.Http.Headers;
  6. using System.Net.Security;
  7. using System.Security.Cryptography.X509Certificates;
  8. using System.Text;
  9. using System.Threading.Tasks;
  10.  
  11. namespace client
  12. {
  13.     class Program
  14.     {
  15.         static void Main(string[] args)
  16.         {
  17.  
  18.             var p = new Program();
  19.             p.RunAsync().Wait();
  20.         }
  21.  
  22.         private async Task RunAsync()
  23.         {
  24.             var handler = new WebRequestHandler();
  25.             handler.ClientCertificateOptions = ClientCertificateOption.Manual;
  26.             var cert = GetClientCert();
  27.             handler.ClientCertificates.Add(cert);
  28.  
  29.             handler.ServerCertificateValidationCallback = delegate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors error)
  30.             {
  31.                 //Ignore errors
  32.                 return true;
  33.             };
  34.             handler.UseProxy = false;
  35.  
  36.             using (var client = new HttpClient(handler))
  37.             {
  38.                 client.BaseAddress = new Uri("https://certdemo.azurewebsites.net/");
  39.                 //client.BaseAddress = new Uri("https://localhost:44315/");
  40.  
  41.                 client.DefaultRequestHeaders.Accept.Clear();
  42.                 client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
  43.  
  44.                 var response = await client.GetAsync("api/values");
  45.                 if (response.IsSuccessStatusCode)
  46.                 {
  47.                     var str = await response.Content.ReadAsStringAsync();
  48.  
  49.                     Console.WriteLine(str);
  50.                 }
  51.                 else
  52.                 {
  53.                     Console.WriteLine("FAIL:" + response.StatusCode);
  54.                 }
  55.  
  56.             }
  57.  
  58.         }
  59.  
  60.         private static X509Certificate GetClientCert()
  61.         {
  62.             X509Store store = null;
  63.             try
  64.             {
  65.                 store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
  66.                 store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
  67.  
  68.                 var certs = store.Certificates.Find(X509FindType.FindBySubjectName, "NetworkingDemoClientCert2", true);
  69.  
  70.                 if (certs.Count == 1)
  71.                 {
  72.                     var cert = certs[0];
  73.                     return cert;
  74.                 }
  75.             }
  76.             finally
  77.             {
  78.                 if (store != null)
  79.                     store.Close();
  80.             }
  81.  
  82.             return null;
  83.         }
  84.     }
  85. }

Just like in the web page, the client works, too.  The source code for the web application is available at https://github.com/kaevans/ClientCertDemo

For More Information

How To Configure TLS Mutual Authentication for Web App

https://github.com/kaevans/ClientCertDemo