Skip navigation
dollars and credit cards with a lock laying on top of them

Caching Issued Security Tokens at the Client

Reducing round trips to the security token service

RELATED: "Generate SAML Tokens Using Windows Identity Foundation" and "Securing Workflow Services with Windows Identity Foundation, Part 1"

Last month's article, "A Crash Course in Windows Identity Foundation," introduced this new column on claims-based and federated security by providing an overview of the features of the Windows Identity Foundation (WIF) platform. Going forward in this column, I'll assume that you've read the introductory column or that you're at least familiar with the participants and flow of communication in a federated security model. This month, I'll focus on a specific scenario related to active federation with Windows clients and Windows Communication Foundation (WCF) services: caching tokens and sharing them between multiple proxies to reduce round trips to the security token service (STS).

The Scenario for Token Caching

Inevitably you'll encounter situations that require reusing or caching security tokens in your Windows clients. Figure 1 captures the high-level picture of this scenario where the proxy for the CustomersService authenticates to the STS and gathers a token that is then passed to the OrdersService and ReportingService.

Figure 1: Sharing tokens issued by an STS across multiple proxies
Figure 1: Sharing tokens issued by an STS across multiple proxies

The goal is to avoid prompting the user for credentials more than once to initialize multiple proxies for multiple federated services (the relying party, or RP). You also want to avoid unnecessary trips to the STS to request a security token for multiple proxies. It is also wise to avoid additional STS trips when re-creating proxies if the channel times out or is faulted for some reason.

Although the specifics of the claims issued in the token is not the subject of this article, it's worth noting that since tokens are issued for a particular RP, they can only be shared across RP services if the following are true:

  • RP services must share the same service certificate.
  • The STS must be aware of all relevant RP addresses used in the AppliesTo of the Request for Security Token (RST) and be able to issue a token with claims that will be useful across all RP services.
  • If tokens are issued for one RP service and to be received by another, the service must allow tokens that potentially have an AudienceUri indicating one of the other RP services.

Assuming you can get past these obstacles, read on, and I will discuss how to share tokens among related proxies. As part of the discussion I'll cover handling session timeouts and token expiration as well.

ClientCredentials and Issued Tokens

Every proxy (channel) exposes a ClientCredentials property, which is used to supply information required to authenticate to the service. For example, you might set the Windows, UserName, or ClientCertificate properties of the ClientCredentials type before making the first call to the service. The following code illustrates setting the UserName credential:

CustomersProxy proxy = new CustomersProxy ();
proxy.ClientCredentials.UserName.UserName = this.Username;
proxy.ClientCredentials.UserName.Password = this.Password;

In federated security scenarios, you will still set the appropriate Windows, UserName, or ClientCertificate credentials if the STS endpoint expects to authenticate users with one of those credential types (keeping in mind that there are other authentication options, such as HttpDigest or another IssuedToken, from a chained STS). In addition, the ClientCredentials type exposes an IssuedToken property, which supplies information about the STS to authenticate to, such as its address and binding requirements. This information can be initialized programmatically; however, when you generate a proxy as I've discussed, this information is initialized from the federated binding configuration.

Figure 2 illustrates how the issued token is acquired.

Figure 2: Requesting an issued token through the IssuedSecurityTokenProvider
Figure 2: Requesting an issued token through the IssuedSecurityTokenProvider

The proxy has a reference to a ClientCredentials instance and supplies the user's credentials required to authenticate to the STS. The runtime triggers the CreateSecurityTokenManager() method of ClientCredentials to construct a SecurityTokenManager—specifically, the ClientCredentialsSecurityTokenManager. A SecurityTokenManager type is responsible for provisioning, authenticating, and serializing tokens. In this scenario, we're interested in overriding how tokens are provisioned. The SecurityTokenManager type supplies an implementation for CreateSecurityTokenProvider() to construct the appropriate token provider responsible for supplying tokens for all outgoing calls. In this case, the IssuedSecurityTokenProvider is used to request an issued token from the STS indicated in the ClientCredentials configuration. This token is then used for the call to the service.

