C# – Disable SSL client certificate on *some* WebAPI controllers

asp.net-web-apicclient-certificateshttpsssl

Edit for future readers: Unfortunately, the bounty awarded answer
doesn't work; nothing I can do about that now. But read my own answer
below (through testing) – confirmed to work with minimal code changes

We have an Azure Cloud Service (WebRole) that's entirely in ASP.NET WebAPI 2.2 (no MVC, front end is Angular). Some of our controllers/REST endpoints talk to a 3rd party cloud service over SSL (client cert auth/mutual auth) and the rest of the controllers/endpoints talk to the HTML5/AngularJS front end, also over SSL (but more traditional server auth SSL). We don't have any non-SSL endpoint. We've enabled Client SSL via a cloud service startup task like:

IF NOT DEFINED APPCMD SET APPCMD=%SystemRoot%\system32\inetsrv\AppCmd.exe
%APPCMD% unlock config /section:system.webServer/security/access

Issue: That setting is site-wide so even when users hit the first page (say https://domain.com, returns the index.html for angularJS) their browser asks them for client SSL cert. (image below)

If there a way to either

  1. Limit the client SSL certificate requests to just the WebAPI controllers that talk to the 3rd party cloud service?

OR

  1. Skip client SSL auth for our front end powering webapi controllers?

Our server's web.config is complex but the relevant snippet is below:

<system.webServer>
  <security>
    <access sslFlags="SslNegotiateCert" />
  </security>
</system.webServer>

And the screenshot of the client hitting a regular WebAPI endpoint yet attempting client SSL Authentication (happens in any browser, Chrome, Firefox or IE)
enter image description here

Best Answer

Unfortunately, cleftheris's answer that's awarded the bounty does not work. It tries to work too late in the HTTP server pipeline/processing to get the client certificate, but this post gave me some ideas.

The solution is based on web.config that calls out for special handling of "directories" (works for virtual folders or WebAPI routes too).

Here is the desired logic:

https://www.server.com/acmeapi/** => SSL with Client Certs

https://www.server.com/** => SSL

Here is the corresponding configuration

<configuration>
  ...
  <system.webServer>
    <!-- This is for the rest of the site -->
    <security>
      <access sslFlags="Ssl" />
    </security>
  </system.webServer>

  <!--This is for the 3rd party API endpoint-->
  <location path="acmeapi">
    <system.webServer>
      <security>
        <access sslFlags="SslNegotiateCert"/>
      </security>
    </system.webServer>
  </location>
...
</configuration>

Bonus points

The above will setup the SSL handshake accordingly. Now you still need to check the client SSL certificate in your code if it's the one you expect. That's done as follows

Controller code:

[RoutePrefix("acmeapi")]
[SslClientCertActionFilter] // <== key part!
public class AcmeProviderController : ApiController
{
    [HttpGet]
    [Route("{userId}")]
    public async Task<OutputDto> GetInfo(Guid userId)
    {
        // do work ...
    }
}

Actual attribute from above that perform SSL Client validation is below. Can be used to decorate the entire controller or just specific methods.

public class SslClientCertActionFilterAttribute : ActionFilterAttribute
{
    public List<string> AllowedThumbprints = new List<string>()
    {
        // Replace with the thumbprints the 3rd party
        // server will be presenting. You can make checks
        // more elaborate but always have thumbprint checking ...
        "0011223344556677889900112233445566778899",
        "1122334455667788990011223344556677889900" 
    };

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var request = actionContext.Request;

        if (!AuthorizeRequest(request))
        {
            throw new HttpResponseException(HttpStatusCode.Forbidden);
        }
    }

    private bool AuthorizeRequest(HttpRequestMessage request)
    {
        if (request==null)
            throw new ArgumentNullException("request");

        var clientCertificate = request.GetClientCertificate();

        if (clientCertificate == null || AllowedThumbprints == null || AllowedThumbprints.Count < 1)
        {
            return false;
        }

        foreach (var thumbprint in AllowedThumbprints)
        {
            if (clientCertificate.Thumbprint != null && clientCertificate.Thumbprint.Equals(thumbprint, StringComparison.InvariantCultureIgnoreCase))
            {
                return true;
            }
        }
        return false;
    }
}
Related Topic