Herein lies some detail regarding how to go about implementing your own custom binary tokens with our Web Services Enhancements 2.0 for Micorosoft .NET. If the topic interests you, read on! If all you want is the code, you can get it here. Note that it is licensed under the Creative Commons Attribution-ShareAlike License.
I had a customer earlier this week interested in understanding how to create a custom binary token for use in WS-Security. While the WSE 2.0 documentation gives you somewhat of a step-by-step, it doesn't provide the paradigm behind why you're doing what you're doing. Furthermore, you cannot take the code from the documentation and compile it. There's also a sample included with WSE 2.0 - CustomBinarySecurityToken - upon which the documentation is based, but again it requires you to not only read to the code but spend a good deal of time thinking about what's going on behind the scenes.
I spent a few hours thinking about what's going on behind the scenes as I wrote my own implementation of a custom binary security token. From the ground up, I’ve essentially recreated the CustomBinarySecurityToken sample (though that wasn’t my intent, that’s just how it turned out!) The up side is that my version has the minimum necessary to get this to work and cuts through a lot of the “stuff” you get in the WSE samples. Rather than posting the detailed code here and providing a line-by-line walkthrough, I've chosen instead to present the paradigm and allow you to map that understanding to the code
What does the content of my custom binary token look like? Well, I choose an XML payload as my “binary” data that looks like this:
Of course, we can make that payload whatever we want, provided we can just end up with a stream of UTF8 encoded bytes.
So, what is necessary to make this work? There are three projects in this solution: CustomToken, CustomTokenClient and CustomTokenService.
Before I delve into HambonesCustomToken itself, note that both the client and the service require a <security> section to register the <binarySecurityTokenManager> (more on that in a moment.)
CustomToken consists of three classes:
Let’s start with CustomTokenManager, as it’s the easier of the two remaining classes. CustomTokenManager is derived from SecurityTokenManager. Its job is to provide a way for WSE to create an instance of your custom token. How does it do this? The TokenType property returns CustomTokenNames.TokenType (which, as you will soon see, is the same value that gets stuffed into the custom token itself.) In this fashion, WSE can receive a token examine its TokenType and then call all of the registered SecurityTokenManagers looking for one that services the given TokenType.
Once WSE has found a SecurityTokenManager for the given TokenType, it calls the LoadTokenFromXml method where my implementation of CustomTokenManager creates a new instance of HambonesCustomToken, passing in the XML element to the constructor.
That’s all there is to CustomTokenManager. Think of it as a factory for tokens of a given type.
Now for the hard part – the implementation of HambonesCustomToken. HambonesCustomToken is derived from BinarySecurityToken. Granted there are a whopping 107 lines of code in my implementation, but most of it is templated out (add to that that I even throw NotImplementedExceptions for some of the stuff and still have it working!)
Let’s start with the two constructors, HambonesCustomToken(string userid) and HambonesCustomToken(XmlElement element). The former is used to create a token from scratch – something that you’re going to send to “the other side.” The argument is a simple string containing a user id that will later be wrapped in a “fancy” XML element and sent as the “binary” payload of the token. The meat of the constructor generates this fancy XML element and then UTF8 encodes the XML string and assigns it to BinarySecurityToken.RawData.
What is BinarySecurityToken.RawData? It’s the base64 encoded “goo” that gets sent as the value of the token to “the other side.” If there’s nothing in RawData, then you’re “token” is essentially empty.
Now for the second constructor, HambonesCustomToken(XmlElement element.) This baby is called on the receiving side in response to CustomTokenManager.LoadTokenFromXml. This constructor simply pushes all work to the LoadXml function (see below.)
HambonesCustomToken.UserId is a public property that simply exposes the user id. ‘nuff said.
HambonesCustomToken.XmlString is a private helper function whose job is to “create” the “goo” that gets sent as the value of the token. It is here that the userid handed to the constructor creates an XML fragment that looks like:
This fragment is then returned as a string, which as mentioned above is then UTF8 encoded into the RawData member.
The real workhorse of this class is the LoadXml helper function. Remember, this function is called on the receiving side in response to CustomSecurityTokenManager creating an instance of our token from an XML fragment. Right off the bat, this method defers to the base class, BinarySecurityToken.LoadXml, to get the ball rolling. Then we take a look at RawData, which gets populated by BinarySecurityToken.LoadXml, to determine if there’s anything for us to do. If it’s not null, then there’s something there and we need to parse it out.
How do we parse it out? By performing the inverse of XmlString. We UTF8 decode the bytes into a string, stuff the string into an XML element and then enumerate all of the child nodes. The node I really care about is the <UserId> node that contains, well, the UserId. Once we find that node, we take it’s InnerText and assign it to the private member field, _userid.
That’s it! Everything else in this class is just boiler plate to fulfill the requirements set forth by inheriting BinarySecurityToken as an abstract base class:
SupportsDigitalSignature and SupportsDigitalEncryption both return false – this token can’t be used for signature and encryption (mostly because I didn’t go through the motions to implement it – remember, the goal here was to do the bare minimum necessary to make this work and demonstrate the results) and I leave the implementation as an exercise to you, the reader. J
Since signature and encryption aren’t implemented, the Key property returns null, and shouldn’t be used in the first place.
IsCurrent always returns true. I assume that if I implemented some lifetime management into my token, then I would check it here and return true or false depending if the token is, well, current or not.
Equals and GetHashCode both throw NotImplementedExceptions J Ultimately, Equals should compare the two RawData byte arrays. As for GetHashCode? Well, I’ve never understood its use or implementation.
Hopefully this walk through and the sample code is enough to get you through the hump and to help you better understand the more complete example that’s included with WSE 2.0.