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());
        }
    }
}


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


Monday, May 13, 2013

Configuring Impersonation in WCF

First of all, there must be a very strong reason to use impersonation.  General consensus these days, is to avoid it.  This is generally because the further down the stack you travel away from the UI the more meaningless the user's credentials become.  Prefer instead to use a "Trusted Subsystem" approach where once the user is authenticated and authorised the subsystems downstream "trust" the upstream systems.

Valid reasons could be integrating with legacy systems or infrastructure requirements.

On the client:

  • Use a binding and security mode that supports passing credentials.
  • The client app must agree to allow the service impersonating its credentials.


            <endpointBehaviors>
                <behavior name="MyEndpointBehaviour">
                    <clientCredentials>
                        <windows allowedImpersonationLevel="Impersonation"/>
                    </clientCredentials>
                </behavior>
            </endpointBehaviors>

On the service:


  • Decorate the service contract implementation with a operation behaviour that allows impersonation.
     [OperationBehavior(Impersonation = ImpersonationOption.Allowed)]
    Possible values are NotAllowed, Allowed or Required. In this case I used Allowed, because Impersonation is one option that could be used when deployed.  (The other option is running the App-Pool with credentials that can access downstream systems, for example SQL).
    
    
  • The service app.config must enable impersonation also.
                <serviceBehaviors>
                    <behavior>
                        <serviceAuthorization impersonateCallerForAllOperations="true" />
                    </behavior>
                </serviceBehaviors>
I found it useful when impersonation is required, to use the Allowed option on the service implementation.  This means that the remainder of the work is config, so you can change it after its deployed.

If you get any weird exceptions while testing, then the most common issue is not configuring the client to allow the service to impersonate it.  This is done with the endpointBehaviour snippet above.  In my case I was getting a service exception in the type initializer inside System.Data.SqlClient.SqlConnection and the inner exception is "requested registry access is not allowed"; enabling impersonation from the client side of the call fixed it.



Sunday, April 28, 2013

Hosting netTcp WCF Service in WAS

Code Download.

For this post I'm using Windows 8, but these steps should work in Windows 7 and Server 2008 also.

Some searching produced a MSDN post showing some commands that need to be executed if you're running Windows 7 or Server 2008. However these are not necessary in Windows 8.


"%WINDIR%\Microsoft.Net\Framework\v4.0.30319\aspnet_regiis" –i –enable

Running this command in Windows 8 returns:

Microsoft (R) ASP.NET RegIIS version 4.0.30319.17929
Administration utility to install and uninstall ASP.NET on the local machine.
Copyright (C) Microsoft Corporation.  All rights reserved.
Start installing ASP.NET (4.0.30319.17929).
This option is not supported on this version of the operating system.  Administr
ators should instead install/uninstall ASP.NET 4.5 with IIS8 using the "Turn Win
dows Features On/Off" dialog,  the Server Manager management tool, or the dism.e
xe command line tool.  For more details please see http://go.microsoft.com/fwlin
k/?LinkID=216771.
Finished installing ASP.NET (4.0.30319.17929).
 
The post also recommended running:
"%WINDIR%\Microsoft.Net\Framework\v4.0.30319\ServiceModelReg.exe" -r  
 
Which also returns something similar:

Microsoft (R) WCF/WF registration tool version 4.5.0.0
Copyright (c) Microsoft Corporation.  All rights reserved.

Administration utility that manages the installation and uninstallation of
WCF and WF components on a single machine.

[Error]This tool is not supported on this version of Windows. Administrators sho
uld instead install/uninstall Windows Communication Foundation features using th
e 'Turn Windows Features On/Off' dialog, or the dism command line tool.
  
 
Install IIS and WAS Hosting components with add/remove windows features:
 

After completing this re-running the aspnet_regiis command above still returns the same response, so this is definitely not necessary in Windows 8.

The above installation of windows components has automatically registered net.tcp with IIS.

Previously under Windows 7 running the following command would have been necessary:
%windir%\system32\inetsrv\appcmd.exe set site "Default Web Site" -+bindings.[protocol='net.tcp',bindingInformation='808:*']

You can check to ensure net.tcp has been enabled for the default web site by opening IIS and looking at the default web site bindings: (If you have multiple sites hosted inside IIS then net.tcp can be added manually using this dialog also)


Open the advanced settings for your virtual directory and ensure net.tcp is an enabled protocol, if not just type it in.

The new service virtual directory will use the "Default App Pool" this will be using the Framework v4, in integrated mode.  If your default app pool is different, create a new app pool with these settings.

You should now be able to browse to the service using a browser.  (The web.config of the service has enabled <serviceMetadata httpGetEnabled="true"/>)

Since httpGetEnabled is on, using the WcfTestClient.exe you can add the service using "http://localhost/WasTcpDemo/CalculatorService.svc".
However, the client config the tool has generated only allows communication with net.tcp for invoking service operations.
Given the web.config also allows for obtaining metadata over net.tcp you can also add the service using the address "net.tcp://localhost/WasTcpDemo/CalculatorService.svc".

Once the Test Client has added the service, you can remove the serviceMetadata element and the MetadataExchange binding to ensure net.tcp communications without exposed metadata.


References:

One Time setup: http://msdn.microsoft.com/en-us/library/ms751527.aspx
This post was based on the MSDN Example: http://msdn.microsoft.com/en-us/library/ms752218.aspx