Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 501
Michel Barnett Avatar asked Feb 05 '23 08:02

Michel Barnett


1 Answers

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
        }
    }

}
like image 189
dstrockis Avatar answered Feb 23 '23 07:02

dstrockis