ASP.NET Web Site + Windows Forms App + WCF Service: Client Credentials ASP.NET Web Site + Windows Forms App + WCF Service: Client Credentials asp.net asp.net

ASP.NET Web Site + Windows Forms App + WCF Service: Client Credentials


Well, I reckon that I'll take a stab at my own question now that I've spent a few hours playing with various approaches.

My first approach was to set up certificate-based authentication between the WCF service and the public-facing Web site (the Web site is a consumer/client of the service). A few test certs generated with makecert, plop them into the Personal, Trusted People, and Trusted Root Certification Authorities (because I couldn't be bothered to generate real ones against our domain's certificate services), some config file modifications, and great, we're all set.

To prevent the Web site from having to maintain username and password information for users, the idea is that once a user is logged into the Web site via Forms Authentication, the Web site can pass just the username (accessible via HttpContext.Current.User.Identity.Name) as an optional UserNameSecurityToken in addition to the X509CertificateSecurityToken that is actually used to secure the message. If the optional username security token is found, then the WCF service would say "Hey, this trusted subsystem says that this user is properly authenticated, so let me set up a MyCustomPrincipal for that user and install it on the current thread so that actual service code can inspect this." If it wasn't, then an anonymous version of MyCustomPrincipal would be installed.

So off I went for five hours trying to implement this, and with the help of various blogs, I was able to do it. (I spent most of my time debugging a problem where I had every single configuration and supporting class correct, and then installed my custom authorizations after I started the host, not before, so none of my effort was actually taking effect. Some days I hate computers.) I had a TrustedSubsystemAuthorizationPolicy that did the X509 certificate validation, installing an anonymous MyCustomPrincipal, a TrustedSubsystemImpersonationAuthorizationPolicy that accepted a username token with a blank password and installed a customer-role MyCustomPrincipal if it saw that the anonymous trusted subsystem principal was already installed, and a UserNameAuthorizationPolicy which did regular username and password based validation for the other endpoints where X509 certificates aren't being used. It worked, and it was wonderful.

But.

The stab-myself-in-the-eyeballs moment came when I was fiddling with the generated client proxy code that the Web site would use to talk to this service. Specifying the UserName on the ClientCredentials property of the generated ClientBase<T> object was easy enough. But the main problem is that credentials are specific to a ChannelFactory, not a particular method invocation.

You see, new()ing up a WCF client proxy is more expensive than you might think. I wrote a quick-and-dirty app to test performance myself: both new()ing up a new proxy and calling a method ten times took about 6 seconds whereas new()ing up a proxy once and calling only the method 10 times cost about 3/5ths of one second. That is just a depressing performance difference.

So I can just implement a pool or a cache for the client proxy, right? Well, no, it's not easily worked around: the client credentials information is at the channel factory level because it might be used to secure the transport, not just the message, and some bindings keep an actual transport open between service calls. Since client credentials are unique to the proxy object, this means that I would have to have a unique cached instance for each user currently on the Web site. That's potentially a lot of proxy objects sitting in memory, and pretty @#$@# close to the problem that I was trying to avoid in the first place! And since I have to touch the Endpoint property anyway to set up the binding for the optional supporting username token, I can't take advantage of the automatic channel factory caching that Microsoft added "for free" in .NET 3.5.

Back to the drawing board: my second approach, and the one that I think that I'll end up using for now, is to stick with the X509 certificate security between the client Web site and the WCF service. I'll just send a custom "UserName" SOAP header in my messages, and the WCF service can inspect that SOAP header, determine if it came from a trusted subsystem such as the Web site, and if so, install a MyCustomPrincipal in a similar manner as before.

Codeproject and random people on Google are wonderful things to have because they helped me get this up and running quickly, even after running into a weird WCF bug when it comes to custom endpoint behaviors in configuration. By implementing message inspectors on the client side and the service side--one to add the UserName header, and one to read it and install the correct principal--this code is in one place where I can simply forget about it. Since I don't have to touch the Endpoint property, I get the built-in channel factory caching for free. And since the ClientCredentials are the same for any user accessing the Web site (indeed, they are always the X509 certificate -- only the value of the UserName header within the message itself changes), adding client proxy caching or a proxy pool is much more trivial.

So that's what I ended up doing. Actual service code in the WCF service can do things like

    // Scenario 1: X509Cert + custom UserName header yields for a Web site customer ...    Console.WriteLine("{0}", Thread.CurrentPrincipal.Identity.Name); // prints out, say, "joe@example.com"    Console.WriteLine("{0}", Thread.CurrentPrincipal.IsInRole(MyRoles.Customer)); // prints out "True"    // Scenario 2: My custom UserNameSecurityToken authentication yields for an employee ...    Console.WriteLine("{0}", Thread.CurrentPrincipal.Identity.Name); // prints out, say, CN=Nick,DC=example, DC=com    Console.WriteLine("{0}", Thread.CurrentPrincipal.IsInRole(MyRoles.Employee)); // prints out "True"    // Scenario 3: Web site doesn't pass in a UserName header ...    Console.WriteLine("{0}", Thread.CurrentPrincipal.Identity.Name); // prints out nothing    Console.WriteLine("{0}", Thread.CurrentPrincipal.IsInRole(MyRoles.Guest)); // prints out "True"    Console.WriteLine("{0}", Thread.CurrentPrincipal.IsInRole(MyRoles.Customer)); // prints out "False"

It doesn't matter how these people got authenticated, or that some are living in SQL server or that some are living in Active Directory: PrincipalPermission.Demand and logging for auditing purposes is now a snap.

I hope this helps some poor soul in the future.


For anonymouse public access use basichttpbinding and also have the following in the web.config file