Using mutual SSL and message security to secure a WCF service
Sometimes, Windows Communication Foundation (WCF) can be tough going. Whilst things will often work straight out of the box, customisation can quickly get complicated. Equally though, the solution can sometimes turn out to be pretty simple but it is the journey to get there that is difficult.
Consider the following scenario:
In this configuration a WCF service was secured by Forefront Threat Management Gateway (TMG) 2010 so that the client must negotiate mutual SSL authentication with TMG before the message can be forwarded on to the service.
From the point of view of the service this was simple message level security because transport security was being taken care of by TMG. The service configuration was therefore the following:
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="myServiceBehaviour">
<serviceCredentials>
<userNameAuthentication
userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="WCFService.CustomUserNameValidator, WCFService" />
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
<bindings>
<basicHttpBinding>
<binding name="basicBinding">
<security mode="TransportWithMessageCredential">
<message clientCredentialType="UserName"/>
</security>
</binding>
</basicHttpBinding>
</bindings>
<services>
<service name="WCFService.Service1" behaviorConfiguration="myServiceBehaviour">
<endpoint
address="https://www.myservice.com.local/service1.svc"
binding="basicHttpBinding"
bindingConfiguration="basicBinding"
contract="WCFService.IService1" />
</service>
</services>
</system.serviceModel>
At first glance the client configuration didn’t seem too difficult either. It should just be a matter of defining a certificate for transport security and username/password for message security, like the following:
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IService1">
<security mode="TransportWithMessageCredential">
<transport clientCredentialType="Certificate" />
<message clientCredentialType="UserName" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="https://www.myservice.com/Service1.svc"
behaviorConfiguration="myEndpointBehaviour"
binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_IService1"
contract="Client.IService1"
name="BasicHttpBinding_IService1" />
</client>
<behaviors>
<endpointBehaviors>
<behavior name="myEndpointBehaviour">
<clientCredentials>
<clientCertificate
storeName="My"
storeLocation="CurrentUser"
findValue="CN=WCF client cert 2" />
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
However, this didn’t work. Inspection of the TMG logs revealed that the client certificate was not being presented to TMG and so the connection was failing at the firewall.
After scratching my head I decided to try and tackle the problem in two stages. First, try to get the message through the firewall and secondly deal with message authentication. I therefore removed message security from the client configuration and set the security to Transport only, like the following:
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IService1">
<security mode="Transport">
<transport clientCredentialType="Certificate" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="https://www.myservice.com/Service1.svc"
behaviorConfiguration="myEndpointBehaviour"
binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_IService1"
contract="Client.IService1"
name="BasicHttpBinding_IService1" />
</client>
<behaviors>
<endpointBehaviors>
<behavior name="myEndpointBehaviour">
<clientCredentials>
<clientCertificate
storeName="My"
storeLocation="CurrentUser"
findValue="CN=WCF client cert 2" />
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
Using this configuration I was able to get the message through the firewall and received a security exception from the service, as expected. However, these attempts revealed a problem. If I define both transport and message security in my client configuration, the client certificate does not get presented to TMG. Alternatively, if I define only transport security then I can get through the firewall but fail authentication at the service.
This shows that WCF is awesome when it ‘just works’ out of the box but you can easily hit scenarios where customisation is required. (I mean, the network configuration described at the beginning of this article isn’t too strange, is it?)
After a bit of experimentation and trawling the internet, I found the solution. I needed to force the client certificate to be included in the call. This can be achieved by adding back in message security and customising the client binding to reference a behaviour extension, shown as <sendClientCertificate> in the following:
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IService1">
<security mode="TransportWithMessageCredential">
<transport clientCredentialType="Certificate" />
<message clientCredentialType="UserName" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="https://www.myservice.com/Service1.svc"
behaviorConfiguration="myEndpointBehaviour"
binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_IService1"
contract="Client.IService1"
name="BasicHttpBinding_IService1" />
</client>
<extensions>
<behaviorExtensions>
<add name="sendClientCertificate"
type="WCFClient.SendClientCertificateBehaviorElement, WCFClient"/>
</behaviorExtensions>
</extensions>
<behaviors>
<endpointBehaviors>
<behavior name="myEndpointBehaviour">
<clientCredentials>
<clientCertificate storeName="My"
storeLocation="CurrentUser"
findValue="CN=WCF client cert 2" />
</clientCredentials>
<sendClientCertificate />
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
The behaviour extension required some supporting code, as follows:
namespace WCFClient
{
using System;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
public class SendClientCertificateBehavior : Attribute, IEndpointBehavior
{
#region IEndpointBehavior Members
public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
Binding bbinding = endpoint.Binding;
BindingElementCollection bec = bbinding.CreateBindingElements();
TransportSecurityBindingElement tsp = bec.Find<TransportSecurityBindingElement>();
HttpsTransportBindingElement httpsBinding = bec.Find<HttpsTransportBindingElement>();
TextMessageEncodingBindingElement encoding = bec.Find<TextMessageEncodingBindingElement>();
httpsBinding.RequireClientCertificate = true;
CustomBinding b = new CustomBinding(tsp, encoding, httpsBinding);
endpoint.Binding = b;
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
throw new NotImplementedException();
}
public void Validate(ServiceEndpoint endpoint)
{
}
#endregion
}
}
What this code essentially does is extract the binding elements defined by the basicHttpBinding configuration within app.config, sets the RequireClientCertificate property for the HttpsTransportBindingElement element, and adds the elements back in to a custom binding.
In this article I have shown that the WCF client configuration for the fairly common scenario of a WCF service requiring message security, situated behind a firewall that mandates mutual SSL, is not entirely straightforward. However, although the solution took some scratching around to find, the resulting code was not too scary after all.
Written by Bradley Cotier
Comments
Anonymous
September 13, 2011
Hi Bradley, I'm facing similar issue with WCF Self hosted service in TransportWithMessageCredential mode. In my case, When i mention client credential type as Certificate for both Transport and Message, the client credential gets passed to the service and mutual authentication works. But my need is to have windows authorization @ message level. Hence for client credential type certificate @ transport and windows as message, i could not get the client credential passed to service. It doesnt throw exception either. When i removed client cert CA on server side, I could still make call without exception/errors. I also mentioned the requireclientcetificate = true via behavior on client side as mentioned in the blog. Still no luck. Any suggestions would be of great help. -AswinAnonymous
September 15, 2011
Well i found the reason why it wasnt behaving the expected way. I created a custom binding with behavior mentioned in the blog and used it across both client and server side & it worked. -AswinAnonymous
November 28, 2011
Could either of you see the web service from outside the fire wall prior to this? I have a similar problem, but our 3rd party sees the service description page fine. When the test app calls the authentication method of the WCF service, it returns the service description page as the response, instead of the small xml file it should be passing...Anonymous
November 28, 2011
Hi Prior to the firewall being in place, the service was configured with message security only. There was a desire to only allow vetted callers to access the service but that the burden should not be placed on the web service server itself, but rather at the firewall layer. As you are not getting a security exception it sounds like you have some other problem with your client config. I would suggest that you get the client/service working without a firewall in place. You then have a good foundation to increment upon. Thanks Brad