Caching Tokens to Avoid Calls to the STS using the Geneva Framework

| | Comments (2) | TrackBacks (0)

There is a lot of talk in the Geneva forum about caching and avoiding unnecessary round trips to the STS when calling downstream Web services. After all of it, I thought that reusing the same ChannelFactory<T> object would result in a single call to the STS for a token and that subsequent calls made with that factory would reuse the SAML token. I thought that a service type like this would suffice:

public class FooService : IFooService
{
    private static readonly ChannelFactory<IBarService> factory = new ChannelFactory<IBarService>();

     public string Echo(string message)
    {
        var proxy = factory.CreateChannel();

        return proxy.ComputeResponse(message);
    }
}

It turns out this is not correct, however. Every time a channel is created, invoking operations on that proxy will result in a call to the STS for a security token (which is really a few service calls for policy and whatnot). At first, I thought that the way to avoid this was to reuse the channel and not the channel factory. This may work if the channel remains open; however, if it is closed, it will get disposed and subsequent calls will fail. I don't know if it is a bad practice in this case to keep the channel open continuously, but, as a rule, holding onto resources for longer than absolutely necessary is to be avoided. Working off this assumption, I sought to find an alternative that would avoid the extra calls to the STS without having to hold onto an open channel object.

A couple of colleagues told me that I needed to cache the SAML token outside of the channel and retrieve them from there by adding an interceptor to the WCF pipeline. They referred me to Cibrax's blog, where he describes the process. After reading that article and Eric Quist's post, I understood that hooking into WCF to replace the default SAML token provider with one that does caching requires the definition of three classes:

  • CacheClientCredentials - Plumbing
  • CacheClientCredentialsSecurityTokenManager - Plumbing
  • CacheSecurityTokenProvider - Actual meat

The code that Cibrax and Eric wrote is really good, and it provides an excellent starting point for doing this in Geneva; however, I needed to tweak their code a bit to get it to work with this new framework. Specifically, I had to make the following changes:

CacheClientCredentials

  • Must inherit from FederatedClientCredentials
  • Must override CloneCore and return a new CacheClientCredentials object
  • May provide a static method for configuring a channel factory object to use CacheClientCredentials (similarly to FederatedClientCredentials - in beta 1 at least)

Something like this:

public class CacheClientCredentials : FederatedClientCredentials
{
    public
CacheClientCredentials() { }

    public CacheClientCredentials(ClientCredentials other) : base(other) { }

    protected override ClientCredentials CloneCore()
    {
        return
new CacheClientCredentials();
    }

    public override SecurityTokenManager CreateSecurityTokenManager()
    {
        return
new CacheClientCredentialsSecurityTokenManager(this);
    }

     public static void ConfigureChannelFactory<T>(ChannelFactory<T> channelFactory)
{
        var
other = channelFactory.Endpoint.Behaviors.Find<ClientCredentials>();

        if (other != null)
            channelFactory.Endpoint.Behaviors.Remove(other.GetType());

        FederatedClientCredentials item = null;

        if (other != null)
            item = new CacheClientCredentials(other);

        channelFactory.Endpoint.Behaviors.Add(item);
    }
}

CacheClientCredentialsSecurityTokenManager

  • Must inherit from FederatedClientCredentialsSecurityTokenManager
  • Has a constructor that accepts a FederatedClientCredentials object that is passed to the base's constructor

Something like this:

public class CacheClientCredentialsSecurityTokenManager : FederatedClientCredentialsSecurityTokenManager

{

    public CacheClientCredentialsSecurityTokenManager(FederatedClientCredentials federatedClientCredentials)

        : base(federatedClientCredentials) { }

 

    public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement)

    {

        var provider = base.CreateSecurityTokenProvider(tokenRequirement);

        var federatedSecurityTokenProvider = provider as IssuedSecurityTokenProvider;

 

        if (federatedSecurityTokenProvider != null && IsIssuedSecurityTokenRequirement(tokenRequirement))

        {

            var federatedClientCredentialsParameters = FindIssuedTokenClientCredentialsParameters(tokenRequirement);

 

            provider = new CacheSecurityTokenProvider(federatedClientCredentialsParameters, federatedSecurityTokenProvider);

        }

 

        return provider;

    }

 

    // Lifted from FederatedClientCredentialsSecurityTokenManager's private method by the same name.

    private static FederatedClientCredentialsParameters FindIssuedTokenClientCredentialsParameters(SecurityTokenRequirement tokenRequirement)

    {

        var parameters = new FederatedClientCredentialsParameters();

        ChannelParameterCollection result;

 

        if (tokenRequirement.TryGetProperty(ServiceModelSecurityTokenRequirement.ChannelParametersCollectionProperty, out result) && result != null)

        {

            foreach (var obj2 in result)

            {

                if (obj2 is FederatedClientCredentialsParameters)

                {

                    break;

                }

            }

        }

 

        return parameters;

    }

}


CacheSecurityTokenProvider

  • Must inherit from FederatedSecurityTokenProvider
  • Must define a constructor that accepts a FederatedClientCredentialsParameters and an IssuedSecurityTokenProvider object
  • Does not need to clone the inner provider because FederatedSecurityTokenProvider does that already

Something like this:

public class CacheSecurityTokenProvider : FederatedSecurityTokenProvider, IDisposable

{

    private bool disposed;

    private readonly IssuedSecurityTokenProvider innerProvider;

 

    public CacheSecurityTokenProvider(FederatedClientCredentialsParameters federatedClientCredentialsParameters,

        IssuedSecurityTokenProvider federatedSecurityTokenProvider)

        : base(federatedClientCredentialsParameters, federatedSecurityTokenProvider)

    {

        innerProvider = federatedSecurityTokenProvider;

        innerProvider.Open();

    }

 

    protected override SecurityToken GetTokenCore(TimeSpan timeout)

    {

        var userName = Thread.CurrentPrincipal.Identity.Name;

        var cacheKey = string.Concat(this.TargetAddress.Uri, IssuerAddress.Uri, userName);

        var securityToken = TokenCacheHelper.GetToken(cacheKey);

        var cacheMiss = securityToken == null || IsSecurityTokenExpired(securityToken);

 

        if (cacheMiss)

        {

            securityToken = innerProvider.GetToken(timeout);

 

            // Only add the token to the cache if caching has been turned on in web/app.config.

            if (CacheIssuedTokens)

            {

                TokenCacheHelper.AddToken(cacheKey, securityToken);

            }

        }

 

        return securityToken;

    }

 

    private static bool IsSecurityTokenExpired(SecurityToken serviceToken)

    {

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

    }

 

    ~CacheSecurityTokenProvider()

    {

        if (!disposed)

            ((IDisposable)this).Dispose();

    }

 

    void IDisposable.Dispose()

    {

        innerProvider.Close();

        disposed = true;

    }

}


Conclusion

Now, having said all this and listing a bunch of code, let's step back for a moment. Caching of SAML tokens is a must to avoid extra round trips to the STS. Doing so requires a bunch of plumbing and everyone will absolutely require this functionality. So, why are we doing this? Geneva is a framework, and as such, IMO, it should be proving things like caching of SAML tokens for us. It has been said by the Geneva team that support for caching of tokens is a top priority, so hopefully the code above will be obsolete very soon.