There is nothing more annoying than having to authenticate every time you launch an application. To avoid this, developers often implement a caching system to store authentication tokens. That being said, this cache must be sufficiently secure to prevent an attacker from retrieving and exploiting them.
This is particularly true for client applications (console and desktop), when the tokens are stored locally on the user’s machine. In this article, we will see how to use the cache provided by MSAL.NET (Microsoft Authentication Library for .NET) to securely store authentication tokens on disk for a .NET client application.
Introducing Microsoft.Identity.Client.Extensions.Msal
MSAL’s IPublicClientApplication
and IConfidentialClientApplication
have properties representing the token cache, UserTokenCache
for user-delegated tokens, and AppTokenCache
for application tokens, respectively. Both properties implement ITokenCache, which has methods to hook into cache save and load events.
The MSAL extensions package Microsoft.Identity.Client.Extensions.Msal uses this mechanism to implement secure and cross-platform cache persistence:
- On Windows, DPAPI is used to encrypt the token cache. The encrypted data is stored in a file in the LocalAppData folder.
- On Mac, the token cache is stored in the Mac KeyChain, which encrypts it on behalf of the user and the application itself.
- On Linux, the token cache is stored in a wallet such as Gnome Keyring or KWallet using LibSecret. Its contents can be visualized using tools such as Gnome Seahorse.
Usage
Here is an example of using the secured file-based cache provided by MSAL with an IPublicClientApplication
:
// Building the MSAL client
var app = PublicClientApplicationBuilder.Create("<client-id>")
.WithAuthority(AadAuthorityAudience.AzureAdMyOrg)
.WithTenantId("<my-tenant-id>")
.WithDefaultRedirectUri()
.Build();
// Initializing the cache storage properties
var storageProperties = new StorageCreationPropertiesBuilder("msalcache.bin", "/path/to/cache/directory/")
.WithLinuxKeyring(
schemaName: "com.myapp.tokencache",
collection: MsalCacheHelper.LinuxKeyRingDefaultCollection,
secretLabel: "MSAL token cache for my app.",
attribute1: new KeyValuePair<string, string>("Version", "1"),
attribute2: new KeyValuePair<string, string>("Product", "MyApp"))
.WithMacKeyChain(
serviceName: "com.myapp.tokencache.service",
accountName: "com.myapp.tokencache.account")
.Build();
// Registering the cache
var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties);
cacheHelper.RegisterCache(app.UserTokenCache);
// Using the MSAL client
var accounts = await app.GetAccountsAsync();
// [...]
When reading this code, it is normal to wonder what values to pass for the parameters of StorageCreationPropertiesBuilder
. Explanations can be found in the documentation for this cache implementation.
That said, I think one of the best ways to understand how to use an API is to look for usage examples, particularly in Microsoft repositories. A great example is the Microsoft Graph plugin for Semantic Kernel.
Going further
You can try implementing your own cache by hooking into the save and load events of the cache via ITokenCache.SetBeforeAccess
, ITokenCache.SetAfterAccess
, ITokenCache.SetBeforeWrite
, or their asynchronous versions.
Another example of MSAL cache implementation is Microsoft.Identity.Web, which allows using a hybrid two-level persistence cache (memory and distributed) for web applications: https://github.com/AzureAD/microsoft-identity-web/blob/2.20.0/src/Microsoft.Identity.Web.TokenCache/MsalAbstractTokenCacheProvider.cs.