Here is a sample TokenCache class implementation using Redis for use with the Active Directory Access Library (ADAL). The library is used for obtaining tokens from Azure AD or AD FS using the OAuth2 protocol. This implementation is intended for web applications acting as OAuth2 or OpenIDConnect clients. Typical use of this class is in the AuthenticationContext constructor:
var authContext = new AuthenticationContext(Authority, new RedisTokenCache(signedInUserID)); |
Note that this is sample code, not intended for production purposes. To use it, you need to provide the Redis connection string in the application’s configuration file with the ‘CacheConnection’ string as key.
public class RedisTokenCache: TokenCache { private string userId; private UserTokenCache Cache; public RedisTokenCache(string signedInUserId) { // associate the cache to the current user of the web app userId = signedInUserId; this.AfterAccess = AfterAccessNotification; this.BeforeAccess = BeforeAccessNotification; this.BeforeWrite = BeforeWriteNotification; // look up the entry in the cache var cache = Utils.Redis.Connection.GetDatabase(); try { var cachedItem = cache.StringGet(userId); if (cachedItem.HasValue) { Cache = JsonConvert.DeserializeObject<UserTokenCache>(cachedItem); this.Deserialize((Cache == null) ? null : MachineKey.Unprotect(Cache.cacheBits, "ADALCache")); } } catch(Exception ex) { Trace.WriteLine("Exception in RedisTokenCache(id): " + ex.Message); Cache = null; } } // clean up the database public override void Clear() { base.Clear(); try { var cache = Utils.Redis.Connection.GetDatabase(); cache.KeyDelete(userId); } catch(Exception ex) { Trace.WriteLine("Exception in RedisTokenCache.Clear: " + ex.Message); } } // 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) { try { var cache = Utils.Redis.Connection.GetDatabase(); var cachedItem = cache.StringGet(userId); if (cachedItem.HasValue) { var status = JsonConvert.DeserializeObject<UserTokenCache>(cachedItem); if ((Cache != null) && (status.LastWrite > Cache.LastWrite)) { Cache = status; this.Deserialize((Cache == null) ? null : MachineKey.Unprotect(Cache.cacheBits, "ADALCache")); } } } catch(Exception ex) { Trace.WriteLine("Exception in RedisTokenCache.BeforeAccessNotification: " + ex.Message); } } } // 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 UserTokenCache { cacheBits = MachineKey.Protect(this.Serialize(), "ADALCache"), LastWrite = DateTime.Now }; // update the Redis cache and the lastwrite try { var cache = Utils.Redis.Connection.GetDatabase(); var cacheItemJson = JsonConvert.SerializeObject(Cache); cache.StringSet(userId, cacheItemJson, TimeSpan.FromDays(1)); // could we use token expiry somehow? } catch (Exception ex) { Trace.WriteLine("Exception in RedisTokenCache.AfterAccessNotification: " + ex.Message); } 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 } public override void DeleteItem(TokenCacheItem item) { base.DeleteItem(item); try { var cache = Utils.Redis.Connection.GetDatabase(); var cachedItem = cache.KeyDelete(userId); } catch (Exception ex) { Trace.WriteLine("Exception in RedisTokenCache.DeleteItem: " + ex.Message); } } } public class Redis { // Redis Connection string info private static Lazy lazyConnection = new Lazy(() => { string cacheConnection = ConfigurationManager.AppSettings["CacheConnection"].ToString(); return ConnectionMultiplexer.Connect(cacheConnection); }); public static ConnectionMultiplexer Connection { get { return lazyConnection.Value; } } } |