SCT Options - Using a cookie approach with SCT's in a farm

So far I've discussed using two techniques for using SCT's in a web farm, pinning and using a database as a caching mechanism.  The third and final technique I'm going to cover is using a cookie-like approach of embedding the information in the extended area of the SCT. 

Advantages of this technique:

  1. Resilent to server failure
  2. Does not require any state to be maintained on the farm - operationally simple
  3. Cost - lower cost when compared to database cache as it doesn't require a redundant database to be in place to maintian state.

Disadvantages

  1. Increased network bandwidth costs
  2. Increased processing - due to processing extended data in payload (this needs to be parsed, even if it isn't decrypted/deserialized).  I did not measure the cost of doing the database retrieval vs. the cost of processing the cookie for cases where the SCT needs to be re-hydrated if its not in cache - my gut feel is that its probably close to a wash..
  3. Requires key management on the server side (key used to encrypt/decrypt the serialized infomration in the cookie)

One of the advantages of using an SCT is that it is singificantly lighter weight than other token types because it relies upon client and server maintaining state.  The schema for the SCT can be viewed here: https://schemas.xmlsoap.org/ws/2004/04/sc/  where one can see that the contents of the SCT is simply an identifier, followed by any content.  Using a cookie technique we put the necessary information to reconstitute the token here.  This approach definitely compromises one of the advantages of an SCT over other token times since we bloat the SCT somewhat.  However we can still achieve significantly better performance, and reduce most of the processing overhead (other than managing the network traffic and parsing the additional area in the DOM) to a one time hit that is still sigificantly less costly than processing the equivalent tokens.  A big part of the cost of processing an XML token is xml signatures and encryption.  In order to perform these operations, a process of cononicalization needs to occur (https://www.w3.org/TR/xml-exc-c14n/) that is very expensive to perform.  We'll avoid this cost and be able to more compactly represent the token by using binary encryption and signing.  While I won't do it here, compression can also be used to reduce this payload.  Finally, while I've implemented a generic technique of getting the XML representation of the base token since that's the only generic approach currently available to serialize/deserialize a token, it would be much better to perform custom serialization  to a more compact representation if you always know your base token type.

Serialization I've described in previous blogs and that remains the same process as when serializing the token for persistence to a database.  I did re-factor my sample to move the serialization logic into its own class out of the token cache class so it was re-usable by the cookie logic. 

I had to make a few additional changes to existing DistributedSCTTokenManager .  First I added a mode to my  with three enumerated values - database, cookie, and none.  I then modified the PersistSecurityToken to add the cookie to the SCT if in cookie mode - this amounts to adding a single additional element to the extensibility section of the SCT, and putting the binary serialized, encrypted, and signed SCT information base64 encoded within that element. 

  public void PersistSecurityToken(string identifier, SecurityContextToken token)
{
if(_cacheMode == CacheMode.database)
{
((DistributedTokenCache)_tokenCache).PersistSecurityToken(identifier, token);
}
else if(_cacheMode == CacheMode.cookie)
{
AddCookieToSCT(token);
}
}

  private void AddCookieToSCT(SecurityContextToken token)
{
byte []data = _serializer.SerializeToBytes(token);
byte []encryptedData = _encryptor.SignAndEncrypt(data);
string serializedAndEncryptedData = Convert.ToBase64String(encryptedData);

     XmlDocument doc = new XmlDocument();

     XmlElement extendedSCT = doc.CreateElement(TokenNames.Prefix,
TokenNames.SCT.ExtensionElement, TokenNames.Namespace);
extendedSCT.InnerText = serializedAndEncryptedData;
token.AnyElements.Add(extendedSCT);
}

Finally I overrode the LoadTokenFromXml method.  In this method, I first just do the base method LoadTokenFromXml, try to retrieve the SCT from the local cache, and if I fail to find it, I then decrypt, validate, and load the SCT from the cookie information, and add the SCT to the local cache. 

  public override SecurityToken LoadTokenFromXml(System.Xml.XmlElement element)
{
SecurityContextToken sct = base.LoadTokenFromXml(element) as SecurityContextToken;

     SecurityContextToken tokenToReturn = TokenCache[sct.Identifier] as SecurityContextToken;
//do this in two stages for performance if token contains state- only load if not in cache
if(tokenToReturn == null)
{
if(_cacheMode == CacheMode.cookie)
{
tokenToReturn = LoadSctFromCookie(element, sct.Identifier);
TokenCache.Add(sct.Identifier, tokenToReturn);
}
}
return tokenToReturn;
}.

  private SecurityContextToken LoadSctFromCookie(System.Xml.XmlElement element, string identifier)
{
string serializedAndEncryptedData = null;
SecurityContextToken sct = null;

     foreach(XmlElement childNode in element.ChildNodes)
{
if (childNode.LocalName == TokenNames.SCT.ExtensionElement &&
childNode.NamespaceURI == TokenNames.Namespace)
{
serializedAndEncryptedData = childNode.InnerText;
break;
}
}

     if(serializedAndEncryptedData != null)
{
byte []encryptedData = Convert.FromBase64String(serializedAndEncryptedData);

        byte []serializedData = _encryptor.DecryptAndValidate(encryptedData);
sct = _serializer.DeserializeSCT(identifier, serializedData);
}
else
{
//Should not happenl.
throw new System.Web.Services.Protocols.SoapException("Server does not support distributed cache for SCTs",
System.Web.Services.Protocols.SoapException.ServerFaultCode);
}
return sct;
}

This one I had tried before, so it came up and running pretty quickly.  Well that's it for SCT's.  I think I'll try something a little more abstract next blog...maybe service factoring, which is something I've struggled with over the last year.