Wednesday, December 21, 2011

When SSL termination occurs at Load balancer we can use ClearBinding for passing UserName or Certificate over Http

We might encounter scenarios where the Load balancer takes care of securing the transport channel and terminates the SSL at the load balancer and decrypts the traffic and redirects to the appropriate server. Now when we want to implement username password authentication on a WCF Service in such scenarios we need to use clear binding. The following article explains on how to create clear binding and provides support for both custom username password and client certificate validation on such scenarios.

First create a class that inherits CustomBinding and let us call it ClearBinding.cs:


using System.ServiceModel;
using System.ServiceModel.Channels;
 
namespace ClearBindingSample
{
    public class ClearBinding : CustomBinding
    {
        private MessageVersion messageVersion = MessageVersion.None;
        private MessageCredentialType clientCredentialType;
 
        public void SetMessageVersion(MessageVersion value)
        {
            messageVersion = value;
        }
 
        public void SetClientCredentialType(MessageCredentialType messageCredentialType)
        {
            clientCredentialType = messageCredentialType;
        }
 
        public override BindingElementCollection CreateBindingElements()
        {
            var res = new BindingElementCollection {new TextMessageEncodingBindingElement() {MessageVersion = messageVersion}};
            
            var securityBindingElement = GetTransport();
            
            if (securityBindingElement != null)
            {
                res.Add(securityBindingElement);
            }
            
            res.Add(new AutoSecuredHttpTransportElement());
            
            return res;
        }
 
        public override string Scheme
        {
            get
            {
                return "http";
            }
        }
 
        private TransportSecurityBindingElement GetTransport()
        {
            if (clientCredentialType == MessageCredentialType.Certificate)
            {
                return SecurityBindingElement.CreateCertificateOverTransportBindingElement();
            }
            if (clientCredentialType == MessageCredentialType.UserName)
            {
                return SecurityBindingElement.CreateUserNameOverTransportBindingElement();
            }
            return null;
        }
    }
}

Now we need to create a class that inherits the StandardBindingElement and let us call it ClearBindingElement.cs:


using System;
using System.Configuration;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
 
namespace ClearBindingSample
{
    internal class ClearBindingElement : StandardBindingElement
    {
        private ConfigurationPropertyCollection properties;
 
        protected override void OnApplyConfiguration(Binding binding)
        {
            var clearBinding = binding as ClearBinding;
            clearBinding.SetMessageVersion(MessageVersion);
            clearBinding.SetClientCredentialType(ClientCredentialType);
        }
 
        protected override Type BindingElementType
        {
            get { return typeof(ClearBinding); }
        }
 
        protected override ConfigurationPropertyCollection Properties
        {
            get
            {
                if (this.properties == null)
                {
                    var properties = base.Properties;
                    properties.Add(new ConfigurationProperty("messageVersion", typeof(MessageVersion), MessageVersion.Soap11, new MessageVersionConverter(), null, ConfigurationPropertyOptions.None));
                    properties.Add(new ConfigurationProperty("clientCredentialType", typeof(MessageCredentialType), MessageCredentialType.None.ToString(), new ClientCredentialTypeConvertor(), null, ConfigurationPropertyOptions.None));
                    this.properties = properties;
                }
                return this.properties;
            }
        }
 
        public MessageVersion MessageVersion
        {
            get
            {
                return (MessageVersion)base["messageVersion"];
            }
            set
            {
                base["messageVersion"] = value;
            }
        }
 
        public MessageCredentialType ClientCredentialType
        {
            get { return (MessageCredentialType)base["clientCredentialType"]; }
            set { base["clientCredentialType"] = value; }
        }
    }
}

Now we inherit the StandardBindingCollectionElement and let us call it ClearCollectionElement.cs


using System.ServiceModel.Configuration;
 
namespace ClearBindingSample
{
    internal class ClearCollectionElement : StandardBindingCollectionElement<ClearBinding, ClearBindingElement>
    {
    }
}

Now we need a type convertor that converts the attribute client credentials to the appropriate type and let us call it ClientCredentialTypeConvertor.cs


using System;
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.Globalization;
using System.ServiceModel;
 
namespace ClearBindingSample
{
    internal class ClientCredentialTypeConvertor : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return ((typeof(string) == sourceType) || base.CanConvertFrom(context, sourceType));
        }
 
        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            return ((typeof(InstanceDescriptor) == destinationType) || base.CanConvertTo(context, destinationType));
        }
 
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (!(value is string))
            {
                return base.ConvertFrom(context, culture, value);
            }
            var str = ((string)value).ToUpper();
            switch (str)
            {
                case "USERNAME":
                    return MessageCredentialType.UserName;
                case "CERTIFICATE":
                    return MessageCredentialType.Certificate;
                case "NONE":
                    return MessageCredentialType.None;
                default:
                    return MessageCredentialType.None;
            }
        }
 
        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if ((typeof(string) != destinationType) || !(value is MessageCredentialType))
            {
                return base.ConvertTo(context, culture, value, destinationType);
            }
            var credentialType = (MessageCredentialType)value;
            if (credentialType == MessageCredentialType.UserName)
            {
                return "UserName";
            }
            if (credentialType == MessageCredentialType.Certificate)
            {
                return "Certificate";
            }
            if (credentialType == MessageCredentialType.None)
            {
                return "None";
            }
            return "None";
        }
    }
}

