This post tries to describe the full story. It will try to keep quiet on all noise on other cool, but unneeded, features. This information is assembled from a rich variety of stuff on the web and error messages provided by the WCF framework itself. The latter are often quite to the point and provide a lot of essential information. This post is just a kind of cookbook recipe, I don’t claim to understand every detail and would appreciate any comment to further clarify the details.
The service
The service is an ASP.NET service, hosted by IIS and configured in the system.ServiceModel part of the web.config.
<system.serviceModel>
<services>
<service behaviorConfiguration="FarmService.CustomerDeskOperationsBehavior"name="FarmService.CustomerDeskOperations">
<endpoint address="" binding="wsHttpBinding" bindingConfiguration="RequestUserName"contract="FarmService.ICustomerDeskOperations">
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service>
</services>
The endpoint address is the root of the IIS site in which it his hosted. To use username authentication you need to use wsHttpBinding. The services functionality is described in the ICustomerDeskOperations contract.
In the binding you specify the credential type as username.
<bindings>
<wsHttpBinding>
<binding name="RequestUserName" >
<security mode="Message">
<message clientCredentialType="UserName"/>
</security>
</binding>
In the servicebehaviour you set up how the username is going to be validated
<behaviors>
<serviceBehaviors>
<behavior name="FarmService.CustomerDeskOperationsBehavior">
<serviceMetadata httpGetEnabled="true"/>
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom"customUserNamePasswordValidatorType="FarmService.Authentication.DistributorValidator, FarmService"/>
<serviceCertificate findValue="Farm" storeLocation="LocalMachine" storeName="TrustedPeople"x509FindType="FindBySubjectName"/>
</serviceCredentials>
</behavior>
The username is custom validated. This is done by the FarmService.Authentication.DistributorValidator class in theFarmService assembly. This class inherits from WCF class UserNamePasswordValidator and overrides the Validatemethod.
public class DistributorValidator : UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
throw new SecurityTokenException("Username and password required");
var repository = new DistributorRepository();
if (! repository.IsKnownDistributor(userName, password))
throw new FaultException(string.Format("Wrong username ({0}) or password ", userName));
}
}
The method validates the incoming username and password in a repository and throws appropriate exceptions when needed. This is really custom code. As long as you don’t throw an exception the service invocation will be accepted.
So far this could have been a copy of many a story on the web. Except for one detail which is absolutely essential. For username password authentication to work your server hosting the service needs an X509 certificate. Else all service invocations will fail. This certificate is specified in the service behavior.
<serviceCertificate findValue="Farm" storeLocation="LocalMachine" storeName="TrustedPeople" x509FindType="FindBySubjectName"/>
First you need a certificate. Instead of buying one (which is bound to a specific server address and thereby as good as useless for testing purposes) you can create your own. The .net framework comes with tools to generate these and there are several tutorials how to use these tools. Far more easier is selfcert a pluralsight tool which takes care of the whole process in a couple of clicks.
What they don’t tell you here is that you have to run the tool as administrator, else it will crash most ungracefully. What the tool is also unclear about is where to store the generated certificate. By default it is stored in MyStore. When validating the certificate it’s trustworthiness depends on the location it is stored. When the store is not trusted a chain of validation is started. Instead of setting up a chain of certificates you can also directly store your certificate in a trusted store.
With these settings the certificate is stored in a trusted location. The name and location match the settings in the service behavior
Troubles don’t end here. After a while, like logging in the next time, the service host will start complaining it cannot find the private key of the certificate with a “Keyset does not exist” error message. What happens it that the service no longer has the access right to read the certificate. What helped me was explicitly setting rights on the certificate’s private key file.
Here I am using a blunt axe by just allowing everybody read rights on the certificate’s private key file. I’m no security expert but I am aware this is absolutely not the way to do things. But hey, I only want to build a service, never asked for this certificate stuff and the only thing I want to do here is get that out of the way in the development process.
Now the service is ready to be consumed by a client
The client
To consume this service add a service reference in the client. The mexHttpBinding in the service configuration enables to read all metadata form the service without any credentials.
Setting up a connection to the client requires some fiddling. Again not all of these settings are clear by default.
var endPoint = new EndpointAddress(new Uri(Farm.FarmUrl), EndpointIdentity.CreateDnsIdentity("Farm"));
var binding = new WSHttpBinding();
binding.Security.Mode = SecurityMode.Message;
binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
var result = new CustomerDeskOperationsClient(binding, endPoint);
result.ClientCredentials.UserName.UserName = Farm.FarmUserName;
result.ClientCredentials.UserName.Password = Farm.FarmPassword;
First we need an endpoint. This is assembled from the url in the client’s configuration, here a constant Farm.FarmUrl. For the custom username authentication to work the endpoint also needs an EndpointIndentity. According to the sparse msdn documentation this is to prevent phishing. The fact that the identity was needed and the parameter had to be the certificate’s name was suggested by the WCF error messages.
The security is set according to the security settings we have seen in the service. Both the username and password are set in UserName property of the ClientCredentails.
Wrapping up
This is it. Now our service and clients are talking. But it took far to much effort to find the right settings. The number is not great, but they all were found to be essential. Finding the right was a process of endlessly weeding out all sidesteps. I hope this well help you to get it done a little faster.
<Update>
The many useful comments and their possibilities are summarized in this sequel. All misty things in this post are clarified there. Be sure to read it.
</Update>
No comments:
Post a Comment