Tuesday, September 2, 2008

Silverlight, WCF and Authentication

Here is what I wanted to do. I wanted my WCF services to be completely disconnected from my Silverlight web application. I wanted third parties to be able to generate proxies without authenticating and use the web services without authenticating against a web site first. I didn't want to rely on impersonation because the user accounts will be created from within the services and new accounts would then need to be registered with SQL Server security. Ick. I just wanted basic WCF services with message level authentication and a web site hosting a Silverlight application built on top of the services. Should be easy, correct?

Well, as anyone who has tried knows, easy isn't the case. However, I was able to get all this working with the help of quite a few articles. And you can bet accomplishing my goal required hacks, custom code and overcoming several mind bending what-the-f***s along the way. Here I document the journey at a high level with links to details if you're interested. Forgive me if the steps aren't in quite the right order or some details are missing; it's taken me a few weeks to tack together all the bits and pieces.

Membership Authentication
I'm not talking about hiding your services behind a web site and piggy-backing authentication on top of the WCF - ASP.NET compatibility features. I'm talking about true, per-operation message level authentication using a membership provider. This is particularly useful because we are able to use the same provider for the services and web site and yet keep them completely separate. Here's the why-hows. What this ends up forcing you to do is set up a BasicHttpBinding with TransportWithMessageCredential security. That will require you to run all of your services over HTTPS. I use IIS 7 which makes it very easy to create your own SSL certificates for development. There are problems though. Problem One - even though we are using TransportWithMessageCredential security on a basicHttpBinding, Silverlight does not yet support it. Problem Two - cross domain policy files do not yet support the HTTPS protocol. Problem Three - because the services are not relying on impersonation, it is a bit difficult to get the identity of the authenticated user. Problem two is easy to solve so I am going to talk about it first.

HTTPS and Cross Domain Policies.
[EDIT: This is no longer an issue in the release of Silverlight 2. Supposedly, clientaccesspolicy.xml now allows cross domain calls between HTTP and HTTPS. Regardless, I still set up my development environment this way for the small debugging advantage.]
Cross domain policy files are what let you call WCF services from Silverlight applications that are downloaded from different domains. The short version of how to deal with this is that you don't. Cross domain policy files simply don't support HTTPS right now. That's OK though. What I have done is to create a virtual directory in IIS under the web site that hosts my Silverlight application that maps to my services. I also made sure that the pages that host my Silverlight application are also protected by SSL. That way, my Silverlight is downloaded from https://www.example.com/ and my services are at https://www.example.com/services. This satisfies the same domain requirement. At first, this might seem like a performance hit, but since the web pages hosting my Silverlight application are infrequently used (once per login in our case), it's not really that bad at all. While this essentially means that the web site and services do need to exist together for now, once support for HTTPS is introduced in the cross domain policy files, all you need to do is create the file, drop it into the services directory and you can move the virtual directory where ever you want. There are a few other details like making sure you allow anonymous access to the services directory. Authentication will be taken care of by the membership provider when the services are run, but you want people to be able to hit your MEX and WSDL files. You also need to make sure to turn off forms authentication for the services directory or your requests will be run through the ASP.NET authentication process. Since no cookies will be sent with your service calls, requests will fail if you don't disable forms authentication for your services. One upshot of hosting your services and web application in IIS is that you can associate both with the same application pool. If you do so, they will use the same worker process. Since the debugger automatically attaches to the web site worker process, you can debug your services without attaching to the services host by hand. Now on to the harder problem, Problem One.



TransportWithMessageCredential Silverlight Compatibility
It doesn't exist in Silverlight 2 Beta 2. You can make your own though. This post describes how to add your own headers to a WCF call in a Silverlight application. There are a few changes that I needed to make. Most code that uses HTTP needs to be changed to HTTPS. For instance, when adding binding elements to the custom binding you need to change the HttpTransportBindingElement to an HttpsTransportBindingElement. You also need to make sure that you set the security mode for the binding to Transport. The proxy generator will dutifully create your client configuration with the TransportWithMessageCredential. It doesn't hurt to leave them in the configuration, just make sure the default configuration is never used; the runtime will error when it tries to parse the security mode because TransportWithMessageCredential does not exist in the security enumeration in the Silverlight runtime. Getting around it is accomplished by providing your own binding and endpoint address as the per the example in the referenced post. The hardest part was writing the security header so that it was compatible with the WCF security headers. To figure this out, I created a small windows application, hooked it up to the services and inspected the messages server side. I'll paste my security header class below since that code is not included in any of the posts I saw. The frustrating thing about all of this work is that it will all be thrown out when security credentials and TransportWithMessageCredential security are support in Silverlight WCF proxies. Ah well.

