I have the following classes:
Program.cs
namespace SelfHostingSamples
{
class Program
{
static void Main(string[] args)
{
// HTTPS
var config = new HttpsSelfHostConfiguration("https://localhost:18080");
config.Routes.MapHttpRoute(
"API Default", "api/{controller}/{id}",
new { id = RouteParameter.Optional });
using (var server = new HttpSelfHostServer(config))
{
server.OpenAsync().Wait();
Console.WriteLine("Press Enter to quit.");
Console.ReadLine();
}
}
}
}
HttpsSelfHostConfiguration
namespace SelfHostingSamples
{
public class HttpsSelfHostConfiguration : HttpSelfHostConfiguration
{
public HttpsSelfHostConfiguration(string baseAddress) : base(baseAddress) { }
public HttpsSelfHostConfiguration(Uri baseAddress) : base(baseAddress) { }
// certificate is null even if it is sent, however, does not enforce on all endpoints
// this effectively allows it to route through the request pipeline as intended
// protected override BindingParameterCollection OnConfigureBinding(HttpBinding httpBinding)
// {
// httpBinding.Security.Mode = HttpBindingSecurityMode.Transport;
// httpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
// return base.OnConfigureBinding(httpBinding);
// }
// sends certificate properly, however, enforces on all endpoints
// this doesn't route through request pipeline because if no cert is attached, it returns 403 which is never configured anywhere
// protected override BindingParameterCollection OnConfigureBinding(HttpBinding httpBinding)
// {
// httpBinding.Security.Mode = HttpBindingSecurityMode.Transport;
// this.ClientCredentialType = HttpClientCredentialType.Certificate;
// this.X509CertificateValidator = System.IdentityModel.Selectors.X509CertificateValidator.None;
// return base.OnConfigureBinding(httpBinding);
// }
// sends certificate properly, however, enforces on all endpoints
// this doesn't route through request pipeline because if no cert is attached, it returns 403 which is never configured anywhere
// protected override BindingParameterCollection OnConfigureBinding(HttpBinding httpBinding)
// {
// httpBinding.Security.Mode = HttpBindingSecurityMode.Transport;
// this.X509CertificateValidator = System.IdentityModel.Selectors.X509CertificateValidator.None;
// return base.OnConfigureBinding(httpBinding);
// }
}
}
TestController
namespace SelfHostingSamples
{
[ClientCertificateAuthentication]
public class TestController : ApiController
{
public string Get()
{
return DateTime.Now.ToString();
}
}
public class DebugController : ApiController
{
public string Get()
{
return "Debug";
}
}
}
ClientCertificateAuthenticationAttribute
namespace SelfHostingSamples
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class ClientCertificateAuthenticationAttribute : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
if (!actionContext.Request.RequestUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.NotAcceptable) // 406
{
ReasonPhrase = "Request does not use HTTPS."
};
return;
}
// Retrieve client certificate from request and check if it exists
X509Certificate2 clientCertificate = actionContext.Request.GetClientCertificate();
if (clientCertificate == null)
{
Console.WriteLine("Client certificate not found in request.");
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized) // 401
{
ReasonPhrase = "Request does not include a client certificate."
};
return;
}
Console.WriteLine(clientCertificate.Subject);
// otherwise, continue with the request
base.OnAuthorization(actionContext);
}
}
}
Additionally, I have configured the netsh binding via
netsh http add sslcert ipport=0.0.0.0:18080 certhash=xyz appid="{xyz}" clientcertnegotiation=enable
Overall, my goal with this sample project is to perform client-certificate authentication. Right now, there are two controllers for testing. Test (which has the ClientCertificateAuthentication attribute) and Debug (which does not). For now, the ClientCertificateAuthentication will only check the presence of a certificate and not perform any validation. As you can see from my HttpsSelfHostConfiguration, I've been messing with different configuration bindings trying to get it to work as intended.
Here's what's intended versus what I have experienced:
Intended Behavior
- A certificate is attached: Requests to Test will proceed, requests to Debug will proceed.
- A certificate is not attached: Requests to Test will fail (401), requests to Debug will proceed.
This is intended behavior because clientCertificate != null. The request to Test will get routed through the attribute and to Debug will just go straight to the controller (ignoring the clientCertificate conditional).
Experienced Behavior
httpBinding.Security.Mode = HttpBindingSecurityMode.Transport;
httpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
return base.OnConfigureBinding(httpBinding);
- A certificate is attached: actionContext.Request.GetClientCertificate() returns null, thus Test fails (401) and requests to Debug proceed.
- A certificate is not attached: actionContext.Request.GetClientCertificate() returns null, thus Test fails (401) and requests to Debug proceed.
This is unintended because even though a certificate is attached, it returns null in the ClientCertificateAuthenticationAttribute.
httpBinding.Security.Mode = HttpBindingSecurityMode.Transport;
this.ClientCredentialType = HttpClientCredentialType.Certificate;
this.X509CertificateValidator = System.IdentityModel.Selectors.X509CertificateValidator.None;
return base.OnConfigureBinding(httpBinding);
- A certificate is attached: actionContext.Request.GetClientCertificate() returns a certificate, thus requests to Test proceed and requests to Debug proceed.
- A certificate is not attached: actionContext.Request.GetClientCertificate() returns null, thus requests to Test fail (403) and requests to Debug fail (403). Notice the 403 error. Where is that coming from?
This is unintended because while retrieving the certificate works, it enforces it on all endpoints and not just Test with the ClientCertificateAuthentication attribute. In the case of no certificate being attached, only Test should fail (since clientCertificate == null condition). Why and where is the 403 error originating from?
Some additional information on how I am testing: I am using both Chrome and Postman to attach client certificates. The issue shouldn't be related to the way I am attaching certificates because in one of the configurations, the certificate is being retrieved properly. I just don't have a good enough understanding of how the bindings are working on a lower level I guess.
It's a bit odd because the first binding enforces it on a per-endpoint basis correctly but doesn't retrieve the certificate, whereas the second binding grabs the certificate properly but requires it even on the endpoint that doesn't have the attribute.
To re-iterate, I am trying to achieve the intended behavior. That is, if a certificate is attached to the request, then Test proceeds (because the request goes from ClientCertificateAuthentication->Controller) and Debug proceeds (straight to Controller). If a certificate is not present, then Test fails (goes to ClientCertificateAuthentication->401) and Debug proceeds (straight to Controller).
I believe it's due to the fact that the client certificate handshake happens much earlier in the stack before reaching the actual application, so when no certificate is present, the handshake fails and perhaps .NET Web API just returns a 403? But like I said, my knowledge of the bindings, the technology, etc. is limited.
Seems related to this unanswered post: ASP NET Web API with Mutual TLS authentication on self hosted server.
Any suggestions would be appreciated. I've also tested with passing the certificate as a header and then just extracting that and it works. In that case, I can kind of "bypass" the server-level authentication and just perform it on the application level. I could then also not have to worry about the netsh clientcertnegotiation=enable binding or any additional bindings.. but if I can get it working as intended, that would be preferable because I don't like the idea of hacky workarounds.