Fixed auth state caching

This commit is contained in:
max 2024-11-04 15:45:38 +01:00
parent 8531079a16
commit 58739c2aea
4 changed files with 66 additions and 45 deletions

View File

@ -1,5 +1,6 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using DotBased.ASP.Auth.Domains.Auth; using DotBased.ASP.Auth.Domains.Auth;
using Microsoft.AspNetCore.Components.Authorization;
namespace DotBased.ASP.Auth; namespace DotBased.ASP.Auth;
@ -12,42 +13,45 @@ public class AuthDataCache
private readonly BasedAuthConfiguration _configuration; private readonly BasedAuthConfiguration _configuration;
private readonly CacheNodeCollection<AuthenticationStateModel> _authenticationStateCollection = []; private readonly AuthStateCacheCollection<AuthenticationStateModel, AuthenticationState> _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 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<AuthenticationStateModel>(state)); public void CacheSessionState(AuthenticationStateModel stateModel, AuthenticationState? state = null) => _authenticationStateCollection[stateModel.Id] =
new AuthStateCacheNode<AuthenticationStateModel, AuthenticationState>(stateModel, state);
public Result<AuthenticationStateModel> RequestSessionState(string id) public Result<Tuple<AuthenticationStateModel, AuthenticationState?>> RequestSessionState(string id)
{ {
if (!_authenticationStateCollection.TryGetValue(id, out var node)) if (!_authenticationStateCollection.TryGetValue(id, out var node))
return Result<AuthenticationStateModel>.Failed("No cached object found!"); return Result<Tuple<AuthenticationStateModel, AuthenticationState?>>.Failed("No cached object found!");
string failedMsg; string failedMsg;
if (node.Object != null) if (node.StateModel != null)
{ {
if (node.IsValidLifespan(_configuration.CachedAuthSessionLifespan)) if (node.IsValidLifespan(_configuration.CachedAuthSessionLifespan))
return Result<AuthenticationStateModel>.Ok(node.Object); return Result<Tuple<AuthenticationStateModel, AuthenticationState?>>.Ok(new Tuple<AuthenticationStateModel, AuthenticationState?>(node.StateModel, node.State));
failedMsg = $"Session has invalid lifespan, removing entry: [{id}] from cache!"; failedMsg = $"Session has invalid lifespan, removing entry: [{id}] from cache!";
} }
else else
failedMsg = $"Returned object is null, removing entry: [{id}] from cache!"; failedMsg = $"Returned object is null, removing entry: [{id}] from cache!";
_authenticationStateCollection.Remove(id); _authenticationStateCollection.Remove(id);
return Result<AuthenticationStateModel>.Failed(failedMsg); return Result<Tuple<AuthenticationStateModel, AuthenticationState?>>.Failed(failedMsg);
} }
} }
public class CacheNode<T> where T : class public class AuthStateCacheNode<TStateModel, TState> 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 DateTime DateCached { get; private set; } = DateTime.Now;
public void UpdateObject(T obj) public void UpdateObject(TStateModel obj)
{ {
Object = obj; StateModel = obj;
DateCached = DateTime.Now; DateCached = DateTime.Now;
} }
@ -55,37 +59,37 @@ public class CacheNode<T> where T : class
/// Checks if the cached object is within the given lifespan. /// Checks if the cached object is within the given lifespan.
/// </summary> /// </summary>
/// <param name="lifespan">The max. lifespan</param> /// <param name="lifespan">The max. lifespan</param>
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) public override bool Equals(object? obj)
{ {
if (obj is CacheNode<T> cacheObj) if (obj is AuthStateCacheNode<TStateModel, TState> cacheObj)
return typeof(T).Equals(cacheObj.Object); return StateModel != null && StateModel.Equals(cacheObj.StateModel);
return false; return false;
} }
public override int GetHashCode() => typeof(T).GetHashCode(); public override int GetHashCode() => typeof(TStateModel).GetHashCode();
public override string ToString() => typeof(T).ToString(); public override string ToString() => typeof(TStateModel).ToString();
} }
public class CacheNodeCollection<TItem> : KeyedCollection<string, CacheNode<TItem>> where TItem : class public class AuthStateCacheCollection<TStateModel, TState> : KeyedCollection<string, AuthStateCacheNode<TStateModel, TState>> where TStateModel : class where TState : class
{ {
protected override string GetKeyForItem(CacheNode<TItem> item) => item.Object?.ToString() ?? string.Empty; protected override string GetKeyForItem(AuthStateCacheNode<TStateModel, TState> item) => item.StateModel?.ToString() ?? string.Empty;
public new CacheNode<TItem>? this[string id] public new AuthStateCacheNode<TStateModel, TState>? this[string id]
{ {
get => TryGetValue(id, out CacheNode<TItem>? nodeValue) ? nodeValue : null; get => TryGetValue(id, out AuthStateCacheNode<TStateModel, TState>? nodeValue) ? nodeValue : null;
set set
{ {
if (value == null) if (value == null)
return; return;
if (TryGetValue(id, out CacheNode<TItem>? nodeValue)) if (TryGetValue(id, out AuthStateCacheNode<TStateModel, TState>? nodeValue))
Remove(nodeValue); Remove(nodeValue);
Add(value); Add(value);
} }
} }
public void Insert(CacheNode<TItem> node) public void Insert(AuthStateCacheNode<TStateModel, TState> node)
{ {
if (Contains(node)) if (Contains(node))
Remove(node); Remove(node);

View File

@ -1,6 +1,13 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace DotBased.ASP.Auth; namespace DotBased.ASP.Auth;
public static class BasedAuthDefaults public static class BasedAuthDefaults
{ {
public const string AuthenticationScheme = "DotBasedAuthentication"; public const string AuthenticationScheme = "DotBasedAuthentication";
public const string StorageKey = "dotbased_session";
public static IComponentRenderMode InteractiveServerWithoutPrerender { get; } =
new InteractiveServerRenderMode(prerender: false);
} }

View File

@ -1,11 +1,9 @@
using System.Security.Claims; using System.Security.Claims;
using DotBased.ASP.Auth.Services; using DotBased.ASP.Auth.Services;
using DotBased.Logging; using DotBased.Logging;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.AspNetCore.Http;
using ILogger = DotBased.Logging.ILogger; using ILogger = DotBased.Logging.ILogger;
namespace DotBased.ASP.Auth; namespace DotBased.ASP.Auth;
@ -18,27 +16,24 @@ public class BasedServerAuthenticationStateProvider : ServerAuthenticationStateP
public BasedServerAuthenticationStateProvider(BasedAuthConfiguration configuration, ProtectedLocalStorage localStorage, SecurityService securityService) public BasedServerAuthenticationStateProvider(BasedAuthConfiguration configuration, ProtectedLocalStorage localStorage, SecurityService securityService)
{ {
_config = configuration; _config = configuration;
//_stateProvider = stateProvider;
_localStorage = localStorage; _localStorage = localStorage;
_securityService = securityService; _securityService = securityService;
_logger = LogService.RegisterLogger(typeof(BasedServerAuthenticationStateProvider)); _logger = LogService.RegisterLogger(typeof(BasedServerAuthenticationStateProvider));
} }
private BasedAuthConfiguration _config; private BasedAuthConfiguration _config;
private ISessionStateProvider _stateProvider; private readonly ProtectedLocalStorage _localStorage;
private ProtectedLocalStorage _localStorage; private readonly SecurityService _securityService;
private SecurityService _securityService;
private ILogger _logger; private ILogger _logger;
private readonly AuthenticationState _loggedInState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>() { new Claim(ClaimTypes.Role, "Admin"),new Claim(ClaimTypes.Role, "nottest"), new Claim(ClaimTypes.Name, "Anon") }, BasedAuthDefaults.AuthenticationScheme))); private readonly AuthenticationState _anonState = new(new ClaimsPrincipal());
private readonly AuthenticationState _anonState = new AuthenticationState(new ClaimsPrincipal());
public override async Task<AuthenticationState> GetAuthenticationStateAsync() public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{ {
var sessionIdResult = await _localStorage.GetAsync<string>("dotbased_session"); var sessionIdResult = await _localStorage.GetAsync<string>(BasedAuthDefaults.StorageKey);
if (!sessionIdResult.Success || sessionIdResult.Value == null) if (!sessionIdResult.Success || sessionIdResult.Value == null)
return _anonState; 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; return stateResult is { Success: true, Value: not null } ? stateResult.Value : _anonState;
} }
} }

View File

@ -5,54 +5,68 @@ using DotBased.ASP.Auth.Domains.Identity;
using DotBased.Extensions; using DotBased.Extensions;
using DotBased.Logging; using DotBased.Logging;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
namespace DotBased.ASP.Auth.Services; namespace DotBased.ASP.Auth.Services;
public class SecurityService public class SecurityService
{ {
public SecurityService(IAuthDataRepository authDataRepository, AuthDataCache dataCache) public SecurityService(IAuthDataRepository authDataRepository, AuthDataCache dataCache, ProtectedLocalStorage localStorage)
{ {
_authDataRepository = authDataRepository; _authDataRepository = authDataRepository;
_dataCache = dataCache; _dataCache = dataCache;
_localStorage = localStorage;
_logger = LogService.RegisterLogger(typeof(SecurityService)); _logger = LogService.RegisterLogger(typeof(SecurityService));
} }
private readonly IAuthDataRepository _authDataRepository; private readonly IAuthDataRepository _authDataRepository;
private readonly AuthDataCache _dataCache; private readonly AuthDataCache _dataCache;
private readonly ProtectedLocalStorage _localStorage;
private readonly ILogger _logger; private readonly ILogger _logger;
public async Task<Result<AuthenticationState>> GetAuthenticationFromSession(string id) public async Task<Result<AuthenticationState>> GetAuthenticationStateFromSessionAsync(string id)
{ {
if (id.IsNullOrWhiteSpace()) if (id.IsNullOrWhiteSpace())
return Result<AuthenticationState>.Failed("No valid id!"); return Result<AuthenticationState>.Failed("No valid id!");
AuthenticationStateModel? authState = null; AuthenticationStateModel? authStateModel = null;
var stateCache = _dataCache.RequestSessionState(id); var stateCache = _dataCache.RequestSessionState(id);
if (!stateCache.Success || stateCache.Value == null) if (!stateCache.Success || stateCache.Value == null)
{ {
var stateResult = await _authDataRepository.GetAuthenticationStateAsync(id); var stateResult = await _authDataRepository.GetAuthenticationStateAsync(id);
if (stateResult is { Success: true, Value: not null }) if (stateResult is { Success: true, Value: not null })
authState = stateResult.Value; {
authStateModel = stateResult.Value;
_dataCache.CacheSessionState(authStateModel);
}
} }
else else
authState = stateCache.Value; {
if (stateCache.Value.Item2 != null)
return Result<AuthenticationState>.Ok(stateCache.Value.Item2);
authStateModel = stateCache.Value.Item1;
}
if (authState == null) if (authStateModel == null)
return Result<AuthenticationState>.Failed("Failed to get state!"); return Result<AuthenticationState>.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 }) if (userResult is not { Success: true, Value: not null })
return Result<AuthenticationState>.Failed("Failed to get user from state!"); return Result<AuthenticationState>.Failed("Failed to get user from state!");
var claims = new List<Claim>() var claims = new List<Claim>()
{ {
new(ClaimTypes.Sid, userResult.Value.Id),
new(ClaimTypes.Name, userResult.Value.Name), new(ClaimTypes.Name, userResult.Value.Name),
new(ClaimTypes.NameIdentifier, userResult.Value.UserName), new(ClaimTypes.NameIdentifier, userResult.Value.UserName),
new(ClaimTypes.Surname, userResult.Value.FamilyName), new(ClaimTypes.Surname, userResult.Value.FamilyName),
new(ClaimTypes.Email, userResult.Value.Email) 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 claimsIdentity = new ClaimsIdentity(claims, BasedAuthDefaults.AuthenticationScheme);
var auth = new AuthenticationState(new ClaimsPrincipal(claimsIdentity)); var authState = new AuthenticationState(new ClaimsPrincipal(claimsIdentity));
return Result<AuthenticationState>.Ok(auth); _dataCache.CacheSessionState(authStateModel, authState);
return Result<AuthenticationState>.Ok(authState);
} }
public async Task<Result<AuthenticationStateModel>> LoginAsync(LoginModel login) public async Task<Result<AuthenticationStateModel>> LoginAsync(LoginModel login)
@ -86,6 +100,7 @@ public class SecurityService
if (!authResult.Success) if (!authResult.Success)
return Result<AuthenticationStateModel>.Failed("Failed to store session to database!"); return Result<AuthenticationStateModel>.Failed("Failed to store session to database!");
_dataCache.CacheSessionState(state); _dataCache.CacheSessionState(state);
await _localStorage.SetAsync(BasedAuthDefaults.StorageKey, state.Id);
return Result<AuthenticationStateModel>.Ok(state); return Result<AuthenticationStateModel>.Ok(state);
} }
catch (Exception e) catch (Exception e)