Protecting Cookies: Once and For All
- by Your DisplayName here!
Every once in a while you run into a situation where you need to temporarily store
data for a user in a web app. You typically have two options here – either store server-side
or put the data into a cookie (if size permits).
When you need web farm compatibility in addition – things become a little bit more
complicated because the data needs to be available on all nodes. In my case I went
for a cookie – but I had some requirements
Cookie must be protected from eavesdropping (sent only over SSL) and client script
Cookie must be encrypted and signed to be protected from tampering with
Cookie might become bigger than 4KB – some sort of overflow mechanism would be nice
I really didn’t want to implement another cookie protection mechanism – this feels
wrong and btw can go wrong as well.
WIF to the rescue. The session management feature already implements the above requirements
but is built around de/serializing IClaimsPrincipals into cookies and back. But if
you go one level deeper you will find the CookieHandler and CookieTransform classes
which contain all the needed functionality.
public class ProtectedCookie
{
private List<CookieTransform>
_transforms;
private ChunkedCookieHandler _handler
= new ChunkedCookieHandler();
// DPAPI protection
(single server)
public ProtectedCookie()
{
_transforms = new List<CookieTransform>
{
new DeflateCookieTransform(),
new ProtectedDataCookieTransform()
};
}
// RSA protection
(load balanced)
public ProtectedCookie(X509Certificate2 protectionCertificate)
{
_transforms = new List<CookieTransform>
{
new DeflateCookieTransform(),
new RsaSignatureCookieTransform(protectionCertificate),
new RsaEncryptionCookieTransform(protectionCertificate)
};
}
// custom transform
pipeline
public ProtectedCookie(List<CookieTransform>
transforms)
{
_transforms = transforms;
}
public void Write(string name, string value, DateTime expirationTime)
{
byte[]
encodedBytes = EncodeCookieValue(value);
_handler.Write(encodedBytes, name, expirationTime);
}
public void Write(string name, string value, DateTime expirationTime,
string domain, string path)
{
byte[]
encodedBytes = EncodeCookieValue(value);
_handler.Write(encodedBytes,
name,
path,
domain,
expirationTime,
true,
true,
HttpContext.Current);
}
public string Read(string name)
{
var bytes
= _handler.Read(name);
if (bytes
== null ||
bytes.Length == 0)
{
return null;
}
return DecodeCookieValue(bytes);
}
public void Delete(string name)
{
_handler.Delete(name);
}
protected virtual byte[]
EncodeCookieValue(string value)
{
var bytes
= Encoding.UTF8.GetBytes(value);
byte[]
buffer = bytes;
foreach (var transform in _transforms)
{
buffer = transform.Encode(buffer);
}
return buffer;
}
protected virtual string DecodeCookieValue(byte[]
bytes)
{
var buffer
= bytes;
for (int i
= _transforms.Count; i > 0; i—)
{
buffer = _transforms[i
- 1].Decode(buffer);
}
return Encoding.UTF8.GetString(buffer);
}
}
HTH