Custom Authentication and Encryption with WCF

From: http://www.rcs-solutions.com/blog/CategoryView,category,WCF.aspx

 

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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值