-
Notifications
You must be signed in to change notification settings - Fork 357
Token cache serialization 2x
In MSAL.NET like in ADAL.NET, a token cache is provided by default. The serialization is provided by default by the ADAL and MSAL libraries for a certain number of platforms where a secure storage is available for a user as part of the platform. This is for instance 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 2.x, you might want to add |
android:allowBackup="false"
to your project to avoid old cached token keeping coming back because Visual Studio deployments are triggering a restore of local storage. See #659
In the case of .NET Framework and .NET core, libraries also provide a default cache but this only lasts for the duration of the application. To understand why, remember that ADAL|MSAL .NET desktop/core applications can be Web applications or Web API, which might use some specific caches mechanisms like databases. To have a persistent token cache application in .NET Desktop or Core developers need to customize the serialization. The way to do it, is different in ADAL.NET and MSAL.NET, though.
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 involved in TokenCache serialization are the following:
-
TokenCache
. MSAL.NET exposes for this class way less public methods and properties than ADAL.NET. Also it's a sealed class and the token cache "events" delegates (as in the case of ADAL.NET) are not available directly in TokenCache, but are defined in an extension class:TokenCacheExtensions
(see below) -
TokenCacheNotification
and the associated even argsTokenCacheNotificationArgs
. Here again theTokenCacheNotificationArgs
only provides theClientId
of the application and a reference to the user for which the token is available -
TokenCacheExtensions
* which is unique to MSAL.NET, provides extension methods for the TokenCache, that application developers need to use only when overriding the serialization.
You probably understood it, in MSAL.NET, the idea is this time that the application developer won't inherit from TokenCache
(it's sealed), but, given an instance of Token cache, s/he will set delegates on the token cache to react to BeforeAccess
and AfterAccess
"events". The BeforeAccess
delegate is responsible to deserialize the cache, whereas the AfterAccess is responsible of serializing the cache.
Since MSAL V2.x you have two 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 4.x and MSAL.NET is explained 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.
Below is an example of a naive implementation of custom serialization of a token cache (here the user token cache for a public client application).
The constructor gets the token cache by calling TokenCacheHelper.GetUserCache()
PublicClientApplication _clientApp = new PublicClientApplication(ClientId, Authority,
TokenCacheHelper.GetUserCache());
This helper class looks like the following.
static class TokenCacheHelper
{
/// <summary>
/// Get the user token cache
/// </summary>
/// <returns></returns>
public static TokenCache GetUserCache()
{
if (usertokenCache == null)
{
usertokenCache = new TokenCache();
usertokenCache.SetBeforeAccess(BeforeAccessNotification);
usertokenCache.SetAfterAccess(AfterAccessNotification);
}
return usertokenCache;
}
static TokenCache usertokenCache;
/// <summary>
/// Path to the token cache
/// </summary>
public static readonly string CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin";
public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
args.TokenCache.Deserialize(File.Exists(CacheFilePath)
? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
null,
DataProtectionScope.CurrentUser)
: null);
}
public static void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (args.HasStateChanged)
{
// reflect changesgs in the persistent store
File.WriteAllBytes(CacheFilePath,
ProtectedData.Protect(args.TokenCache.Serialize(),
null,
DataProtectionScope.CurrentUser)
);
}
}
}
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 MSAL of the same generation or older, on the same platform), you can get inspired by the following code:
string cacheFolder = Path.GetFullPath(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + @"..\..\..\..");
string adalV3cacheFileName = Path.Combine(cacheFolder, "cacheAdalV3.bin");
string unifiedCacheFileName = Path.Combine(cacheFolder, "unifiedCache.bin");
TokenCache tokenCache = FilesBasedTokenCacheHelper.GetUserCache(unifiedCacheFileName, adalV3cacheFileName);
This time the helper class looks like the following:
using System.IO;
using System.Security.Cryptography;
using Microsoft.Identity.Client;
using Microsoft.Identity.Core.Cache;
namespace CommonCacheMsal2
{
/// <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 accross ADAL/MSAL on the same platform. Should not be <c>null</c></param>
/// <returns></returns>
public static TokenCache GetUserCache(string unifiedCacheFileName, string adalV3CacheFileName)
{
UnifiedCacheFileName = unifiedCacheFileName;
AdalV3CacheFileName = adalV3CacheFileName;
if (usertokenCache == null)
{
usertokenCache = new TokenCache();
usertokenCache.SetBeforeAccess(BeforeAccessNotification);
usertokenCache.SetAfterAccess(AfterAccessNotification);
}
return usertokenCache;
}
/// <summary>
/// Token cache
/// </summary>
static TokenCache usertokenCache;
/// <summary>
/// File path where the token cache is serialiazed 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 serialiazed with the legacy ADAL V3 format
/// </summary>
public static string AdalV3CacheFileName { get; private set; }
public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
CacheData cacheData = new CacheData
{
UnifiedState = ReadFromFileIfExists(UnifiedCacheFileName),
AdalV3State = ReadFromFileIfExists(AdalV3CacheFileName)
};
args.TokenCache.DeserializeUnifiedAndAdalCache(cacheData);
}
public static void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (args.HasStateChanged)
{
CacheData cacheData = args.TokenCache.SerializeUnifiedAndAdalCache();
// reflect changesgs in the persistent store
WriteToFileIfNotNull(UnifiedCacheFileName, cacheData.UnifiedState);
if (!string.IsNullOrWhiteSpace(AdalV3CacheFileName))
{
WriteToFileIfNotNull(AdalV3CacheFileName, cacheData.AdalV3State);
}
}
}
/// <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 = (protectedBytes != null) ? ProtectedData.Unprotect(protectedBytes, null,
DataProtectionScope.CurrentUser) : null;
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 = ProtectedData.Protect(blob, null, DataProtectionScope.CurrentUser);
File.WriteAllBytes(path, protectedBytes);
}
else
{
File.Delete(path);
}
}
}
}
In the case of Web Apps or Web APIs, the cache can be very different, leveraging the session, or a Redis cache, or a database. Below is an example of Session cache extracted from the Models/MSALSesssionCache.cs file in the Azure-Samples/active-directory-dotnet-webapp-openidconnect-v2 code sample.
The way to use it is illustrated in the HomeController#L56
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, this.HttpContext).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(clientId,
redirectUri,
new ClientCredential(appKey),
userTokenCache, null);
And the implementation of the Token cache itself is the following:
using Microsoft.Identity.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;
namespace WebApp_OpenIDConnect_DotNet.Models
{
public class MSALSessionCache
{
private static ReaderWriterLockSlim SessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
string UserId = string.Empty;
string CacheId = string.Empty;
HttpContextBase httpContext = null;
TokenCache cache = new TokenCache();
public MSALSessionCache(string userId, HttpContextBase httpcontext)
{
// not object, we want the SUB
UserId = userId;
CacheId = UserId + "_TokenCache";
httpContext = httpcontext;
Load();
}
public TokenCache GetMsalCacheInstance()
{
cache.SetBeforeAccess(BeforeAccessNotification);
cache.SetAfterAccess(AfterAccessNotification);
Load();
return cache;
}
public void SaveUserStateValue(string state)
{
SessionLock.EnterWriteLock();
httpContext.Session[CacheId + "_state"] = state;
SessionLock.ExitWriteLock();
}
public string ReadUserStateValue()
{
string state = string.Empty;
SessionLock.EnterReadLock();
state = (string)httpContext.Session[CacheId + "_state"];
SessionLock.ExitReadLock();
return state;
}
public void Load()
{
SessionLock.EnterReadLock();
cache.Deserialize((byte[])httpContext.Session[CacheId]);
SessionLock.ExitReadLock();
}
public void Persist()
{
SessionLock.EnterWriteLock();
// Optimistically set HasStateChanged to false. We need to do it early to avoid losing changes made by a concurrent thread.
// Reflect changes in the persistent store
httpContext.Session[CacheId] = cache.Serialize();
SessionLock.ExitWriteLock();
}
// Triggered right before MSAL needs to access the cache.
// Reload the cache from the persistent store in case it changed since the last access.
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
Load();
}
// Triggered right after MSAL accessed the cache.
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (args.HasStateChanged)
{
Persist();
}
}
}
}
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