The New Token Cache in ADAL v2

image

The release candidate of ADAL v2 introduces a new cache model, which makes possible to cache tokens in middle tier apps and dramatically simplifies creating custom caches.
In this post I’ll give you  a brief introduction to the new model. You can see the new cache in action in our updates samples.

Limitations of the ADAL v1 Model

The token cache in ADAL plays a key role in keeping the programming model approachable. The fact that ADAL saves tokens (of all kinds: access, refresh, id) and token metadata (requesting client, target resource, user who obtained the token, tenant, whether the refresh token is an MRRT…) allows you to simply keep calling AcquireToken, knowing that behind the scenes ADAL will make the absolute best use of the cached information to give you the token you need while minimizing prompts.

Another role fulfilled by the ADAL cache is to offer you a view on the current authentication status of your application: by interrogating the cache you can discover whether you have access to a given resource, if you have tokens for a specific set of user accounts, and so on.

In ADAL v1, the cache implementation reflected the primary target scenarios of that version of the library: native clients. The cache implemented as an IDictionary, with a specific key type which reflected the domain-specific info necessary for handling tokens. That fulfilled the two functions outlined earlier, keeping track of the data we needed and offering an easy way of querying the collection (via LINQ). That did its job for native clients, however that was unsuitable for use on mid-tier apps. Think of a web app with few millions of users, each of them with few tokens stored for calling API in the context of their sessions – the resulting dictionary would have been pretty hard to scale. For that reason, AcquireToken* implementations in ADAL v1 meant to prevalently run on the server side did not make use of ADAL’s cache.

Another shortcoming of the v1 model was that providing a custom cache required you to implement IDictionary, not rocket science but certainly an onerous task. Furthermore: many of the elements in the key type were really meant for your own queries and were never used by AcquireToken. We were aware of the complications, but given the asymmetry between producers of custom cache implementations and consumers of such classes (the latter vastly outnumbering the former) we made the tradeoff. When this was picked up in v2, though our awesome dev team found a way of avoiding the tradeoff altogether – designing a new model that delivers on both functions AND that is a breeze to implement!

The New Cache Model

The idea of the new cache model is pretty simple: ADAL manages the cache structure as a private implementation detail, but gives you the means to 1) provide a persistence layer for it, so that you can use your favorite store to hold it and 2) it still allows you to obtain views of the cache content, so that you can gain insights on the capabilities of your application without being exposed to the internals to how the various cache entries are actually maintained. It sounds pretty awesome, right? Smile

You create a custom cache by deriving from the new TokenCache class, and passing an instance of such class at AuthenticationContext construction time. Here there’s how it looks like in VS’ Class View:

image

There’s quite a lot of stuff there, but in fact you need to touch only 3-4 things.
Here there’s how it works.

TokenCache features three notifications, BeforeAccess, BeforeWrite and AfterAccess, that are activated whenever ADAL does work against the cache. Those notifications offer you the opportunity of keeping in sync the storage of your choice with the in-memory cache that ADAL uses.

Say that you start your app for the very first time, and you make a call to AcquireToken(resource1, clientId, redirectUri). From the cache’s point of view, how does that unfold?

  1. ADAL needs to check the cache to see if there is already an access token for resource1 obtained by client1, or if there is a refresh token good for obtaining such an access token, and whatever other private heuristic you don’t need to worry about. Right before it reads the cache, ADAL calls the BeforeAccess notification. Here, you have the opportunity of retrieving your persisted cache blob from wherever you chose to save it, and pump it in ADAL. You do so by passing that blob to Deserialize.
    Note that you can apply all kind of heuristics to decide whether the existing inmemory copy is still OK to reduce the times in which you access your persistent store.
  2. As we said, this is the first time that the application runs: hence the cache will (typically) be empty. Hence, ADAL pops out the authentication UX and guides the user through the authentication experience. Once it obtains a new token, it needs to save it in the cache: but right before that, it invokes the BeforeWrite notification. That gives you the opportunity of applying whatever concurrency strategy you want to enact: for example, on a mid tier app you might decide to place a lock on your blob – so that other nodes in your farm possibly attempting a write at the same time would avoid producing conflicting updates. If you are optimistic, of course you can decide to simply do nothing heer :Winking smile
  3. After ADAL added the new token in its in-memory copy of the cache, it calls the AfterAccess notification. That notification is in fact called every time ADAL accessed the cache, not just when a write took place: however you can always tell if the current operation resulted in a cache change, as in that case the property HasStateChanged will be set to true. If that is the case, you will typically call Serialize() to get a binary blob representing the latest cache content – and save it in your storage. After that, it will be your responsibility to clear whatever lock you might have set.
    Very important: ADAL NEVER automatically reset HasStateChanged to false. You have to do it in your own code once you are satisfied that you handled the event correctly.