Similarly, we need a convertor that translates message version into an appropriate type understood by the system and let us call it MessageVersionConvertor.cs:


using System;
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.Globalization;
using System.ServiceModel.Channels;
 
namespace ClearBindingSample
{
    internal class MessageVersionConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return ((typeof(string) == sourceType) || base.CanConvertFrom(context, sourceType));
        }
 
        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            return ((typeof(InstanceDescriptor) == destinationType) || base.CanConvertTo(context, destinationType));
        }
 
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (!(value is string))
            {
                return base.ConvertFrom(context, culture, value);
            }
            var str = (string)value;
            switch (str)
            {
                case "Soap11WSAddressing10":
                    return MessageVersion.Soap11WSAddressing10;
 
                case "Soap12WSAddressing10":
                    return MessageVersion.Soap12WSAddressing10;
 
                case "Soap11WSAddressingAugust2004":
                    return MessageVersion.Soap11WSAddressingAugust2004;
 
                case "Soap12WSAddressingAugust2004":
                    return MessageVersion.Soap12WSAddressingAugust2004;
 
                case "Soap11":
                    return MessageVersion.Soap11;
 
                case "Soap12":
                    return MessageVersion.Soap12;
 
                case "None":
                    return MessageVersion.None;
 
                case "Default":
                    return MessageVersion.Default;
            }
            throw new ArgumentOutOfRangeException("messageVersion", str, "The argument must be of type System.ServiceModel.Channels.MessageVersion");
        }
 
        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if ((typeof(string) != destinationType) || !(value is MessageVersion))
            {
                return base.ConvertTo(context, culture, value, destinationType);
            }
            var version = (MessageVersion)value;
            if (version == MessageVersion.Default)
            {
                return "Default";
            }
            if (version == MessageVersion.Soap11WSAddressing10)
            {
                return "Soap11WSAddressing10";
            }
            if (version == MessageVersion.Soap12WSAddressing10)
            {
                return "Soap12WSAddressing10";
            }
            if (version == MessageVersion.Soap11WSAddressingAugust2004)
            {
                return "Soap11WSAddressingAugust2004";
            }
            if (version == MessageVersion.Soap12WSAddressingAugust2004)
            {
                return "Soap12WSAddressingAugust2004";
            }
            if (version == MessageVersion.Soap11)
            {
                return "Soap11";
            }
            if (version == MessageVersion.Soap12)
            {
                return "Soap12";
            }
            if (version != MessageVersion.None)
            {
                throw new ArgumentOutOfRangeException("messageVersion", value, "The argument must be of type System.ServiceModel.Channels.MessageVersion");
            }
            return "None";
        }
    }
}

In order for your Username password to be passed over the channel we need to create a class that supports ITransportTokenAssertionProvider and let us call it AutoSecuredHttpSecurityCapabilities.cs:


using System.Net.Security;
using System.ServiceModel.Channels;
 
namespace ClearBindingSample
{
    public class AutoSecuredHttpSecurityCapabilities : ISecurityCapabilities
    {
        public ProtectionLevel SupportedRequestProtectionLevel
        {
            get { return ProtectionLevel.EncryptAndSign; }
        }
 
        public ProtectionLevel SupportedResponseProtectionLevel
        {
            get { return ProtectionLevel.EncryptAndSign; }
        }
 
        public bool SupportsClientAuthentication
        {
            get { return true; }
        }
 
        public bool SupportsClientWindowsIdentity
        {
            get { return false; }
        }
 
        public bool SupportsServerAuthentication
        {
            get { return true; }
        }
    }
}


Now we need a HttpTransportBindingElement that supports the provision of specifying the transport capabilities and let us call it AutoSecuredHttpTransportElement.cs:


using System.ServiceModel.Channels;
 
namespace ClearBindingSample
{
    public class AutoSecuredHttpTransportElement : HttpTransportBindingElement, ITransportTokenAssertionProvider
    {
        public override T GetProperty<T>(BindingContext context)
        {
            if (typeof(T) == typeof(ISecurityCapabilities))
                return (T)(ISecurityCapabilities)new AutoSecuredHttpSecurityCapabilities();
 
            return base.GetProperty<T>(context);
        }
 
        public System.Xml.XmlElement GetTransportTokenAssertion()
        {
            return null;
        }
    }
}


Now we have created a new binding element called ClearBinding. Now open your web.config and add this new extension as shown below:


<system.serviceModel>
    <extensions>
        <bindingExtensions>
            <add name="clearBinding" type="ClearBindingSample.ClearCollectionElement, ClearBindingSample"/>
        </bindingExtensions>
    </extensions>
    <bindings>
        <clearBinding>
            <binding name="clearUsername" messageVersion="Soap11" clientCredentialType="Username"/>
            <binding name="clearCertificate" messageVersion="Soap11" clientCredentialType="Certificate"/>
        </clearBinding>
    </bindings>
</system.serviceModel>

Now you can use this binding in your endpoint and you have achieved passing username password over http without any issues.

No comments: