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
- Limit the client SSL certificate requests to just the WebAPI controllers that talk to the 3rd party cloud service?
OR
- 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)
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;
}
}
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.
Unfortunately, this can't be configured at the controller level. The server decides which controller to use based on the contents of an HTTP request (usually the request path). SSL protects the contents of an HTTP message by encrypting it, and the request path is part of the encrypted message. The SSL channel needs to get set up before any HTTP messages are sent, which is why the configuration of the SSL channel (whether the server tries to negotiate a client cert or not, for example) can't rely on the contents of any HTTP messages.
So here are your options:
Spin up a second web role that's configured to not negotiate client certs. You'll need a second domain for this, as it's essentially a separate service. So you'd have https://domain.com pointing to the non-client-cert one and https://foo.domain.com pointing to the one that does require client certs.
Use the same web role, but set up a second port for IIS to listen on, and have that one configured to not negotiate a client cert. Using non-standard ports is a pain, though, since one of your clients is going to have to do https://domain.com:444 (or some other port besides 443).
Disable client cert negotiation across the board. This might not work depending on how your service accesses the client certificate, but typically when you access the ClientCertificate property on a System.Web.HttpRequest object (or equivalent), it will negotiate for a certificate on demand. This means it transparently tears down the existing SSL session and sets up a new one, this time challenging the client for the cert. This is pretty inefficient, as setting up an SSL connection in the first place takes several round-trips, and doing it twice is painful. But depending on your available options, the performance requirements for the requests that use client certs, and whether you'll get a lot of connection reuse out of keep-alives, this option may make sense.
Hope this helps.
来源:https://stackoverflow.com/questions/27323880/disable-ssl-client-certificate-on-some-webapi-controllers