Managing Security Context Tokens in a Web Farm

 

Chris Keyser
Microsoft Corporation

November 2004

Applies to:

   Microsoft .NET Framework 1.1
   Web Services Enhancements 2.0 for Microsoft .NET (WSE 2.0)
   WS-Trust specification
   Ws-SecureConversation specification
   Ws-SecurityPolicy specification

Summary: The WS-SecureConversation specification defines a Web service security protocol to enable more efficient processing of message exchanges between a Web service and a client. In order to achieve this efficiency, an interchange occurs between the client and service using Web Services Enhancements 2.0 to establish a secure context to maintain state information on the client and service. WSE 2.0 supports a secure conversation between a single service instance and a client. This article discusses techniques for extending the WSE 2.0 implementation to support managing the secure context token state across a farm hosting multiple instances of a service. (20 printed pages)

Download the associated Code-TokenCache.msi code sample.

Contents

Why Use Secure Conversation, and What Is the Problem?
Approaches for Managing Security Context Token State in a Service Farm
   Routing Client Requests to the Same Service Instance
Sharing SCT State Using a Database Cache
   Sharing the State Information Using SCT Extensibility
Implementing Database Persisted Token Caching    Extending the SecurityContextTokenService    Extending the SecurityContextTokenManager Implementing the DatabaseTokenCache    Cleaning Up the Database Cache    Serializing and Deserializing the SCT Implementing Cookie-Based Persistence    Extending the SecurityContextTokenManager Conclusion
   About the Author

Why Use Secure Conversation, and What Is the Problem?

A security context token (SCT) is a lightweight token that can be established for multiple message exchanges between two endpoints using the protocol defined in the WS-SecureConversation specification. In most cases it only makes sense to establish a secure context if it is anticipated that multiple messages will be exchanged between a client and a service endpoint. You should think of an SCT as security optimization mechanism. The WS-SecureConversation specification builds on both Web Services Security SOAP Message Security (supplants WS-Security) and WS-Trust. To get the most value from this article, you should look through the WS-SecureConversation specification and have a baseline familiarity with WSE security. To explore the WSE security model, see WS-Security Drilldown in Web Services Enhancements 2.0.

If you've seen what the payloads of various security tokens actually look like, you know that they can be pretty large, and the associated transport and parsing logic can add measurable overhead. For instance, a Kerberos based security token can be well over 1K in size. An SCT avoids this overhead by relying on the service retaining the key information, as well as the BaseToken. The BaseToken is the security token that provided the credentials used to establish the secure context. Typically the base token will contain additional information that the service depends upon to properly handle security in conjunction with the SCT. For example, identity information, group information, and authorization information may all be provided by the base token referenced by the SCT. It is beyond the scope of this article to consider the various methods for establishing the security token identifying the client as an authenticated user. For the purposes of the sample code, we will use a UsernameToken to establish this context, but this could easily be a Kerberos token, X509 token, custom XML token, or a custom binary token. The bottom line is that when the client requests an SCT, the service relies upon another token containing established security credentials in order to issue the SCT.

If you peruse the WS-SecureConversation specification or at least look at the schema for the specification, you'll notice that the defined payload of the SCT is very light. Only one mandatory element is defined for an SCT—Identifier. While a third-party approach to issuing a SecuirtyContextToken is not prohibited, the anticipated model most often used is based on a conversation established between the client and target service, rather than a third-party Security Token Service (STS) model. Otherwise, a secure back channel between the STS and the target service is needed to share the base token and secret information for the established SCT. WSE has a really cool feature, auto issuing (configured with <autoIssueSecurityContextToken> in the web.config file of the issuing service) where the Framework picks up an incoming SCT request from a client directly to the service endpoint and handles it "auto-magically" for you.

Of course nothing in life, or SCTs, is free. As mentioned earlier, state is retained on the service side that allows the parties to more efficiently manage security. The service uses the SCT's mandatory identifier element to pull the token containing the key and base token information out of a cache to validate a message secured with the SCT. Out of the box, WSE 2.0 stores the SCTs in an in-memory cache. This works fine if you only have a single instance of the service, but as soon as the service requires failover and/or scale-out mandates a service farm, this model becomes problematic. Fortunately, WSE was designed with rich extensibility in mind, so extending support for this functionality ends up being not very difficult once all the "moving parts" are understood.

