Thursday, April 29, 2010

WCF, wsHttpBinding and userName authentication

Ok, before we begin here is what you need: a website with SSL properly configured using a certificate with a root certificate. You need to install the root certificate on the client from which you are going to be accessing the web-service.

SSL is required because WCF will not allow you to send username/password over an insecure channel. WCF is stringent about this and hence it doesnt even allow a plain self-signed certificate. The cert needs to have a root that is part of the trusted authority certs.

Without a root certificate, you will get the following error:

Could not establish trust relationship for the SSL/TLS secure channel with authority 'fullyQualifiedDomainName'.

For instructions on creating a website cert that has a root cert, see my previous post: Using MakeCert to create a certificate with a trusted root for an IIS website

1. Create a WCF Service website

image

2. This will create IService.cs (the service contract), Service.cs (the service implementation) and Service.svc (the webservice access point). We will be using the default implementation to for this tutorial.

3. Create the UserNameValidator class

Add a class to the app_code folder called UserNameValidator

Add the following code to the file

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;

/// <summary>
/// Summary description for UserNameValidator
/// </summary>
public class UsernameValidator : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        // validate arguments
        if (string.IsNullOrEmpty(userName))
            throw new ArgumentNullException("userName");
        if (string.IsNullOrEmpty(password))
            throw new ArgumentNullException("password");

        // check if the user is not test
        if (userName != "test" || password != "test")
            throw new SecurityTokenException("Unknown username or password");
    }
}

This code inherits from the UserNamePasswordValidator class and checks for the username “test” and password “test”.

4. First a simple test

Lets test the service using basicHttpBinding

<system.serviceModel>
        <behaviors>
            <serviceBehaviors>
                <behavior name="defaultProfile">
                    <serviceMetadata httpGetEnabled="true"/>
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <bindings>
      <basicHttpBinding>
        <binding name="basic"></binding>
      </basicHttpBinding>
        </bindings>
        <services>
            <service behaviorConfiguration="defaultProfile" name="Service">
                <endpoint address="" binding="basicHttpBinding" bindingConfiguration="basic" name="basicService" contract="IService"/>
             </service>
        </services>
    </system.serviceModel>

The above configuration enables you to access the service using basicHttpBinding.

Once you have made sure that the service works, move on to the next step. (use WcfTestClient.exe to help you testing the service – "C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\WcfTestClient.exe")

5. Convert the webservice to use wsHttpBinding without SSL.

For this you need to change your configuration as follows:

<system.serviceModel>
        <behaviors>
            <serviceBehaviors>
                <behavior name="defaultProfile">
                    <serviceMetadata httpGetEnabled="true"/>
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <bindings>
      <basicHttpBinding>
        <binding name="basic"></binding>
      </basicHttpBinding>
           
<wsHttpBinding>
                <binding name="wsPlainBinding"></binding>
            </wsHttpBinding>
        </bindings>
        <services>
            <service behaviorConfiguration="defaultProfile" name="Service">
                <endpoint address="" binding="wsHttpBinding" bindingConfiguration="wsPlainBinding" name="wsPlainService" contract="IService"/>
            </service>
        </services>
    </system.serviceModel>

6. Migrate your site into IIS.

Visual Stuido’s web server (Casini) does not support ssl (https), so you need to move your into IIS.

Easiest way to do this is to create a virtual folder that points to your development folder.

Once that is done, test your webservice once again, this time running it from within IIS. (you will also need to change the settings in VS so that it uses IIS for debugging).

image

7. Update your configuration to use the UserNameValidator class as the custom validator

Here is the updated configuration:

<system.serviceModel>
        <behaviors>
            <serviceBehaviors>
                <behavior name="defaultProfile">
                    <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
                   
<serviceCredentials>
                        <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="UsernameValidator, App_Code"/>
                    </serviceCredentials>
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <bindings>
      <basicHttpBinding>
        <binding name="basic"></binding>
      </basicHttpBinding>
            <wsHttpBinding>
               
<binding name="wsSecureBinding">
                    <security mode="TransportWithMessageCredential">
                        <message clientCredentialType="UserName"/>
                    </security>
                </binding>
        <binding name="wsPlainBinding"></binding>
      </wsHttpBinding>
        </bindings>
        <services>
            <service behaviorConfiguration="defaultProfile" name="Service">
                <endpoint address="" binding="wsHttpBinding" bindingConfiguration="wsSecureBinding" name="wsSecureService" contract="IService"/>
            </service>
        </services>
    </system.serviceModel>

You should be able to view your service over https.

The only way to test the service now is to create a console app with a service reference to the https endpoint for your service. The code in your client will look like this

using (ServiceReference1.ServiceClient sc = new ServiceReference1.ServiceClient())
            {
                sc.ClientCredentials.UserName.UserName = "test";
                sc.ClientCredentials.UserName.Password = "test";
                string returndata = sc.GetData(100);
            }

8. Bringing it all together

Here is a reference configuration that brings all of the above configurations together, by providing multiple end-points:

/basic – WS-1, no security
/wsPlainBinding– WS-* no authentication
/wsSecureNoUserNameBinding – WS-* – transport security, no authentication
/wsSecureService– WS-* – transport security with authentication

<system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="defaultProfile">
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
          <serviceCredentials>
            <userNameAuthentication userNamePasswordValidationMode="Custom"
              includeWindowsGroups="false" customUserNamePasswordValidatorType="UsernameValidator, App_Code" />
            <windowsAuthentication includeWindowsGroups="false" />
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <bindings>
      <basicHttpBinding>
        <binding name="basic"></binding>
      </basicHttpBinding>
      <wsHttpBinding>
        <binding name="wsSecureBinding">
          <security mode="TransportWithMessageCredential">
            <transport clientCredentialType="None" />
            <message clientCredentialType="UserName" negotiateServiceCredential="false" establishSecurityContext="false"/>
          </security>
        </binding>
        <binding name="wsSecureNoUserNameBinding">
          <security mode="Transport">
                <transport clientCredentialType="None" />
                <message clientCredentialType="None" negotiateServiceCredential="false" establishSecurityContext="false" />
            </security>
        </binding>
        <binding name="wsPlainBinding">
            <security mode="None">
                <transport clientCredentialType="None" />
                <message clientCredentialType="None" negotiateServiceCredential="false" establishSecurityContext="false" />
            </security>
        </binding>
      </wsHttpBinding>
    </bindings>
    <services>
      <service behaviorConfiguration="defaultProfile" name="Service">
        <endpoint address="/basic" binding="basicHttpBinding" bindingConfiguration="basic"
          name="insecureService" contract="IService" />
        <endpoint address="/wsPlainBinding" binding="wsHttpBinding" bindingConfiguration="wsPlainBinding"
          name="wsService" contract="IService" />
        <endpoint address="/wsSecureNoUserNameBinding" binding="wsHttpBinding"
          bindingConfiguration="wsSecureNoUserNameBinding" name="wsSemiSecureService"
          contract="IService" />
        <endpoint address="/wsSecureService" binding="wsHttpBinding"
          bindingConfiguration="wsSecureBinding" name="wsSecureService"
          contract="IService" />
      </service>
    </services>
  </system.serviceModel>

No comments: