I'm working on an application which is going to use WCF heavily for communications between a client side application and a server-based service application. One aspect of the application that I've been ignoring during testing was WCF authentication. During testing the application has been running on my local network. I had enabled connection based authentication (ex.SSL) and it is using TCP as it's binding since I do a lot of callback messaging. In the back of my mind I realized that when it's deployed the client will be on a different network, communicating over the internet, so there may be some issues with the current security model.

A week ago I finally got around to looking at it and it completely breaks when the client isn't local, since it was using Windows authentication to do it's authentication. Microsoft has a nice manual which walks through the various ways of configuring security in WCF, which you should choose (and why) under various scenarios. Scenarios, Patterns, and Implementation Guidance for Web Services Enhancements (WSE) 3.0 from their patterns & practices team. Based on a number of factors, it suggested I use SSL along with message based security. I'm not going to be hosting this in IIS, so HTTPS wasn't an option for the SSL connection. I also wasn't using Windows authentication and didn't want to deal with some of the issues of the other credential types (IssuedToken, Digest, etc.). So I decided on UserName.

I now had two things to fix - first, I needed to implement my own UserName authentication scheme (again, I like making my life hard so I wasn't using the default authentication provider in ASP.NET that used SQL Server). This turns out to be pretty simple by inheriting from the UserNamePasswordValidator class in the System.IndentityMode.Selectors namespace and overriding the Validate method:

using System;
using System.Collections.Generic;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Text;

namespace MySampleApp.Server

{

    /// <summary>

    /// This class is responsible for validating the username/password

    /// used by the connecting WCF client.

    /// </summary>

    /// <remarks>

    /// <para>

    /// The server is configured to use this class in the Service Behavior

    /// serviceCredentials section of the config file.

    /// CustomUserNamePasswordValidatorType is set to SampleValidator

    /// UseNamePasswordValidationMode is set to Custom

    /// </para>

    /// </remarks>

    /// <developer>Paul Mrozowski</developer>

    /// <created>10/13/2008</created>

    public class SampleValidator : UserNamePasswordValidator

    {

        public override void Validate(string userName, string password)

        {

            // TODO: Finish, this is just here to test out the idea.

            if (userName != "user" || password != "pass")

                throw new SecurityTokenException("Unknown user.");

        }

    }

}

 

If the passed in username/password is invalid, throw an exception. Easy. I added this new class to my server project, then modified the app.config file to let WCF know to use this for authentication.

<bindings>
      <nettcpbinding>
          <binding name="StandardServerBinding" maxbuffersize="8192000" 
              maxreceivedmessagesize="8192000" listenbacklog="5000" maxconnections="1000"> 
              <readerquotas maxdepth="24" maxstringcontentlength="8192000" maxarraylength="8192000" 
               maxbytesperread="8192000" maxnametablecharcount="8192000" />
              <reliablesession inactivitytimeout="01:00:00" />
              <security mode="Message">                  
                  <message clientcredentialtype="UserName" />
              </security>
          </binding>
      </nettcpbinding>
  </bindings>
        <behaviors>
            <servicebehaviors>
                <behavior name="RegisterBehavior">					
                    <servicedebug includeexceptiondetailinfaults="true" />
                    <servicemetadata />
                    <servicethrottling maxconcurrentcalls="48" maxconcurrentsessions="5000" 
                      maxconcurrentinstances="5000" />
                    <servicecredentials>
                        <usernameauthentication usernamepasswordvalidationmode="Custom" 
          customusernamepasswordvalidatortype="MySampleApp.Server.SampleValidator,MySampleApp.Server" 
                          cachelogontokens="true" cachedlogontokenlifetime="01:00:00" />
                    </servicecredentials>					
                </behavior>
                <behavior name="ThrottlingBehavior">
                    <servicethrottling maxconcurrentcalls="48" maxconcurrentsessions="5000" 
                        maxconcurrentinstances="5000" />
                </behavior>
            </servicebehaviors>
        </behaviors>		
        <services>
            <service name="MySampleApp.ServerProcess" behaviorconfiguration="RegisterBehavior">
                <endpoint name="PrimaryEndpoint" contract="MySampleApp.Server.Contracts.IServer" 
                   binding="netTcpBinding" address="net.tcp://localhost" 
                   bindingconfiguration="StandardServerBinding" />
                <endpoint name="MEXEndpoint" contract="IMetadataExchange" 
                   binding="mexHttpBinding" address="http://localhost/MEX/" bindingconfiguration="" />
            </service>
        </services>

You add a <serviceCreditials\userNameAuthentication section to the ServiceBehavior and specify a Custom userNamePasswodValidationMode. I tell it to use the full name of the class, the second part of that after the comma tells it which assembly it's located in. In the netTcpBinding you can see that I've enabled security on the message, and told it to use the UserName authentication type.

        <bindings>
            <netTcpBinding>
                <binding name="NewBinding0" maxBufferSize="8192000" maxReceivedMessageSize="8192000">
                    <readerQuotas maxDepth="24" maxStringContentLength="8192000"
                        maxArrayLength="8192000" maxBytesPerRead="8192000" maxNameTableCharCount="8192000" />
                    <reliableSession inactivityTimeout="01:00:00" />
                    <security mode="Message">
                        <transport clientCredentialType="None" protectionLevel="None" />
                        <message clientCredentialType="UserName" />
                    </security>
                </binding>
            </netTcpBinding>

I've set the security mode to Message and clientCredentialType to UserName as well. Now I needed to somehow get my client application to pass the user name and password. I had used Visual Studio and let it build my client proxy, so I thought I'd be able to set this immediately after I created the proxy, ex:

InstanceContext context = new InstanceContext(this);

 

this.m_client = new ServerClient(context);

this.m_client.ClientCredentials.UserName.UserName = "user";

this.m_client.ClientCredentials.UserName.Password = "pass";

Unfortunately, that doesn't work - I was getting "Object is read only" when I attempted to do this. From some searching I was able to find out that this can occur when the connection has already been opened, but in my case I hadn't done that yet. I spent a bunch of time trying to figure out why it wouldn't work and finally ended up putting the code inside of the proxy class VS had generated by editing the constructor and setting things up there.

public ServerClient(System.ServiceModel.InstanceContext callbackInstance) :

        base(callbackInstance)

{       

    base.ClientCredentials.UserName.UserName = "user";

    base.ClientCredentials.UserName.Password = "pass";      

}

At this point, I thought I was all set to go. I fired things up and it immediately faulted the connection. I opened up the WCF Service Configuration Editor (C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\svcconfigeditor.exe) and enabled tracing on both the server and client and re-ran things so I could capture a trace. Once that was done I opened up the WCF Svc Trace Log Viewer (which is part of the Windows SDK, mine is located at: C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\SvcTraceViewer.exe). After looking through the log, it appeared to be failing with a "The security protocol cannot secure the outgoing message" error.

Again, I did a bunch of searching, reading, and testing and finally realized that it wanted to encrypt the connection but couldn't. I mentioned earlier that I decided to use SSL for the connection, but when I was working on this I didn't realize that it didn't magically decide how to do that. It looked like I could use an X509 certificate to enable encryption, but I wasn't at all thrilled with the prospect of trying to get that cert. install on client machines. From the couple of times I've seen it needed, it never seems to run particularly smoothly (when it works, it just works, but when it fails, you have no idea why it's failing). So I essentially wanted to be able to load my cert. at runtime instead of loading it into the Windows cert. store. I could buy a real cert., but for this app. it just wasn't necessary. VS includes some tools for self-signing certificates so I looked into that instead.

