Implementing AuthDataCache

This commit is contained in:
Max 2024-09-27 02:38:18 +02:00
parent c092b8a679
commit 0fed89e140
9 changed files with 218 additions and 154 deletions

View File

@ -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<AuthenticationStateModel> _authenticationStateCollection = [];
public Result PurgeSessionFromCache(string id) => _authenticationStateCollection.Remove(id) ? Result.Ok() : Result.Failed("Failed to purge session state from cache!");
public async Task<Result<AuthenticationStateModel>> RequestAuthStateAsync(string id)
{
if (_authenticationStateCollection.TryGetValue(id, out var node))
{
if (node.Object == null)
{
_authenticationStateCollection.Remove(id);
return Result<AuthenticationStateModel>.Failed($"Returned object is null, removing entry [{id}] from cache!");
}
if (node.IsValidLifespan(_configuration.CachedAuthSessionLifespan))
return Result<AuthenticationStateModel>.Ok(node.Object);
}
var dbResult = await DataRepository.GetAuthenticationStateAsync(id);
if (!dbResult.Success || dbResult.Value == null)
{
_authenticationStateCollection.Remove(id);
return Result<AuthenticationStateModel>.Failed("Unknown session state!");
}
if (node == null)
node = new CacheNode<AuthenticationStateModel>(dbResult.Value);
else
node.UpdateObject(dbResult.Value);
if (node.Object != null)
return Result<AuthenticationStateModel>.Ok(node.Object);
return node.Object != null ? Result<AuthenticationStateModel>.Ok(node.Object) : Result<AuthenticationStateModel>.Failed("Failed to get db object!");
}
/*
*
*/
}
public class CacheNode<T> 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;
}
/// <summary>
/// Checks if the cached object is within the given lifespan.
/// </summary>
/// <param name="lifespan">The max. lifespan</param>
public bool IsValidLifespan(TimeSpan lifespan) => DateCached.Add(lifespan) < DateTime.Now;
public override bool Equals(object? obj)
{
if (obj is CacheNode<T> 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<TItem> : KeyedCollection<string, CacheNode<TItem>> where TItem : class
{
protected override string GetKeyForItem(CacheNode<TItem> item) => item.Object?.ToString() ?? string.Empty;
}

View File

@ -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<BasedAuthConfiguration>? configurationAction = null)
{
_services = services;
Configuration = new BasedAuthConfiguration();
configurationAction?.Invoke(Configuration);
services.AddSingleton<BasedAuthConfiguration>(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<AuthService>();
services.AddScoped<AuthenticationStateProvider, BasedServerAuthenticationStateProvider>();
services.AddAuthentication(options =>
{
options.DefaultScheme = BasedAuthenticationHandler.AuthenticationScheme;
}).AddScheme<BasedAuthenticationHandlerOptions, BasedAuthenticationHandler>(BasedAuthenticationHandler.AuthenticationScheme, null);
services.AddAuthorization();
services.AddCascadingAuthenticationState();
}
public BasedAuthConfiguration Configuration { get; }
private readonly IServiceCollection _services;
}

View File

@ -25,14 +25,18 @@ public class BasedAuthConfiguration
/// </summary>
public TimeSpan AuthenticationStateMaxAgeBeforeExpire { get; set; } = TimeSpan.FromDays(7);
/// <summary>
/// How long a session state will be cached (default: 15 min)
/// </summary>
public TimeSpan CachedAuthSessionLifespan { get; set; } = TimeSpan.FromMinutes(15);
/// <summary>
/// Can be used to seed a default user and/or group for first time use.
/// </summary>
public Action<IAuthDataProvider>? SeedData { get; set; }
public Action<IAuthDataRepository>? SeedData { get; set; }
public Type? AuthDataProviderType { get; private set; }
public Type? AuthDataRepositoryType { get; private set; }
public void SetDataProviderType<TDataProviderType>() where TDataProviderType : IAuthDataProvider =>
AuthDataProviderType = typeof(TDataProviderType);
public void SetDataRepositoryType<TDataProviderType>() where TDataProviderType : IAuthDataRepository =>
AuthDataRepositoryType = typeof(TDataProviderType);
public Type? SessionStateProviderType { get; private set; }

View File

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

View File

@ -11,24 +11,23 @@ public static class DotBasedAuthDependencyInjection
/// <summary>
/// Use the DotBased authentication implementation
/// </summary>
/// <remarks>Use the app.UseAuthentication() and app.UseAuthorization()!</remarks>
/// <remarks>Use UseBasedServerAuth()!</remarks>
/// <param name="services">Service collection</param>
/// <param name="configurationAction">DotBased auth configuration</param>
public static IServiceCollection AddBasedServerAuth(this IServiceCollection services, Action<BasedAuthConfiguration>? configurationAction = null)
{
/*var authBuilder = new BasedAuthBuilder(services, configurationAction);
return authBuilder;*/
var Configuration = new BasedAuthConfiguration();
configurationAction?.Invoke(Configuration);
services.AddSingleton<BasedAuthConfiguration>(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<AuthDataCache>();
services.AddScoped<AuthService>();
services.AddScoped<AuthenticationStateProvider, BasedServerAuthenticationStateProvider>();
@ -50,9 +49,9 @@ public static class DotBasedAuthDependencyInjection
var authConfig = app.Services.GetService<BasedAuthConfiguration>();
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;

View File

@ -3,31 +3,18 @@ using DotBased.ASP.Auth.Domains.Identity;
namespace DotBased.ASP.Auth;
public interface IAuthDataProvider
public interface IAuthDataRepository
{
/*
* Identity
*/
// User
public Task<Result> CreateUserAsync(UserModel user);
public Task<Result> UpdateUserAsync(UserModel user);
public Task<Result> DeleteUserAsync(UserModel user);
public Task<Result<UserModel>> GetUserAsync(string id, string email, string username);
public Task<ListResult<UserItemModel>> GetUsersAsync(int start = 0, int amount = 30, string search = "");
// Group
public Task<Result> CreateGroupAsync(GroupModel group);
public Task<Result> UpdateGroupAsync(GroupModel group);
public Task<Result> DeleteGroupAsync(GroupModel group);
public Task<Result<GroupModel>> GetGroupAsync(string id);
public Task<ListResult<GroupItemModel>> GetGroupsAsync(int start = 0, int amount = 30, string search = "");
/*
* Auth
*/
// AuthenticationState
public Task<Result> CreateAuthenticationStateAsync(AuthenticationStateModel authenticationState);
public Task<Result> UpdateAuthenticationStateAsync(AuthenticationStateModel authenticationState);
public Task<Result> DeleteAuthenticationStateAsync(AuthenticationStateModel authenticationState);

View File

@ -1,83 +0,0 @@
using DotBased.ASP.Auth.Domains.Auth;
using DotBased.ASP.Auth.Domains.Identity;
namespace DotBased.ASP.Auth;
/// <summary>
/// In memory data provider, for testing only!
/// </summary>
public class MemoryAuthDataProvider : IAuthDataProvider
{
private Dictionary<string, UserModel> _userDict = [];
private Dictionary<string, GroupModel> _groupDict = [];
private Dictionary<string, AuthenticationStateModel> _authenticationDict = [];
public async Task<Result> CreateUserAsync(UserModel user)
{
throw new NotImplementedException();
}
public async Task<Result> UpdateUserAsync(UserModel user)
{
throw new NotImplementedException();
}
public async Task<Result> DeleteUserAsync(UserModel user)
{
throw new NotImplementedException();
}
public async Task<Result<UserModel>> GetUserAsync(string id, string email, string username)
{
throw new NotImplementedException();
}
public async Task<ListResult<UserItemModel>> GetUsersAsync(int start = 0, int amount = 30, string search = "")
{
throw new NotImplementedException();
}
public async Task<Result> CreateGroupAsync(GroupModel group)
{
throw new NotImplementedException();
}
public async Task<Result> UpdateGroupAsync(GroupModel group)
{
throw new NotImplementedException();
}
public async Task<Result> DeleteGroupAsync(GroupModel group)
{
throw new NotImplementedException();
}
public async Task<Result<GroupModel>> GetGroupAsync(string id)
{
throw new NotImplementedException();
}
public async Task<ListResult<GroupItemModel>> GetGroupsAsync(int start = 0, int amount = 30, string search = "")
{
throw new NotImplementedException();
}
public async Task<Result> CreateAuthenticationStateAsync(AuthenticationStateModel authenticationState)
{
throw new NotImplementedException();
}
public async Task<Result> UpdateAuthenticationStateAsync(AuthenticationStateModel authenticationState)
{
throw new NotImplementedException();
}
public async Task<Result> DeleteAuthenticationStateAsync(AuthenticationStateModel authenticationState)
{
throw new NotImplementedException();
}
public async Task<Result<AuthenticationStateModel>> GetAuthenticationStateAsync(string id)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,91 @@
using System.Diagnostics.CodeAnalysis;
using DotBased.ASP.Auth.Domains.Auth;
using DotBased.ASP.Auth.Domains.Identity;
namespace DotBased.ASP.Auth;
/// <summary>
/// In memory data provider, for testing only!
/// </summary>
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")]
public class MemoryAuthDataRepository : IAuthDataRepository
{
private readonly List<UserModel> _userList = [];
private readonly List<GroupModel> _groupList = [];
private readonly List<AuthenticationStateModel> _authenticationStateList = [];
public async Task<Result> 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<Result> UpdateUserAsync(UserModel user)
{
if (_userList.All(x => x.Id != user.Id))
return Result.Failed("User does not exist!");
return Result.Ok();
}
public Task<Result> DeleteUserAsync(UserModel user)
{
throw new NotImplementedException();
}
public Task<Result<UserModel>> GetUserAsync(string id, string email, string username)
{
throw new NotImplementedException();
}
public Task<ListResult<UserItemModel>> GetUsersAsync(int start = 0, int amount = 30, string search = "")
{
throw new NotImplementedException();
}
public Task<Result> CreateGroupAsync(GroupModel group)
{
throw new NotImplementedException();
}
public Task<Result> UpdateGroupAsync(GroupModel group)
{
throw new NotImplementedException();
}
public Task<Result> DeleteGroupAsync(GroupModel group)
{
throw new NotImplementedException();
}
public Task<Result<GroupModel>> GetGroupAsync(string id)
{
throw new NotImplementedException();
}
public Task<ListResult<GroupItemModel>> GetGroupsAsync(int start = 0, int amount = 30, string search = "")
{
throw new NotImplementedException();
}
public Task<Result> CreateAuthenticationStateAsync(AuthenticationStateModel authenticationState)
{
throw new NotImplementedException();
}
public Task<Result> UpdateAuthenticationStateAsync(AuthenticationStateModel authenticationState)
{
throw new NotImplementedException();
}
public Task<Result> DeleteAuthenticationStateAsync(AuthenticationStateModel authenticationState)
{
throw new NotImplementedException();
}
public Task<Result<AuthenticationStateModel>> GetAuthenticationStateAsync(string id)
{
throw new NotImplementedException();
}
}

View File

@ -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<Result<AuthenticationStateModel>> LoginAsync(LoginModel login)
{
if (login.UserName.IsNullOrWhiteSpace())
return Result<AuthenticationStateModel>.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<AuthenticationStateModel>.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
}
}