Note State is maintained on the client for most token types. This is required, as the client needs to use the secret information issued by the proof token. The proof token is returned in the RequestSecurityTokenResponse (RSTR) that is issued by a security token service every time the client subsequently uses the token. Since state generally needs to be tracked in all cases whenever a proof token is issued, the handling of an SCT ends up being no different client side than other token types.

Approaches for Managing Security Context Token State in a Service Farm

In a Web service farm there are two approaches to deal with the state problem; the infrastructure can ensure a client is always routed back to the same service instance which has the state in-memory, or logic can be put in place to share the SCT information between instances. Not too surprisingly, these options will look like familiar techniques to Web developers that have managed state in a farm. In this article, I'll cover three techniques that can be used:

  1. Route the client requests to the same server instance for the duration of the secure conversation.
  2. Support a database cache on the server side for sharing the SCT information across the farm.
  3. Put the state information for the SCT into the extensibility area of the SCT.

Routing Client Requests to the Same Service Instance

Most Web developers are familiar with this approach from their ASP days to maintain session state within a Web farm; it is logically similar to using ASP sessions and server affinity. If you kept server-side session state, this was really the only option available before .NET, other than writing your own distributed session management solution. This approach can also be used in the case of SCTs. Since most Web services today are accessed via http, the logic is typically already built into existing Web farm infrastructures such as Network Load Balancing (NLB) or load balancing hardware to support session affinity. These load balancers set up affinity for a particular service instance based on such values as client IP address/address class, or a cookie value, such as session ID. A common term used to describe this approach is "pinning" the client session to a server instance. This may be a perfectly adequate solution for your service, and this is certainly the easiest option to implement. There is no additional development work required to implement this approach; typically, you just flip a switch in your load balancer, and you're off to the races. The in-memory cache implemented by the default WSE logic will work fine for actually caching the token.

The advantages of this approach are:

  • Easiest to implement—no additional coding required
  • Supports all base token types efficiently (more on this below)
  • Minimizes network transport overhead

The disadvantages of this solution are:

  • Not resilient to server failures. Since the client is pinned to a particular instance, if the server goes down, the SCT is no longer valid. If the message subsequently gets re-routed to another available server hosting the service, it will be rejected since that server will have no knowledge of either the established SCT, or the security token originally used to negotiate the secure context.
  • Can introduce imbalances in loading—this is a typical problem with pinning a session to a particular instance. Imbalances can be especially significant where some Internet service providers multiplex many users over a few IP addresses, causing a distortion in number of request coming from a small IP address range.
  • Reliant upon some type of session affinity management. This is typically available with http load balancers, but may not be available as a feature for other protocols.
  • This is a point-to-point approach. Messages transiting multiple hops won't have an http connection directly between the client and service. While this is not an issue in most implementations today, this will become more problematic as infrastructures increase sophistication.

The first disadvantage mentioned above, the lack of resiliency to server failures, can be compensated by client-side logic: a smart client-side proxy that caches the original security token, traps the failure due to a missing SCT server side, and subsequently re-establishes the secure conversation with the service. Don't be tempted to turn off session affinity if client-side compensation is put in place with the assumption that SCTs will just be established if another service instance is visited. This will result in the generation many SCT negotiations, as every time another request is routed to a different server instance a new token will be negotiated, and any potential advantages gained by use of SCTs will quickly evaporate.

One of the advantages mentioned above is that this approach supports all types of base tokens efficiently. A limitation with the other two approaches I will cover is that they require the base token information to be serialized. Since the WSE SecurityToken class is not itself serializable, derived classes cannot be serializable (unless they explicitly implement the ISerializable interface). For most tokens types, there is a way around this limitation. The XML representation of a token can be retrieved with GetXml, and the token can be reconstituted with LoadXml, but this adds significant additional payload and processing costs over binary serialization. A technique to mitigate this cost that will be covered later is to always cache the token locally once reconstituted so at least the processing hit is a one-time occurrence per token per server.

For a high-reliability system, this approach would be a poor choice unless the retry logic was implemented in a smart proxy. This logic is difficult to do in a generic fashion. I recommend avoiding this option if high reliability is a requirement. While choosing the simplest solution that meets your needs is almost always the best choice, I am also bothered by the fact that this relies on out-of-band information for session affinity and requires point-to-point communications that seem to violate the spirit of messaging within service-oriented architecture (SOA).

Sharing SCT State Using a Database Cache

The second technique is to use a database cache to share the tokens. In this case, when a Security Context Token is established, the token information gets serialized and stored in a central database by the cache. When a request gets routed to a service instance, the token cache first checks its local in-memory cache for the SCT instance. If the SCT can't be found in the local cache, then the SCT information is retrieved from the database, deserialized, and added into the local cache.

The advantages of the database cache approach are:

  • Minimizes network transport overhead
  • Resilient to service instance failures

The disadvantages are:

  • Operational complexity is highest of all approaches. Database has to be maintained for caching. If a session database is already being maintained and has sufficient capacity, then this could piggyback off of the existing session database.
  • Cost—database needs to be available for caching. For high-availability systems, this would need to be a clustered database, since an outage would take down all services that rely on SCTs in your farm for security.
  • A slight performance hit over the service affinity approach, assuming retrieved tokens are cached locally in memory (one-time hit per token instance for database retrieval per service instance).

To realize this approach, we'll need to take the following steps:

  1. Extend the SecurityContextTokenService to re-cache the token after it is completely constructed. (This is caused by an issue with the current WSE implementation. The token is added to the cache before it is completed. This will be fixed in the next WSE release.)
  2. Extend the SecurityContextTokenManager in order to override the TokenCache property, and replace the cache with an implementation that manages a central database cache. Also, add initialization logic for configuring the cache database.
  3. Extend MRUTokenCache to add database persistence/retrieval logic.
  4. Create classes for serializing and deserializing SecurityContextTokens.
  5. Add logic for persisting and retrieving the serialized tokens from the database.

Sharing the State Information Using SCT Extensibility

This approach uses a conceptual model well understood by all Web developers—using cookies as a means to manage session state rather than relying upon you back-end services using stateful sessions. In this case, however the state information is embedded within the extensibility area of the SCT itself. The primary difference in this approach is, instead of putting the serialized information into a database, it gets put back into the token. As a result, the information goes onto the wire as part of the message and the included information needs to be protected with encryption and signed to prevent tampering.

At this point it may not seem so obvious as to why SCT offers any advantage when the "cookie" approach is used. After all, the primary advantage of using an SCT is that it is significantly more lightweight than the base token it replaces during the session, and this approach definitely bloats the SCT. 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 much less costly than processing other token types.

A big part of the cost of processing an XML token is dealing with xml signatures and encryption to validate the token, and to decrypt the token key. In many cases this involves asymmetric public-key–based encryption and signing that is significantly more process intensive. Additionally, in order to perform these operations, a process of xml canonicalization on the token needs to occur (more information is available from the W3C Web site) that is costly to perform. We can avoid this cost and can more compactly represent the token by using binary encryption and signing on the binary serialized information, so we end up with the same protection as with XML signing/encryption, but without the performance hit. Note that this is possible because in the case of an SCT, we don't worry about the ability of an intermediary or client to understand the extended content for interoperability. Only the service that originally established the SCT needs to unpack its contents. The only requirement imposed on the client is to round-trip the extended information. While I won't do it in this article, compression can also be used to reduce this payload. In the sample, I've implemented an approach of using the XML representation of the base token, since that's the only generic approach serialization available. Custom serialization to a more compact representation of the base token would be better. In many cases, it would be sufficient to just serialize and reconstitute the principal associated with the base token type and get rid of the token all together. The bottom line is that this approach will still achieve significant transport overhead and processing advantages over the use of other token types, especially if forethought is used to include only what is necessary.

Advantages of this technique:

  • Resilient to service-instance failure
  • Simplest to support operationally—does not require a state database or special network routing
  • Lower cost when compared to a database cache, as it doesn't require a redundant database to be in place to maintain state

Disadvantages:

  • Increased network bandwidth costs
  • Increased processing, due to processing extended data in payload (this needs to be parsed, even if it isn't decrypted/deserialized)
  • Requires key management on the server side (key used to encrypt/decrypt the serialized information in the cookie needs to be configured for all service instances)

This is the approach that I generally prefer, as you probably gathered, given my long-winded explanation of why it's really not that bad. Based on my experience, if I can avoid maintaining session-related state on the server, I absolutely will—it just makes scalability and manageability much easier to handle. If you manage session state in your farm that requires a session database infrastructure, then I would favor the database cache approach (I might question why you maintain session state for services, but that's another a discussion for another day.), but keep the SCT cache information out of your line-of-business database.

