WCF provides support for sessions where a key is negotiated between the client and service once for the duration of the connection. In addition, the client’s identity in terms of claims, is calculated upon the creation of the session. The contents of the session key and the client's identity are stored in a SecurityContextToken (SCT). Internally sessions work in two different ways tailored for different scenarios.
The first scenario is when the client connects to a single ServiceHost. In this mode (the default), each ServiceHost maintains an internal cache of the SCT’s. The client and host secure messages by using the negotiated token and placing the SCT ID into the message header. Each party uses the SCT ID to find the SCT token in its respective cache to perform cryptographic functions (signing, encrypting, ...).
The second scenario deals with web farms. WCF provides a cookie mode for this scenario. In this mode, an SCT is negotiated, however a cookie is created from the SCT and the client sends the cookie with each message. The service cracks the cookie before processing the message. It is worth noting that WCF has a performance enhancement that caches cookies once cracked and first uses the ID sent in the message to check the cache and uses it if found.
There is a particular problem that users are hitting that this blog addresses. When cookie mode is specified for web farms and server pinning is not deployed (the client always returns to the same server). There will most likely be a slighlty difference in the time between servers. In this case it is possible that a server in the farm will reject a cookie created from a different server in the farm. This happens because of normal clock skew coupled with the fact that, WCF in cookie mode, does not adjust for clock skew. The particular case where it fails is: Cookie is created on server1 @ time == 12:00. The client sends a message that gets handled by server2 @ 11:59.59 according to server2. The WCF runtime will crack the cookie and create an internal SCT with KeyEffectiveTime = 12:00. WCF will throw thinking the SCTs time is not valid.
The code posted in this blog, adjusts the SCT that is created by applying an user defined skew. It is a bit tricky as some of the properties and fields that needed to be set are internal. Also there is a bit of understanding required to hook a custom SecurityTokenAuthenticator for SCTs, which is the basis of the code.
Essentially what this code does, is place a hook just before the SCT is added to the ServiceHost cache. It first checks for cookie mode and if so, adjusts the time on the SCT prior to adding it to the cache. The performance enhancement mentioned above, removes the issue of the skew being applied multiple times. So I didn't need to code for that.
I have included the entire project in this blog. Here are some comments on the highlights of the code.
1. Replace the ServiceHost default ServiceCredentials with the SCT { program.cs }. This is required plumbing when you need to custom token processing. SCTs are token and this code provides custom handling.
ServiceHost sh = new ServiceHost( typeof( WorkService ), new Uri( baseAddress ) );
sh.AddServiceEndpoint( typeof( IWorkContract ), cb, baseAddress );
sh.Description.Behaviors.Remove<ServiceCredentials>();
sh.Description.Behaviors.Add( new SctServiceCredentials() );
2. Return SctSecurityTokenManager { SctServiceCredentials.cs }. This is required as WCF runtime calls this method to obtain an instance of SecurityTokenManager that servers up the objects that know how to work with tokens.
public override SecurityTokenManager CreateSecurityTokenManager()
{
return new SctServiceCredentialsSecurityTokenManager( this );
}
3. For SCT tokens return our wrapped resolver {SctServiceCredentialsSecurityTokenManager.cs}. This code is called when a SecurityTokenAuthenticator (the objects that know how to authenticate or validate tokens) is required for specific token type.
if ( requirement.TokenType == ServiceModelSecurityTokenTypes.SecureConversation )
{
SecurityTokenResolver innerStr = null;
sta = base.CreateSecurityTokenAuthenticator( requirement, out innerStr );
// Adjust all cookies by reducing keyEffectiveTime and tokenEffectiveTime by 5 minutes
str = new SctResolver( innerStr as SecurityContextSecurityTokenResolver,
TimeSpan.FromMinutes( -5 ) );
SetSCTResolverForCookie( sta, str );
}
4. When SCT is about to be added to the cache, adjust time
public bool TryAddContext(SecurityContextSecurityToken token)
{
//this additional check could be made for performance reasons
//&& DateTime.UtcNow < token.KeyEffectiveTime.ToUniversalTime() )
if ( token.IsCookieMode )
{
ApplySkewToToken( token );
}
return _innerSctResolver.TryAddContext( token );
}
void ApplySkewToToken( SecurityContextSecurityToken token )
{
Type type = ReflectionHelper.GetTypeTokenSec( "SecurityContextSecurityToken" );
ReflectionHelper.SetField( token, "keyEffectiveTime", CalculateSkewedTime(
token.KeyEffectiveTime, _skew ) );
// if tokenEffectiveTime is before KeyEffectiveTime, runtime throws.
// So both have to be adjusted the same amount
DateTime tokenEffectiveTime = ( DateTime ) ReflectionHelper.GetField( type, token,
"tokenEffectiveTime" );
ReflectionHelper.SetField( token, "tokenEffectiveTime", CalculateSkewedTime(
tokenEffectiveTime, _skew ) );
That is all there is to it. You can have a +ve or –ve skew just by passing a timespan to the SctResolver constructor. I have included a handy piece of code ReflectionHelper that is helpful when calling or setting privates or internals.