-
Notifications
You must be signed in to change notification settings - Fork 378
custom token cache in public client applications
This article is about custom token cache implementations for public client applications. For context and more general information about token cache serialization see the parent page:Token cache serialization
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;
}
}- 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
- 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
- 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
- High Availability
- Regional
- 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