Skip to content
This repository has been archived by the owner on Sep 7, 2023. It is now read-only.

Cross platform Token Cache

Peter M edited this page Dec 28, 2022 · 26 revisions

Problem statement

MSAL requires developers to implement their own logic for persisting the token cache on .NET Core and .NET Classic desktop applications. Providing a secure location is a difficult problem, especially storage that can be accessed from C# APIs on Mac and Linux. For more details about token cache serialization and different scenarios, see these docs.

Goals

  • Provide a robust, secure and configurable token cache persistence implementation across Windows, Mac and Linux for public client applications (rich clients, CLI applications, etc.).
  • Token cache storage can be accessed by multiple processes concurrently.
  • Provide a higher-level event that signals when accounts are added or removed from the cache.

Non Goals

  • This cache implementation is not suitable for web app / web API scenarios, where storing the cache should be done in distributed storage (Redis, SQL Server, etc.) or in memory. See web samples for server-side implementations.

Thread safety

The cache logic is not thread safe, but it is cross-process safe. This means that if your app calls MSAL APIs from multiple threads, you should synchronize those calls. But 2 apps, or 2 instances of the same app, will access the token cache in an orderly fashion.

Code

Referencing the NuGet package

Add the Microsoft.Identity.Client.Extensions.Msal NuGet package to your project.

Configuring the token cache

All the arguments are explained in the API docs. For an example, see this config in the sample app.

 var storageProperties =
     new StorageCreationPropertiesBuilder(Config.CacheFileName, Config.CacheDir)
     .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);
         

Advanced scenario: the CacheChanged event

In situations where 2 or more apps share the same token cache, you can subscribe to an event to figure out if the new accounts are added or removed while the app is still running. Not necessary when a single app uses the token cache.

cacheHelper.CacheChanged += (object sender, CacheChangedEventArgs args) =>
{
    Console.WriteLine($"Cache Changed, Added: {args.AccountsAdded.Count()} Removed: {args.AccountsRemoved.Count()}");
};

event The image above shows 2 applications that use the same client ID sharing the token cache. One of the apps logs in a new user, and both apps get notified of an account being added to the cache.

Sample

Have a look at a simple console app using this token cache. We use this for testing on Windows, Mac and Linux.

Security Boundary

On Windows and Linux, the token cache is scoped to the user session, i.e. all applications running on behalf of the user can access the cache. Mac offers a more restricted scope, ensuring that only the application that created the cache can access it, and prompting the user if others apps want access.

Encryption and storage

Windows

DPAPI is used to encrypt the token cache. The encrypted data is stored in a file in LocalAppData folder.

Mac

The token cache is stored in the Mac KeyChain, which encrypts it on behalf of the user and the application itself.

Linux

The token cache is stored in the a wallet such as Gnome Keyring or KWallet using LibSecret. Its contents can be visualised using tools such as Gnome Seahorse.

linux token cache

Linux Fallback

KeyRings does not work in headless mode (e.g. when connected over SSH or when running Linux in a container) due to a dependency on X11. To overcome this, a fallback to a plaintext file can be configured. See this example for how to configure it.

Mac and Windows fallback

In rare cases, encryption at rest fails. App developers can configure plaintext credential storage as a fallback.

Important warning about plaintext fallback

Storing tokens in plaintext fallback is dangerous. Bearer tokens are not cryptographically bound to a machine and can be stolen. In particular, the refresh token can be used to get access tokens for many resources.

It is important to warn end-users before falling back to plaintext. End-users should ensure they store the tokens in a secure location (e.g. encrypted disk) and must understand they are responsible for their safety.

We recommend that UI applications do NOT use the fallback and just rely on MSAL's internal memory cache, which will reset when the app is restarted. CLI applications, where each command needs a token, may need to use a fallback.

Sharing the cache between multiple apps

Our vision for Single Sign On (SSO) across multiple applications is that a 3rd application - a broker - must intervene to perform account and device management. Today, there are brokers for Android (Authenticator, Company Portal), iOS (Authenticator), Windows (WAM), Mac (SSO Extension, deployed via Company Portal), Linux (in preview). MSAL libraries integrate with some of the brokers only.

As a stop gap solution, if you want SSO between your .NET, python or Java apps, consider using the same client ID for all your apps. Note that once you go down this route, you cannot make individual changes to applications (e.g. you cannot enable MFA for one app but not the others).

Other language implementations

Similar functionality exists in Java and Python libraries:

Architecture

For an architectural overview see cache architecture diagram

Cross process synchronisation is done using file locks, since this is the only mechanism available on all platforms. The eventing is also done using files and a file watcher. For the event to work, the cache is deserialized a second time.

Battle tested

Visual Studio family of apps, Azure Powershell and Azure CLI all use this approach.