Who are you?
Problem number three is getting the identity of the user. I could not find the identity in any of the normal places: OperationContext, HttpContext, Thread.CurrentPrincipal. Nothing. My understanding is that this is because I am not using impersonation, nor do I want to, but if someone knows how to configure things a different way, I'm all ears. My solution was to add a message inspector. Implementing IDispatchMessageInspector allows you to inspect the contents of a message after the credentials in the message have been authenticated. With a little XPath, the authenticated username falls right into your hands, er code... whatever. The point is that you now know the user's identity and can provide it to your routines for authorization purposes. One little gotcha about writing message inspectors is that message can only be read or copied once. Here's an article that explains how to get around it and a conversation on why it works the way it does. Yes, it's an old 'Indigo' article, but it is still relevant.
[EDIT] So I was rereading this article recently because apparently a bunch of people have found it. One change that I made sometime since Silverlight 2 released is that I found out where WCF stuffs identity information. OperationContext.Current.ServiceSecurityContext.PrimaryIdentity. That's it. The message inspectors are no longer necessary if they ever were. I don't know if that's a Silverlight 2 thing or not, but you should all be using the final release now anyway.[/EDIT]


Summary
That's more or less it. Again, I'm sorry for the lack of detail, but others have already done most of the documenting, I just had to put it all together. If you have questions, you can email me. My contact information should be on the side of the blog there somewhere. Silverlight RTM or RTW or CTP or whatever comes next is due out fairly soon from my understanding. At that time I don't think all that code required to solve problem two will be needed any more. But if you need to get this working now like we did, it is possible. And it's not that much work once you understand how all the pieces parts fit together.



The code I promised
Somewhere up above I said that I would provide a little bit of code to demonstrate how to write WCF compatible security headers from a class inheriting from MessageHeader. The credentials object is one of my own devising that keeps the password encrypted in memory and I store it at the top level in my Silverlight Application object, app.xaml. But is should be obvious what information goes where. Oh, except for the username token. In other WCF applications, this is the string "uuid-" followed by a random Guid selected for that instance of the application followed by "-1". e.g. "uuid-12345678-1234-1234-1234-123456789012-1". Here's the code:



public class SecurityHeader : MessageHeader
{

protected override void OnWriteHeaderContents(System.Xml.XmlDictionaryWriter writer, MessageVersion messageVersion)
{
var timestamp = DateTime.Now;
var application = Application.Current as App;
var credentials = application.Credentials;

var secUtilNamespace =
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
var secExtNamespace =
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";

writer.WriteStartElement(
"Timestamp", secUtilNamespace);
writer.WriteAttributeString(
"Id", secUtilNamespace, "_0");
writer.WriteElementString(
"Created", secUtilNamespace, timestamp.ToString("O"));
writer.WriteElementString(
"Expires", secUtilNamespace, timestamp.AddMinutes(5).ToString("O"));
writer.WriteEndElement();
writer.WriteStartElement(
"UsernameToken", secExtNamespace);
writer.WriteAttributeString(
"Id", secUtilNamespace, credentials.UsernameToken);
writer.WriteElementString(
"Username", secExtNamespace, credentials.Username);
writer.WriteStartElement(
"Password", secExtNamespace);
writer.WriteAttributeString(
"Type", secExtNamespace, "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText");
writer.WriteString(credentials.Password);
writer.WriteEndElement();
writer.WriteEndElement();
}

public override string Name
{
get { return "Security"; }
}

public override string Namespace
{
get { return "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"; }
}

public override bool MustUnderstand
{
get{ return true; }
}
}