In my last post I described the hassles to get a simple WCF service with username password authentication to work in the real world. Which was not as easy as it looked at first sight. Having weeded out all the unnecessary outgrows of the WCF framework I had it working though the solution was somewhat misty. After that the many useful comments have taught how to nurture my crop and which seeds to plant to turn it into a nice flower bed. Time for a round up.
The problem
The moment you start using authorization, or even authentication, in WCF you have to deal with (X509) certificates. Period. These certificates are used to secure the communication between the WCF service and client consumer. There are many ways to handle this security in WCF. The two most common ways are
- Transport security. The communication itself is encrypted with the certificate. This boils down to talking over https instead of over http. The problem is that you have to set up some infrastructure here. In the development stage a major issue (for us..) is that https requires a full IIS setup. It does not work with Cassini, the local web server which comes with Visual Studio. Using https in production requires extra maintenance as well, especially when service requests will be coming from a whole different array of sources.
- Message security. The message is encrypted using the certificate and can now safely travel over any port using plain http. In development this works well with Cassini and in production it makes life easy for system management. The downside of message security is that it requires some special attention setting up. In my last post I described a way to take those hurdles. Right now I will summarize the clearest way to get that done.
Configuring the service
Message security is set up in the configuration of the service. See the previous post for details.
<behaviors>
<serviceBehaviors>
<behavior name=“FarmService.CustomerDeskOperationsBehavior“>
<serviceMetadata httpGetEnabled=“true“/>
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode=“Custom“ customUserNamePasswordValidatorType=“FarmService.Authentication.DistributorValidator, FarmService“ />
<serviceCertificate findValue=“Farm“ storeLocation=“LocalMachine“ storeName=“My“ x509FindType=“FindBySubjectName“/>
</serviceCredentials>
</behavior>
Installing the certificate is not that difficult. With the tool Selfcert it’s just a couple of clicks.
The big problem is that when starting up the service host needs access to the private key of the certificate. This private key file is well protected. Misleading is the fact that the service host will have access to this keyfile right after installing the certificate. But it will not always have access the next time the host logs in. Which can lead to a pretty disappointing scenario. You set up the service and all seems to work well. But right after the next reboot (thanks to some kind of update) the service doesn’t work anymore because the keyfile is now inaccessible. In my last post I described a way to fiddle with the access rights. Which was somewhat misty. Thank goodness I’m not the only one who has been struggling with this. There is even a tool in the WIndows Resource Kits which does just the thing we need here.
Windows HTTP Services Certificate Configuration Tool is a command line tool to grant specific users read rights on a certificate’s private key file. It is a quite spartan command line tool. To give myself (and hence Cassini) access rights to the certificate described in above’s config I have to type.
C:\Program Files (x86)\Windows Resource Kits\Tools>winhttpcertcfg -g -c LOCAL_MACHINE\My -s Farm -a gekko-Software\Peter
On IIS the account to grant access to the private key is the default application Pool
C:\Program Files (x86)\Windows Resource Kits\Tools>winhttpcertcfg -g -c LOCAL_MACHINE\My -s Farm -a DefaultAppPool
The tool works well and has (built-in) documentation. Now the service is and stays available.
What UserNamePasswordValidator.Validate doesn’t tell you
Validating the user credentials is done by the Validate method. As long as this method does not throw an exception the credentials will be accepted. In all examples, including mine, this method throws some fancy exceptions like a SecurityTokenException. But the method might also throw some unintended exception. For instance failing to connect to the database when trying to validate the exception there. The problem is that whatever exception is thrown, the client will always receive the same error message:
An unsecured or incorrectly secured fault was received from the other party. See the inner FaultException for the fault code and detail. —> System.ServiceModel.FaultException: An error occurred when verifying security for the message.
This is quite misleading. As you cannot see the difference between unaccepted credentials and a (configuration) bug in your code. What you have to watch out for as well is that this Validate method is invoked before almost anything else, you cannot rely on any configuration being in place yet. As not only myself has found out.
Configuring the client
There are two things to a certificate. First of all it has to be in the right format to be read by a client consuming the service. There is no escaping that. The certificates produced by SelfCert are. The next step is that the certificate has to be trusted. Which depends on the location of the certificate in the certificate store. Checking the validity of a certificate is by default a chain which ends in a trusted location. Which can end at Verisign or another root certificate provider. A certificate which will be trusted by everybody on the web has to be bought and is bound to a specific domain. Certificates created with Selfcert are (of course) not trusted.
The nice thing in WCF is that the service consuming client does not have to check the trustworthiness of the certificate. And it only takes just one line of code to do that. Thanks to Yaron, who has a nice blog on WCF and shows up everywhere where there is a WCF issue, for pointing that out.
result.ClientCredentials.UserName.UserName = Farm.FarmUserName;
result.ClientCredentials.UserName.Password = Farm.FarmPassword;
result.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None;
The last line does the trick. The client does need a “well formed” certificate but it explicitly states that is does not care about validating it’s origin.
This property can also be set server side. In our case that was not necessary, but experience has learned that it might be needed in your scenario. It can be set server side in the config
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode=“Custom“ customUserNamePasswordValidatorType=“FarmService.Authentication.DistributorValidator, FarmService“ />
<serviceCertificate findValue=“Farm“ storeLocation=“LocalMachine“ storeName=“My“ x509FindType=“FindBySubjectName“ />
<clientCertificate>
<authentication certificateValidationMode=“None“ />
</clientCertificate>
</serviceCredentials>
And that’s all. In the end it looks so simple.. Just add water.