Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

WCF Service over HTTPS with custom username and password validator in IIS

5.00/5 (13 votes)
19 Feb 2010CPOL5 min read 121.9K  
How to host a WCF HTTPS service with a custom username validator, in IIS.

Introduction

The task in hand can be stated as follows: Create a WCF service hosted in IIS7 over HTTPS with a custom user and password validator. Use Transport mode, and no message encryption. Sounds easy, right?

Background

It is a common task to design a service secured by HTTPS with the ability to use a custom validator (e.g., against SQL or other custom authentication scheme). What is wanted is a simple secure service with very little coding.

There are tons of articles describing custom username validation with Transport or Message security mode binding over the net (a large collection is available on CodePlex, or just search in Google for "WCF username transport"), but when dealing with a production environment, I found so much problems, so I though it would be useful to mention them in this article.

I suppose you already have some experience with WCF; you will not found here a complete example, rather tips on how to set it all up to work correctly.

Service

Building a service secured with HTTPS is easy. In the following documentation and examples, we declare the contract, implementation, and configuration section for our service:

For contracts, we define IDC.cs as follows:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace SafeService
{
 [ServiceContract]
  public interface IDC
  {
    [OperationContract]
    bool DoSomethingSecurely(string sParameter);
  }
}

For simplicity, we omit data contracts.

The implementation of the service contract is in DCService.svc:

C#
using System;
using System.Security;
using System.IdentityModel.Selectors;

namespace SafeService
{  
  public class DCService : IDC
  {
    public bool DoSomethingSecurely(string sParam)
    {
      return sParam == "isthissafeservice?";
    }
  }

  public class UNValidator : UserNamePasswordValidator
  {
    public UNValidator()
      : base()
    {
    }
    public override void Validate(string userName, string password)
    {
      if (userName == "test" && password == "the best")
        return;
      throw new System.IdentityModel.Tokens.SecurityTokenException(
                "Unknown Username or Password");
    }
  }
}

Notice that we also defined a class UNValidator which will be used later as a custom username and password validator.

Now we have to define the configuration of the service. We will discuss the individual parts first; notice that only the <system.serviceModel> and <diagnostics> sections of your config file are presented here!

XML
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- here are your server, appllication and other settings-->
     <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="SafeServiceConf" 
                        maxReceivedMessageSize="65536">
                    <security mode="TransportWithMessageCredential">
                        <message clientCredentialType="UserName"/>
                    </security>
                    <readerQuotas   maxArrayLength="65536" 
                                    maxBytesPerRead="65536 
                                    maxStringContentLength="65536"/>
                </binding>
            </wsHttpBinding>
        </bindings>
        <services>
          <service behaviorConfiguration="SafeSerice.ServiceBehavior" 
                   name="SafeService.DCService">
              <endpoint address="/safe" 
                        binding="wsHttpBinding" 
                        contract="SafeService.IDC" 
                        bindingConfiguration="SafeServiceConf">
                  <identity>
                  <dns value="localhost"/>
                  </identity>                  
              </endpoint>
              <endpoint address="mex" binding="mexHttpsBinding" 
                                      contract="SafeService.IDC" />
           </service>
          </services>
        <behaviors>
          <serviceBehaviors>
            <behavior name="SafeSerice.ServiceBehavior">
                      <serviceMetadata httpGetEnabled="true" />
                      <serviceDebug includeExceptionDetailInFaults="true" />
                      <serviceCredentials>
                        <userNameAuthentication 
                             userNamePasswordValidationMode="Custom"  
                             customUserNamePasswordValidatorType=
                                        "SafeService.UNValidator,SafeService"/>
                        </serviceCredentials>                    
                     </behavior>
          </serviceBehaviors>
        </behaviors>
 
        <!-- comment this if you don't want to make diag trace -->
        <diagnostics>
            <messageLogging maxMessagesToLog="30000"
                    logEntireMessage="true"
                    logMessagesAtServiceLevel="false"
                    logMalformedMessages="true"
                    logMessagesAtTransportLevel="true">
                <filters>
                    <clear/>
                </filters>
            </messageLogging>
        </diagnostics>
    </system.serviceModel>

    <!-- comment this if you don't want to make diag trace -->
    <system.diagnostics>
        <sources>
            <source name="System.ServiceModel" 
                     switchValue="Warning, ActivityTracing" 
                     propagateActivity="true" >
                <listeners>
                    <add name="xml" />
                </listeners>
            </source>
            <source name="System.ServiceModel.MessageLogging" 
                      switchValue="Warning">
                <listeners>
                    <add name="xml" />
                </listeners>
            </source>
        </sources>
        <sharedListeners>
            <add name="xml" 
              type="System.Diagnostics.XmlWriterTraceListener" 
              initializeData="C:\Temp\Server2.svclog" />
        </sharedListeners>
        <trace autoflush="true" indentsize="4"/>
    </system.diagnostics>
</configuration>

The most problematic things are hidden in the configuration of the service, as expected...

Binding

We defined WsHttpBinding with security mode TransportWithMessageCredentials. You might ask, why not Transport. The answer is, if we want to use a simple custom username validator under IIS, we must use TransportWithMessageCredentials, even it has a large overhead. Under IIS, a custom validator will not work with Transport security. At least, I couldn't get it to work, and it is also mentioned here.

