Wednesday, May 15, 2013

WCF Security: wsHttpBinding with a Custom User Name Validator (IIS)

This post covers hosting a WCF service in IIS using wsHttpBinding, a custom Username Password validator and using TransportWithMessageCredential security mode.  Also keep in mind that some security modes behave differently under IIS than self hosting. This post only applies to hosting in IIS. See this post for using Basic Security with IIS.


To use custom user name/password style credentials with an IIS hosted Http service the following config is used:


            <wsHttpBinding>

                <binding name="UserNameHttpBindingConfig">
                    <security mode="TransportWithMessageCredential">
                        <message clientCredentialType="UserName" />
                    </security>
                </binding>
            </wsHttpBinding>

NOTE: The way the credentials are passed to the service are distinctly different to Http Basic credentials - this is not Basic Auth.  What is actually happening is the credentials are passed as part of the SOAP envelope.

<?xml version="1.0" encoding="utf-8" ?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:a="http://www.w3.org/2005/08/addressing"
            xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <s:Header>
        <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT</a:Action>
        <a:MessageID>urn:uuid:1a54b138-c3aa-46a5-b80c-1581aa6432b8</a:MessageID>
        <a:ReplyTo>
            <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
        </a:ReplyTo>
        <a:To s:mustUnderstand="1">https://localhost/WcfUsernameOverWsHttpWithTransport/Service1.svc</a:To>
        <o:Security s:mustUnderstand="1"
                    xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
            <u:Timestamp u:Id="_0">
                <u:Created>2013-05-14T21:17:03.746Z</u:Created>
                <u:Expires>2013-05-14T21:22:03.746Z</u:Expires>
            </u:Timestamp>
            <o:UsernameToken u:Id="uuid-8f7509e1-b84b-4291-adb5-23ae3f83d2cd-1">
                <o:Username>
                    <!-- Removed-->
                </o:Username>
                <o:Password>
                    <!-- Removed-->
                </o:Password>
            </o:UsernameToken>
        </o:Security>
    </s:Header>
    <s:Body>
        <t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
            <t:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</t:TokenType>
            <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
            <t:Entropy>
                <!-- Removed-->
            </t:Entropy>
            <t:KeySize>256</t:KeySize>
        </t:RequestSecurityToken>
    </s:Body>
</s:Envelope>



The next likely problem is an exception about the SSL certificate being used in IIS.  Usually in development self-signed certificates are used (who has the money to buy development SSL certs?)
You'll get this error if you are using a self-signed cert:

SecurityNegotiationException

To fix it you'll need to add an event handler to change the default service certificate validation on the client.  THIS IS DEFINITELY NOT RECOMMENDED IN PRODUCTION CODE!  It basically tells the client to ignore all certificate problems.  But when using self-signed certs in dev, it is useful.

 // Ignore any certificate errors - results from using self-signed certs.
            System.Net.ServicePointManager.ServerCertificateValidationCallback += (se, cert, chain, sslerror) => true;




You will get a strange exception when authentication fails:
MessageSecurityException
This is the expected response you'll get when the password is incorrect and any Exception is thrown from the custom validator (including Fault Exceptions). This is very misleading, because if you search for this, you'll get a great deal of solutions talking about differences in service and client clocks.

Service Contract:


namespace WcfUsernameOverWsHttpWithTransport
{
    [ServiceContract]
    public interface IService1
    {
        [OperationContract]
        bool DoSomething(string param);
    }
}

Service Implementation

<%@ ServiceHost Language="C#" Debug="true" Service="WcfUsernameOverWsHttpWithTransport.Service1" CodeBehind="Service1.svc.cs" %>

namespace WcfUsernameOverWsHttpWithTransport
{
    public class Service1 : IService1
    {
        public bool DoSomething(string param)
        {
            return param == "Foo bar";
        }
    }
}

Service web.config:

Note: targeting .Net 4.0 or 4.5 does not impact this example, either works fine.

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0"/>
    <httpRuntime/>
  </system.web>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="HttpBindingConfig">
          <security mode="TransportWithMessageCredential">
            <message clientCredentialType="UserName"/>
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
    <services>
      <service name="WcfUsernameOverWsHttpWithTransport.Service1">
        <endpoint address="" binding="wsHttpBinding" contract="WcfUsernameOverWsHttpWithTransport.IService1" bindingConfiguration="HttpBindingConfig"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
          <serviceCredentials>
            <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="WcfUsernameOverWsHttpWithTransport.UserNameValidator,WcfUsernameOverWsHttpWithTransport"/>
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/>
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
    <directoryBrowse enabled="true"/>
  </system.webServer>
</configuration>

Client App.Config




<?xml version="1.0"?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
    </startup>
    <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="WSHttpBinding_IService1">
                    <security mode="TransportWithMessageCredential">
                        <transport clientCredentialType="None" />
                        <message clientCredentialType="UserName" />
                    </security>
                </binding>
            </wsHttpBinding>
        </bindings>
        <client>
            <endpoint address="https://localhost/WcfUsernameOverWsHttpWithTransport/Service1.svc"
                binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1"
                contract="ServiceReference1.IService1" name="WSHttpBinding_IService1" />
        </client>
    </system.serviceModel>
</configuration>


Client Proxy Code:


namespace ClientConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread.Sleep(1000);

            // Ignore any certificate errors - results from using self-signed certs.
            System.Net.ServicePointManager.ServerCertificateValidationCallback += (se, cert, chain, sslerror) => true;

            var ep = new EndpointAddress(new Uri("https://localhost/WcfUsernameOverWsHttpWithTransport/Service1.svc"));
            var binding = new WSHttpBinding(SecurityMode.TransportWithMessageCredential)
                                  {
                                      SendTimeout = TimeSpan.FromSeconds(70),
                                      BypassProxyOnLocal = false
                                  };
            binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
            var proxy = new ServiceReference1.Service1Client(binding, ep);
            proxy.ClientCredentials.UserName.UserName = "test";
            proxy.ClientCredentials.UserName.Password = "pass99"; // pass99

            object response = string.Empty;
            try
            {
                response = proxy.DoSomething("Foo bar");
                Console.WriteLine("Authenticated.");
            }
            catch (MessageSecurityException)
            {
                Console.WriteLine("Your password is wrong.");
            }

            Console.WriteLine(response.ToString());
        }
    }
}



References


No comments:

Post a Comment