Vampire

Monday, March 27, 2006

 

Client-side token cache for WCF

WCF by default maintains a cache for security tokens per channel instance (A channel is related to a contract). Therefore, it is not possible to reuse the same token for different channel instances.

Consider the following sample, a client application that consumes different services using a SAML token.




IHelloWorldChannel helloWorldService = factory.CreateChannel();


string response = helloWorldService.HelloWorld("John Doe");


Console.WriteLine(response);


helloWorldService = factory.CreateChannel();


response = helloWorldService.HelloWorld("John Doe 2");


Console.WriteLine(response);


factory = new ChannelFactory("anotherService");


helloWorldService = factory.CreateChannel();


response = helloWorldService.HelloWorld("John Doe 3");


Console.WriteLine(response);




In this case, I used three different channel instances and therefore a different SAML token for each service. (Each channel made an addition call to the STS in order to ask for a SAML token).


Fortunately, WCF provides a way to cache tokens outside the scope of a channel and reuse them later until they expire.

During the course of this post, I will show the required steps to build a client-side token cache to reuse tokens obtained from a STS.




First of all, I created a custom ClientCredentials class in order to return a custom SecurityTokenManager class. The SecurityTokenManager class is a kind of entry point to modify the process involved in the creation of a security token.




///

/// Custom implementation

///


class CustomClientCredentials : ClientCredentials

{

public CustomClientCredentials()

: base()

{

}


protected CustomClientCredentials(ClientCredentials other)

: base(other)

{

}


protected override ClientCredentials CloneCore()

{

return new CustomClientCredentials(this);

}



///

/// Returns a custom security token manager

///


///

public override System.IdentityModel.Selectors.SecurityTokenManager CreateSecurityTokenManager()

{

return new CustomClientCredentialsSecurityTokenManager(this);

}

}




Secondly, I declared my own SecurityTokenManager.




class CustomClientCredentialsSecurityTokenManager : ClientCredentialsSecurityTokenManager

{

private static Dictionary providers = new Dictionary();


public CustomClientCredentialsSecurityTokenManager(ClientCredentials credentials)

: base(credentials)

{

}


///

/// Returns a custom token provider when a issued token is required

///


public override System.IdentityModel.Selectors.SecurityTokenProvider CreateSecurityTokenProvider(System.IdentityModel.Selectors.SecurityTokenRequirement tokenRequirement)

{

if (this.IsIssuedSecurityTokenRequirement(tokenRequirement))

{

IssuedSecurityTokenProvider baseProvider = (IssuedSecurityTokenProvider)base.CreateSecurityTokenProvider(tokenRequirement);


CustomIssuedSecurityTokenProvider provider = new CustomIssuedSecurityTokenProvider(baseProvider);


return provider;

}

else

{

return base.CreateSecurityTokenProvider(tokenRequirement);

}

}

}




For this sample, I only want to cache issued tokens (Tokens obtained from a STS) and thefore I am using the IsIssuedSecurityTokenRequeriment method to determine if the channel is requesting an issued token or not.




Lastly, I created a simple Cache helper and a custom token provider to reuse the issued tokens.




///

/// Helper class used as cache for security tokens

///


class TokenCache

{

private const int DefaultTimeout = 1000;


private static Dictionary tokens = new Dictionary();

private static ReaderWriterLock tokenLock = new ReaderWriterLock();



private TokenCache()

{

}


public static SecurityToken GetToken(Uri endpoint)

{

SecurityToken token = null;

tokenLock.AcquireReaderLock(DefaultTimeout);

try

{

tokens.TryGetValue(endpoint, out token);


return token;

}

finally

{

tokenLock.ReleaseReaderLock();

}

}


public static void AddToken(Uri endpoint, SecurityToken token)

{

tokenLock.AcquireWriterLock(DefaultTimeout);

try

{

if (tokens.ContainsKey(endpoint))

tokens.Remove(endpoint);


tokens.Add(endpoint, token);

}

finally

{

tokenLock.ReleaseWriterLock();

}

}

}




///

/// Custom token provider. This class keeps the tokens outside of the channel

/// so they can be reused

///


class CustomIssuedSecurityTokenProvider : IssuedSecurityTokenProvider

{

private IssuedSecurityTokenProvider innerProvider;


///

/// Constructor

///


public CustomIssuedSecurityTokenProvider(IssuedSecurityTokenProvider innerProvider)

: base()

{

this.innerProvider = innerProvider;


this.CacheIssuedTokens = innerProvider.CacheIssuedTokens;

this.IdentityVerifier = innerProvider.IdentityVerifier;

this.IssuedTokenRenewalThresholdPercentage = innerProvider.IssuedTokenRenewalThresholdPercentage;

this.IssuerAddress = innerProvider.IssuerAddress;

this.IssuerBinding = innerProvider.IssuerBinding;


foreach (IEndpointBehavior behavior in innerProvider.IssuerChannelBehaviors)

{

this.IssuerChannelBehaviors.Add(behavior);

}


this.KeyEntropyMode = innerProvider.KeyEntropyMode;

this.MaxIssuedTokenCachingTime = innerProvider.MaxIssuedTokenCachingTime;

this.MessageSecurityVersion = innerProvider.MessageSecurityVersion;

this.SecurityAlgorithmSuite = innerProvider.SecurityAlgorithmSuite;

this.SecurityTokenSerializer = innerProvider.SecurityTokenSerializer;

this.TargetAddress = innerProvider.TargetAddress;


foreach (XmlElement parameter in innerProvider.TokenRequestParameters)

{

this.TokenRequestParameters.Add(parameter);

}


this.innerProvider.Open();

}




///

/// Gets the security token

///


///

///

protected override System.IdentityModel.Tokens.SecurityToken GetTokenCore(TimeSpan timeout)

{

SecurityToken securityToken = null;

if (this.CacheIssuedTokens)

{

securityToken = TokenCache.GetToken(this.innerProvider.IssuerAddress.Uri);

if (securityToken == null || !IsServiceTokenTimeValid(securityToken))

{

securityToken = innerProvider.GetToken(timeout);

TokenCache.AddToken(this.innerProvider.IssuerAddress.Uri, securityToken);

}

}

else

{

securityToken = innerProvider.GetToken(timeout);

}


return securityToken;

}




///

/// Checks the token expiration.

/// A more complex algorithm can be used here to determine whether the token is valid or not.

///


private bool IsServiceTokenTimeValid(SecurityToken serviceToken)

{

return (DateTime.UtcNow <= serviceToken.ValidTo.ToUniversalTime());

}




~CustomIssuedSecurityTokenProvider()

{

this.innerProvider.Close();

}




The provider is quite simple, it caches the tokens by IssuerAddress and checks the token expiration before returning it. When the token is expired, it gets a new token calling the inner token provider.

In order to register the CustomClientCredentials class, the following configuration is required (Using the "type" attribute)











Comments: Post a Comment



<< Home

Archives

September 2004   February 2005   February 2006   March 2006   July 2006  

This page is powered by Blogger. Isn't yours?

My Photo
Name: