Token cache serialization in MSAL.NET

After Microsoft Authentication Library (MSAL) acquires a token, it caches that token. Public client applications (desktop and mobile apps) should try to get a token from the cache before acquiring a token by another method. Acquisition methods on confidential client applications manage the cache themselves. This article discusses default and custom serialization of the token cache in MSAL.NET.

Quick summary

The recommendation is:

  • When you're writing a desktop application, use the cross-platform token cache as explained in Desktop apps.
  • Do nothing for mobile and UWP apps. MSAL.NET provides secure storage for the cache.
  • In ASP.NET Core web apps and web APIs, use Microsoft.Identity.Web as a higher-level API. You'll get token caches and much more. See ASP.NET Core web apps and web APIs.
  • In the other cases of web apps and web APIs:
    • If you request tokens for users in a production application, use a distributed token cache (Redis, SQL Server, Azure Cosmos DB, distributed memory). Use token cache serializers available from Microsoft.Identity.Web.TokenCache.
    • Otherwise, if you want to use an in-memory cache:
      • If you're only using AcquireTokenForClient, either reuse the confidential client application instance and don't add a serializer, or create a new confidential client application and enable the shared cache option.

        A shared cache is faster because it's not serialized. However, the memory will grow as tokens are cached. The number of tokens is equal to the number of tenants times the number of downstream APIs. An app token is about 2 KB in size, whereas tokens for a user are about 7 KB in size. It's great for development, or if you have few users.

      • If you want to use an in-memory token cache and control its size and eviction policies, use the Microsoft.Identity.Web in-memory cache option.

  • If you build an SDK and want to write your own token cache serializer for confidential client applications, inherit from Microsoft.Identity.Web.MsalAbstractTokenCacheProvider and override the WriteCacheBytesAsync and ReadCacheBytesAsync methods.

The Microsoft.Identity.Web.TokenCache NuGet package provides token cache serialization within the Microsoft.Identity.Web library.

Extension method Description
AddInMemoryTokenCaches Creates a temporary cache in memory for token storage and retrieval. In-memory token caches are faster than the other cache types, but their tokens aren't persisted between application restarts, and you can't control the cache size. In-memory caches are good for applications that don't require tokens to persist between app restarts. Use an in-memory token cache in apps that participate in machine-to-machine auth scenarios like services, daemons, and others that use AcquireTokenForClient (the client credentials grant). In-memory token caches are also good for sample applications and during local app development. Microsoft.Identity.Web versions 1.19.0+ share an in-memory token cache across all application instances.
AddSessionTokenCaches The token cache is bound to the user session. This option isn't ideal if the ID token contains many claims, because the cookie will become too large.
AddDistributedTokenCaches The token cache is an adapter against the ASP.NET Core IDistributedCache implementation. It enables you to choose between a distributed memory cache, a Redis cache, a distributed NCache, or a SQL Server cache. For details about the IDistributedCache implementations, see Distributed memory cache.

In-memory token cache

Here's an example of code that uses the in-memory cache in the ConfigureServices method of the Startup class in an ASP.NET Core application:

#using Microsoft.Identity.Web
using Microsoft.Identity.Web;

public class Startup
{
 const string scopesToRequest = "user.read";
  
  public void ConfigureServices(IServiceCollection services)
  {
   // code before
   services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
           .AddMicrosoftIdentityWebApp(Configuration)
             .EnableTokenAcquisitionToCallDownstreamApi(new string[] { scopesToRequest })
                .AddInMemoryTokenCaches();
   // code after
  }
  // code after
}

AddInMemoryTokenCaches is suitable in production if you request app-only tokens. If you use user tokens, consider using a distributed token cache.

Token cache configuration code is similar between ASP.NET Core web apps and web APIs.

Distributed token caches

Here are examples of possible distributed caches:

