-
Notifications
You must be signed in to change notification settings - Fork 365
Token cache serialization
This article is for MSAL.NET 3.x. If you are still interested in MSAL.NET 2.x, see Token cache serialization in MSAL.NET 2.x
In MSAL.NET, an in-memory token cache is provided by default. To take advantage of the in memory cache you will have to keep the Client Application in memory as MSAL.NET doesn't have any statics for the cache. Please note this is different from ADAL!
Additionally, the serialization is provided by default for a certain number of platforms where a secure storage is available for a user as part of the platform. This is the case of UWP, Xamarin.iOS, Xamarin.Android.
Note that when you migrate a Xamarin.Android project from MSAL.NET 1.x to MSAL.NET 3.x, you might want to add |
android:allowBackup="false"
to your project to avoid old cached tokens from coming back because Visual Studio deployments are triggering a restore of local storage. See #659
In the case of .NET Framework and .NET core, if you don't do anything extra, the in-memory token cache lasts for the duration of the application. To understand why serialization is not provided out of the box, remember MSAL .NET desktop/core applications can be console or Windows applications (which would have access to the file system), but also Web applications or Web API, which might use some specific cache mechanisms like databases, distributed caches, redis caches etc .... To have a persistent token cache application in .NET Desktop or Core, you will need to customize the serialization.
Remember:
This feature is not available on mobile platforms (UWP, Xamarin.iOS, Xamarin.Android) because MSAL already defines a secure and performant serialization mechanism. .Net desktop and .Net core applications, on the other hand, have varied architectures, and MSAL cannot implement a serialization mechanism that fits all purposes (e.g. web sites may choose to store tokens in a Redis cache, desktop apps in an encrypted file etc.)
The classes and interfaces involved in token cache serialization are the following:
-
ITokenCache
, which defines events to subscribe to token cache serialization requests, as well as methods to serialize or de-serialize the cache at various formats (ADAL v3.0, MSAL 2.x and MSAL 3.x = ADAL v5.0) -
TokenCacheCallback
is a callback passed to the events so that you can handle the serialization. they will be called with arguments of typeTokenCacheNotificationArgs
. -
TokenCacheNotificationArgs
only provides theClientId
of the application and a reference to the user for which the token is available
Important
MSAL.NET creates token caches for you and provides you with the IToken
cache. Then, you read the caches using the application's UserTokenCache
and AppTokenCache
properties. You are not supposed to implement the interface yourself. Your responsibility, when you implement a custom token cache serialization, is to:
- React to
BeforeAccess
andAfterAccess
"events". TheBeforeAccess
delegate is responsible to deserialize the cache, whereas theAfterAccess
one is responsible for serializing the cache. - Part of these events store or load blobs, which are passed through the event argument to whatever storage you want.
The strategies are different depending on if you are writing a token cache serialization for a public client application (Desktop), or a confidential client application (Web App / Web API, Daemon app).
Since MSAL V2.x you have several options, depending on if you want to serialize the cache only to the MSAL.NET format (unified format cache which is common with MSAL, but also across the platforms), or if you also want to also support the legacy Token cache serialization of ADAL V3.
The customization of Token cache serialization to share the SSO state between ADAL.NET 3.x, ADAL.NET 5.x and MSAL.NET is explained in part of the following sample: active-directory-dotnet-v1-to-v2
Note: The MSAL.NET 1.1.4-preview token cache format is no longer supported in MSAL 2.x. If you have applications leveraging MSAL.NET 1.x, your users will have to re-sign-in. On the other hand, the migration from ADAL 4.x (and 3.x) is supported.
MSAL.NET provides a cross platform token cache in a separate library named Microsoft.Identity.Client.Extensions.Msal, which source code is available from https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet.
Add the Microsoft.Identity.Client.Extensions.Msal NuGet package to your project.
See See https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/wiki/Cross-platform-Token-Cache for details. Here is an example of usage of the cross platform token cache.
var storageProperties =
new StorageCreationPropertiesBuilder(Config.CacheFileName, Config.CacheDir, Config.ClientId)
.WithLinuxKeyring(
Config.LinuxKeyRingSchema,
Config.LinuxKeyRingCollection,
Config.LinuxKeyRingLabel,
Config.LinuxKeyRingAttr1,
Config.LinuxKeyRingAttr2)
.WithMacKeyChain(
Config.KeyChainServiceName,
Config.KeyChainAccountName)
.Build();
IPublicClientApplication pca = PublicClientApplicationBuilder.Create(clientId)
.WithAuthority(Config.Authority)
.WithRedirectUri("http://localhost") // make sure to register this redirect URI for the interactive login
.Build();
// This hooks up the cross-platform cache into MSAL
var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties );
cacheHelper.RegisterCache(pca.UserTokenCache);
Below is an example of a naive implementation of custom serialization of a token cache for desktop applications. Here the user token cache in a file in the same folder as the application.
After you build the application, you enable the serialization by calling TokenCacheHelper.EnableSerialization()
passing the application UserTokenCache
app = PublicClientApplicationBuilder.Create(ClientId)
.Build();
TokenCacheHelper.EnableSerialization(app.UserTokenCache);
This helper class looks like the following:
static class TokenCacheHelper
{
public static void EnableSerialization(ITokenCache tokenCache)
{
tokenCache.SetBeforeAccess(BeforeAccessNotification);
tokenCache.SetAfterAccess(AfterAccessNotification);
}
/// <summary>
/// Path to the token cache
/// </summary>
public static readonly string CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin3";
private static readonly object FileLock = new object();
private static void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
lock (FileLock)
{
args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath)
? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
null,
DataProtectionScope.CurrentUser)
: null);
}
}
private static void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (args.HasStateChanged)
{
lock (FileLock)
{
// reflect changesgs in the persistent store
File.WriteAllBytes(CacheFilePath,
ProtectedData.Protect(args.TokenCache.SerializeMsalV3(),
null,
DataProtectionScope.CurrentUser)
);
}
}
}
}
A preview of a product quality token cache file based serializer for public client applications (for desktop applications running on Windows, Mac and linux) is available from the Microsoft.Identity.Client.Extensions.Msal open source library. You can include it in your applications from the following nuget package: Microsoft.Identity.Client.Extensions.Msal.
Disclaimer. The Microsoft.Identity.Client.Extensions.Msal library is an extension over MSAL.NET. Classes in these libraries might make their way into MSAL.NET in the future, as is or with breaking changes.
If you want to implement token cache serialization both with the Unified cache format (common to ADAL.NET 4.x and MSAL.NET 2.x, and with other MSALs of the same generation or older, on the same platform), you can get inspired by the following code:
string appLocation = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location;
string cacheFolder = Path.GetFullPath(appLocation) + @"..\..\..\..");
string adalV3cacheFileName = Path.Combine(cacheFolder, "cacheAdalV3.bin");
string unifiedCacheFileName = Path.Combine(cacheFolder, "unifiedCache.bin");
IPublicClientApplication app;
app = PublicClientApplicationBuilder.Create(clientId)
.Build();
FilesBasedTokenCacheHelper.EnableSerialization(app.UserTokenCache,
unifiedCacheFileName,
adalV3cacheFileName);
This time the helper class looks like the following:
using System;
using System.IO;
using System.Security.Cryptography;
using Microsoft.Identity.Client;
namespace CommonCacheMsalV3
{
/// <summary>
/// Simple persistent cache implementation of the dual cache serialization (ADAL V3 legacy
/// and unified cache format) for a desktop applications (from MSAL 2.x)
/// </summary>
static class FilesBasedTokenCacheHelper
{
/// <summary>
/// Get the user token cache
/// </summary>
/// <param name="adalV3CacheFileName">File name where the cache is serialized with the
/// ADAL V3 token cache format. Can
/// be <c>null</c> if you don't want to implement the legacy ADAL V3 token cache
/// serialization in your MSAL 2.x+ application</param>
/// <param name="unifiedCacheFileName">File name where the cache is serialized
/// with the Unified cache format, common to
/// ADAL V4 and MSAL V2 and above, and also across ADAL/MSAL on the same platform.
/// Should not be <c>null</c></param>
/// <returns></returns>
public static void EnableSerialization(ITokenCache tokenCache, string unifiedCacheFileName, string adalV3CacheFileName)
{
UnifiedCacheFileName = unifiedCacheFileName;
AdalV3CacheFileName = adalV3CacheFileName;
tokenCache.SetBeforeAccess(BeforeAccessNotification);
tokenCache.SetAfterAccess(AfterAccessNotification);
}
/// <summary>
/// File path where the token cache is serialized with the unified cache format
/// (ADAL.NET V4, MSAL.NET V3)
/// </summary>
public static string UnifiedCacheFileName { get; private set; }
/// <summary>
/// File path where the token cache is serialized with the legacy ADAL V3 format
/// </summary>
public static string AdalV3CacheFileName { get; private set; }
private static readonly object FileLock = new object();
public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
lock (FileLock)
{
args.TokenCache.DeserializeAdalV3(ReadFromFileIfExists(AdalV3CacheFileName));
try
{
args.TokenCache.DeserializeMsalV3(ReadFromFileIfExists(UnifiedCacheFileName));
}
catch(Exception ex)
{
// Compatibility with the MSAL v2 cache if you used one
args.TokenCache.DeserializeMsalV2(ReadFromFileIfExists(UnifiedCacheFileName));
}
}
}
public static void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (args.HasStateChanged)
{
lock (FileLock)
{
WriteToFileIfNotNull(UnifiedCacheFileName, args.TokenCache.SerializeMsalV3());
if (!string.IsNullOrWhiteSpace(AdalV3CacheFileName))
{
WriteToFileIfNotNull(AdalV3CacheFileName, args.TokenCache.SerializeAdalV3());
}
}
}
}
/// <summary>
/// Read the content of a file if it exists
/// </summary>
/// <param name="path">File path</param>
/// <returns>Content of the file (in bytes)</returns>
private static byte[] ReadFromFileIfExists(string path)
{
byte[] protectedBytes = (!string.IsNullOrEmpty(path) && File.Exists(path))
? File.ReadAllBytes(path) : null;
byte[] unprotectedBytes = encrypt ?
((protectedBytes != null) ? ProtectedData.Unprotect(protectedBytes, null, DataProtectionScope.CurrentUser) : null)
: protectedBytes;
return unprotectedBytes;
}
/// <summary>
/// Writes a blob of bytes to a file. If the blob is <c>null</c>, deletes the file
/// </summary>
/// <param name="path">path to the file to write</param>
/// <param name="blob">Blob of bytes to write</param>
private static void WriteToFileIfNotNull(string path, byte[] blob)
{
if (blob != null)
{
byte[] protectedBytes = encrypt
? ProtectedData.Protect(blob, null, DataProtectionScope.CurrentUser)
: blob;
File.WriteAllBytes(path, protectedBytes);
}
else
{
File.Delete(path);
}
}
// Change if you want to test with an un-encrypted blob (this is a json format)
private static bool encrypt = true;
}
}
In the case of web apps or web APIs, caching should be handled differently than for public client applications. It's highly recommended to leverage a distributed cache, e.g. Redis, Cosmos, or SQL Server.
Important:
For web apps and web APIs, there should be one token cache per user (per account) and thus the cache should be serialized per account. The
TokenCacheNotificationArgs
contains a cache key which can be used to partition the cache.For service-to-service scenarios, you will typically be using
AcquireTokenForClient
and theTokenCache
key is not (yet) optimized for proper partitioning if you have a large number of resources you call. For most scenarios only a few resources are called, however there are scenarios where the resource count can be significant. If you are having a model where you don't fully control the number of resources and the number is higher than 10, it's highly recommended that you use the resource tenant as a partitioning key.
If you are using ASP.NET Core or ASP.NET on .NET Framework, Microsoft.Identity.Web provides token cache serialization for you:
- ASP.NET Core: The best is to leverage Microsoft.Identity.Web which already does the right thing. See Token cache serialization.
- ASP.NET classic (MVC): See Token cache serialization for MSAL.NET in ASP.NET apps.
- The details described for usage with ASP.NET classic (MVC) can be used to leverage Microsoft.Identity.Web for distributed caching if you are not using a framework. An example of the usage without a framework can be found in the ConfidentialClientTokenCache sample.
Examples of how to use token caches for web apps and web APIs are available in the ASP.NET Core web app tutorial in the phase 2-2 Token Cache. For implementations have a look at the TokenCacheProviders folder in the Microsoft.Identity.Web repository.
Microsoft.Identity.Web is available as a NuGet package.
When acquiring a token for a service principal, i.e. on behalf of an application, you use the Confidential Client grant.
In this case, the token issuer (AAD), only emits Access Tokens. IDTokens are not created because ID Tokens are related to users. Refresh Tokens are not created for security reasons. The structure of the token cache is different, as it only focuses on access tokens, which anyway have short expiration.
To serialize the content of this cache:
IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder
.Create(s_clientIdForConfidentialApp)
.WithClientSecret(s_confidentialClientSecret)
.Build();
// Instruct MSAL how to serialise the cache, but use AppTokenCache instead of the UserTokenCache
cca.AppTokenCache.SetBeforeAccess(notificationArgs => ...);
cca.AppTokenCache.SetAfterAccess(notificationArgs => ...);
var result = await cca.AcquireTokenForClientAsync(...)
MSAL has some internal code specifically to enable the ability to interact with legacy ADAL cache. When MSAL and ADAL are not used side by side (therefore the legacy cache is not used), the related legacy cache code is unnecessary. MSAL 4.25.0 adds the ability to disable legacy ADAL cache code and improve cache usage performance. See pull request #2309 for performance comparison before and after disabling the legacy cache. Call .WithLegacyCacheCompatibility(false)
on an application builder like below.
var builder = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithClientSecret(clientSecret)
.WithLegacyCacheCompatibility(false)
.Build();
Sample | Platform | Description |
---|---|---|
active-directory-dotnet-desktop-msgraph-v2 | Desktop (WPF) | Windows Desktop .NET (WPF) application calling the Microsoft Graph API. ![]() |
active-directory-dotnet-v1-to-v2 | Desktop (Console) | Set of Visual Studio solutions illustrating the migration of Azure AD v1.0 applications (using ADAL.NET) to Azure AD v2.0 applications, also named converged applications (using MSAL.NET), in particular Token Cache Migration |
- Home
- Why use MSAL.NET
- Is MSAL.NET right for me
- Scenarios
- Register your app with AAD
- Client applications
- Acquiring tokens
- MSAL samples
- Known Issues
- AcquireTokenInteractive
- WAM - the Windows broker
- .NET Core
- Maui Docs
- Custom Browser
- Applying an AAD B2C policy
- Integrated Windows Authentication for domain or AAD joined machines
- Username / Password
- Device Code Flow for devices without a Web browser
- ADFS support
- Acquiring a token for the app
- Acquiring a token on behalf of a user in Web APIs
- Acquiring a token by authorization code in Web Apps
- High Availability
- Token cache serialization
- Logging
- Exceptions in MSAL
- Provide your own Httpclient and proxy
- Extensibility Points
- Clearing the cache
- Client Credentials Multi-Tenant guidance
- Performance perspectives
- Differences between ADAL.NET and MSAL.NET Apps
- PowerShell support
- Testing apps that use MSAL
- Experimental Features
- Proof of Possession (PoP) tokens
- Using in Azure functions
- Extract info from WWW-Authenticate headers
- SPA Authorization Code