The steps to realize this approach significantly overlap with the database caching approach:

  1. Extend the SecurityContextTokenService to add the extended token on creation (this will not be required after the next WSE release).
  2. Extend the SecurityContextTokenManager in order to perform special handling for serializing and deserializing tokens.
  3. Create classes for serializing and deserializing SecurityContextTokens.
  4. Add classes for handling the binary encryption and signing of the SCT payload.

Implementing Database Persisted Token Caching

When I wrote this section, I showed a single implementation supporting both approaches. I reconsidered the approach after receiving feedback and decided a single implementation increased the complexity without really offering any advantages. I revised the approach to implement separate token managers, one for cookie and one for database. In the following sections I'll first run through the database approach and then discuss how the cookie implementation differs.

Extending the SecurityContextTokenService

The SecurityContextTokenService provides the standard WSE logic for issuing security context tokens to a client. When developing the sample to illustrate this solution, I didn't think I'd need to extend this logic. However, during implementation, I had a moment of clarity that actually muddled my pristine design. I had intended to intercept the point where the token was put into the token cache to perform this logic. As it turned out, the token was not yet complete at that point and I ended up serializing an incomplete token. A simple extension needed to be made to the SecurityContextTokenService to re-cache the token once completed. The next release of WSE addresses this issue, so we'll be able to revert to the standard implementation for the SecurityContextTokenService.

public class DistributedCacheSCTService: SecurityContextTokenService 
{
   protected override RequestSecurityTokenResponse IssueSecurityToken(
         SecurityTokenMessage request) 
   {
   // invoke the base implementation to create token
   RequestSecurityTokenResponse response = base.IssueSecurityToken(request);
   // now filled up the SCT's properties
   base.SetupIssuedToken(request, response);
    //Get the SCT token manager instance 
   SecurityContextTokenManager mgr =            
      SecurityTokenManager.GetSecurityTokenManagerByTokenType(
      WSTrust.TokenTypes.SecurityContextToken;
   SecurityContextToken token = 
      response.RequestedSecurityToken.SecurityToken
       as SecurityContextToken;
   //recache the completed SCT.
   SecurityContext sctMgr = (DistributedCacheSCTManager)mgr;
   sctMgr.CacheSecurityToken(token)
   return response;
   }
}

Finally I need to get WSE to use the DistributedCacheSCTService to issue the security tokens. This is done in web.config with following entry under the Microsoft.web.services2 section:

    <tokenIssuer>
       <autoIssueSecurityContextToken enabled="true" 
   type="SecureConvCodeService.DistributedCacheSCTService,
   SecureConvCodeService" />

Extending the SecurityContextTokenManager

In WSE, for each type of token handled there is an associated specialized security token manager that knows how to deal with aspects of managing that token type. The only requirement for a security token manager is that it implements the ISecurityTokenManager interface; however, there is also a base SecurityTokenManager implementation that would more commonly be used if you are implementing your own custom tokens. In this case, we don't even need to do that—we really just need to derive from the WSE provided class for managing Security Context Tokens, which is the SecurityContextTokenManager class. For the database approach, we derive:

public class DatabaseSCTManager:SecurityContextTokenManager

As one might anticipate, this class is really the linchpin in dealing with the distributed tokens. We need the manager to do several things:

  1. When constructed, validate the configuration and perform appropriate initialization.
  2. Manage the persistence of the SCT to the database, when constructed, using the appropriate mechanism.
  3. Handle loading the token back from the database if not in the local cache when processing an incoming SCT.

The token manager implementation needs external configuration information in order to determine how to initialize. In particular, the manager needs a connection string for the database to use as a cache. WSE provides a mechanism for specifying configuration information for your token manager. In order to use our derived implementation of the SecurityContextTokenManager, we need to make a configuration entry in the config file. Any child elements of that entry will be passed into a constructor that takes an XmlNodeList as a single argument. Once you understand this feature, using it is quite simple. The below snippet sets up our derived token manager as the manager for SCTs and adds custom configuration information for the manager. For the DatabaseSCTManager, we configure:

<securityTokenManager 
    type="SecureConvCodeService.DatabaseSCTManager,SecureConvCodeService" 
    xmlns:wssc="https://schemas.xmlsoap.org/ws/2004/04/sc" 
    qname="wssc:SecurityContextToken">
   <!—You should follow best practices for managing sensitive 
configuration information like connection strings and keys in a 
production environment. Guidance is located here: 
https://msdn.microsoft.com/library/default.asp?url=/library/en-
us/dnnetsec/html/SecNetch08.asp and here: https://msdn.microsoft.com/library/default.asp?url=/library/en-
us/dnnetsec/html/SecNetch12.asp -->
   <connectionString>
     Integrated Security=False;database=TokenCache;server=ckeyser-server;
User Id=foo;password=bar;
   </connectionString>
</securityTokenManager>

I chose to encapsulate the serialization logic in a SCT serializer class to better separate concerns, and it could easily be re-used by the CookieSCTManager. I had in mind extending the configuration logic if necessary to allow this serialization implementation to be replaced at startup with a more efficient serializer.

For the DatabaseSCTManager, we simply do our database-specific initialization:

public DatabaseSCTManager(XmlNodeList list ): base(list)
{
   ...parse out the configuration information
...initialize members
   // perform mode specific initialization
       ...validate and setup configuration for database mode
      //create DatabaseTokenCache
      _cache = new DatabaseTokenCache(_connectionString, CACHE_SIZE, 
           serializer);
}

...followed by overloading the TokenCache property:

protected override ISecurityTokenCache TokenCache
{
   get
   {
      return _cache;
   }
}

Implementing the DatabaseTokenCache

The DatabaseTokenCache provides the logic for storing and retrieving tokens from a central database. This approach was taken for a database-backed cache, since the necessary logic can be placed behind the cache implementation. It's worth mentioning that. for this implementation, I chose to treat the Clear and Remove operations on the cache as operations only on the local cache, not on the database cache. My reasoning for taking this approach is that the cache is not responsible for managing the lifetime of the SCT. Relying upon Clear and Remove for token revocation is beyond the scope of this article, and such a solution would need to more broadly consider revocation of all security token types for that identity.

I derived the DatabaseTokenCache class from the MRUSecurityTokenCache. This way, I can rely upon the MRUSecurityTokenCache implementation for my local token cache, and overload methods to add and retrieve tokens from the database cache if not found locally. I also extended a few other methods such as Contains to look at the database if the token is not found locally.

To initialize the token cache, we need to provide information gathered from the configuration of the token manager. The constructor therefore accepts the connection string for the data source, as well as a serializer. For this implementation, the serializer is a single implementation, although if I were to move to production, I would probably enhance this area of logic to make the serializer pluggable via configuration settings and constructed using reflection in the DatabaseSCTManager constructor.

public DatabaseTokenCache(SCTSerializer serializer, int capacity, string 
           connectionString): base(capacity)
{
   _connectionString = connectionString;
   _sctSerializer = serializer;
}

Next we intercept the points at which a token is added and read from the cache, and add logic to persist/retrieve the token from the database.

public override void Add(string identifier, SecurityToken token)
{
   base.Add (identifier, token);
   PersistSecurityToken(identifier, token); 
}

public override SecurityToken this[string identifier]
{
   get
   {
      SecurityToken token = base[identifier];
      if(token == null)
      {
         //try pulling from distributed cache...
         token = ReadFromStore(identifier);
         if(token != null)
{
             base.Add(identifier, token);
                             }
      }
                return token;
      }
}

Finally, we need to ensure the proper behavior of Contains to also determine if the token is in the central cache if it isn't in the local cache.

public override bool Contains(string identifier)
{
bool isInCache = base.Contains(identifier);
if(isInCache == false)
{
//try pulling from distributed cache...
SecurityToken token = ReadFromStore(identifier);

if(token != null)
{
base.Add(identifier, token);   
isInCache = true;
}
}
return isInCache;
}

Cleaning Up the Database Cache

When using the database cache, a process will need to execute periodically to clean out expired tokens. When I created the database cache, I inserted two values to assist in this process: expired time and issued time. I included issued time, since if no lifetime is specified, then the expired time will be many centuries in the future. The best way to manage this issue is to assign a lifetime for your issued security context tokens. In lieu of assigning a lifetime, you will either need to put in some type of generation-handling logic to determine when to remove an SCT from service, or arbitrarily make a decision when to purge. This runs the risk of inconsistent behavior of your services. (If it is in the local cache, then the SCT will be used by the service; otherwise, it will fail.) Eventually this condition will go away as worker processes are recycled. Your policy in this case could also piggyback off of your operational procedures; for instance, if you cycle your servers on a weekly basis, you could purge tokens at that time.

Serializing and Deserializing the SCT

The sample implemented for this article uses a generic method of serializing and deserializing tokens. Not to sound like a broken record, but I'll reiterate that this is not an efficient way to serialize tokens, and I will modify this logic for my specific implementation to serialize the tokens in a more compact way for a production implementation. As previously mentioned, tokens are unfortunately not serializable. There are a few reasons this design choice was made, and efficient token serialization is not as simple as one might hope. There are two tokens we need to be concerned with in this case—the first is the SCT itself, and the second is the "base token" of the SCT. When establishing a secure conversation, the client provides another security token to prove its authenticity and establish identity of the client before the SCT is issued. In the case of the secure conversation example extended for the sample, a UsernameToken is used for client credentials for the SCT request. If the credentials are accepted, then this token becomes the "base" token of the issued SCT. The base token typically contains identity- and perhaps authorization-related information for the authenticated client.

The approach I took assumes that tokens that are not SCTs are stateless; i.e., they can be reconstituted based upon the information contained within the token xml (and perhaps some metadata known on each service, such as the private key of a public certificate installed on all servers in the farm, or a symmetric key specified in the configuration information of the service). The SCT is a special case. GetXml returns just the lightweight payload containing only the identifier, so this needs custom logic for serialization. Here is the class I defined to capture all of the information to serialize:

[Serializable] 
private class SCTData
{
   public string Identifier;
   public string Id;
   public byte[] KeyBytes;
   public string TokenIssuer;
   public DateTime Created;
   public DateTime Expires;
   public string BaseTokenType;
   public string SerializedBaseTokenData;
}

I set the relevant data values from the SCT being serialized (Created and Expired come from Lifetime, which can't be serialized): SerializedBaseToken contains the XML retrieved from the base token using GetXml. This class is then serialized this into a byte stream.

When I tested this, I didn't have Network Load Balancing (NLB) set up, so I created a test case to see if this logic worked by not placing the token in the in-memory cache when first added and expecting to see the token reconstituted from the database. Things didn't go quite as planned. The base token was a UsernameToken, which has a nonce value. WSE tracks the nonce values and detects reuse of a nonce, which indicates a highly probable replay attack. While I am "rehydrating" the token with the LoadTokenFromXml method on the token manager, the manager expects to load the token from a message. Since the nonce has been used before, it throws a security fault. The problem is that I was using an artificial means to test my caching logic by not putting the token in my local in-memory cache. Typically, if the manager had seen it before, the token would be cached in memory. Otherwise it loads it from the database or extended SCT area. Since the replay detection also relies upon an in-memory cache of nonces, the second server instance will not have the nonce in memory, and therefore it will not cause a problem. For the purposes of testing, I was able to circumvent this issue by one more configuration setting, this one for the UsernameTokenManager:

<securityTokenManager 
type="SecureConvCodeService.CustomUsernameTokenManager,SecureConvCodeService"     
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-
wssecurity-secext-1.0.xsd" qname="wsse:UsernameToken" >
    <replayDetection enabled="false"/>
</securityTokenManager>

I'd like to point out that this is an artifact of my testing rather than a failure of the WSE 2.0 implementation. When you go into production, this setting most definitely should be turned back on. It should always be the case that the token is in the in-memory cache when this method is used, and the used nonce is in-memory. If this were being reconstituted within a service instance that had not issued the original token, then no exception would be thrown with this check enabled.

Someone who is experienced with WSE and WS-Trust might be tempted to make this more "efficient" by removing the Id from the SCT data described above. An SCT has both an Identifier element, which is used to globally identify the SCT across its lifetime, and a wsu:Id value, which is used within a message to refer to the SCT token, such as the within encryption and signature blocks as the token used for the operation. Or it can come from a derived key definition, as the token that provides the basis for the key derivation. The wsu:Id is expected to be consistent within a specific message. When I implemented this solution, the Id value was not persisted; from a technical standpoint, it should have been fine if the value changed across messages. I was stumped when the SecurityToken failed to load. I was fortunate enough to have an excellent WSE design engineer who helped me work through the issue. She discovered that there is a minor issue with the current SCT implementation that requires the wsu:Id to be consistent across messages. This will be fixed in the next release of WSE.

Finally you'll notice that I included the Identifier in the serialized information. We need to ensure that the token can't be "fooled" by taking the serialized information from the extended section of one SCT and substituting it in another. By including the Identifier, when we deserialize the information for the SCT, we can validate that the identifier in the serialized information matches the identifier of the SCT being reconstituted, and therefore binds the binary blob to a specific SCT instance.

Implementing a cookie-based solution is very similar to the database solution. I will point out the differences in implementing a cookie based approach and rely on the discussion above for many of the common components. We still need to override the SecurityContextTokenService with the same implementation, and token serialization is handled identically. However, for the cookie approach we cannot isolate the logic to a central database cache, as access to the XML payload within the received SCT is required to restore the token. As a result, persistence needs to be handled at a higher level in the token manager. The differences are:

  • We no longer need to derive another token cache implementation.
  • We need to handle persistence within our derived SecurityContextTokenManager class.
  • We need to worry about binary encryption and signing since the serialized information will go onto the wire.

Extending the SecurityContextTokenManager

The CookieSCTManager requires more complex handling by the token manager than the DatabaseTokenManager, but does not require extending the token cache. We start with overriding the SecurityContextTokenManager:

public class CookieSCTManager: SecurityContextTokenManager

Configuration is very similar to the database case, but for the SCT manager, we specify an encryption key to use in binary encryption/signing.

<securityTokenManager 
    type="SecureConvCodeService.CookieSCTManager,SecureConvCodeService" 
    xmlns:wssc="https://schemas.xmlsoap.org/ws/2004/04/sc" 
    qname="wssc:SecurityContextToken">
   <!—Follow best practices for managing sensitive configuraiton information 
like connection strings and keys in a production environment. -->
    <secretKey>9zgyMgCz8K631HYRV+Qj+Q==</secretKey>
 </securityTokenManager>

The CookieSCTManager overloads two methods, CacheSecurityToken and LoadTokenFromXml. In CacheSecurityToken, we add the serialized information after performing binary encryption and signing into the extended SCT area.

public override void CacheSecurityToken(SecurityToken token)
{
     MRUSecurityTokenCache localCache = (MRUSecurityTokenCache)base.TokenCache;
     if(localCache.Contains(sctToken.Identifier) == false)
     {
       //We only want to do this step if the token is complete.  
           //Right now this is a work around - should be fixed next release.
   if(sctToken.BaseToken != null)
    {
     ...serialize the SCT information
     ...binary sign and encrypt the serialized SCT information
     ...add the information into the extended SCT section
   base.CacheSecurityToken(token);  //add to local cache
            }
     }
}

Finally, we need to overload the method invoked by the WSE framework on the token manager to handle deserializing the tokens it manages from the XML provided. In this method, LoadTokenFromXml, we need to add special handling that attempts to load the token from the extended SCT content if it's not in the local cache. If the token had to be loaded from the extended SCT content section, then we add the loaded token to our local in-memory 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)
    {
   tokenToReturn = ReadFromCookie(element, sct.Identifier);
   if (tokenToReturn != null)
   {
   TokenCache.Add(sct.Identifier, tokenToReturn);
   }         
     }
     return tokenToReturn;
}

Conclusion

We discussed the advantages of using WS-SecureConversation and why it presents some challenges in a Web farm, and reviewed techniques for extending the WSE implementation of WS-SecureConversation support to work in a farm environment. In the process. we also demonstrated how to extend and configure a security context token manager and security context token service.

I'd like to say a special thanks to Fred Chong, an excellent architect that I collaborated with when originally addressing this issue, and who developed the conceptual model for sharing SCTs in a farm environment. I'd also like to thank HongMei Ge, Hervey Wilson, Jon Wagner, and Lucien Kleijkers for reviewing and providing great feedback to improve the article and implementation model.

About the Author

Chris Keyser is a solution architect within the Architecture Strategy group and graduated in Computer Science from Dartmouth College in 1984. Chris likes to write code and has a strong interest in Web services, and distributed and real-time systems. He spent the decade prior to joining Microsoft working for a series of start-up companies (B2B/e-commerce, voice biometrics and physical layer network switching) using a variety of technologies in real time and business system development. For the first five years out of college Chris raised havoc in the United States Navy as an engineering officer on a nuclear power ship, the USS Virginia, that was a total immersion experience in systems engineering (and, well, maybe a teensy bit of entertainment in between)—an education that is very useful to this day.

Chris can be reached via his blog.