mirror of
https://github.com/hmaxnl/DotBased.git
synced 2025-01-18 10:04:20 +01:00
Fixed auth state caching
This commit is contained in:
parent
8531079a16
commit
58739c2aea
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user