Search code examples
c#wcfauthenticationwcf-securitynettcpbinding

WCF and custom Authentication (username/password)


I have Windows Forms clients that connect to a WCF service over the internet. WCF service is hosted in a Windows Service running under dedicated Windows account. I want to authenticate the clients against database by checking username+password pair.

In order to avoid hundreds of methods like:

public int Add(string User, string Password, int A, int B)

I used the following guide to override UserNamePasswordValidator class:

http://blog.clauskonrad.net/2011/03/how-to-wcf-and-custom-authentication.html

My custom authenticator class matches the example

class UsernameAuthentication : UserNamePasswordValidator
    {
        public override void Validate(string userName, string password)
        {
            //Will be checked against database
            var ok = (userName == "Ole") && (password == "Pwd");
            if (ok == false)
                throw new AuthenticationException("u/p does not match");
        }
    }

My server config is:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
  <system.serviceModel>

    <services>
      <service name="TestWCFCustomAuth.CalculatorService" behaviorConfiguration="customCred">
        <endpoint address="CalcSvc"
                  binding="netTcpBinding"
                  bindingConfiguration="secUP"
                  contract="ServiceInterface.ICalculatorService"/>
        <endpoint address="mex" binding="mexHttpBinding" contract="ServiceInterface.ICalculatorService" />
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:81/"/>
            <add baseAddress="net.tcp://localhost:82/"/>
          </baseAddresses>
        </host>
      </service>
    </services>

    <bindings>
      <netTcpBinding>
        <binding name="secUP">
          <security mode="Message">
            <message clientCredentialType="UserName"/>
          </security>
        </binding>
      </netTcpBinding>
    </bindings>

    <behaviors>
      <serviceBehaviors>
        <behavior name="customCred">
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
          <serviceCredentials>
            <!--Service identity + encryption certificate-->
            <serviceCertificate findValue="SHKIService" storeLocation="LocalMachine" storeName="Root" x509FindType="FindBySubjectName"/>
            <userNameAuthentication userNamePasswordValidationMode="Custom"
              customUserNamePasswordValidatorType="TestWCFCustomAuth.UsernameAuthentication, TestWCFCustomAuth" />
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

And the client is auto-generated:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
    
    <system.serviceModel>
        <bindings>
            <netTcpBinding>
                <binding name="NetTcpBinding_ICalculatorService">
                    <security mode="Message">
                        <transport sslProtocols="None" />
                        <message clientCredentialType="UserName" />
                    </security>
                </binding>
            </netTcpBinding>
            <wsHttpBinding>
                <binding name="MetadataExchangeHttpBinding_ICalculatorService">
                    <security mode="None" />
                </binding>
            </wsHttpBinding>
        </bindings>
        <client>
            <endpoint address="net.tcp://localhost:82/CalcSvc" binding="netTcpBinding"
                bindingConfiguration="NetTcpBinding_ICalculatorService" contract="ServiceReference1.ICalculatorService"
                name="NetTcpBinding_ICalculatorService">
                <identity>
                    <certificate encodedValue="AwAAAAEAAAAUAAAA5Miz/2tl3pOxgQepIM62wzcAU+8gAAAAAQAAAK0BAAAwggGpMIIBCqADAgECAgkArWjRy0f0w98wCgYIKoZIzj0EAwIwFjEUMBIGA1UEAxMLU0hLSVNlcnZpY2UwHhcNMjEwMzI5MjEzNjUwWhcNMjYwMzI5MjEzNjUwWjAWMRQwEgYDVQQDEwtTSEtJU2VydmljZTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAXE/ZelSAJ7+qjUTECzz2ZHFHTd6KtYsrbgdvBw3Xqt/G9R4IO01Rvxltir7FMfHzLgzsa4LdLkY3yAk/4rOqTPqAXY/sd/TpZOOB7ntZx02BEUvNiKouVu+yzIBMQxyW4aGZGOftiSOA28VKxNnaARN97/IoYyO0FhN+4vlKaPlTGFQMAoGCCqGSM49BAMCA4GMADCBiAJCAa88D2F5LuEFF0BL2+Vn1xIGSrjLo1YpiJk0DNJEbF0OOzH+xuKk/8H4yjQGO/yMmI8/pQWeU36Bu/D2xxJ0XqvtAkIA9udnx+h7lAsAYOtFMT12qHkVHInGWTzGHjNF0nrOldURa7X8B+tDeYrDJGBD+/9R2E4koJeGb0ubAmUl4Hrwyik=" />
                </identity>
            </endpoint>
            <endpoint address="http://localhost:81/mex" binding="wsHttpBinding"
                bindingConfiguration="MetadataExchangeHttpBinding_ICalculatorService"
                contract="ServiceReference1.ICalculatorService" name="MetadataExchangeHttpBinding_ICalculatorService" />
        </client>
    </system.serviceModel>
</configuration>

I made a self-signed certificate and imported it into the Root directory, so the certificate configuration is OK too.

Test-client code is:

var client = new CalculatorServiceClient("NetTcpBinding_ICalculatorService");
client.ClientCredentials.UserName.UserName = "Ole";
client.ClientCredentials.UserName.Password = "Pwd";
var res = client.Add(1, 2);
Console.WriteLine("Result: {0}", res);

Although my code is nearly identical to the guide, I get an error:

System.ServiceModel.Security.SecurityNegotiationException: 'The caller was not authenticated by the service.'
Inner Exception:
FaultException: The request for security token could not be satisfied because authentication failed.

I searched for an answer for several days and tried many other configurations. My best guess is that there is some other kind of authentication happening behind the scenes? If that's so - how do I disable that?


Solution

  • The steps I was missing are as follows:

    1. Give the Windows service account a "Read" permission to certificate's private key as described here here at

    To grant permission on the private key to the account one can use Certificate Snap-In of mmc. One can start mms.exe, choose "Add/Remove Snap-in" in the "File" menu, choose "Certificates" Snap-in and to choose "Computer account" of the Local computer. Then one should select the SSL certificate of Personal store and then use context menu "Manage Private Keys...".

    1. The computer running a client app needs to have the certificate in CurrentUser\Trusted People store (even if the client and server are running on the same computer). So I used mmc tool from previous step to export the certificate without private key into a file and then imported certificate from that file into CurrentUser\Trusted People store. I suppose I'll have to include this file into my client's installation package.

    2. Also this time I used makecert.exe tool to create certificate as suggested in the msdn reference provided by @Theobald Du The commands are:

      makecert.exe -sr LocalMachine -ss MY -a sha1 -n CN=<I used server's IP address> -sky exchange -pe

    P.S. Also I was missing makecert.exe on that machine, so I downloaded Microsoft SDKs from microsoft