The IssuedToken property of ClientCredentials is an instance of the IssuedTokenClientCredential type. This type has a property to control caching tokens: CachedIssuedTokens. By default, this property is set to true, which means that tokens will be cached until they expire and reused to call the service instead of re-authenticating to the STS. This reduces overhead for an individual proxy but does not provide the ability to share that cached token across multiple proxies. To share an issued token across proxies, you can create a custom IssuedSecurityTokenProvider that will draw a token from a shared cache if one exists, rather than calling the STS for each proxy.

Caching Issued Tokens for Multiple Proxies

To produce a shared token cache at the client, a custom IssuedSecurityTokenProvider can be created to check the local token cache prior to calling the STS to request a token. To install a custom IssuedSecurityTokenProvider, you must also create a custom ClientCredentialsSecurityTokenManager and ClientCredentials type. The custom ClientCredentials type can be shared between proxies if the configuration requirements are identical, and this will lead both proxies to the same custom IssuedSecurityTokenProvider—thus the same cached token, if one exists in the token cache. The architecture for this scenario is illustrated in Figure 3.

Figure 3: Sharing CachedClientCredentials between two proxies
Figure 3: Sharing CachedClientCredentials between two proxies

  • Here is a summary of the new types created:

    SecurityTokenCache: a custom type that holds a SecurityToken reference and issued an event when the token has changed, so the client application can respond if interested.
  • CachedClientCredentials: a custom ClientCredentials type that holds a reference to the SecurityTokenCache and overrides CreateSecurityTokenManager() to create a custom CachedClientCredentialsSecurityTokenManager.
  • CachedClientCredentialsSecurityTokenManager: a custom ClientCredentialsSecurityTokenManager type that holds a reference of the SecurityTokenCache and overrides CreateSecurityTokenProvider() to create a custom CachedIssuedSecurityTokenProvider.
  • CachedIssuedSecurityTokenProvider: a custom IssuedSecurityTokenProvider that holds a reference to the SecurityTokenCache and overrides GetTokenCore() to customize how the issued token is acquired—returning the cached token if valid, otherwise calling the STS for a new token and updating the cache.

Another possible view of the architecture in Figure 3 is shown in Figure 4.

Figure 4: Using a new CachedClientCredentials for each proxy, still sharing the token cache
Figure 4: Using a new CachedClientCredentials for each proxy, still sharing the token cache

In my view, this is a better way to share the token cache between two proxies using the same customized object model just summarized. In this case, each proxy gets a new instance of the CachedClientCredentials, CachedClientCredentialsSecurityTokenManager, and CachedIssuedSecurityTokenManager—but the same SecurityTokenCache. This is a bit more pure since the item the proxies are really sharing is the issued token, not the other configuration settings—and though they may be the same for all proxies calling a group of RP services, it is probably best to decouple those elements that may differ.

In the next sections I'll explain the code that implements this scenario.

SecurityTokenCache

There are many possible views for how to create a custom security token cache. You might have a very complicated client application that calls many different groups of services and requires caching issued tokens for each group. Or, you may just have an application that calls a number of related RP services, and each can share the issued token. The latter is the more common scenario in my experience, and so I have kept this implementation very simple. Figure 5 shows the code for a custom SecurityTokenCache, which includes a validation method to check that the token has not expired.

public class SecurityTokenCache
{
    private SecurityToken _Token;
    public SecurityToken Token
    {
    get
    {
        return _Token;
    }
    set
    {
        _Token = value;
        if (TokenUpdated != null)
        {
        TokenUpdated(this, null);
        }
    }
    }

public bool IsValidToken()
    {
    if (this._Token == null)
        return false;

     return (DateTime.UtcNow <= this._Token.ValidTo.ToUniversalTime());
    }

public event EventHandler TokenUpdated;
}