Those are the main moving parts you need to handle. Other important aspects to consider concern the lifecycle of the cache instance outside of its use from AcquireToken. For example, you’ll likely want to populate the cache from your store at construction time; you’ll want to override Clear and DeleteItem to ensure that you reflect cache state changes; and so on.

You might wonder why you can’t just wait for the first access and leave to the notifications to do the first initialization. That’s tricky. You could do that, but then if you’d need to query the cache before requesting the first token you’d be in trouble. Think of a multi-tenant client: on first access you’ll use common as the authority, but for subsequent accesses you want to use the authority corresponding to the user that actually signed in and initialized the app to its own tenant. If you don’t do that, you’ll never hit the cache during AcquireToken given that using “common” is equivalent to say “I don’t know what tenant to use”.

If you want to query the cache, you can call ReadItems() to pull out an IEnumerable of TokenCacheItems.

Pretty easy, right?

Here there’s one of my favorite benefits of this model: from ADAL v2 RC on, AcquireTokenByAuthorizationCode does save tokens in the cache! This will result in a tremendous reduction in the amount of code that it is necessary on middle tier applications, as you’ll see in the updated samples.

Examples

Here there’s a simple example. Say that I am writing a Windows desktop app, and I want to save tokens so that I don’t have to re-authenticate every single time I launch the application. I decide to save the tokens in a DPAPI-protected file. Here there’s a super simple cache implementation doing that:

// This is a simple persistent cache implementation for a desktop application.
// It uses DPAPI for storing tokens in a local file.
class FileCache : TokenCache
{
    public string CacheFilePath;
    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 FileCache(string filePath=@".\TokenCache.dat")
    {
        CacheFilePath = filePath;
        this.AfterAccess = AfterAccessNotification;
        this.BeforeAccess = BeforeAccessNotification;
        lock (FileLock)
        {
            this.Deserialize(File.Exists(CacheFilePath) ? 
                ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath), 
                                        null, 
                                        DataProtectionScope.CurrentUser) 
                : null);
        }
    }

    // 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(File.Exists(CacheFilePath) ?  
                ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
                                        null,
                                        DataProtectionScope.CurrentUser) 
                : null);
        }
    }

    // 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
                File.WriteAllBytes(CacheFilePath, 
                    ProtectedData.Protect(this.Serialize(),
                                            null,
                                            DataProtectionScope.CurrentUser));
                // once the write operation took place, restore the HasStateChanged bit to false
                this.HasStateChanged = false;
            }                
        }
    }
}

That is really super-simple code, if compared to having to implement an entire IDictionary.

Want to see something a bit more challenging?

Say that I have a web application. My web app connects to web APIs on behalf of its users. Every user has a set of tokens that are saved in a SQL DB, so that when they sign in to the web app they can directly perform their web API calls without having to re-authenticate/repeat consent. The new ADAL cache model makes it pretty easy to achieve this: we can have a flat list of blobs, all representing an ADAL cache for a specific web user. When the user signs in, we retrieve the corresponding blob and use it to initialize his/her token cache. That’s exactly how we implemented the cache in the updated multitenant samples. Here there’s the implementation:

