Using client certificate authentication in Azure

Client certificate authentication (CCA) is a relatively easy way to secure communication between two parties.  Of course, both parties must be privy to the certificate and there is more to it than just configuration; code will need to be written.

Setting up CCA

When deploying to multiple environments, your different environments may have different certificates or no authentication at all (for lower environments).  This can prove frustrating to implement as your code is suddenly filled with if statements looking for the correct thumbprint in the specific environment.  Enter web.config and web.config transforms.

With a handful of class files you can set up implementing CCA via web.config configurations.

Files to compare incoming certificate to configured values

  1. CertificateHelper.cs – Static helper methods that can be reused to validate certificates against one another.
  2. CertificateAuthenticationHandler.cs – This is a basic CCA handler.  It will work OOTB, but you’ll need to wire up configurations if you choose not to use the following web.config class.
  3. WebConfigCertificateAuthenticationHandler.cs – This is a fully implemented class that uses a web.config configuration section to validate the incoming certificate.

Files to set up web.config configurations (self-explanatory)

  1. CertificateAuthenticationConfigurationSection.cs
  2. CertificateElementCollection.cs
  3. CertificateElement.cs

Other used files

  1. Certificate.cs – A POCO that allows apples to apples comparison of certificate data.
NOTE: It should be noted that the conversion from the web.config objects to the Certificate POCO object is done in my code using AutoMapper.  AutoMapper just uses convention based patterns to move the data from CertificateElement.cs to Certificate.cs.  It is invoked in WebConfigCertificateAuthenticationHandler.cs, line 40.  Sorry, didn’t feel like rewriting all my code to pull out my factory and decoupling patterns.

Files

using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;

namespace Demo
{
    public static class CertificateHelper
    {
        /// <summary>
        /// Determines if the certificates match.
        /// </summary>
        /// <param name="configuredCertificate">The configured certificate.</param>
        /// <param name="incomingCertificate">The incoming certificate.</param>
        /// <returns></returns>
        public static bool DoCertificatesMatch(Certificate configuredCertificate, X509Certificate incomingCertificate)
        {
            var subject = new Dictionary<string, string>
            {
                {"CN", configuredCertificate.SubjectContactName},
                {"C", configuredCertificate.SubjectCountry},
                {"ST", configuredCertificate.SubjectState},
                {"L", configuredCertificate.SubjectCity},
                {"O", configuredCertificate.SubjectOrganization},
                {"OU", configuredCertificate.SubjectOrganizationUnit}
            };

            var issuer = new Dictionary<string, string>
            {
                {"CN", configuredCertificate.IssuerContactName},
                {"C", configuredCertificate.IssuerCountry},
                {"ST", configuredCertificate.IssuerState},
                {"L", configuredCertificate.IssuerCity},
                {"O", configuredCertificate.IssuerOrganization},
                {"OU", configuredCertificate.IssuerOrganizationUnit}
            };

            var incomingSubject = incomingCertificate.Subject.Split(',')
                .ToDictionary(x => x.Split('=')[0], y => y.Split('=')[1]);
            var incomingIssuer = incomingCertificate.Issuer.Split(',')
                .ToDictionary(x => x.Split('=')[0], y => y.Split('=')[1]);

            return incomingSubject
                       .Where(x => subject.ContainsKey(x.Key))
                       .All(x => subject[x.Key] == x.Value)
                   && incomingIssuer
                       .Where(x => issuer.ContainsKey(x.Key))
                       .All(x => issuer[x.Key] == x.Value);
        }