I've made some assumptions in this implementation:

The client application is responsible for constructing as many of these SecurityTokenCache instances as it requires for the number of issued tokens it needs to create for related groups of proxies. This will usually be just one.

  • The client application may want to know when the token is updated by the CachedIssuedSecurityTokenProvider—and so the TokenUpdated event is fired whenever this happens.

This type is supplied to the CachedClientCredentials type when constructed.

CachedClientCredentials

The CachedClientCredentials type is responsible for creating a custom SecurityTokenManager, which will in turn create a custom IssuedSecurityTokenProvider for this scenario. The implementation of the CachedClientCredentials type is shown in Figure 6. It holds a reference to the SecurityTokenCache and passes so that the custom CachedClientCredentialsSecurityTokenManager has access to it once constructed. The CreateSecurityTokenManager() override is the heart of this custom implementation—shown highlighted in Figure 6.

public class CachedClientCredentials: ClientCredentials
{
    public SecurityTokenCache TokenCache { get; private set; }

public CachedClientCredentials(SecurityTokenCache tokenCache): base()
    {
    this.TokenCache = tokenCache;
    }

public CachedClientCredentials(SecurityTokenCache tokenCache, ClientCredentials clientCredentials)
    : base(clientCredentials)
    {
    this.TokenCache = tokenCache;
    }

public CachedClientCredentials(CachedClientCredentials clientCredentials): base(clientCredentials)
    {
    this.TokenCache = clientCredentials.TokenCache;
    }

public override System.IdentityModel.Selectors.SecurityTokenManager CreateSecurityTokenManager()
    {
// Begin Callout A
    return new CachedClientCredentialsSecurityTokenManager
((CachedClientCredentials)this.Clone());
// End Callout A

}

protected override ClientCredentials CloneCore()
    {
    return new CachedClientCredentials(this);
    }
}

The client code will construct a new CachedClientCredentials type and pass in the original ClientCredentials type to preserve any settings initialized from configuration. Figure 7 shows the code to create the token cache, create a proxy, remove the original ClientCredentials behavior, create a new CachedClientCredentialsBehavior passing the token cache and original behavior, and then set up the UserName credentials to supply credentials to call the STS.

this.TokenCache = new SecurityTokenCache();

this._Proxy = new CustomersServiceProxy();

ClientCredentials oldCreds = this._Proxy.Endpoint.Behaviors.Remove();
CachedClientCredentials newCreds = new CachedClientCredentials(this.TokenCache, oldCreds);
this._Proxy.Endpoint.Behaviors.Add(newCreds);
this._Proxy.ClientCredentials.UserName.UserName = this.Username;
this._Proxy.ClientCredentials.UserName.Password = this.Password;
this._Proxy.Open();

Shortly I'll describe how to share this between proxies and respond to updated tokens.

CachedClientCredentialsSecurityTokenManager

As I mentioned, the CachedClientCredentials type creates a custom SecurityTokenManager—the CachedClientCredentialsSecurityTokenManager. The heart of this implementation is the override to CreateSecurityTokenProvider(), which returns a new CachedIssuedSecurityTokenProvider to the runtime. Figure 8 shows this implementation.

public class CachedClientCredentialsSecurityTokenManager : ClientCredentialsSecurityTokenManager
{
    public CachedClientCredentialsSecurityTokenManager(CachedClientCredentials clientCredentials):
base(clientCredentials)
    {
    }

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

    IssuedSecurityTokenProvider provider = base.CreateSecurityTokenProvider(tokenRequirement) as
IssuedSecurityTokenProvider;

    if (provider == null)
        return base.CreateSecurityTokenProvider(tokenRequirement);

    CachedIssuedSecurityTokenProvider cachedProvider = new
CachedIssuedSecurityTokenProvider(provider, (CachedClientCredentials)this.ClientCredentials);
        return cachedProvider;
    }

}

