From 0fed89e140c1500495e01f1f836416f107dcdbce Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 27 Sep 2024 02:38:18 +0200 Subject: [PATCH] Implementing AuthDataCache --- DotBased.ASP.Auth/AuthDataCache.cs | 91 +++++++++++++++++++ DotBased.ASP.Auth/BasedAuthBuilder.cs | 36 -------- DotBased.ASP.Auth/BasedAuthConfiguration.cs | 12 ++- .../Domains/Auth/AuthenticationStateModel.cs | 12 ++- .../DotBasedAuthDependencyInjection.cs | 19 ++-- ...DataProvider.cs => IAuthDataRepository.cs} | 15 +-- DotBased.ASP.Auth/MemoryAuthDataProvider.cs | 83 ----------------- DotBased.ASP.Auth/MemoryAuthDataRepository.cs | 91 +++++++++++++++++++ DotBased.ASP.Auth/Services/AuthService.cs | 13 +-- 9 files changed, 218 insertions(+), 154 deletions(-) create mode 100644 DotBased.ASP.Auth/AuthDataCache.cs delete mode 100644 DotBased.ASP.Auth/BasedAuthBuilder.cs rename DotBased.ASP.Auth/{IAuthDataProvider.cs => IAuthDataRepository.cs} (88%) delete mode 100644 DotBased.ASP.Auth/MemoryAuthDataProvider.cs create mode 100644 DotBased.ASP.Auth/MemoryAuthDataRepository.cs diff --git a/DotBased.ASP.Auth/AuthDataCache.cs b/DotBased.ASP.Auth/AuthDataCache.cs new file mode 100644 index 0000000..c01a47b --- /dev/null +++ b/DotBased.ASP.Auth/AuthDataCache.cs @@ -0,0 +1,91 @@ +using System.Collections.ObjectModel; +using DotBased.ASP.Auth.Domains.Auth; + +namespace DotBased.ASP.Auth; + +public class AuthDataCache +{ + public AuthDataCache(IAuthDataRepository dataRepository, BasedAuthConfiguration configuration) + { + DataRepository = dataRepository; + _configuration = configuration; + } + + public readonly IAuthDataRepository DataRepository; + private readonly BasedAuthConfiguration _configuration; + + private readonly CacheNodeCollection _authenticationStateCollection = []; + + public Result PurgeSessionFromCache(string id) => _authenticationStateCollection.Remove(id) ? Result.Ok() : Result.Failed("Failed to purge session state from cache!"); + + public async Task> RequestAuthStateAsync(string id) + { + if (_authenticationStateCollection.TryGetValue(id, out var node)) + { + if (node.Object == null) + { + _authenticationStateCollection.Remove(id); + return Result.Failed($"Returned object is null, removing entry [{id}] from cache!"); + } + + if (node.IsValidLifespan(_configuration.CachedAuthSessionLifespan)) + return Result.Ok(node.Object); + } + + var dbResult = await DataRepository.GetAuthenticationStateAsync(id); + if (!dbResult.Success || dbResult.Value == null) + { + _authenticationStateCollection.Remove(id); + return Result.Failed("Unknown session state!"); + } + + if (node == null) + node = new CacheNode(dbResult.Value); + else + node.UpdateObject(dbResult.Value); + if (node.Object != null) + return Result.Ok(node.Object); + return node.Object != null ? Result.Ok(node.Object) : Result.Failed("Failed to get db object!"); + } + + /* + * + */ +} + +public class CacheNode where T : class +{ + public CacheNode(T obj) + { + Object = obj; + } + public T? Object { get; private set; } + public DateTime DateCached { get; private set; } = DateTime.Now; + + public void UpdateObject(T obj) + { + Object = obj; + DateCached = DateTime.Now; + } + + /// + /// Checks if the cached object is within the given lifespan. + /// + /// The max. lifespan + public bool IsValidLifespan(TimeSpan lifespan) => DateCached.Add(lifespan) < DateTime.Now; + + public override bool Equals(object? obj) + { + if (obj is CacheNode cacheObj) + return typeof(T).Equals(cacheObj.Object); + return false; + } + + public override int GetHashCode() => typeof(T).GetHashCode(); + public override string ToString() => typeof(T).ToString(); +} + +public class CacheNodeCollection : KeyedCollection> where TItem : class +{ + protected override string GetKeyForItem(CacheNode item) => item.Object?.ToString() ?? string.Empty; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/BasedAuthBuilder.cs b/DotBased.ASP.Auth/BasedAuthBuilder.cs deleted file mode 100644 index c5e276c..0000000 --- a/DotBased.ASP.Auth/BasedAuthBuilder.cs +++ /dev/null @@ -1,36 +0,0 @@ -using DotBased.ASP.Auth.Scheme; -using DotBased.ASP.Auth.Services; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.Extensions.DependencyInjection; - -namespace DotBased.ASP.Auth; - -public class BasedAuthBuilder -{ - public BasedAuthBuilder(IServiceCollection services, Action? configurationAction = null) - { - _services = services; - Configuration = new BasedAuthConfiguration(); - configurationAction?.Invoke(Configuration); - - services.AddSingleton(Configuration); - if (Configuration.AuthDataProviderType == null) - throw new ArgumentNullException(nameof(Configuration.AuthDataProviderType), $"No '{nameof(IAuthDataProvider)}' configured!"); - services.AddScoped(typeof(IAuthDataProvider), Configuration.AuthDataProviderType); - if (Configuration.SessionStateProviderType == null) - throw new ArgumentNullException(nameof(Configuration.SessionStateProviderType), $"No '{nameof(ISessionStateProvider)}' configured!"); - services.AddScoped(typeof(ISessionStateProvider), Configuration.SessionStateProviderType); - - services.AddScoped(); - - services.AddScoped(); - services.AddAuthentication(options => - { - options.DefaultScheme = BasedAuthenticationHandler.AuthenticationScheme; - }).AddScheme(BasedAuthenticationHandler.AuthenticationScheme, null); - services.AddAuthorization(); - services.AddCascadingAuthenticationState(); - } - public BasedAuthConfiguration Configuration { get; } - private readonly IServiceCollection _services; -} \ No newline at end of file diff --git a/DotBased.ASP.Auth/BasedAuthConfiguration.cs b/DotBased.ASP.Auth/BasedAuthConfiguration.cs index 4800b61..e4c74a4 100644 --- a/DotBased.ASP.Auth/BasedAuthConfiguration.cs +++ b/DotBased.ASP.Auth/BasedAuthConfiguration.cs @@ -25,14 +25,18 @@ public class BasedAuthConfiguration /// public TimeSpan AuthenticationStateMaxAgeBeforeExpire { get; set; } = TimeSpan.FromDays(7); /// + /// How long a session state will be cached (default: 15 min) + /// + public TimeSpan CachedAuthSessionLifespan { get; set; } = TimeSpan.FromMinutes(15); + /// /// Can be used to seed a default user and/or group for first time use. /// - public Action? SeedData { get; set; } + public Action? SeedData { get; set; } - public Type? AuthDataProviderType { get; private set; } + public Type? AuthDataRepositoryType { get; private set; } - public void SetDataProviderType() where TDataProviderType : IAuthDataProvider => - AuthDataProviderType = typeof(TDataProviderType); + public void SetDataRepositoryType() where TDataProviderType : IAuthDataRepository => + AuthDataRepositoryType = typeof(TDataProviderType); public Type? SessionStateProviderType { get; private set; } diff --git a/DotBased.ASP.Auth/Domains/Auth/AuthenticationStateModel.cs b/DotBased.ASP.Auth/Domains/Auth/AuthenticationStateModel.cs index ab4fc1f..9a163b4 100644 --- a/DotBased.ASP.Auth/Domains/Auth/AuthenticationStateModel.cs +++ b/DotBased.ASP.Auth/Domains/Auth/AuthenticationStateModel.cs @@ -2,5 +2,15 @@ namespace DotBased.ASP.Auth.Domains.Auth; public class AuthenticationStateModel { - + public string Id { get; set; } = string.Empty; + + public override bool Equals(object? obj) + { + if (obj is AuthenticationStateModel authStateModel) + return authStateModel.Id == Id; + return false; + } + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => Id.GetHashCode(); + public override string ToString() => Id; } \ No newline at end of file diff --git a/DotBased.ASP.Auth/DotBasedAuthDependencyInjection.cs b/DotBased.ASP.Auth/DotBasedAuthDependencyInjection.cs index 8762aaa..7c1fb23 100644 --- a/DotBased.ASP.Auth/DotBasedAuthDependencyInjection.cs +++ b/DotBased.ASP.Auth/DotBasedAuthDependencyInjection.cs @@ -11,24 +11,23 @@ public static class DotBasedAuthDependencyInjection /// /// Use the DotBased authentication implementation /// - /// Use the app.UseAuthentication() and app.UseAuthorization()! + /// Use UseBasedServerAuth()! /// Service collection /// DotBased auth configuration public static IServiceCollection AddBasedServerAuth(this IServiceCollection services, Action? configurationAction = null) { - /*var authBuilder = new BasedAuthBuilder(services, configurationAction); - return authBuilder;*/ var Configuration = new BasedAuthConfiguration(); configurationAction?.Invoke(Configuration); services.AddSingleton(Configuration); - if (Configuration.AuthDataProviderType == null) - throw new ArgumentNullException(nameof(Configuration.AuthDataProviderType), $"No '{nameof(IAuthDataProvider)}' configured!"); - services.AddScoped(typeof(IAuthDataProvider), Configuration.AuthDataProviderType); + if (Configuration.AuthDataRepositoryType == null) + throw new ArgumentNullException(nameof(Configuration.AuthDataRepositoryType), $"No '{nameof(IAuthDataRepository)}' configured!"); + services.AddScoped(typeof(IAuthDataRepository), Configuration.AuthDataRepositoryType); if (Configuration.SessionStateProviderType == null) throw new ArgumentNullException(nameof(Configuration.SessionStateProviderType), $"No '{nameof(ISessionStateProvider)}' configured!"); services.AddScoped(typeof(ISessionStateProvider), Configuration.SessionStateProviderType); - + + services.AddSingleton(); services.AddScoped(); services.AddScoped(); @@ -50,9 +49,9 @@ public static class DotBasedAuthDependencyInjection var authConfig = app.Services.GetService(); if (authConfig == null) throw new NullReferenceException($"{nameof(BasedAuthConfiguration)} is null!"); - if (authConfig.AuthDataProviderType == null) - throw new NullReferenceException($"{nameof(authConfig.AuthDataProviderType)} is null, cannot instantiate an instance of {nameof(IAuthDataProvider)}"); - var dataProvider = (IAuthDataProvider?)Activator.CreateInstance(authConfig.AuthDataProviderType); + if (authConfig.AuthDataRepositoryType == null) + throw new NullReferenceException($"{nameof(authConfig.AuthDataRepositoryType)} is null, cannot instantiate an instance of {nameof(IAuthDataRepository)}"); + var dataProvider = (IAuthDataRepository?)Activator.CreateInstance(authConfig.AuthDataRepositoryType); if (dataProvider != null) authConfig.SeedData?.Invoke(dataProvider); return app; diff --git a/DotBased.ASP.Auth/IAuthDataProvider.cs b/DotBased.ASP.Auth/IAuthDataRepository.cs similarity index 88% rename from DotBased.ASP.Auth/IAuthDataProvider.cs rename to DotBased.ASP.Auth/IAuthDataRepository.cs index 910d3c8..f1ed79c 100644 --- a/DotBased.ASP.Auth/IAuthDataProvider.cs +++ b/DotBased.ASP.Auth/IAuthDataRepository.cs @@ -3,31 +3,18 @@ using DotBased.ASP.Auth.Domains.Identity; namespace DotBased.ASP.Auth; -public interface IAuthDataProvider +public interface IAuthDataRepository { - /* - * Identity - */ - - // User public Task CreateUserAsync(UserModel user); public Task UpdateUserAsync(UserModel user); public Task DeleteUserAsync(UserModel user); public Task> GetUserAsync(string id, string email, string username); public Task> GetUsersAsync(int start = 0, int amount = 30, string search = ""); - - // Group public Task CreateGroupAsync(GroupModel group); public Task UpdateGroupAsync(GroupModel group); public Task DeleteGroupAsync(GroupModel group); public Task> GetGroupAsync(string id); public Task> GetGroupsAsync(int start = 0, int amount = 30, string search = ""); - - /* - * Auth - */ - - // AuthenticationState public Task CreateAuthenticationStateAsync(AuthenticationStateModel authenticationState); public Task UpdateAuthenticationStateAsync(AuthenticationStateModel authenticationState); public Task DeleteAuthenticationStateAsync(AuthenticationStateModel authenticationState); diff --git a/DotBased.ASP.Auth/MemoryAuthDataProvider.cs b/DotBased.ASP.Auth/MemoryAuthDataProvider.cs deleted file mode 100644 index 1843e4c..0000000 --- a/DotBased.ASP.Auth/MemoryAuthDataProvider.cs +++ /dev/null @@ -1,83 +0,0 @@ -using DotBased.ASP.Auth.Domains.Auth; -using DotBased.ASP.Auth.Domains.Identity; - -namespace DotBased.ASP.Auth; -/// -/// In memory data provider, for testing only! -/// -public class MemoryAuthDataProvider : IAuthDataProvider -{ - private Dictionary _userDict = []; - private Dictionary _groupDict = []; - private Dictionary _authenticationDict = []; - - public async Task CreateUserAsync(UserModel user) - { - throw new NotImplementedException(); - } - - public async Task UpdateUserAsync(UserModel user) - { - throw new NotImplementedException(); - } - - public async Task DeleteUserAsync(UserModel user) - { - throw new NotImplementedException(); - } - - public async Task> GetUserAsync(string id, string email, string username) - { - throw new NotImplementedException(); - } - - public async Task> GetUsersAsync(int start = 0, int amount = 30, string search = "") - { - throw new NotImplementedException(); - } - - public async Task CreateGroupAsync(GroupModel group) - { - throw new NotImplementedException(); - } - - public async Task UpdateGroupAsync(GroupModel group) - { - throw new NotImplementedException(); - } - - public async Task DeleteGroupAsync(GroupModel group) - { - throw new NotImplementedException(); - } - - public async Task> GetGroupAsync(string id) - { - throw new NotImplementedException(); - } - - public async Task> GetGroupsAsync(int start = 0, int amount = 30, string search = "") - { - throw new NotImplementedException(); - } - - public async Task CreateAuthenticationStateAsync(AuthenticationStateModel authenticationState) - { - throw new NotImplementedException(); - } - - public async Task UpdateAuthenticationStateAsync(AuthenticationStateModel authenticationState) - { - throw new NotImplementedException(); - } - - public async Task DeleteAuthenticationStateAsync(AuthenticationStateModel authenticationState) - { - throw new NotImplementedException(); - } - - public async Task> GetAuthenticationStateAsync(string id) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/DotBased.ASP.Auth/MemoryAuthDataRepository.cs b/DotBased.ASP.Auth/MemoryAuthDataRepository.cs new file mode 100644 index 0000000..7e0e87b --- /dev/null +++ b/DotBased.ASP.Auth/MemoryAuthDataRepository.cs @@ -0,0 +1,91 @@ +using System.Diagnostics.CodeAnalysis; +using DotBased.ASP.Auth.Domains.Auth; +using DotBased.ASP.Auth.Domains.Identity; + +namespace DotBased.ASP.Auth; +/// +/// In memory data provider, for testing only! +/// +[SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] +public class MemoryAuthDataRepository : IAuthDataRepository +{ + private readonly List _userList = []; + private readonly List _groupList = []; + private readonly List _authenticationStateList = []; + + public async Task CreateUserAsync(UserModel user) + { + if (_userList.Any(x => x.Id == user.Id || x.Email == user.Email)) + return Result.Failed("User already exists."); + _userList.Add(user); + return Result.Ok(); + } + + public async Task UpdateUserAsync(UserModel user) + { + if (_userList.All(x => x.Id != user.Id)) + return Result.Failed("User does not exist!"); + + return Result.Ok(); + } + + public Task DeleteUserAsync(UserModel user) + { + throw new NotImplementedException(); + } + + public Task> GetUserAsync(string id, string email, string username) + { + throw new NotImplementedException(); + } + + public Task> GetUsersAsync(int start = 0, int amount = 30, string search = "") + { + throw new NotImplementedException(); + } + + public Task CreateGroupAsync(GroupModel group) + { + throw new NotImplementedException(); + } + + public Task UpdateGroupAsync(GroupModel group) + { + throw new NotImplementedException(); + } + + public Task DeleteGroupAsync(GroupModel group) + { + throw new NotImplementedException(); + } + + public Task> GetGroupAsync(string id) + { + throw new NotImplementedException(); + } + + public Task> GetGroupsAsync(int start = 0, int amount = 30, string search = "") + { + throw new NotImplementedException(); + } + + public Task CreateAuthenticationStateAsync(AuthenticationStateModel authenticationState) + { + throw new NotImplementedException(); + } + + public Task UpdateAuthenticationStateAsync(AuthenticationStateModel authenticationState) + { + throw new NotImplementedException(); + } + + public Task DeleteAuthenticationStateAsync(AuthenticationStateModel authenticationState) + { + throw new NotImplementedException(); + } + + public Task> GetAuthenticationStateAsync(string id) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Services/AuthService.cs b/DotBased.ASP.Auth/Services/AuthService.cs index 308e512..1bb19c1 100644 --- a/DotBased.ASP.Auth/Services/AuthService.cs +++ b/DotBased.ASP.Auth/Services/AuthService.cs @@ -7,20 +7,20 @@ namespace DotBased.ASP.Auth.Services; public class AuthService { - public AuthService(IAuthDataProvider dataProvider) + public AuthService(AuthDataCache dataCache) { - _dataProvider = dataProvider; + _dataCache = dataCache; _logger = LogService.RegisterLogger(typeof(AuthService)); } - private readonly IAuthDataProvider _dataProvider; + private readonly AuthDataCache _dataCache; private readonly ILogger _logger; public async Task> LoginAsync(LoginModel login) { if (login.UserName.IsNullOrWhiteSpace()) return Result.Failed("Username argument is empty!"); - var userResult = await _dataProvider.GetUserAsync(string.Empty, login.Email, login.UserName); + //var userResult = await _dataProvider.GetUserAsync(string.Empty, login.Email, login.UserName); //TODO: validate user password and create a session state return Result.Failed(""); } @@ -29,7 +29,7 @@ public class AuthService { if (state.IsNullOrWhiteSpace()) return Result.Failed($"Argument {nameof(state)} is empty!"); - var stateResult = await _dataProvider.GetAuthenticationStateAsync(state); + /*var stateResult = await _dataProvider.GetAuthenticationStateAsync(state); if (!stateResult.Success || stateResult.Value == null) return stateResult; var authState = stateResult.Value; @@ -38,6 +38,7 @@ public class AuthService var updatedStateResult = await _dataProvider.UpdateAuthenticationStateAsync(authState); if (updatedStateResult.Success) return updatedStateResult; _logger.Warning(updatedStateResult.Message); - return updatedStateResult; + return updatedStateResult;*/ + return Result.Failed($"Argument {nameof(state)} is empty!"); // <- TEMP } } \ No newline at end of file