        /// <summary>
        /// Determines if the certificates match.
        /// </summary>
        /// <param name="configuredCertificate">The configured certificate.</param>
        /// <param name="incomingCertificate">The incoming certificate.</param>
        /// <returns></returns>
        public static bool DoCertificatesMatch(Certificate configuredCertificate, X509Certificate2 incomingCertificate)
        {
            if (!string.IsNullOrWhiteSpace(configuredCertificate.Thumbprint))
            {
                return configuredCertificate.Thumbprint == incomingCertificate.Thumbprint;
            }

            return DoCertificatesMatch(configuredCertificate, (X509Certificate)incomingCertificate);
        }
    }
}
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    public class CertificateAuthenticationHandler: DelegatingHandler
    {
        /// <summary>
        /// Gets or sets the certificates.
        /// </summary>
        /// <value>
        /// The certificates.
        /// </value>
        public virtual Certificate[] Certificates { get; set; }

        /// <summary>
        /// Gets or sets the validation error message.
        /// </summary>
        /// <value>
        /// The validation error message.
        /// </value>
        protected string ValidationErrorMessage { get; set; } = "An error occurred validating the certificate.";

        /// <summary>
        /// Sends the asynchronous.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <param name="cancellationToken">The cancellation token.</param>
        /// <returns></returns>
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var cert = request.GetClientCertificate();

            if (!ValidateCertificate(request, cert))
                return Task.Factory.StartNew(() => request.CreateResponse(HttpStatusCode.Unauthorized, ValidationErrorMessage), cancellationToken);

            return base.SendAsync(request, cancellationToken);
        }

        /// <summary>
        /// Sends the asynchronous.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <param name="cancellationToken">The cancellation token.</param>
        /// <param name="skipCertCheck">if set to <c>true</c> [skip cert check].</param>
        /// <returns></returns>
        protected Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken, bool skipCertCheck)
        {
            return skipCertCheck ? base.SendAsync(request, cancellationToken) : SendAsync(request, cancellationToken);
        }

        /// <summary>
        /// Validates the certificate.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <param name="certificate">The certificate.</param>
        /// <returns></returns>
        protected virtual bool ValidateCertificate(HttpRequestMessage request, X509Certificate2 certificate)
        {
            if (certificate == null)
            {
                ValidationErrorMessage = "An incoming certificate could not be found.";
                return false;
            }

            if (Certificates == null || Certificates.Length == 0)
            {
                ValidationErrorMessage = "No configuration is made to validate the incoming certificate against.";
                return false;
            }

            return Certificates.Any(configuredCert => IsCertificateMatch(certificate, configuredCert));
        }

        /// <summary>
        /// Determines whether [is certificate match] [the specified incoming certificate].
        /// </summary>
        /// <param name="incomingCertificate">The incoming certificate.</param>
        /// <param name="configuredCertificate">The configured certificate.</param>
        /// <returns>
        ///   <c>true</c> if [is certificate match] [the specified incoming certificate]; otherwise, <c>false</c>.
        /// </returns>
        protected virtual bool IsCertificateMatch(X509Certificate2 incomingCertificate,
                    Certificate configuredCertificate)
        {
            return CertificateHelper.DoCertificatesMatch(configuredCertificate, incomingCertificate);
        }
    }
}
using System.Configuration;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    public class WebConfigCertificateAuthenticationHandler : CertificateAuthenticationHandler
    {
        private IObjectMapper _mapper;

        /// <summary>
        /// Gets or sets the mapper.
        /// </summary>
        /// <value>
        /// The mapper.
        /// </value>
        public IObjectMapper Mapper
        {
            get { return _mapper ?? (_mapper = AbstractFactory.Factory.Create<IObjectMapper>()); }
            set { _mapper = value; }
        }

        /// <summary>
        /// Gets or sets the configuration section.
        /// </summary>
        /// <value>
        /// The configuration section.
        /// </value>
        public CertificateAuthenticationConfigurationSection ConfigurationSection { get; set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="WebConfigCertificateAuthenticationHandler"/> class.
        /// </summary>
        public WebConfigCertificateAuthenticationHandler()
        {
            ConfigurationSection = ConfigurationManager.GetSection("certificateAuthentication") as CertificateAuthenticationConfigurationSection;
            var configurationElements = ConfigurationSection?.Certificates?.Cast<CertificateElement>().ToArray();
            Certificates = Mapper.Map<Certificate[]>(configurationElements);
        }

        /// <summary>
        /// Sends the asynchronous.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <param name="cancellationToken">The cancellation token.</param>
        /// <returns></returns>
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return ConfigurationSection.Disabled ? SendAsync(request, cancellationToken, true) : base.SendAsync(request, cancellationToken);
        }
    }
}
using System.Configuration;

namespace Demo
{
    public class CertificateAuthenticationConfigurationSection : ConfigurationSection
    {
        /// <summary>
        /// Gets the certificates.
        /// </summary>
        /// <value>
        /// The certificates.
        /// </value>
        [ConfigurationProperty("certificates", IsDefaultCollection = true)]
        public CertificateElementCollection Certificates => (CertificateElementCollection)this["certificates"];

        /// <summary>
        /// Gets a value indicating whether this <see cref="CertificateAuthenticationConfigurationSection"/> is disabled.
        /// </summary>
        /// <value>
        ///   <c>true</c> if disabled; otherwise, <c>false</c>.
        /// </value>
        [ConfigurationProperty("disabled", DefaultValue = false)]
        public bool Disabled => (bool)this["disabled"];
    }
}
using System;
using System.Configuration;

namespace Demo
{
    [ConfigurationCollection(typeof(CertificateElement))]
    public class CertificateElementCollection : ConfigurationElementCollection
    {
        /// <summary>
        /// When overridden in a derived class, creates a new <see cref="T:System.Configuration.ConfigurationElement" />.
        /// </summary>
        /// <returns>
        /// A newly created <see cref="T:System.Configuration.ConfigurationElement" />.
        /// </returns>
        protected override ConfigurationElement CreateNewElement()
        {
            return new CertificateElement();
        }

