Wednesday, May 15, 2013

WCF Security: wsHttpBinding Basic Security hosting in IIS


When hosting a service in IIS using Transport security and Basic credentials are required, it is IIS that resolves the basic credentials not the service.  But the service config is still set to SecurityMode Transport with client credential type Basic. Basic auth must be configured and required by IIS config. See here.  If anonymous access is still allowed then the credentials will be ignored. (At least I couldn't make it work when anonymous was allowed as well). When self hosting this scenario, it must be the service that resolves the credentials.  This post only applies to IIS.

Service config is set to this:


            <wsHttpBinding>
                <binding name="HttpBindingConfig">
                    <security mode="Transport">
                        <transport clientCredentialType="Basic"/>
                    </security>
                </binding>
            </wsHttpBinding>

If you omit or set the clientCredentialType to None, you will get this exception.

System.ServiceModel.ServiceActivationException

Basic Auth also needs to be configured explicitly in IIS virtual directory:




The SOAP message that is sent to the service from the client does not contain the credentials, this is distinctly different to using Username credentials in the previous post.


<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:a="http://www.w3.org/2005/08/addressing">
    <s:Header>
        <a:Action s:mustUnderstand="1">http://tempuri.org/IService1/DoSomething</a:Action>
        <a:MessageID>urn:uuid:f512a5ed-9a56-4460-8c59-f7ed48d3d42c</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/WcfBasicOverWsHttpWithTransport/Service1.svc</a:To>
    </s:Header>
    <s:Body>
        <DoSomething xmlns="http://tempuri.org/">
            <param>Foo bar</param>
        </DoSomething>
    </s:Body>
</s:Envelope>

The credentials in this case are in the HTTP headers. Using Fiddler you can see the credentials being passed.


These credentials are only encoded using Base64 they are NOT encrypted and can easily be reversed back to their username and password text values.This is why SSL is crucial with Basic credentials.

Again same as the last post using Username security, 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;

Also the same as the last post you will receive a strange exception when authentication fails:
MessageSecurityException
This is the expected response you'll get when the username or password is incorrect. 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 WcfBasicOverWsHttpWithTransport
{
    [ServiceContract]
    public interface IService1
    {
        [OperationContract]
        bool DoSomething(string param);
    }
}

Service Implementation

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

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

Service web.config:


<?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="Transport">
                        <transport clientCredentialType="Basic"/>
                    </security>
                </binding>
            </wsHttpBinding>
        </bindings>
        <services>
            <service name="WcfBasicOverWsHttpWithTransport.Service1">
                <endpoint address=""
                          binding="wsHttpBinding"
                          contract="WcfBasicOverWsHttpWithTransport.IService1"
                          bindingConfiguration="HttpBindingConfig"/>
            </service>
        </services>
        <behaviors>
            <serviceBehaviors>
                <behavior>
                    <serviceMetadata httpGetEnabled="true"
                                     httpsGetEnabled="true"/>
                    <serviceDebug includeExceptionDetailInFaults="true"/>
                </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="Transport">
                        <transport clientCredentialType="None" />
                    </security>
                </binding>
            </wsHttpBinding>
        </bindings>
        <client>
            <endpoint address="https://localhost/WcfBasicOverWsHttpWithTransport/Service1.svc"
                binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1"
                contract="ServiceReference1.IService1" name="WSHttpBinding_IService1" />
        </client>
    </system.serviceModel>
</configuration>

Client 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/WcfBasicOverWsHttpWithTransport/Service1.svc"));
            var binding = new WSHttpBinding(SecurityMode.Transport)
                                  {
                                      SendTimeout = TimeSpan.FromSeconds(70),
                                      BypassProxyOnLocal = false
                                  };
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
            var proxy = new ServiceReference1.Service1Client(binding, ep);
            proxy.ClientCredentials.UserName.UserName = "username1";
            proxy.ClientCredentials.UserName.Password = "pass99"; 

            object response = string.Empty;
            try
            {
                response = proxy.DoSomething("Foo bar");
                Console.WriteLine("Authenticated.");
            }
            catch (MessageSecurityException)
            {
                Console.WriteLine("Your password is wrong.");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Something else went wrong \n" + ex);
            }

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


No comments:

Post a Comment