Cross platform Token Cache - AzureAD/microsoft-authentication-extensions-for-dotnet GitHub Wiki
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()}");
};
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 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:
- https://github.com/AzureAD/microsoft-authentication-extensions-for-python
- https://github.com/AzureAD/microsoft-authentication-extensions-for-java
- https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/extensions/msal-node-extensions/README.md - nodeJS
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.