Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Disable SSL client certificate on *some* WebAPI controllers?

Tags:

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

like image 483
DeepSpace101 Avatar asked Dec 05 '14 20:12

DeepSpace101


People also ask

Can I configure my app to require a certificate only on certain paths?

Can I configure my app to require a certificate only on certain paths? This isn't possible. Remember the certificate exchange is done at the start of the HTTPS conversation, it's done by the server before the first request is received on that connection so it's not possible to scope based on any request fields.

How do I pass a client certificate to web API?

Install the Certificate. cer certificate in your Trusted Root Certification Authorities for the Local Machine store using MMC (right-click over the Trusted Root Certification Authorities folder | All Tasks | Import). Install the ClientCert. pfx certificate in the Personal store of Local Computer using MMC.


2 Answers

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;     } } 
like image 169
DeepSpace101 Avatar answered Oct 21 '22 06:10

DeepSpace101


You could simply allow plain http traffic on the web.config level and write a custom delegating handler in the Web Api pipeline for this. You can find a client cert delegating handler here and here. Then you could make this handler active "Per-Route" like found in this example here:

This is what your route configuration would look like.

public static class WebApiConfig {     public static void Register(HttpConfiguration config)     {         config.Routes.MapHttpRoute(             name: "Route1",             routeTemplate: "api/{controller}/{id}",             defaults: new { id = RouteParameter.Optional }         );          config.Routes.MapHttpRoute(             name: "Route2",             routeTemplate: "api2/{controller}/{id}",             defaults: new { id = RouteParameter.Optional },             constraints: null,             handler: new CustomCertificateMessageHandler()  // per-route message handler         );          config.MessageHandlers.Add(new SomeOtherMessageHandler());  // global message handler     } } 

Please note that in case you need "per-route" delegating handlers you must not put them in the global message handler list.

like image 39
cleftheris Avatar answered Oct 21 '22 04:10

cleftheris