If you open a Visual Studio command prompt (hiding in the Visual Studio Tools subdirectory of Visual Studio on the Start menu), it sets up the pathing so you can use the various command line tools. The first thing I needed to do was create a key for the server:

makecert -r -pe -n "CN=RCS Solutions, Inc." -b 01/01/2008 -e 12/31/2099 -sky exchange 
Where:
Server.cer -sv Server.pvk Server.cer - the certificate (public key)
Server.pvk - the private key

When it runs, it prompts for a password used to encrypt the private key. You can leave it blank, but it's suggested you fill it in (so I did).

Next I decided to merge the public and private key into a single file with a PFX extension. It requires you to re-enter the password you used to encrypt the private key (or pass in in the command line, which is what I did).

pvk2pfx.exe -pvk Server.pvk -spc Server.cer -pfx Server.pfx -pi mycertpassword

At this point I added all three files to my project. I also needed to get my proxy to use this cert. Since it's not a real certificate (meaning there isn't a chain of trust established between my cert at some known/trusted provider) I had to disable certificate validation. Yeah, pretty secure stuff ;-)

           ServiceHost host = null;

           try

           {

               // Specifically not calling Dispose() on host since it also calls Close and if we've

               // disposed after calling Close that will cause errors.

               host = new ServiceHost(typeof(ServerProcess));

 

               string dir = System.IO.Directory.GetCurrentDirectory();

 

               X509Certificate2 cert = new X509Certificate2(dir + "\\Server.pfx", @"mycertpassword");

               host.Credentials.ClientCertificate.Authentication.CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.None;                               

               host.Credentials.ServiceCertificate.Certificate = cert;

 

               host.Open();

 

I repeated the above sequence for my client, creating it's own public/private key. I added the same code to configure the cert, except I put it in the pre-generated proxy constructor.

public ServerClient(System.ServiceModel.InstanceContext callbackInstance) :

        base(callbackInstance)

{       

    base.ClientCredentials.UserName.UserName = "1";

    base.ClientCredentials.UserName.Password = "2";

 

    // TODO: Fill in with correct credentials

 

    string dir = System.IO.Directory.GetCurrentDirectory();

    X509Certificate2 cert = new X509Certificate2(dir + "\\Client.pfx", @"mycertpassword");

    base.ClientCredentials.ClientCertificate.Certificate = cert;       

    base.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.None;         

 

}

 

I thought I was good to go, so I started everything back up. It failed with the same error as before. Yet more searching revealed that I needed to include a copy of the server's certificate inside of the client's app.config (I have no idea why, it seems like it should happen automatically as part of it's public key exchange). The problem was getting it in the format it required. I tried a few of the various ways that were suggested, but didn't have much luck with them. I finally regenerated the client side proxy using the svcutil command line app. and pointing it to a MEX (Metadata EXchange) endpoint and it magically created the certificate value I needed. I cut and pasted that code into my production proxy.

       <client>
            <endpoint address="net.tcp://localhost/" binding="netTcpBinding"
                bindingConfiguration="PrimaryEndpoint" contract="Server" name="PrimaryEndpoint">
                <identity>
			<certificate encodedValue="AwAAAA(long value removed)" />                    
                </identity>
            </endpoint>

When I restarted everything it was finally able to open a connection. I still need to figure out how to get my WCF service and IIS to share the same port, but I'm making some progress at least.


 
Tuesday, 09 December 2008 07:16:30 (Eastern Standard Time, UTC-05:00)
2 certs are needed on client side, the message exchange from the client to the service will be digitally signed first by the client's X.509 Digital Certificate (private-key) and then encrypted for the WCF service with the service's certificate (public-key)
jps
Friday, 30 January 2009 15:30:09 (Eastern Standard Time, UTC-05:00)
Great article. Thank you. Followed it from the beginning to the end, and it took me awhile. At the end got a problem with getting endcodedValue. svcutils.exe gives me
<identity>
<servicePrincipalName value="xxx" />
</identity>
instead of
<identity>
<certificate encodedValue="AwAAAA(long value removed)" />
</identity>

I am wondering if you had same problem, since I am going through every problem you did. Or, may be you just have an idea what could be wrong.

Thank you,
Max.
Tuesday, 10 February 2009 21:01:50 (Eastern Standard Time, UTC-05:00)
I'm not sure, but do you have a MEX endpoint exposed? I pointed svcutils to that endpoint - maybe that's why it worked for me?
Saturday, 16 May 2009 19:38:19 (Eastern Standard Time, UTC-05:00)
Hi!
Please, I need help :)

I can not setting auto host.

"static void Main(string[] args)
{
using (ServiceHost serviceHost = new ServiceHost(typeof(ServiceNet)))
{
serviceHost.Open();
.....
"

serviceHost.Open(); ---->
InvalidOperationExcepton:
The ChannelDispatcher at 'net.tcp://localhost:9000/ServiceNet/' with contract(s) '"IServiceNet"' is unable to open its IChannelListener.

App.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.web>
<roleManager enabled="true" defaultProvider="NetSqlRoleManager">
<providers>
<add name="NetSqlRoleManager"
type="System.Web.Security.SqlRoleProvider"
connectionStringName="NetDB"
applicationName="/"/>
</providers>
</roleManager>
<membership defaultProvider="NetSqlMembershipProvider">
<providers>
<add name="NetSqlMembershipProvider"
type="System.Web.Security.SqlMembershipProvider"
connectionStringName="NetDB"
applicationName="/"/>
</providers>
</membership>
<compilation debug="true" />
</system.web>
<connectionStrings>
<add name="NetDB"
connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=C:\aspnetdb.mdf;User Instance=true"
providerName="System.Data.SqlClient"/>
</connectionStrings>
<!-- When deploying the service library project, the content of the config file must be added to the host's
app.config file. System.Configuration does not support config files for libraries. -->
<system.serviceModel>
<services>
<service name="NET.ServiceNet" behaviorConfiguration="NET.ServiceBehavior">
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:9000/ServiceNet/"/>
</baseAddresses>
</host>
<endpoint address="net.tcp://localhost:9000/ServiceNet/"
binding="netTcpBinding"
contract="NET.IServiceNet"
bindingName="NetTcpEndPoint"
bindingConfiguration="NetTcp">
</endpoint>
<endpoint address="mex"
binding="mexTcpBinding"
contract="IMetadataExchange"/>
</service>
</services>
<bindings >
<netTcpBinding>
<binding name="NetTcp">
<security mode="Message">
<transport clientCredentialType="None" protectionLevel="None"/>
<message clientCredentialType="UserName"/>
</security>
</binding>
</netTcpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="NET.ServiceBehavior">
<serviceCredentials>
<userNameAuthentication
includeWindowsGroups="false"
membershipProviderName="NetSqlMembershipProvider"
userNamePasswordValidationMode="MembershipProvider">
</userNameAuthentication>
</serviceCredentials>
<serviceAuthorization
roleProviderName="NetSqlRoleManager"
principalPermissionMode="UseAspNetRoles"
impersonateCallerForAllOperations="false">
</serviceAuthorization>
<serviceMetadata httpGetEnabled="false" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>


Please, help to find in than problem!

Monday, 18 May 2009 08:37:05 (Eastern Standard Time, UTC-05:00)
"netTcpBinding" does not support <message clientCredentialType="UserName"/>
Monday, 24 August 2009 09:30:59 (Eastern Standard Time, UTC-05:00)
Great article. The certificate based message security works great.
But I have one big problem:
If on clientside something is wrong with the installed certificates or the client certificate is missing the service is going to faulted state immediately. The error is thrown before my custom code is executed. It has to be restarted manually on the server. Is there some parameter in the configuration that prevents this behaviour?

Best regards
Alex form Berlin, Germany
Alexander Kleinwaechter
Name
E-mail
(will show your gravatar icon)
Home page

Comment (Some html is allowed: a@href@title, b, i, strike) where the @ means "attribute." For example, you can use <a href="" title=""> or <blockquote cite="Scott">.  

Enter the code shown (prevents robots):

Live Comment Preview