maxReceiveMessageSize, readerQuotas

Be aware to set these large enough, or you will get strange errors or even time outs; the service will tell you nothing about the small message size.

security

Even if we use HTTPS (a.k.a. Transport), security is achieved with the SOAP message authentication mechanism, so we have to set the <security> element with the <message> child specified. We set clientCredentialType to custom. By this, we enable the use of the custom username and password validator.

serviceMetadata

Do not forget to enable this during development, or you probably will not be able to get the service metadata when building the client. If enabled, you can also browse and test the service mex endpoint from the browser. Disable this for production.

serviceCerdentials, userNameAuthentiocation

These elements allow you to specify that the custom user name and password validator will be implemented by our previously defined class. customUserNamePasswordValidatorType defines the type of the class implementing the validator. The syntax is, as in the other cases: "classname with namespace, assembly name".

diagnostics

These sections are useful when you are not able to get it to work and you don't know what to do. Activate diagnostics make client requests and then open the trace files - Microsoft Trace viewer will open the files.

So, this is our safe service. We put it on IIS, and 99%, it will not work. We have to setup IIS carefully too.

IIS

You have to take note of the following points to do, or check these things for a given environment:

  • Create website
  • Create separate application pool (production)
  • Enable WCF activation in server settings (not IIS, it's in the Turn On/Off system features on 2008 Server, or W7 if you're developing)
  • Enable the HTTPS protocol
  • Add HTTPS binding
  • In production, remove HTTP binding
  • Enable anonymous authentication, disable all others
  • Create server certificate, add one to the site (for testing, follow the sample on CodePlex)

This should be all. After that, test your service in the browser. Maybe somebody could point out any other pitfalls you might have.

Client

This is easy. In the client, you add the service reference; you can use disco, or enter the address as I did (in my case, http://localhost/DocumentCenterWCF/SafeService.svc).

But be aware, you must temporarily allow httpGetEnabled or httpsGetEnabled in the service config to be able to get meta-data.

Once you have your service reference added, you may want to start to use the client. Use the standard approaches for constructing the client, setting the credentials, binding and endpoints, and for calling methods repeatedly. You might get strange time-out errors (channel time out or something like that). The trace will show some errors, but I bet you will not be able to find out any more. The only thing that pointed me to solution was something about object activation. Activation needs deactivation...

The problem is you have to explicitly call the Close method on the ChannelFactory of the client. A smarter and more robust solution is to use using, because the client implements IDisposable.

So, in this case, I use something like this, which I found useful in a few cases of creating WCF clients:

C#
/// <summary>
/// Helper to get instance of the disposable client
/// </summary>
/// <returns></returns>
protected DCService.DCClient getWCF()
{
  //endpoint -do not forget to add endpoint address after svc
  EndpointAddress ep = new EndpointAddress(new Uri(
     "https://localhost/SafeService/safeService.svc/safe"));
  //transport encrypting with SOAP message authentication
  WSHttpBinding bind = new WSHttpBinding(
           SecurityMode.TransportWithMessageCredential);
  //10 sec timeouts
  bind.ReceiveTimeout = new TimeSpan(0, 0, 10); 
  bind.SendTimeout = new TimeSpan(0, 0, 10); 
  bind.BypassProxyOnLocal = false; // to be able bedugging eg. with fiddler
  bind.UseDefaultWebProxy = true; //use default system proxy
  //this we need for our custom credentials
  bind.Security.Message.ClientCredentialType = 
                        MessageCredentialType.UserName;
  //construct client
  DCService.DCClient cli = new DCService.DCClient(bind, ep);
  //pass custom credentials
  cli.ClientCredentials.UserName.UserName = WS_user;
  cli.ClientCredentials.UserName.Password = WS_pwd;
  return cli;
}

/// <summary>
/// Call the service
/// </summary>
/// <param name="something"></param>
/// <returns></returns>
public bool DoSomething(string something)
{
  using (var cli = getWCF())
  {
    //call to ensure object disposing and thus channel closing
    return cli.DoSomethingSecurely(something);
  }
}

Please note, this is only an example; the proxy and endpoint addresses should be configurable, but I like to configure client things in the code using my own settings, and this example should illustrates that I have put some things as URL hard-coded.

Final thing - you will meet with certificate errors if you are developing and have not installed a trusted certificate. During development, I use a simple certificate workaround, but be aware to make the proper check for the certificate in production to make the service secure!

C#
//my WCF service connector class constructor
public DCConnector()
{
  //SET certificate validation callback
  ServicePointManager.ServerCertificateValidationCallback = new 
    System.Net.Security.RemoteCertificateValidationCallback(
    ValidateServerCertificate);
}

//my certificate validation
public static bool ValidateServerCertificate(
      object sender,
      X509Certificate certificate,
      X509Chain chain,
      SslPolicyErrors sslPolicyErrors)
{
  //only in development! skip certificatefaults
  return true;
}

WCF is nice, reliable, and efficient. But you have to think a lot all the time and try to understand the whole framework. Copy/Paste in the case of WCF is not always the good choice.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)