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:

image_4_70DB39F5[1]

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