One of the things you'll notice in the implementation is that a custom provider is created only if the provider is an IssuedSecurityTokenProvider. Note that the CachedIssuedSecurityTokenProvider is passed a reference to the CachedClientCredentials instance, so that it has access to the cached token.

CachedIssuedSecurityTokenProvider

A partial listing of the CachedIssuedSecurityTokenProvider is shown in Figure 9. I've omitted some of the noise related to implementing ICommunicationObject and IDisposable. The GetTokenCore() override is where the magic happens. The code checks to see whether there is a valid security token in the cache, and if so, returns it. If a valid token doesn't exist, the base functionality to retrieve a token is called and the token cache is updated. The token is considered valid if its ValidTo property is greater than the current UTC time.

public class CachedIssuedSecurityTokenProvider: IssuedSecurityTokenProvider, ICommunicationObject,
IDisposable
{
    private CachedClientCredentials ClientCredentials { get; set; }
    private IssuedSecurityTokenProvider InnerProvider {get; set;}

public CachedIssuedSecurityTokenProvider(IssuedSecurityTokenProvider provider,
CachedClientCredentials clientCredentials):base()
    {
    this.InnerProvider = provider;
    this.ClientCredentials = clientCredentials;

    this.CacheIssuedTokens = provider.CacheIssuedTokens;
    this.IdentityVerifier = provider.IdentityVerifier;
    this.IssuedTokenRenewalThresholdPercentage =
provider.IssuedTokenRenewalThresholdPercentage;
    this.IssuerAddress = provider.IssuerAddress;
    this.IssuerBinding = provider.IssuerBinding;

    foreach (IEndpointBehavior item in provider.IssuerChannelBehaviors)
        this.IssuerChannelBehaviors.Add(item);

    this.KeyEntropyMode = provider.KeyEntropyMode;
    this.MaxIssuedTokenCachingTime = provider.MaxIssuedTokenCachingTime;
    this.MessageSecurityVersion = provider.MessageSecurityVersion;
    this.SecurityAlgorithmSuite = provider.SecurityAlgorithmSuite;
    this.SecurityTokenSerializer = provider.SecurityTokenSerializer;
    this.TargetAddress = provider.TargetAddress;

    foreach (XmlElement item in provider.TokenRequestParameters)
        this.TokenRequestParameters.Add(item);

}

protected override System.IdentityModel.Tokens.SecurityToken GetTokenCore(TimeSpan timeout)
    {
    SecurityToken securityToken = null;

    if (this.ClientCredentials.TokenCache.IsValidToken())
    {
        securityToken = this.ClientCredentials.TokenCache.Token;
    }
    else
    {
        securityToken = this.InnerProvider.GetToken(timeout);
        this.ClientCredentials.TokenCache.Token = securityToken;
    }

    return securityToken;
    }

}

Sharing CachedClientCredentials and Updating Client Claims

Figure 4 illustrated a design where each proxy has its own CachedClientCredentials reference but shares the SecurityTokenCache. Figure 10 illustrates the code to achieve this by initializing each instance of the CachedClientCredentials with the same SecurityTokenCache instance. The code also illustrates gathering client claims (as discussed earlier) and hooking the TokenUpdated event so that client claims can be refreshed if a new token is retrieved.

this.TokenCache = new SecurityTokenCache();

customersProxy = new CustomersServiceProxy();
ClientCredentials oldCreds = customersProxy.Endpoint.Behaviors.Remove();
CachedClientCredentials newCreds = new CachedClientCredentials(this.TokenCache, oldCreds);
customersProxy.Endpoint.Behaviors.Add(newCreds);
customersProxy.ClientCredentials.UserName.UserName = this.Username;
customersProxy.ClientCredentials.UserName.Password = this.Password;

ordersProxy = new OrdersServiceProxy();
ClientCredentials oldCreds = ordersProxy.Endpoint.Behaviors.Remove();
newCreds = new CachedClientCredentials(this.TokenCache, oldCreds);
ordersProxy.Endpoint.Behaviors.Add(newCreds);
ordersProxy.ClientCredentials.UserName.UserName = this.Username;
ordersProxy.ClientCredentials.UserName.Password = this.Password;

