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.
This is good enough until the front-end has to call a back-end system as the original user. To do this, the front-end needs to provide an STS with the original caller's token. Unlike the claims implementation initially provided with WCF, the Geneva Framework keeps the original SAML token around for just this purpose. When the front-end RP needs to retrieve a delegation token or ActAs token, as it is also called, it switches from the role of the RP and takes on that of the subject in the familiar WS-Trust communication triangle. When it does this, it needs to provide the STS with the token of the original caller in the RST sent to the STS. The original token that the front-end received is what is referred to as a bootstrap token because it is sent later to the STS to get an ActAs token when bootstrapping the delegation process. This process is shown in the following sketch.