Search code examples
azure-active-directoryadal

Recommended ADAL token cache for Web API?


I'm building a .NET core Web API that’s secured using AAD and which uses ADAL to call a downstream API using the on-behalf-of flow…. similar to this Azure Sample:

https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof

What are the best practices for the token cache that should be used in a scenario like this?

  • Is the default cache acceptable?

  • Should you ever not have a cache?

    AuthenticationContext authContext = new AuthenticationContext(authority, null)

  • If you should build your own then is there a good reference implementation to use?


Solution

  • The right token cache for you to use is very subjective, and really depends on your architecture, performance requirements, and so on.

    The default cache used by ADAL is an in-memory cache, which means that it may not persist across requests that your API receives. Furthermore, the default cache used by ADAL.NET is a static class, meaning that two different requests to your API might pick up the same cache object, which is often unexpected since those two requests are likely for different users. So using the default ADAL cache is generally not recommended - it really depends on how your web server works.

    Instead, we recommend passing null as the token cache if you can manage the performance hit, or preferably implementing your own token cache.

    If you want to implement your own cache, it will save your app from having to make an outbound HTTP request to AAD (via ADAL) on every single incoming request. A sample ADAL cache implementation using the .NET entity framework is available here, and copied below as well:

    using Microsoft.IdentityModel.Clients.ActiveDirectory;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Data.Entity;
    using System.Linq;
    using System.Web;
    
    namespace TodoListWebApp.DAL
    {
    
        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
            }
        }
    
    }