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

Architectureasp.netauthenticationwcf

Let's say that I'm considering designing a WCF service whose primary purpose is to provide broad services that can be used by three disparate applications: a public-facing Web site, an internal Windows Forms application, and a wireless mobile device. The purpose of the service is twofold: (1) to consolidate code related to business processes in a central location and (2) to lock down access to the legacy database, finally and once and for all hiding it behind one suite of services.

Currently, each of the three applications has its own persistence and domain layers with slightly different views of the same database. Instead of all three applications talking to the database, they would talk to the WCF service, enabling new features from some clients (the mobile picker can't currently trigger processes to send e-mail, obviously) and centralizing notification systems (instead of a scheduled task polling the database every five minutes for new orders, just ping the overhead paging system when the AcceptNewOrder() service method is invoked by one of these clients). All in all, this sounds pretty sane so far.

In terms of overall design, however, I'm stumped when it comes to security. The Windows Forms application currently just uses Windows principals; employees are stored in Active Directory, and upon application startup, they can login as the current Windows user (in which case no password is required) or they can supply their domain name and password. The mobile client doesn't have any concept of a user; its connection to the database is a hardcoded string. And the Web site has thousands of users stored in the legacy database. So how do I implement the identity model and configure the WCF endpoints to deal with this?

In terms of the Windows Forms application, this is no great issue: the WCF proxy can be initiated once and can hang around in memory, so I only need the client credentials once (and can prompt for them again if the proxy ever faults). The mobile client can just be special cased and use an X509 certificate for authentication against the WCF service. But what do I do about the Web site?

In the Web site's case, anonymous access to some services is allowed. And for the services that require authentication in the hypothetical "Customer" role, I obviously don't want to have to authenticate them on each and every request for two reasons:

  • I need their username and password each time. Storing this pair of information pretty much anywhere–the session, an encrypted cookie, the moon–seems like a bad idea.
  • I would have to hit the users table in the database for each request. Ouch.

The only solution that I can come up with is to treat the Web site as a trusted subsystem. The WCF service expects a particular X509 certificate from the Web site. The Web site, using Forms Authentication internally (which invokes an AuthenticateCustomer() method on the service that returns a boolean result), can add an additional claim to the list of credentials, something like "joe@example.com is logged in as a customer." Then somehow a custom IIdentity object and IPrincipal could be constructed on the service with that claim, the WCF service being confident that the Web site has properly authenticated the customer (it will know that the claim hasn't been tampered with, at least, because it'll know the Web site's certificate ahead of time).

With all of that in place, the WCF service code would be able to say things like [PrincipalPermission.Demand(Role=MyRoles.Customer)] or [PrincipalPermission.Demand(Role=MyRoles.Manager)], and the Thread.CurrentPrincipal would have something that represented a user (an e-mail address for a customer or a distinguished name for an employee, both of them useful for logging and auditing).

In other words, two different endpoints would exist for each service: one that accepted well-known client X509 certificates (for the mobile devices and the Web site), and one that accept Windows users (for the employees).

Sorry this is so long. So the question is: Does any of this make sense? Does the proposed solution make sense? And am I making this too complicated?

Best Answer

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.