// or use a distributed Token Cache by adding
   services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
           .AddMicrosoftIdentityWebApp(Configuration)
             .EnableTokenAcquisitionToCallDownstreamApi(new string[] { scopesToRequest }
               .AddDistributedTokenCaches();

// Distributed token caches have a L1/L2 mechanism.
// L1 is in memory, and L2 is the distributed cache
// implementation that you will choose below.
// You can configure them to limit the memory of the 
// L1 cache, encrypt, and set eviction policies.
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options => 
  {
    // Optional: Disable the L1 cache in apps that don't use session affinity
    //                 by setting DisableL1Cache to 'true'.
    options.DisableL1Cache = false;
    
    // Or limit the memory (by default, this is 500 MB)
    options.L1CacheOptions.SizeLimit = 1024 * 1024 * 1024; // 1 GB

    // You can choose if you encrypt or not encrypt the cache
    options.Encrypt = false;

    // And you can set eviction policies for the distributed
    // cache.
    options.SlidingExpiration = TimeSpan.FromHours(1);
  });

// Then, choose your implementation of distributed cache
// -----------------------------------------------------

// good for prototyping and testing, but this is NOT persisted and it is NOT distributed - do not use in production
services.AddDistributedMemoryCache();

// Or a Redis cache
// Requires the Microsoft.Extensions.Caching.StackExchangeRedis NuGet package
services.AddStackExchangeRedisCache(options =>
{
 options.Configuration = "localhost";
 options.InstanceName = "SampleInstance";
});

// You can even decide if you want to repair the connection
// with Redis and retry on Redis failures. 
services.Configure<MsalDistributedTokenCacheAdapterOptions>(options => 
{
  options.OnL2CacheFailure = (ex) =>
  {
    if (ex is StackExchange.Redis.RedisConnectionException)
    {
      // action: try to reconnect or something
      return true; //try to do the cache operation again
    }
    return false;
  };
});

// Or even a SQL Server token cache
// Requires the Microsoft.Extensions.Caching.SqlServer NuGet package
services.AddDistributedSqlServerCache(options =>
{
 options.ConnectionString = _config["DistCache_ConnectionString"];
 options.SchemaName = "dbo";
 options.TableName = "TestCache";
});

// Or an Azure Cosmos DB cache
// Requires the Microsoft.Extensions.Caching.Cosmos NuGet package
services.AddCosmosCache((CosmosCacheOptions cacheOptions) =>
{
    cacheOptions.ContainerName = Configuration["CosmosCacheContainer"];
    cacheOptions.DatabaseName = Configuration["CosmosCacheDatabase"];
    cacheOptions.ClientBuilder = new CosmosClientBuilder(Configuration["CosmosConnectionString"]);
    cacheOptions.CreateIfNotExists = true;
});

For more information, see:

The usage of distributed cache is featured in the ASP.NET Core web app tutorial in the phase 2-2 token cache.

Monitor cache hit ratios and cache performance

MSAL exposes important metrics as part of AuthenticationResult.AuthenticationResultMetadata object. You can log these metrics to assess the health of your application.

Metric Meaning When to trigger an alarm?
DurationTotalInMs Total time spent in MSAL, including network calls and cache. Alarm on overall high latency (> 1 second). Value depends on token source. From the cache: one cache access. From Azure Active Directory (Azure AD): two cache accesses plus one HTTP call. First ever call (per-process) will take longer because of one extra HTTP call.
DurationInCacheInMs Time spent loading or saving the token cache, which is customized by the app developer (for example, save to Redis). Alarm on spikes.
DurationInHttpInMs Time spent making HTTP calls to Azure AD. Alarm on spikes.
TokenSource Source of the token. Tokens are retrieved from the cache much faster (for example, ~100 ms versus ~700 ms). Can be used to monitor and alarm the cache hit ratio. Use with DurationTotalInMs.
CacheRefreshReason Reason for fetching the access token from the identity provider. Use with TokenSource.

Next steps

The following samples illustrate token cache serialization.

Sample Platform Description
active-directory-dotnet-desktop-msgraph-v2 Desktop (WPF) Windows Desktop .NET (WPF) application that calls the Microsoft Graph API. Diagram that shows a topology with a desktop app client flowing to Azure Active Directory by acquiring a token interactively and to Microsoft Graph.
active-directory-dotnet-v1-to-v2 Desktop (console) Set of Visual Studio solutions that illustrate the migration of Azure AD v1.0 applications (using ADAL.NET) to Microsoft identity platform applications (using MSAL.NET). In particular, see Token cache migration and Confidential client token cache.
ms-identity-aspnet-webapp-openidconnect ASP.NET (net472) Example of token cache serialization in an ASP.NET MVC application (using MSAL.NET). In particular, see MsalAppBuilder.