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:
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:
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!
="1.0"="UTF-8"
<configuration>
<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>
<diagnostics>
<messageLogging maxMessagesToLog="30000"
logEntireMessage="true"
logMessagesAtServiceLevel="false"
logMalformedMessages="true"
logMessagesAtTransportLevel="true">
<filters>
<clear/>
</filters>
</messageLogging>
</diagnostics>
</system.serviceModel>
<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:
protected DCService.DCClient getWCF()
{
EndpointAddress ep = new EndpointAddress(new Uri(
"https://localhost/SafeService/safeService.svc/safe"));
WSHttpBinding bind = new WSHttpBinding(
SecurityMode.TransportWithMessageCredential);
bind.ReceiveTimeout = new TimeSpan(0, 0, 10);
bind.SendTimeout = new TimeSpan(0, 0, 10);
bind.BypassProxyOnLocal = false;
bind.UseDefaultWebProxy = true;
bind.Security.Message.ClientCredentialType =
MessageCredentialType.UserName;
DCService.DCClient cli = new DCService.DCClient(bind, ep);
cli.ClientCredentials.UserName.UserName = WS_user;
cli.ClientCredentials.UserName.Password = WS_pwd;
return cli;
}
public bool DoSomething(string something)
{
using (var cli = getWCF())
{
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!
public DCConnector()
{
ServicePointManager.ServerCertificateValidationCallback = new
System.Net.Security.RemoteCertificateValidationCallback(
ValidateServerCertificate);
}
public static bool ValidateServerCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
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.