When the application is first loaded and each proxy is initialized, the first proxy to attempt a call will retrieve a token and populate the cache to be shared by the other proxy.

Session and Token Lifetime

At this point I've discussed how to share a cached token at the client with multiple proxies and provided one approach to synchronizing client-side claims with the latest token. But there is one more issue that can bite you with federated security scenarios, and that is secure session timeout and faulted channels.

Any time a transport session exists, it is subject to timeout at the server when the channel is inactive. In addition, any uncaught exceptions thrown at the service will fault the service channel, which results in the proxy becoming unusable. The interesting thing is that the user may not necessarily care about being aware of timeouts or communication exceptions—and just prefer that the application create a new channel, so that they can keep working with the application. Based on the implementation I've discussed thus far, we know that an invalid token will trigger another call to the STS to retrieve a fresh issued token. But what happens in these other two scenarios?

As for session timeout, the client may not be aware of it until trying to make a call to the service—at which point, the call fails. Since there is no way to confirm the failed call was the result of a timeout, what we want is for the failed call to be retried after recreating the proxy. If the retry fails, we have a bigger communication issue, but if it succeeds, a new token is issued, the cache is updated, the client claims are updated, the UI is rebound, and all is well in the world.

If an exception is thrown by the service, and that exception is not a CommunicationException, the user should be presented with the error, but the next attempt to use the proxy will fail because the channel is in a faulted state. The user doesn't need to know about this as they were already informed of the originating exception that caused the faulted channel. So, what we want is to re-create the proxy when the channel is faulted and if the next call executes properly. Once again, things are well in the world.

I'm providing only a high-level summary of this issue because I've discussed it in greater detail in a separate white paper and in a few short webcasts at http://wcfguidanceforwpf.codeplex.com. In addition, I've created an ExceptionHandlingWCFProxyGenerator at http://wcfproxygenerator.codeplex.com, which automates creating a special proxy base class that handles these specific issues and lets you shield the user from unnecessary exceptions. (For more information about the proxy generator, see "An Elegant Solution for WCF Proxy Exception-Handling," asp.netPRO October 2009.) The side benefit is that the same proxy also helps us to re-create and gather a new issued token when the channel is faulted.

Supporting CardSpace Scenarios

I had to make an addition to the ClientCredentials type to support caching issued tokens with CardSpace scenarios. I provided an override for GetInfoCardSecurityToken(), since this is called in lieu of the IssuedSecurityTokenProvider when the proxy is configured to invoke CardSpace. Figure 11 shows the implementation.

  protected override System.IdentityModel.Tokens.SecurityToken GetInfoCardSecurityToken(bool
requiresInfoCard, CardSpacePolicyElement[] chain, SecurityTokenSerializer tokenSerializer)
{
        SecurityToken securityToken = null;

    if (this.TokenCache.IsValidToken())
        {
        securityToken = this.TokenCache.Token;
        }
        else
        {
        try
        {
            securityToken = base.GetInfoCardSecurityToken(requiresInfoCard, chain, tokenSerializer);
            this.TokenCache.Token = securityToken;

        }
        catch (UserCancellationException cancelEx)
        {
            throw new Exception("Login cancelled by user.", cancelEx);
        }
         }

    return securityToken;
}

Token Caching: A Useful Technique

I've focused on the subject of caching issued tokens for scenarios where multiple proxies will be used from Windows client applications in a federated security scenario. By using token caching in situations that allow it, you can avoid unneeded STS requests and enable more efficient authentication. For more information on the subject of Windows client features relevant to claims-based and federated security, see my resources at claimsbasedwpf.codeplex.com.

You can find resources for this article at www.dasblonde.net/downloads/wif/cachingissuedtokens.zip.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish