From 58739c2aeadc7e608359a20cd3ada2d447284c22 Mon Sep 17 00:00:00 2001 From: max Date: Mon, 4 Nov 2024 15:45:38 +0100 Subject: [PATCH] Fixed auth state caching --- DotBased.ASP.Auth/AuthDataCache.cs | 52 ++++++++++--------- DotBased.ASP.Auth/BasedAuthDefaults.cs | 7 +++ .../BasedServerAuthenticationStateProvider.cs | 15 ++---- DotBased.ASP.Auth/Services/SecurityService.cs | 37 +++++++++---- 4 files changed, 66 insertions(+), 45 deletions(-) diff --git a/DotBased.ASP.Auth/AuthDataCache.cs b/DotBased.ASP.Auth/AuthDataCache.cs index 10bfd1c..76287e4 100644 --- a/DotBased.ASP.Auth/AuthDataCache.cs +++ b/DotBased.ASP.Auth/AuthDataCache.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using DotBased.ASP.Auth.Domains.Auth; +using Microsoft.AspNetCore.Components.Authorization; namespace DotBased.ASP.Auth; @@ -12,42 +13,45 @@ public class AuthDataCache private readonly BasedAuthConfiguration _configuration; - private readonly CacheNodeCollection _authenticationStateCollection = []; + private readonly AuthStateCacheCollection _authenticationStateCollection = []; public Result PurgeSessionState(string id) => _authenticationStateCollection.Remove(id) ? Result.Ok() : Result.Failed("Failed to purge session state from cache! Or the session was not cached..."); - public void CacheSessionState(AuthenticationStateModel state) => _authenticationStateCollection.Insert(new CacheNode(state)); + public void CacheSessionState(AuthenticationStateModel stateModel, AuthenticationState? state = null) => _authenticationStateCollection[stateModel.Id] = + new AuthStateCacheNode(stateModel, state); - public Result RequestSessionState(string id) + public Result> RequestSessionState(string id) { if (!_authenticationStateCollection.TryGetValue(id, out var node)) - return Result.Failed("No cached object found!"); + return Result>.Failed("No cached object found!"); string failedMsg; - if (node.Object != null) + if (node.StateModel != null) { if (node.IsValidLifespan(_configuration.CachedAuthSessionLifespan)) - return Result.Ok(node.Object); + return Result>.Ok(new Tuple(node.StateModel, node.State)); failedMsg = $"Session has invalid lifespan, removing entry: [{id}] from cache!"; } else failedMsg = $"Returned object is null, removing entry: [{id}] from cache!"; _authenticationStateCollection.Remove(id); - return Result.Failed(failedMsg); + return Result>.Failed(failedMsg); } } -public class CacheNode where T : class +public class AuthStateCacheNode where TStateModel : class where TState : class { - public CacheNode(T obj) + public AuthStateCacheNode(TStateModel stateModel, TState? state) { - Object = obj; + StateModel = stateModel; + State = state; } - public T? Object { get; private set; } + public TStateModel? StateModel { get; private set; } + public TState? State { get; private set; } public DateTime DateCached { get; private set; } = DateTime.Now; - public void UpdateObject(T obj) + public void UpdateObject(TStateModel obj) { - Object = obj; + StateModel = obj; DateCached = DateTime.Now; } @@ -55,37 +59,37 @@ public class CacheNode where T : class /// Checks if the cached object is within the given lifespan. /// /// The max. lifespan - public bool IsValidLifespan(TimeSpan lifespan) => DateCached.Add(lifespan) < DateTime.Now; + 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); + if (obj is AuthStateCacheNode cacheObj) + return StateModel != null && StateModel.Equals(cacheObj.StateModel); return false; } - public override int GetHashCode() => typeof(T).GetHashCode(); - public override string ToString() => typeof(T).ToString(); + public override int GetHashCode() => typeof(TStateModel).GetHashCode(); + public override string ToString() => typeof(TStateModel).ToString(); } -public class CacheNodeCollection : KeyedCollection> where TItem : class +public class AuthStateCacheCollection : KeyedCollection> where TStateModel : class where TState : class { - protected override string GetKeyForItem(CacheNode item) => item.Object?.ToString() ?? string.Empty; + protected override string GetKeyForItem(AuthStateCacheNode item) => item.StateModel?.ToString() ?? string.Empty; - public new CacheNode? this[string id] + public new AuthStateCacheNode? this[string id] { - get => TryGetValue(id, out CacheNode? nodeValue) ? nodeValue : null; + get => TryGetValue(id, out AuthStateCacheNode? nodeValue) ? nodeValue : null; set { if (value == null) return; - if (TryGetValue(id, out CacheNode? nodeValue)) + if (TryGetValue(id, out AuthStateCacheNode? nodeValue)) Remove(nodeValue); Add(value); } } - public void Insert(CacheNode node) + public void Insert(AuthStateCacheNode node) { if (Contains(node)) Remove(node); diff --git a/DotBased.ASP.Auth/BasedAuthDefaults.cs b/DotBased.ASP.Auth/BasedAuthDefaults.cs index 51009df..824740b 100644 --- a/DotBased.ASP.Auth/BasedAuthDefaults.cs +++ b/DotBased.ASP.Auth/BasedAuthDefaults.cs @@ -1,6 +1,13 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + namespace DotBased.ASP.Auth; public static class BasedAuthDefaults { public const string AuthenticationScheme = "DotBasedAuthentication"; + public const string StorageKey = "dotbased_session"; + + public static IComponentRenderMode InteractiveServerWithoutPrerender { get; } = + new InteractiveServerRenderMode(prerender: false); } \ No newline at end of file diff --git a/DotBased.ASP.Auth/BasedServerAuthenticationStateProvider.cs b/DotBased.ASP.Auth/BasedServerAuthenticationStateProvider.cs index 6ac6a46..b2b39c4 100644 --- a/DotBased.ASP.Auth/BasedServerAuthenticationStateProvider.cs +++ b/DotBased.ASP.Auth/BasedServerAuthenticationStateProvider.cs @@ -1,11 +1,9 @@ using System.Security.Claims; using DotBased.ASP.Auth.Services; using DotBased.Logging; -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; -using Microsoft.AspNetCore.Http; using ILogger = DotBased.Logging.ILogger; namespace DotBased.ASP.Auth; @@ -18,27 +16,24 @@ public class BasedServerAuthenticationStateProvider : ServerAuthenticationStateP public BasedServerAuthenticationStateProvider(BasedAuthConfiguration configuration, ProtectedLocalStorage localStorage, SecurityService securityService) { _config = configuration; - //_stateProvider = stateProvider; _localStorage = localStorage; _securityService = securityService; _logger = LogService.RegisterLogger(typeof(BasedServerAuthenticationStateProvider)); } private BasedAuthConfiguration _config; - private ISessionStateProvider _stateProvider; - private ProtectedLocalStorage _localStorage; - private SecurityService _securityService; + private readonly ProtectedLocalStorage _localStorage; + private readonly SecurityService _securityService; private ILogger _logger; - private readonly AuthenticationState _loggedInState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(new List() { new Claim(ClaimTypes.Role, "Admin"),new Claim(ClaimTypes.Role, "nottest"), new Claim(ClaimTypes.Name, "Anon") }, BasedAuthDefaults.AuthenticationScheme))); - private readonly AuthenticationState _anonState = new AuthenticationState(new ClaimsPrincipal()); + private readonly AuthenticationState _anonState = new(new ClaimsPrincipal()); public override async Task GetAuthenticationStateAsync() { - var sessionIdResult = await _localStorage.GetAsync("dotbased_session"); + var sessionIdResult = await _localStorage.GetAsync(BasedAuthDefaults.StorageKey); if (!sessionIdResult.Success || sessionIdResult.Value == null) return _anonState; - var stateResult = await _securityService.GetAuthenticationFromSession(sessionIdResult.Value); + var stateResult = await _securityService.GetAuthenticationStateFromSessionAsync(sessionIdResult.Value); return stateResult is { Success: true, Value: not null } ? stateResult.Value : _anonState; } } \ No newline at end of file diff --git a/DotBased.ASP.Auth/Services/SecurityService.cs b/DotBased.ASP.Auth/Services/SecurityService.cs index 57a5f74..c17da21 100644 --- a/DotBased.ASP.Auth/Services/SecurityService.cs +++ b/DotBased.ASP.Auth/Services/SecurityService.cs @@ -5,54 +5,68 @@ using DotBased.ASP.Auth.Domains.Identity; using DotBased.Extensions; using DotBased.Logging; using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; namespace DotBased.ASP.Auth.Services; public class SecurityService { - public SecurityService(IAuthDataRepository authDataRepository, AuthDataCache dataCache) + public SecurityService(IAuthDataRepository authDataRepository, AuthDataCache dataCache, ProtectedLocalStorage localStorage) { _authDataRepository = authDataRepository; _dataCache = dataCache; + _localStorage = localStorage; _logger = LogService.RegisterLogger(typeof(SecurityService)); } private readonly IAuthDataRepository _authDataRepository; private readonly AuthDataCache _dataCache; + private readonly ProtectedLocalStorage _localStorage; private readonly ILogger _logger; - public async Task> GetAuthenticationFromSession(string id) + public async Task> GetAuthenticationStateFromSessionAsync(string id) { if (id.IsNullOrWhiteSpace()) return Result.Failed("No valid id!"); - AuthenticationStateModel? authState = null; + AuthenticationStateModel? authStateModel = null; var stateCache = _dataCache.RequestSessionState(id); if (!stateCache.Success || stateCache.Value == null) { var stateResult = await _authDataRepository.GetAuthenticationStateAsync(id); if (stateResult is { Success: true, Value: not null }) - authState = stateResult.Value; + { + authStateModel = stateResult.Value; + _dataCache.CacheSessionState(authStateModel); + } } else - authState = stateCache.Value; + { + if (stateCache.Value.Item2 != null) + return Result.Ok(stateCache.Value.Item2); + authStateModel = stateCache.Value.Item1; + } - if (authState == null) - return Result.Failed("Failed to get state!"); + if (authStateModel == null) + return Result.Failed("Failed to get auth state!"); - var userResult = await _authDataRepository.GetUserAsync(authState.UserId, string.Empty, string.Empty); + var userResult = await _authDataRepository.GetUserAsync(authStateModel.UserId, string.Empty, string.Empty); if (userResult is not { Success: true, Value: not null }) return Result.Failed("Failed to get user from state!"); var claims = new List() { + new(ClaimTypes.Sid, userResult.Value.Id), new(ClaimTypes.Name, userResult.Value.Name), new(ClaimTypes.NameIdentifier, userResult.Value.UserName), new(ClaimTypes.Surname, userResult.Value.FamilyName), new(ClaimTypes.Email, userResult.Value.Email) }; - claims.AddRange(userResult.Value.Roles.Select(role => new Claim(ClaimTypes.Role, role.Name)).ToList()); + //TODO: combine group, user roles + claims.AddRange(userResult.Value.Groups.Select(group => new Claim(ClaimTypes.GroupSid, group.Id))); + claims.AddRange(userResult.Value.Roles.Select(role => new Claim(ClaimTypes.Role, role.Name))); var claimsIdentity = new ClaimsIdentity(claims, BasedAuthDefaults.AuthenticationScheme); - var auth = new AuthenticationState(new ClaimsPrincipal(claimsIdentity)); - return Result.Ok(auth); + var authState = new AuthenticationState(new ClaimsPrincipal(claimsIdentity)); + _dataCache.CacheSessionState(authStateModel, authState); + return Result.Ok(authState); } public async Task> LoginAsync(LoginModel login) @@ -86,6 +100,7 @@ public class SecurityService if (!authResult.Success) return Result.Failed("Failed to store session to database!"); _dataCache.CacheSessionState(state); + await _localStorage.SetAsync(BasedAuthDefaults.StorageKey, state.Id); return Result.Ok(state); } catch (Exception e)