-
Notifications
You must be signed in to change notification settings - Fork 214
Token cache serialization
It's relatively expensive to get an access token, because it requires an HTTP request to the token endpoint. Therefore, it's good to cache tokens whenever possible. ADAL.NET provides a default implementation for public client applications, in most platforms. It does not provide a default implementation for Web App / Web APIs, and for desktop applications.
In ADAL.NET a token cache is provided by default. The serialization is provided by default by the ADAL 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 and Xamarin.Android.
On iOS, you will need to enable keychain groups in the Entitlements.plist
file to the custom entitlements, in order for tokens to be persisted in the key chain.
Also, ensure the iOS bundle identifier matches the keyvault entitlement. When signing a certificate, make sure XCode uses the same Apple Id.
You might need to include the app identifier prefix in the key chain groups in Entitlements.plist
. The app identifier prefix is created when you register an application with Apple and have a profile created with them.
From ADAL 4.x, you can specify the Keychain Security Group to use for persisting the token cache. This enables you to share the token cache between several applications having the same keychain security group (ADAL.NET and MSAL.NET Xamarin.iOS applications as well as native iOS applications developed with ADAL.objc or MSAL.objc). For this you need to set the KeychainSecurityGroup
property of AuthenticationContext
to the same value in all the applications.
In the case of .NET Framework and .NET core, the libraries also provide a default cache but this only lasts for the duration of the application. To understand why, remember that ADAL .NET desktop/core applications can be Web applications or Web API, which might use some specific cache mechanisms like databases, a Redis cache, etc .... 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 for public client applications and confidential client applications.
The types participating in the Token cache customization in ADAL.NET are:
TokenCache
-
TokenCache.TokenCacheNotification
andTokenCacheNotificationArgs
which is a delegate (and its associated event arg) that represents the signature of methods which will react to cache events (AfterAccess
,BeforeAccess
, andBeforeWrite
) -
TokenCacheItem
which represents the information about items in the token cache.
When application developers using ADAL.NET want to implement a custom serialization, they will inherit from TokenCache
and set at least the BeforeAccess
(to deserialize the cache from its persistent storage) and AfterAccess
(to serialize the cache to its persistent storage).
In ADAL V4.x you have two options, depending on if you want to serialize the cache only to the ADAL V3 format, or if you also want to serialize it with the new unified cache format, which is common with MSAL, but also across the platforms.
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 in the following sample: active-directory-dotnet-v1-to-v2
Below is an example of a secure implementation of cache serialization for a desktop public client application (.NET Framework). This serializes the cache with the ADAL 3.x format, and this code can be used in ADAL 3.x and ADAL 4.x applications if you don't want additional compatibility with MSAL (next generation's library)
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.IO;
using System.Security.Cryptography;
namespace ADALV3Sample
{
// This is a simple persistent cache implementation for an ADAL V3 desktop application
class FilesBasedAdalV3TokenCache : TokenCache
{
public string CacheFilePath { get; }
private static readonly object FileLock = new object();
// Initializes the cache against a local file.
// If the file is already present, it loads its content in the ADAL cache
public FilesBasedAdalV3TokenCache(string filePath)
{
CacheFilePath = filePath;
this.AfterAccess = AfterAccessNotification;
this.BeforeAccess = BeforeAccessNotification;
lock (FileLock)
{
this.Deserialize(ReadFromFileIfExists(CacheFilePath));
}
}
// Empties the persistent store.
public override void Clear()
{
base.Clear();
File.Delete(CacheFilePath);
}
// Triggered right before ADAL needs to access the cache.
// Reload the cache from the persistent store in case it changed since the last access.
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
lock (FileLock)
{
this.Deserialize(ReadFromFileIfExists(CacheFilePath));
}
}
// Triggered right after ADAL accessed the cache.
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (this.HasStateChanged)
{
lock (FileLock)
{
// reflect changes in the persistent store
WriteToFileIfNotNull(CacheFilePath, this.Serialize());
// once the write operation took place, restore the HasStateChanged bit to false
this.HasStateChanged = false;
}
}
}
/// <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 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);
}
}
}
}
ADAL V4, in addition to the pre-existing Deserialize(byte[] adalState)
method, introduces methods that enable you to serialize and deserialize the cache in a dual format:
public class TokenCache
{
…
public void DeserializeAdalAndUnifiedCache(CacheData cacheData);
public CacheData SerializeAdalAndUnifiedCache();
…
}
With CacheData
being a new data structure, shared by both ADAL V4 and MSAL V2, and containing blobs corresponding to the token cache in both the ADAL V3 format, and the new Unified cache format, which is shared between ADAL and MSAL and across platforms (See next paragraph for an example for iOS)
namespace Microsoft.Identity.Core.Cache
{
public class CacheData
{
public byte[] AdalV3State { get; set; }
public byte[] UnifiedState { get; set; }
}
}
In ADAL.NET V4, you can add serialization with the new Unified cache format (which is really a json blob), therefore getting compatibility with MSAL.NET V2.x. The code to write is then slightly different as you will serialize both cache representations to two different files. Here is some ready to reuse code for .NET framework applications:
using Microsoft.Identity.Core.Cache;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.IO;
using System.Security.Cryptography;
namespace ADALV4Sample
{
// This is a simple persistent cache implementation for a desktop application (from ADAL 4.x)
class FilesBasedTokenCache : TokenCache
{
public string AdalV3CacheFilePath { get; }
public string UnifiedCacheFilePath { get; }
private static readonly object FileLock = new object();
// Initializes the cache against a local file.
// If the file is already present, it loads its content in the ADAL cache
public FilesBasedTokenCache(string adalV3FilePath, string unifiedCacheFilePath)
{
AdalV3CacheFilePath = adalV3FilePath;
this.AfterAccess = AfterAccessNotification;
this.UnifiedCacheFilePath = unifiedCacheFilePath;
this.BeforeAccess = BeforeAccessNotification;
lock (FileLock)
{
CacheData cacheData = new CacheData();
cacheData.AdalV3State = ReadFromFileIfExists(AdalV3CacheFilePath);
cacheData.UnifiedState = ReadFromFileIfExists(UnifiedCacheFilePath);
this.DeserializeAdalAndUnifiedCache(cacheData);
}
}
// Empties the persistent store.
public override void Clear()
{
base.Clear();
File.Delete(AdalV3CacheFilePath);
File.Delete(UnifiedCacheFilePath);
}
// Triggered right before ADAL needs to access the cache.
// Reload the cache from the persistent store in case it changed since the last access.
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
lock (FileLock)
{
CacheData cacheData = new CacheData();
cacheData.AdalV3State = ReadFromFileIfExists(AdalV3CacheFilePath);
cacheData.UnifiedState = ReadFromFileIfExists(UnifiedCacheFilePath);
this.DeserializeAdalAndUnifiedCache(cacheData);
}
}
// Triggered right after ADAL accessed the cache.
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (this.HasStateChanged)
{
lock (FileLock)
{
// reflect changes in the persistent store
CacheData cacheData = this.SerializeAdalAndUnifiedCache();
WriteToFileIfNotNull(AdalV3CacheFilePath, cacheData.AdalV3State);
WriteToFileIfNotNull(UnifiedCacheFilePath, cacheData.UnifiedState);
// once the write operation took place, restore the HasStateChanged bit to false
this.HasStateChanged = false;
}
}
}
/// <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 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);
}
}
}
}
The default default token cache implementation in ADAL.NET is intended for native client apps, and is not suitable for web apps:
- It is a static instance, and not thread safe.
- It doesn't scale to large numbers of users, because tokens from all users go into the same dictionary.
- It can't be shared across web servers in a farm.
Instead, you should implement a custom token cache that derives from the ADAL TokenCache class but is suitable for a server environment and provides the desirable level of isolation between tokens for different users.
ASP.NET Web applications may want to persist tokens to a database. This can be tricky because of the extra concern of encryption. You have an example of a production ready cache when you create a new ASP.NET application with Visual Studio, setting the authentication to "work & school account", and checking the "directory read" checkbox. The resulting project will have some code for the cache.
For more details, see the DbTokenCache class in TodoListService/DAL/DbTokenCache.cs in the active-directory-dotnet-webapi-onbehalfof sample on GitHub.
You might also want read Azure / Architecture / Manage Identity in Multitenant Applications / Cache access tokens which discusses Encrypting cached tokens and a distributed token cache implementation.
ASP.NET Core provides a notion of distributed cache. The following Stack overflow question shows an implementation of a distributed cache for ADAL.NET: .Net Core 2.0 - Get AAD access token to use with Microsoft Graph
Sample | Platform | Description |
---|---|---|
active-directory-dotnet-native-desktop | Desktop (WPF) -> ASP.NET | A .NET 4.5 WPF application that authenticates a user and calls web API using Azure AD and OAuth 2.0 access tokens. This is a client side file cache |
active-directory-dotnet-webapi-onbehalfof | Desktop (WPF), SPA (JavaScript), Web API (ASP.NET MVC) | A .NET 4.5 MVC Web API protected by Azure AD that receives tokens from a client and uses ADAL to get tokens for calling the Microsoft Graph. Shows a database cache on the service side |
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 ADAL.NET?
- Register your app with AAD
- AuthenticationContext
- Acquiring Tokens
- Calling a protected API
- Acquiring a token interactively
- Acquiring tokens silently
- Using Device Code Flow
- Using Embedded Webview and System Browser in ADAL.NET and MSAL.NET
- With no user
- In the name of a user
- on behalf of (Service to service calls)
- by authorization code (Web Apps)
- Use async controller actions
- Exception types
- using Broker on iOS and Android
- Logging
- Token Cache serialization
- User management
- Using ADAL with a proxy
- Authentication context in multi-tenant scenarios
- Troubleshooting MFA in a WebApp or Web API
- Provide your own HttpClient
- iOS Keychain Access