Mmmm cookies (Technical Debt Part 2/3)
During the last month, I’ve been working on paying off some of the technical debt that has been collecting interest over the course of KitchenPC’s development. Most of these have been stability changes, or just things that, when fixed, remove roadblocks to a smoother development process. I’d like to go over these in a three part blog series that, like the Hitchhikers Guide to the Galaxy trilogy, will consist of five parts.
Part 2/3: Mmmm cookies
The authorization and session management code in KitchenPC is among the oldest and most cobweb infested in the entire code base. The design is fairly straight-forward, and designed to be completely stateless so that any HTTP request could be handled by any server at any time. Here’s how it works; I keep track of each user using three UUIDs in the database. The first UUID is the user’s public ID. This ID can be shared with anyone, and was actually used in several URLs on the old version of the site, which allowed users to subscribe to other users and see their public profile. This is also the primary key for the Users table in the database. The second UUID is called the Security ID. This was also stored in the database, but never surfaced in any URL or HTML. It wouldn’t be possible to see the Security ID of any other user besides yourself. The third UUID is called the Session ID. This UUID changes every time you log on to the site using the Logon form or the Facebook connect mechanism.
After the user is authenticated, all three of these UUIDs are joined into a single byte array and then Base64 encoded into a cookie. This auth token is passed into any web service call as a way of representing the currently logged in user, and those three UUIDs are then de-serialized and validated against the database. In order to hack the database, you’d need to know not only the user’s ID and security ID, but also their current valid session ID which changes on every logon. It may not be industry grade security, but it’s fairly secure and works well.
The one problem with this approach is sessions must be maintained in the database. To make matters worse, my design only supported a single session per user, as there was just one Session UUID column in the users table. When users would logon with another browser, then that browser would have their currently valid session and the first browser would then show a “Session expired” error message. This was sometimes not trapped, and weird “Unknown Error” messages would popup. Most people just use a single computer at a time, so this wasn’t a huge deal. However, the design just wasn’t going to cut it for mobile and tablet apps, when I want multiple users on multiple devices to use the same KitchenPC account. A redesign was in order.
One approach would be to extend the current design. I could create a sessions table, where a single user could have zero or more sessions. Each time a user logged on, they would create a new session and have a valid cookie pointing to that session UUID. I believe systems such as GMail work this way, as they allow you to list your currently active sessions. The main drawback, in my opinion, was that it becomes messy to clean up after stale sessions. Ultimately, you’d need some sort of cleanup process that ran every so often and deleted abandoned sessions. You’d also have to keep timestamps on each session to keep it active, which would mean database updates on every web service call. This could lead to scalability problems later on.
Instead, I took an approach modeled after the built in ASP.NET FormsAuthentication class, which runs as an HttpModule to validate each request. Unlike my current implementation, the server does not keep track of valid sessions at all. Everything needed to validate the authenticity of the session is stored within the cookie itself. For example, a cookie might contain the string “Mike” as well as a UUID representing my user ID. Upon each request, the server would say, “Yup that looks like a KitchenPC user account to me” and the request would be serviced under the credentials of that user account.
One major problem with this design. What if the cookie is forged? In other words, what if you changed the cookie to “Bob” and found out Bob’s user Id.
To prevent this, cookies are encrypted with a private key only known on the server. It would now be difficult, if not impossible, to purposely change the cookie to represent another valid user. Luckily, the .NET Framework has all the tools necessary to encrypt your cookies. My new implementation uses a Triple DEA block cipher, which is a pretty good algorithm for this sort of encryption. First, you’d want to serialize your session data to a byte array, which can be done manually or using the BinaryFormatter class. We’ll call that byte array rawBytes. Next, you’d encrypt the data to a new byte array:
TripleDES des = TripleDES.Create(); des.Key = key; des.IV = iv; MemoryStream encryptionStream = new MemoryStream(); CryptoStream encrypt = new CryptoStream(encryptionStream, des.CreateEncryptor(), CryptoStreamMode.Write); encrypt.Write(rawBytes, 0, rawBytes.Length); encrypt.FlushFinalBlock(); encrypt.Close(); byte[] encBytes = encryptionStream.ToArray();
In the code above, we create a new instance of the TripleDES class. The Key and IV properties are set to a byte array which represent your private key. These values need to be the same when you decrypt the cookie, so it would be wise to store them in a configuration file somewhere.
We then create a CryptoStream, and write the rawBytes array containing your unencrypted session information. We can then call encryptionStream.ToArray() to get the encrypted bytes out. You’d then serialize this out (perhaps as a Base64 string) to a cookie.
There’s one small problem with this. Though it would be nearly impossible to forge a valid cookie, one could still tamper with the cookie bytes and cause who knows what havoc. For example, if my encrypted bytes were [1, 2, 3, 4, 5] and I changed that to [1, 2, 3, 4, 6], this would still decrypt, just not to what I had originally encryted. This is because encryption algorithms don’t self-validate. It’s not like in the movies where if you crack the key, you get a big “Access Granted!” flashing message on the screen. This is actually good, as you don’t actually know when or if you’ve successfully decrypted the message; you just know that some bytes went in, some bytes came out.
However, we want to make our sessions tamper resistant. If someone changes even a single byte in the cookie, I want to just toss that cookie out and ignore it. To do this, you’d use a validation hash to store a hash of the unencrypted bytes. SHA-256 is a great cryptographic hash function for that purpose.
HMACSHA256 hmac = new HMACSHA256(valKey); byte[] hash = hmac.ComputeHash(rawBytes);
valKey is an array of bytes, usually 64 bytes long, that would also be kept securely on your server. So long as you used the same valKey each time, the bytes you pass in to ComputeHash would hash the same each time.
So, now we’ve encrypted rawBytes to byte array called encBytes, as well as stored a hash of the original data stored in a byte array called hash. We’d now want to combine them into a single byte array as such:
byte[] ret = encBytes.Concat<byte>(hash).ToArray();
You now have a single byte array that you can serialize to a Base64 string and store in a cookie. Users would pretty much have no way to change the data without generating a new hash, which they could not do since they don’t have your hash key (valKey).
To decrypt the cookie, you’d do the same thing in reverse:
private static byte[] DecryptCookie(byte[] encBytes) { TripleDES des = TripleDES.Create(); des.Key = key; des.IV = iv; HMACSHA256 hmac = new HMACSHA256(valKey); int valSize = hmac.HashSize / 8; int msgLength = encBytes.Length - valSize; byte[] message = new byte[msgLength]; byte[] valBytes = new byte[valSize]; Buffer.BlockCopy(encBytes, 0, message, 0, msgLength); Buffer.BlockCopy(encBytes, msgLength, valBytes, 0, valSize); MemoryStream decryptionStreamBacking = new MemoryStream(); CryptoStream decrypt = new CryptoStream(decryptionStreamBacking, des.CreateDecryptor(), CryptoStreamMode.Write); decrypt.Write(message, 0, msgLength); decrypt.FlushFinalBlock(); decrypt.Close(); byte[] decMessage = decryptionStreamBacking.ToArray(); //Verify key matches byte[] hash = hmac.ComputeHash(decMessage); if (valBytes.SequenceEqual(hash)) { return decMessage; } throw new SecurityException("Auth Cookie appears to have been tampered with!"); }
First, we split apart the byte array into its two components; the decrypted bytes and the 64 byte hash.
Once again we setup a TripleDES instance, only this time we decrypt the data to the stream. You’ll notice I use hmac.HashSize / 8 instead of hard coding in 64, since technically you can use any length key you want and I wanted my code to be flexible. I then have an array called message, which contains the TripleDES encrypted session data, and valBytes, which contain the validation bytes from the hash. I then decrypt message to a byte array called decMessage, and then compute the hash on that unencryted message. If it matches valBytes, then we know the key is legit.
Why Re-invent the Wheel?
Internally, this is very similar to what FormsAuthentication does. So why did I create my own implementation? First off, because I wanted to. I actually enjoy implementing this sort of thing. But mainly because I didn’t find the built-in support very flexible. The ASP.NET mechanism only allows you to store a username as a string, and no other information on the user. I wanted to be able to store the user’s UUID, as well as their name and perhaps a few other pieces of information. I wanted to serialize this as my own datatype, allowing me to extract user data without having to hit the database for more information. It also made the transition between my old code and the new code a lot easier, without the need to re-factor a lot of code. The old cookie could be deserialized to a data type that was used in various methods, constructors, etc in the code base. I wanted the new code to work in a similar way.
After doing quite a bit of testing with the new auth code, I must say I’m quite pleased. Sessions last forever, and users don’t get random “Session Expired” popups when they logged on with another computer. The code is also a lot faster. I don’t need to check the authenticity of a cookie in the database each page load or web method call. I can decrypt the data and validate the hash to make sure the information within the cookie is valid. I can now be logged on to KitchenPC on all my computers and not worry about a thing!
The one drawback of this approach is these cookies are fairly long. Mine is about 700 bytes or so. This means another 700 bytes is passed in to each HTTP request, which can hurt performance. This can be optimized by minimizing the data you’re storing in the encrypted package, and also using shorter (albeit less secure) keys and hashes. I’m also thinking I could do my own serialization rather than using the BinaryFormatter class, which embeds things such as type data.
In the end, I implemented all this code as an IHttpModule which will run on each request, setting the value of HttpContext.Current.User if the cookie is valid, which is a fairly good approach. You would then just add this class in the <httpModules> section of your web.config. I was planning on sharing the entire code, but I’m afraid this post is just getting too long. I’d be happy to share it with anyone who asks though! Just let me know. That’s it for now!
Hello! This is kind of off topic but I need some guidance
from an established blog. Is it very difficult to set up your
own blog? I’m not very techincal but I can figure things out pretty quick. I’m thinking about creating my
own but I’m not sure where to begin. Do you have any ideas or suggestions? Many thanks
Hey I just use WordPress.com – It takes about 5 minutes to setup and you don’t have to host anything. Good luck!
Hi there! Do you use Twitter? I’d like to follow you if that would be okay.
I’m absolutely enjoying your blog and look forward to new
updates.
I do! @KitchenPC