Pablo M. Cibraro (aka Cibrax)

My thoughts on Web Services and .NET development

News

Pablo Cibraro's VisualCV

Blogs

Personal

Programming

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<IAnotherChannel>("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.

 

/// <summary>

/// Custom implementation

/// </summary>

class CustomClientCredentials : ClientCredentials

{

  public CustomClientCredentials()

      : base()

  {

  }

  protected CustomClientCredentials(ClientCredentials other)

     : base(other)

  {

  }

  protected override ClientCredentials CloneCore()

  {

    return new CustomClientCredentials(this);

  }

  /// <summary>

  /// Returns a custom security token manager

  /// </summary>

  /// <returns></returns>

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

  {

    return new CustomClientCredentialsSecurityTokenManager(this);

  }

}

 

Secondly, I declared my own SecurityTokenManager.

 

class CustomClientCredentialsSecurityTokenManager : ClientCredentialsSecurityTokenManager

{

  private static Dictionary<Uri, CustomIssuedSecurityTokenProvider> providers = new Dictionary<Uri, CustomIssuedSecurityTokenProvider>();

  public CustomClientCredentialsSecurityTokenManager(ClientCredentials credentials)

       : base(credentials)

  {

  }

  /// <summary>

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

  /// </summary>

  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.

 

/// <summary>

/// Helper class used as cache for security tokens

/// </summary>

class TokenCache

{

   private const int DefaultTimeout = 1000;

   private static Dictionary<Uri, SecurityToken> tokens = new Dictionary<Uri, SecurityToken>();

   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();

     }

   }

 }

 

 /// <summary>

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

 /// so they can be reused

 /// </summary>

 class CustomIssuedSecurityTokenProvider : IssuedSecurityTokenProvider

 {

   private IssuedSecurityTokenProvider innerProvider;

   /// <summary>

   /// Constructor

   /// </summary>

   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();

    }                              

 

   /// <summary>

   /// Gets the security token

   /// </summary>

   /// <param name="timeout"></param>

   /// <returns></returns>

   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;

   }

 

   /// <summary>

   /// Checks the token expiration.

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

   /// </summary>

   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)

 

<behavior name="ServiceBehavior">

  <clientCredentials type="CustomClientCredentials, MyAssembly">

  </clientCredentials>

</behavior>

 

Posted: Mar 27 2006, 06:09 PM by cibrax | with 7 comment(s)
Filed under:

Comments

Community Blogs said:

I finally decided to publish a STS implementation for WCF. (It isbased onone of my previous posts, &quot;Implementing

# September 8, 2006 10:40 AM

John Meade said:

Pablo,

Perhaps you can help me to understand what appears to be a missing piece. An RST message having been sent to an STS, an RSTR message returns, conaining an issued SAML token serialized into the body of the response.  Since RST and RSTR support was pulled from WCF, I need to somehow deserialize the SAML token from the body of the response prior to caching it, but I can find nothing on how to actually do so.

What might you recommend?

# November 17, 2006 4:47 PM

Matt said:

John,

 The "Federation" sample in the Windows SDK has some sample classes to do this in the RTM of WCF.

Pablo,

 Great post, this is really helpful.

# August 2, 2007 8:35 PM

Zuker On Foundations said:

Some references I&#39;d like to save for potential personal future use. Client-side token cache for WCF

# May 11, 2008 6:49 AM

Rob said:

How would I go about making the token cache serializable? I'm having problems with the SecurityToken class.

Thanks

# June 13, 2008 5:58 AM

cibrax said:

Hi Rob,

You will probably have to write the SecurityToken to an string (and the opposite to read it), if you want to store it in a database or other kind of cache different from memory.

Regards,

Pablo.  

# June 13, 2008 11:33 AM

Katie said:

Using wsFederationHttpBinding, the following is the traffic flow :

[1] .Net <--  RST/RSTR --> .Net STS to obtain an SAML Token

[2] .Net --------  RST with BinarySecret  -----> My STS

[3] .Net <-----  RSTR with BinarySecret ------- My STS

.Net --- traffic signed/encrypted with SCT from [3] ---> my app

 ** at this point, I am able to decrypt/verify the message

.Net <-------  traffic signed/encrypted with SCT from [3]  ---- my app

.Net choked with cannot locate the SCT, it seems like the .Net client did not cache the SCT token it received from [3].  Is there anyway to force the Token from [3] to be cached by .Net client ?

# March 13, 2009 3:21 PM
Leave a Comment

(required) 

(required) 

(optional)

(required)