        /// <summary>
        /// Gets the element key for a specified configuration element when overridden in a derived class.
        /// </summary>
        /// <param name="element">The <see cref="T:System.Configuration.ConfigurationElement" /> to return the key for.</param>
        /// <returns>
        /// An <see cref="T:System.Object" /> that acts as the key for the specified <see cref="T:System.Configuration.ConfigurationElement" />.
        /// </returns>
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((CertificateElement)element).Thumbprint;
        }
    }
}
using System.Configuration;

namespace Demo
{
    public class CertificateElement : ConfigurationElement
    {
        /// <summary>
        /// Gets or sets the thumbprint.
        /// </summary>
        /// <value>
        /// The thumbprint.
        /// </value>
        [ConfigurationProperty("thumbprint", DefaultValue = "")]
        public string Thumbprint {
            get { return (string)this["thumbprint"]; }
            set { this["thumbprint"] = value; }
        }

        /// <summary>
        /// Gets or sets the subject country.
        /// </summary>
        /// <value>
        /// The subject country.
        /// </value>
        [ConfigurationProperty("subjectCountry", DefaultValue = "")]
        public string SubjectCountry
        {
            get { return (string)this["subjectCountry"]; }
            set { this["subjectCountry"] = value; }
        }

        /// <summary>
        /// Gets or sets the state of the subject.
        /// </summary>
        /// <value>
        /// The state of the subject.
        /// </value>
        [ConfigurationProperty("subjectState", DefaultValue = "")]
        public string SubjectState
        {
            get { return (string)this["subjectState"]; }
            set { this["subjectState"] = value; }
        }

        /// <summary>
        /// Gets or sets the subject city.
        /// </summary>
        /// <value>
        /// The subject city.
        /// </value>
        [ConfigurationProperty("subjectCity", DefaultValue = "")]
        public string SubjectCity
        {
            get { return (string)this["subjectCity"]; }
            set { this["subjectCity"] = value; }
        }

        /// <summary>
        /// Gets or sets the subject organization.
        /// </summary>
        /// <value>
        /// The subject organization.
        /// </value>
        [ConfigurationProperty("subjectOrganization", DefaultValue = "")]
        public string SubjectOrganization
        {
            get { return (string)this["subjectOrganization"]; }
            set { this["subjectOrganization"] = value; }
        }

        /// <summary>
        /// Gets or sets the subject organization unit.
        /// </summary>
        /// <value>
        /// The subject organization unit.
        /// </value>
        [ConfigurationProperty("subjectOrganizationUnit", DefaultValue = "")]
        public string SubjectOrganizationUnit
        {
            get { return (string)this["subjectOrganizationUnit"]; }
            set { this["subjectOrganizationUnit"] = value; }
        }

        /// <summary>
        /// Gets or sets the name of the subject contact.
        /// </summary>
        /// <value>
        /// The name of the subject contact.
        /// </value>
        [ConfigurationProperty("subjectContactName", DefaultValue = "")]
        public string SubjectContactName {
            get { return (string)this["subjectContactName"]; }
            set { this["subjectContactName"] = value; }

        }

        /// <summary>
        /// Gets or sets the issuer country.
        /// </summary>
        /// <value>
        /// The issuer country.
        /// </value>
        [ConfigurationProperty("issuerCountry", DefaultValue = "")]
        public string IssuerCountry
        {
            get { return (string)this["issuerCountry"]; }
            set { this["issuerCountry"] = value; }
        }

        /// <summary>
        /// Gets or sets the state of the issuer.
        /// </summary>
        /// <value>
        /// The state of the issuer.
        /// </value>
        [ConfigurationProperty("issuerState", DefaultValue = "")]
        public string IssuerState {
            get { return (string)this["issuerState"]; }
            set { this["issuerState"] = value; }

        }

        /// <summary>
        /// Gets or sets the issuer city.
        /// </summary>
        /// <value>
        /// The issuer city.
        /// </value>
        [ConfigurationProperty("issuerCity", DefaultValue = "")]
        public string IssuerCity
        {
            get { return (string)this["issuerCity"]; }
            set { this["issuerCity"] = value; }
        }

        /// <summary>
        /// Gets or sets the issuer organization.
        /// </summary>
        /// <value>
        /// The issuer organization.
        /// </value>
        [ConfigurationProperty("issuerOrganization", DefaultValue = "")]
        public string IssuerOrganization
        {
            get { return (string)this["issuerOrganization"]; }
            set { this["issuerOrganization"] = value; }
        }

        /// <summary>
        /// Gets or sets the issuer organization unit.
        /// </summary>
        /// <value>
        /// The issuer organization unit.
        /// </value>
        [ConfigurationProperty("issuerOrganizationUnit", DefaultValue = "")]
        public string IssuerOrganizationUnit
        {
            get { return (string)this["issuerOrganizationUnit"]; }
            set { this["issuerOrganizationUnit"] = value; }
        }

