Let's say you've got a web service that exposes data (we could hardly say that you didn't! :D). This particular data always relates to a particular user; for example, financial transactions are always viewed from the perspective of the "user". If it's me, it's my transactions. If it's you, it's your transactions. This means every call to the web service needs to identify who the user is (or you can use a session, but let's put that idea aside for now).

However, lets also say that these calls are "on behalf" of the user and not from them directly. So we need a way of authenticating who is putting the call through on behalf of the user.

This scenario can occur in a user-centric web application, where the web application runs on its own server and talks to a business service on a separate server. The web application server talks on behalf of the user with the business server.

So wouldn't it be nice if we could do the following with the WCF service that enables the web application server to talk to the business server: authenticate the user at the message level using their username and password and authenticate the web application server at the transport level by checking its certificate to ensure that it is an expected and trusted server?

Currently in WCF the out-of-the-box bindings net.tcp and WSHttp offer authentication at either message level or the transport level, but not both. TransportWithMessageCredential security is exactly that: transport security (encryption) with credentials at the message level. So how can we authenticate at both the transport and message level then?

The answer: create a custom binding (note: I will focus on using the net.tcp binding as a base here). I found the easiest way to do this is to continue doing your configuration in the XML, but at runtime copy and slightly modify the netTcp binding from the XML configuration. There is literally one switch you need to enable. Here's the code on the service side:

ServiceHost businessHost = new ServiceHost(typeof(DHTestBusinessService));
    
ServiceEndpoint endpoint = businessHost.Description.Endpoints[0];
    
BindingElementCollection bindingElements = endpoint.Binding.CreateBindingElements();
    
SslStreamSecurityBindingElement sslElement = bindingElements.Find<SslStreamSecurityBindingElement>();

//Turn on client certificate validation
sslElement.RequireClientCertificate = true; 

CustomBinding newBinding = new CustomBinding(bindingElements);
NetTcpBinding oldBinding = (NetTcpBinding)endpoint.Binding;
newBinding.Namespace = oldBinding.Namespace;
endpoint.Binding = newBinding;

Note that you need to run this code before you Open() on your ServiceHost.

You do exactly the same thing on the client side, except you get the ServiceEndpoint in a slightly different manner:

DHTestBusinessServiceClient client = new DHTestBusinessServiceClient();

ServiceEndpoint endpoint = client.Endpoint;

//Same code as the service goes here

You'd think that'd be it, but you'd be wrong. :) This is where it gets extra lame. You're probably attributing your concrete service methods with PrincipalPermission to restrict access based on the roles of the service user, like this:

[PrincipalPermission(SecurityAction.Demand, Role = "MyRole")]

This technique will start failing once you apply the above changes. The reason is because the user's PrimaryIdentity (which you get from OperationContext.Current.ServiceSecurityContext.PrimaryIdentity) will end up being an unknown, username-less, unauthenticated IIdentity. This is because there are actually two identities representing the user: one for the X509 certificate used to authenticate over Transport, and one for the username and password credentials used to authenticate at Message level. When I reverse engineered the WCF binaries to see why it wasn't giving me the PrimaryIdentity I found that it has an explicit line of code that causes it to return that empty IIdentity if it finds more than one IIdentity. I guess it's because it's got no way to figure out which one is the primary one.

This means using the PrincipalPermission attribute is out the window. Instead, I wrote a method to mimic its functionality that can deal with multiple IIdentities:

private void AssertPermissions(IEnumerable<string> rolesDemanded)
{
    IList<IIdentity> identities = OperationContext.Current.ServiceSecurityContext.AuthorizationContext.Properties["Identities"] as IList<IIdentity>;
            
    if (identities == null)
        throw new SecurityException("Unauthenticated access. No identities provided.");

    foreach (IIdentity identity in identities)
    {
        if (identity.IsAuthenticated == false)
            throw new SecurityException("Unauthenticated identity: " + identity.Name);
    }

    IIdentity usernameIdentity = identities.Where(id => id.GetType().Equals(typeof(GenericIdentity)))
                                           .SingleOrDefault();
            
    string[] userRoles = Roles.GetRolesForUser(usernameIdentity.Name);

    foreach (string demandedRole in rolesDemanded)
    {
        if (userRoles.Contains(demandedRole) == false)
            throw new SecurityException("Access denied: authorisation failure.");
    }
}

It's not pretty (especially the way I detect the username/password credential IIdentity), but it works! Now, at the top of your service methods you need to call it like this:

AssertPermissions(new [] {"MyRole"});

Ensure that your client is providing a client certificate to the server by setting the client certificate element in your XML config under an endpoint behaviour's client credentials section:

<clientCertificate storeLocation="LocalMachine" 
                   storeName="My" 
                   x509FindType="FindBySubjectDistinguishedName" 
                   findValue="CN=mycertficatename"/>

Now, I mentioned earlier that the business web service should be authenticating the web application server clients using these provided certificates. You could use chain trust (see the Chain Trust and Certificate Authorities section of this page for more information) to accept any client that was signed by any of the default root authorities, but this doesn't really provide exact authentication as to who is allowed to connect. This is because any server that has a certificate that is signed by any trusted authority will authenticate fine! What you need is to create your own certificate authority and issue your own certificates to your clients (I covered this process in a previous blog titled "Using Makecert to Create Certificates for Development").

However, to get WCF to only accept clients signed by a specific authority you need to write your own certificate validator to plug into the WCF service. You do this by inheriting from the X509CertificateValidator class like this:

public class DHCertificateValidator 
    : X509CertificateValidator
{
    private static readonly X509CertificateValidator ChainTrustValidator;
    private const X509RevocationMode ChainTrustRevocationMode = X509RevocationMode.NoCheck;
    private const StoreLocation AuthorityCertStoreLocation = StoreLocation.LocalMachine;
    private const StoreName AuthorityCertStoreName = StoreName.Root;
    private const string AuthorityCertThumbprint = "e12205f07ce5b101f0ae8f1da76716e545951b22";

    static DHCertificateValidator()
    {
        X509ChainPolicy policy = new X509ChainPolicy();
        policy.RevocationMode = ChainTrustRevocationMode;
        ChainTrustValidator = CreateChainTrustValidator(true, policy);
    }

    public override void Validate(X509Certificate2 certificate)
    {
        ChainTrustValidator.Validate(certificate);

        X509Store store = new X509Store(AuthorityCertStoreName, AuthorityCertStoreLocation);
          
        store.Open(OpenFlags.ReadOnly);
        X509Certificate2Collection certs = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, certificate.IssuerName.Name, true);

        if (certs.Count != 1)
            throw new SecurityTokenValidationException("Cannot find the root authority certificate");

        X509Certificate2 rootAuthorityCert = certs[0];
        if (String.Compare(rootAuthorityCert.Thumbprint, AuthorityCertThumbprint, true) != 0)
            throw new SecurityTokenValidationException("Not signed by our certificate authority");

        store.Close();
    }
}

As you can see, the class re-uses the WCF chain trust mechanism to ensure that the certificate still passes chain trust requirements. But it then goes a step further and looks up the issuing certificate in the computer's certificate repository to ensure that it is the correct authority. The "correct" authority is defined by the certificate thumbprint defined as a constant at the top of the class. You can get this from any certificate by inspecting its properties. (As an improvement to this class, it might be beneficial for you to replace the constants at the top of the class with configurable options dragged from a configuration file).

To configure the WCF service to use this validator you need to set the some settings on the authentication element in your XML config under the service behaviour's service credential's client certificate section, like this:

<authentication certificateValidationMode="Custom" 
                customCertificateValidatorType="DigitallyCreated.DH.Business.DHCertificateValidator, MyAssemblyName" 
                trustedStoreLocation="LocalMachine" 
                revocationMode="NoCheck" />

And that's it! Now you are authenticating at transport level as well as at message level using certificates and usernames and passwords!

This post was derived from my question at StackOverflow and the answer that I researched up and posted (to answer my own question). See the StackOverflow question here.