WCF Authentication: Custom Username and Password Validator


There are a number of authentication techniques supported by WCF. For instance Windows Authentication, X509 Certificates, Issued Tokens, and Username and Password are all mechanisms that can be used for authentication.


These client credential types are configured as part of the binding configuration for an endpoint. For example the config section below customises the netTcpBinding to use an X509 certificate:


<netTcpBinding>


  <binding name=tcpWithMessageSecurity>


    <security mode=Message >


      <message clientCredentialType=Certificate/>


    </security>


  </binding>


</netTcpBinding>


If you change the Client Credential Type to “UserName”, you can then pass username and passwords to the service and authenticate the client based on these credentials.


Sending unencrypted username and password over any communication framework is usually not advisable. Therefore, when you opt in for the UserName client credential type, WCF insists that your service must also reference a service certificate that contains a private key. The public key in this certificate is used to protect the confidentiality of the username and password sent to the service. The private key is then used by the service to obtain those encrypted credentials.


If a server certificate is not specified, at service start-up time, you will receive the following exception: “The service certificate is not provided. Specify a service certificate in ServiceCredentials.”


By default, when a user name and password is used for authentication, WCF uses Windows to validate the username and password. Having said that, WCF is one of the world’s most extensible communication frameworks and it should not surprise you if I say that you can take control of the username and password validation:


This first step is to create a “validator” by inheriting from the UserNamePasswordValidator class and override the Validate method (Add a reference to System.IdentityModel.dll first):


public class CustomUserNameValidator : UserNamePasswordValidator


{


 public override void


  Validate(string userName, string password)


 {


  // perform the validation logic


 }


 


The next step is to configure the service to use this custom validator. As this is the local behavior of the service, you will need to specify the custom validator as part of your service behavior configuration:


<serviceBehaviors>


  <behavior name=CustomValidator>


    <serviceCredentials>


 


      <userNameAuthentication


        userNamePasswordValidationMode=Custom


        customUserNamePasswordValidatorType=


           MyAssembly.CustomUserNameValidator, MyAssembly/>


 


      <serviceCertificate


        findValue=localhost


        x509FindType=FindBySubjectName


        storeLocation=CurrentUser


        storeName=My />


    </serviceCredentials>


  </behavior>


 


As you can see, there are two elements defined as part of the serviceCredentials section. The userNameAuthentication element specifies that a custom validator is used and the serviceCertificate element provides a reference to the X509 certificate used by the service to ensure the integrity of the client credentials. See here for information on how to create a temporary certificate for use during development.


Do not forget to set service’s behaviourConfiguration property to the name of the service behavior defined above:


<service name= behaviorConfiguration=CustomValidator>


Both client and service endpoints need to enable Username and Password authentication mode on their bindings:


<netTcpBinding>


  <binding name=tcpWithMessageSecurity>


    <security mode=Message >


      <message clientCredentialType=UserName/>


    </security>


  </binding>


</netTcpBinding>


Our service is now fully configured.  The final step is to provide the username and password to the client proxy which can then be encrypted and sent to the service. If you are using the ChannelFactory directly to create a proxy then use the code below to set the username and password programmatically:


ChannelFactory<ISimpleService> factory =


 new ChannelFactory<ISimpleService>(endpointConfigurationName);


factory.Credentials.UserName.UserName = “Pedram”;


factory.Credentials.UserName.Password = “Password01”;


However, if SvcUtil or Visual Studio has generated a proxy for you, use the code below to set the username and password:


proxy.ClientCredentials.UserName.UserName = “Pedram”;


proxy.ClientCredentials.UserName.Password = “Password01”;


 


Please note that there is no way to set the username and password in config.


Depending on the type of service certificate, you may need to specify a DNS identity for your client endpoints. This is used when the client authenticates the service to ensure that it is talking to the expected service and not any other hoax service intercepting your calls to the genuine service. The exception below is an indication of this:


Identity check failed for outgoing message. The expected DNS identity of the remote endpoint was ‘X’ but the remote endpoint provided DNS claim ‘Y’. If this is a legitimate remote endpoint, you can fix the problem by explicitly specifying DNS identity ‘Y’ as the Identity property of EndpointAddress when creating channel proxy.


You can modify the client endpoint to include an identity element such as the one below (see here for more information on service identity):


<identity>


  <dns value=pedramr/>


</identity>


You are now ready to test the service.


You can also download a sample application which tries this technique over netTcp and wsHttp bindings from here (built using Visual Sudio 2008 beta 2 and is compatible with .NET Framework 3.0). Follow the steps below before running the sample application:


          Create a certificate


          Modify the serviveCertificate elements defined in both client and service App.config files to reference your new certificate (you may decide to change the FindType as well)


          Specify a correct DNS identity value for the client endpoints


Let me know what you thought of this article…


References:


          Selecting a WCF Credential type


          Bypassing certificate validation


          Common WCF Security Scenarios


          Transport Security with Basic Authentication


          Service Identity and Authentication


          How To Use the ASP.NET Membership Provider

WCFSecurity.zip

Comments (33)

  1. JanW says:

    Thanks for the guide. However, one of the issues here is that any exceptions thrown in the Validate() method will not be reported back to the client properly. All you will get is a MessageSecurityException. This has been reported several times (e.g. here: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=575748&SiteID=1) Unfortunatley Microsoft did not come up with an adequate response yet.

  2. Jon says:

    I created a certificate "CN=SignedByCA" and updated the App.Config files to refer to its thumbprint. Now when I try to start the host, I get the following exception on host.Open():

    The certificate ‘CN=SignedByCA’ must have a private key that is capable of key exchange. The process must have access rights for the private key.

    I’m not sure how to give the host access to the private key. I’m starting the host process using VS 2008.

    Regards,

    Jon

  3. Diego says:

    I’m having the same issue Jon mentioned. I’m also getting the message:

    The certificate ‘CN=SignedByCA’ must have a private key that is capable of key exchange. The process must have access rights for the private key.

    Please…. help :S

  4. To the two above: Use the FindPrivateKey utility located in the WCF samples to find the location of the private key, and then set the permissions accordingly.

    I spent many hours today while having the same problem, and I came up with the following solution

    To install a self signed certificate

    makecert.exe -sr LocalMachine -ss MY -a sha1 -n CN=%SERVER_NAME% -sky exchange -pe

    certmgr.exe -add -r LocalMachine -s My -c -n %SERVER_NAME% -r LocalMachine -s TrustedPeople

    To use the FindKeyUtility

    FindPrivateKey.exe TrustedPeople LocalMachine Private key directory: C:ProgramDataMicrosoftCryptoRSAMachineKeys Private key file name: 756e9ecb7bb8ed83bf80031497479997_8a4ee4f0-1f8d-4d2e-b1bf-fff1d5b15e61

    And then, you can go to the specified folder and change the permissions…

    Moreover, I needed to set this in the client’s config file

        <behaviors>

           <endpointBehaviors>

             <behavior name="ClientCertificateBehavior">

               <clientCredentials>

                 <serviceCertificate>

                     <authentication certificateValidationMode="PeerOrChainTrust" />

                 </serviceCertificate>

               </clientCredentials>

             </behavior>

           </endpointBehaviors>

         </behaviors>

    and add this to the endpoint configuration

    behaviorConfiguration="ClientCertificateBehavior"

    Finally, pay attention to the

    <identity>

                       <dns value="localhost" />

                   </identity>

    element, in the client.

    The dns value must much the one in the certificate.

    Good luck!!

  5. Thiarley Fontenele says:

    Hi,

    If I authenticate the user using peertrust, How can I get his certificate inside the Operation Contract ?

    I need the certificate to get others informations.

  6. Jernej Logar says:

    Hi!

    I was also getting the message:

    The certificate ‘CN=SignedByCA’ must have a private key that is capable of key exchange. The process must have access rights for the private key.

    I solved that by importing the certificate together with the private key, using a pfx fiel which I generated wtih pvk2pfx.

  7. alik levin's says:

    This is a digest of WCF Security resources I was collecting for some time. Drop me a comment in case

  8. Fernando Alves says:

    In some of my implemented OperationContract methods i need to log with others method informations, and console output the validated credentials (username/password), how can i get the respective instanciated credential values?

  9. Fernando Alves says:

    Nevermind, ServiceSecurityContext.Current.PrimaryIdentity.Name

  10. 江南白衣 says:

    This is a digest of WCF Security resources I was collecting for some time. Drop me a comment in case it is useful.

  11. Arjan Hordijk says:

    I had the same problem.

    These were the steps I took to solve the problem:

    I removed the generated personal key from the store and issued the following request: makecert -sk SignedByCA -iv TempCA.pvk -n "CN=localhost" -ic TempCA.cer SignedByCA.cer -sr LocalMachine -ss My -sky exchange -pe

    The created certificate was added to the personal certifcate store of the local computer.

    After placing this certificate in the personal certificate store of the current user (drag and drop) the error was gone.

    Hope this will help…..

    We are not the only ones (Also see this post: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=1180892&SiteID=1)

  12. Mathew Upchurch says:

    Great article but my problem is I’m authenticated on my web site already (setting the generic principal)… not I need to call my services layer… but I no longer have password available…  any best practices for how to deal with this?

  13. VG says:

    My Service is hosted under IIS with SSL enabled.

    This works without SSL enabled, but doesn’t work with SSL on.

    Any tips?

  14. John Doe says:

    Good post.

    Complemented with http://www.digwin.com/view/howto-use-makecert-for-trusted-root-certification-authority-and-ssl-certificate-issuance , it made for an easy implementation of UserName validation. : )

  15. angeltq says:

    Cannot find the X.509 certificate using the following search criteria: StoreName ‘My’, StoreLocation ‘CurrentUser’, FindType ‘FindByThumbprint’, FindValue ‘8e f9 c6 6f 4e a0 0c 49 4f 84 69 fb de c6 a7 e1 79 01 5b 6e’.

    Help me!

  16. Gr1nch says:

    Anyone know how to catch any exception we throw in our Validate() function so we might have some means of returning a code for why validation failed? It would seem that regardless of what SecurityTokenException I throw, the only exception actually caught on the client side is the generic one. I can see my exception if I run diagnostics, but I need the client to see it to detect such things as lockout and other failure reasons…

  17. Mike says:

    2 Gr1nch

    1) Throw a FaultException from Validate(…)

    2) Catch MessageSecurityException on the client.

    3) In the InnerException property you will find the FaultException you threw.

    Keep in mind: generic FaultExceptions don’t work here as a filter on client side, but you can, for example, use FaultCode during FaultException initialization to be sure that an InnerException the client got from MessageSecurityException is indeed the exception you threw in Validate(…).

  18. #.think.in says:

    #.think.in infoDose #28 (29th Apr – 8th May)

  19. #.think.in says:

    #.think.in infoDose #28 (29th Apr – 8th May)

  20. Thankful says:

    Exactly what I needed!!!

    Thanks a whole lot for the post.

  21. Karan says:

    Hi, I am getting this exception as described in the test above.

    “Identity check failed for outgoing message. The expected DNS identity of the remote endpoint was ‘X’ but the remote endpoint provided DNS claim ‘Y’. If this is a legitimate remote endpoint, you can fix the problem by explicitly specifying DNS identity ‘Y’ as the Identity property of EndpointAddress when creating channel proxy.”

    I am using an endpoint identity as SPN. Any idea how to get this to work with endpoint Identity set as SPN.

    Thanks.

  22. virus says:

    i under basichttpbinding bind manner, communicate with silverlight3,

    i do not need https and certificate,i use http and username and password only to make authentication

    so what i can do ? and how i can do?

    msn:jorden008@hotmail.com

    thank you for your email!!

    <serviceBehaviors>

    <behavior name="WcfService.Service1Behavior">

    <serviceMetadata httpGetEnabled="true" />

    <serviceDebug includeExceptionDetailInFaults="false"/>

    <serviceCredentials >

    <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="WcfService.MyValidator, WcfService"/>

    </serviceCredentials>

    </behavior>

    </serviceBehaviors>

    do not work well??why??

  23. Mohammad Vaqqas says:

    Thanks Pedram for such a great post.

    And thanks to Dimitris-Ilias Gkanatsios whose inputs were really helpful to me.

  24. Nupur Bakshi says:

    Hello Sir,

    I am developing a project in WCF in which I am using WCF Message Based Security with UserNamePassword Client credentials for authenticating the Client.I have also created X509 certificates and provide access to Claims using Claims Based Authorization.Everything is working fine as When I initially call the method of the service it asks for the authentication of the client and after authenticating the Client it provides access to the method which is accurately done but when I call another method from the same service it again asks for authentication which is creating a problem as I don't want to authenticate the Client again and again on every method call of the same service.Is there any way to retain the Client Credentials so that I would not have to re-authenticate the Client on every method call.Please help me in this.This is my first project in WCF so please provide me the step by step guidance on how to do it.I will be thankful to you.

  25. imran says:

    Hi

    first of all this post helped me a lot and i have created security certificate using some help from this post.

    My problem is I have to call service using js/jquery request. so far I have not found any solution. could you please guide me.

    thanks

  26. I want to download this sample to study, but  the link(blogs.msdn.com/…/5295857.ashx) doesn't work,  who can send me a copy? thanks

    email:freedomle@gmail.com

  27. I think this is exactly what I need but I am still having a problem in getting my wcf to work.  I have been working on this solution for a while now.  Is this the proper place for me to place my code and to ask for help?

  28. Roger, the link on the page seems to work.

  29. Here is my situation.  I have to consume a WCF service on an external server.  From what I understand the security layer is on a network appliance.  The service will be hosted on either a sunapp or weblogic server.

    Test environment:  Client and test server service in VS 2010 that are both on IIS (over http).

    Production environment: Client will be on IIS and server service will be on either a sunapp or weblogic server (over https).

    Right now, in test, I am getting the following error "The remote server returned an error: (401) Unauthorized.)" when I try to consume the service hosted on IIS from my client.  I want to pass the username and password in clear text.  The security settings in IIS for both the server and client are set to Enable anonymous access.  ONLY the server has basic authentication checked.

    The question is, how do I accomplish this in test and then move it to production based on the different hosting servers?

    ***client config***

    system.serviceModel>

           <bindings>

               <basicHttpBinding>

                   <binding name="BasicHttpBinding_IService1" closeTimeout="00:01:00"

                       openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"

                       allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"

                       maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"

                       messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"

                       useDefaultWebProxy="true">

                       <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"

                           maxBytesPerRead="4096" maxNameTableCharCount="16384" />

                       <security mode="TransportCredentialOnly">

                           <transport clientCredentialType="Basic" proxyCredentialType="None"

                               realm="" />

                           <message clientCredentialType="UserName" algorithmSuite="Default" />

                       </security>

                   </binding>

               </basicHttpBinding>

           </bindings>

           <client>

               <endpoint address="http://localhost/CAServer/services/Service1.svc&quot;

                   binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService1"

                   contract="ServiceReference1.IService1" name="BasicHttpBinding_IService1" />

           </client>

       </system.serviceModel>

    ***server config***

    <system.serviceModel>

           <behaviors>

               <serviceBehaviors>

                   <behavior name="CAServer.Service1_Behavior">

                       <serviceMetadata httpGetEnabled="true" />

                       <serviceDebug includeExceptionDetailInFaults="false" />

                       <serviceCredentials>

                           <userNameAuthentication

                                       userNamePasswordValidationMode="Custom"

                                       customUserNamePasswordValidatorType="CAServer.abc.def.ghi.Service1, YepYep"/>

                       </serviceCredentials>

                   </behavior>

               </serviceBehaviors>

           </behaviors>

           <services>

               <service behaviorConfiguration="CAServer.Service1_Behavior"

                   name="CAServer.abc.def.ghi.Service1">

                   <endpoint address="http://localhost/CAServer/services/Service1.svc&quot;

                       binding="basicHttpBinding" bindingConfiguration="RequestUserName"

                       contract="CAServer.abc.def.ghi.IService1" />

                   <endpoint address="mex" binding="basicHttpBinding" contract="IMetadataExchange" bindingConfiguration="RequestUserName" />

               </service>

           </services>

           <bindings>

               <basicHttpBinding>

                   <binding name="RequestUserName">

                       <security mode="TransportCredentialOnly">

                           <transport clientCredentialType="Basic" proxyCredentialType="Basic"></transport>

                       </security>

                   </binding>

               </basicHttpBinding>

           </bindings>

           <serviceHostingEnvironment multipleSiteBindingsEnabled="false" />

       </system.serviceModel

    ***client code***

    Dim x As New ServiceReference1.Service1Client

    x.ClientCredentials.UserName.UserName = "dave"

    x.ClientCredentials.UserName.Password = "dave"

    x.Validate("dave", "dave")

    ***server code interface***

    Imports System.ServiceModel

    ' NOTE: You can use the "Rename" command on the context menu to change the interface name "IService1" in both code and config file together.

    Namespace abc.def.ghi

       <ServiceContract()>

       Public Interface IService1

           <OperationContract()>

           Sub DoWork()

           <OperationContract()>

           Sub Validate(ByVal userName As String, ByVal password As String)

       End Interface

    End Namespace

    ***server code service***

    Imports System.ComponentModel

    Imports System.IdentityModel.Selectors

    Imports System.IdentityModel.Tokens

    Imports System.Runtime.Serialization

    Imports System.ServiceModel

    Namespace abc.def.ghi

       Public Class Service1

           Inherits UserNamePasswordValidator

           Implements IService1

           Public Sub DoWork() Implements abc.def.ghi.IService1.DoWork

           End Sub

           Public Overrides Sub Validate(ByVal userName As String, ByVal password As String) Implements abc.def.ghi.IService1.Validate

               If String.IsNullOrEmpty(userName) OrElse String.IsNullOrEmpty(password) Then

                   Throw New SecurityTokenException("Username and password required")

               End If

               userName = "dave"

               password = "dave"

               If userName <> "dave" And password <> "dave" Then

                   Throw New FaultException(String.Format("Wrong username ({0}) or password ", userName))

               End If

           End Sub

       End Class

    End Namespace

    Please help.

  30. Since this is going through IIS I realized that I need to actually pass my network id and pwd to the Validate function in the client code.  However, things have changed.  I am now required to consume a web service that uses UserTokenName passed in the headers.

    I have everything set up correctly (I think).  I cannot seem to find code that I can use on the server side to confirm that the values I entered into the client config are correct.  Any help would be appreciated.

  31. وحید says:

    مرسی پدرام جان. خیلی مفید بود

  32. Karan Khanna says:

    Thanks for this super cool post I liked a lot, have a look below. http://www.ebiconference.com have a look.

Skip to main content