        /// <summary>
        /// Gets or sets the name of the issuer contact.
        /// </summary>
        /// <value>
        /// The name of the issuer contact.
        /// </value>
        [ConfigurationProperty("issuerContactName", DefaultValue = "")]
        public string IssuerContactName
        {
            get { return (string)this["issuerContactName"]; }
            set { this["issuerContactName"] = value; }
        }
    }
}
namespace Demo
{
    public class Certificate
    {
        /// <summary>
        /// Gets or sets the thumbprint.
        /// </summary>
        /// <value>
        /// The thumbprint.
        /// </value>
        public string Thumbprint { get; set; }

        /// <summary>
        /// Gets or sets the subject country.
        /// </summary>
        /// <value>
        /// The subject country.
        /// </value>
        
        public string SubjectCountry { get; set; }

        /// <summary>
        /// Gets or sets the state of the subject.
        /// </summary>
        /// <value>
        /// The state of the subject.
        /// </value>
        public string SubjectState { get; set; }

        /// <summary>
        /// Gets or sets the subject city.
        /// </summary>
        /// <value>
        /// The subject city.
        /// </value>
        public string SubjectCity { get; set; }

        /// <summary>
        /// Gets or sets the subject organization.
        /// </summary>
        /// <value>
        /// The subject organization.
        /// </value>
        public string SubjectOrganization { get; set; }

        /// <summary>
        /// Gets or sets the subject organization unit.
        /// </summary>
        /// <value>
        /// The subject organization unit.
        /// </value>
        public string SubjectOrganizationUnit { get; set; }

        /// <summary>
        /// Gets or sets the name of the subject contact.
        /// </summary>
        /// <value>
        /// The name of the subject contact.
        /// </value>
        public string SubjectContactName { get; set; }

        /// <summary>
        /// Gets or sets the issuer country.
        /// </summary>
        /// <value>
        /// The issuer country.
        /// </value>
        public string IssuerCountry { get; set; }

        /// <summary>
        /// Gets or sets the state of the issuer.
        /// </summary>
        /// <value>
        /// The state of the issuer.
        /// </value>
        public string IssuerState { get; set; }

        /// <summary>
        /// Gets or sets the issuer city.
        /// </summary>
        /// <value>
        /// The issuer city.
        /// </value>
        public string IssuerCity { get; set; }

        /// <summary>
        /// Gets or sets the issuer organization.
        /// </summary>
        /// <value>
        /// The issuer organization.
        /// </value>
        public string IssuerOrganization { get; set; }

        /// <summary>
        /// Gets or sets the issuer organization unit.
        /// </summary>
        /// <value>
        /// The issuer organization unit.
        /// </value>
        public string IssuerOrganizationUnit { get; set; }

        /// <summary>
        /// Gets or sets the name of the issuer contact.
        /// </summary>
        /// <value>
        /// The name of the issuer contact.
        /// </value>
        public string IssuerContactName { get; set; }
    }
}

Validating a certificate via web.config

Once you have copied the files into your project(s), you will then need to input the following into your web.config:

<configuration>
    <configSections>
        ...
        <section name="certificateAuthentication" type="Comcast.Cs.Web.WebApi.Configuration.CertificateAuthenticationConfigurationSection, Comcast.Cs.Web.WebApi" />
    </configSections>
    ...
    <certificateAuthentication [disabled="true"]>
        <add thumbprint="<thumbprint>"/>
        ...
    </certificateAuthentication>
</configuration>

As you can see from the example, it’s possible to disable the configuration with the disabled keyword on the certificateAuthentication element.  You must use this instead of emptying the thumbprint element list; an empty thumbprint list will result in an unauthorization as you haven’t disabled authentication but haven’t given the code anything to validate against.

Thumbprint is just one certificate attribute you can compare against.  When you don’t own the incoming certificate and thumbprint doesn’t work, there are a variety of properties you can use to identify a match.  Furthermore, if you’re expecting different certificates from multiple groups or applications, the code is already set to handle this.

Setting up your Azure resource for CCA

Azure must be told that certificates are going to be used for authentication.  To do that, navigate to resources.azure.com.  Once logged in, navigate to

subscriptions
  <subscription name>
    resourceGroups
      <resource group name>
         <resource name>
           providers
             Microsoft.Web
               sites
                 <site name>

Once there, enable Read/Write at the top and then click the Edit button.  Find clientCertEnabled and change the value to true.  Then click the Put button.  Your site is now ready to take certificates.

Advertisements

Leave a 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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

Create a website or blog at WordPress.com

Up ↑

%d bloggers like this: