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?
The steps I was missing are as follows:
The computer running a client app needs to have the certificate in
CurrentUser\Trusted Peoplestore (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 intoCurrentUser\Trusted Peoplestore. I suppose I'll have to include this file into my client's installation package.Also this time I used
makecert.exetool 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 -peP.S. Also I was missing makecert.exe on that machine, so I downloaded Microsoft SDKs from microsoft