public class PerWebUserCache
{
    [Key]
    public int EntryId { get; set; }
    public string webUserUniqueId { get; set; }
    public byte[] cacheBits { get; set; }
    public DateTime LastWrite { get; set; }
}

public class EFADALTokenCache: TokenCache
{
    private TodoListWebAppContext db = new TodoListWebAppContext();
    string User;
    PerWebUserCache Cache;
    
    // constructor
    public EFADALTokenCache(string user)
    {
       // associate the cache to the current user of the web app
        User = user;
        
        this.AfterAccess = AfterAccessNotification;
        this.BeforeAccess = BeforeAccessNotification;
        this.BeforeWrite = BeforeWriteNotification;

        // look up the entry in the DB
        Cache = db.PerUserCacheList.FirstOrDefault(c => c.webUserUniqueId == User);
        // place the entry in memory
        this.Deserialize((Cache == null) ? null : Cache.cacheBits);
    }

    // clean up the DB
    public override void Clear()
    {
        base.Clear();
        foreach (var cacheEntry in db.PerUserCacheList)
            db.PerUserCacheList.Remove(cacheEntry);
        db.SaveChanges();
    }

    // Notification raised before ADAL accesses the cache.
    // This is your chance to update the in-memory copy from the DB, if the in-memory version is stale
    void BeforeAccessNotification(TokenCacheNotificationArgs args)
    {
        if (Cache == null)
        {
            // first time access
            Cache = db.PerUserCacheList.FirstOrDefault(c => c.webUserUniqueId == User);
        }
        else
        {   // retrieve last write from the DB
            var status = from e in db.PerUserCacheList
                         where (e.webUserUniqueId == User)
                         select new
                         {
                             LastWrite = e.LastWrite
                         };
            // if the in-memory copy is older than the persistent copy
            if (status.First().LastWrite > Cache.LastWrite)
            //// read from from storage, update in-memory copy
            {
                Cache = db.PerUserCacheList.FirstOrDefault(c => c.webUserUniqueId == User);
            }
        }
        this.Deserialize((Cache == null) ? null : Cache.cacheBits);
    }
    // Notification raised after ADAL accessed the cache.
    // If the HasStateChanged flag is set, ADAL changed the content of the cache
    void AfterAccessNotification(TokenCacheNotificationArgs args)
    {
        // if state changed
        if (this.HasStateChanged)
        {
            Cache = new PerWebUserCache
            {
                webUserUniqueId = User,
                cacheBits = this.Serialize(),
                LastWrite = DateTime.Now
            };
            //// update the DB and the lastwrite                
            db.Entry(Cache).State = Cache.EntryId == 0 ? EntityState.Added : EntityState.Modified;                
            db.SaveChanges();
            this.HasStateChanged = false;
        }
    }
    void BeforeWriteNotification(TokenCacheNotificationArgs args)
    {
        // if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry
    }
}

Also in this case, the implementation is pretty self explanatory: I disregarded locks and only added a little timestamp check to avoid swapping potentially sizable blobs from the DB when it’s not necessary.

Wrap

I have a confession to make: although I was always bummed by the lack of a viable token caching solution on the server side, and consequent need for complex code for confidential clients, I didn’t believe that a cache redesign would have been possible so late in the cycle given the time constraints we are up against (we want to release soon!!). However the dev team was very passionate about solving that problem, and worked very hard to deliver a design that blew me away – it satisfied all requirements without affecting schedule! So big kudos to the dev team, especially to Afshin Smile

All the new features discussed here apply to both ADAL .NET and ADAL for Windows Store. However, that does NOT change the defaults: ADAL .NET OOB cache remains in-memory, ADAL for Windows Store retains its default persistent cache. All the news only apply to how you’d implement a custom cache, should you choose to write one. If you need a starting point, you can look at the above snippets. If you want to see them in action, you’ll find them (and possibly others) in our github samples.

Enjoy!

Leave a Reply

Your email address will not be published. Required fields are marked *