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
OR
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)
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;
}
}