Refactor: Web Consolidation and Identity Stabilization #40
@@ -1,114 +1,108 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using NexusReader.Application.Abstractions.Services;
|
using NexusReader.Application.Abstractions.Services;
|
||||||
using NexusReader.UI.Shared.Constants;
|
using NexusReader.Application.Constants;
|
||||||
|
|
||||||
namespace NexusReader.UI.Shared.Services;
|
namespace NexusReader.UI.Shared.Services;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Custom AuthenticationStateProvider that manages user sessions using local storage.
|
||||||
|
* </summary>
|
||||||
|
* <remarks>
|
||||||
|
* SECURITY NOTE: Currently roles are stored in local storage as a comma-separated string
|
||||||
|
* for UI reactivity. In a production environment, roles should be extracted from a
|
||||||
|
* cryptographically signed JWT or validated via a back-channel to prevent client-side
|
||||||
|
* role escalation. Consider using ProtectedBrowserStorage for sensitive claims.
|
||||||
|
* </remarks>
|
||||||
|
*/
|
||||||
public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
||||||
{
|
{
|
||||||
private readonly INativeStorageService _storageService;
|
private readonly INativeStorageService _storageService;
|
||||||
private const string TokenKey = StorageKeys.AuthToken;
|
private AuthenticationState? _cachedState;
|
||||||
|
|
||||||
public NexusAuthenticationStateProvider(INativeStorageService storageService)
|
public NexusAuthenticationStateProvider(INativeStorageService storageService)
|
||||||
{
|
{
|
||||||
_storageService = storageService;
|
_storageService = storageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearCache()
|
|
||||||
{
|
|
||||||
_cachedState = null;
|
|
||||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
private AuthenticationState? _cachedState;
|
|
||||||
|
|
||||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||||
{
|
{
|
||||||
|
if (_cachedState != null) return _cachedState;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_cachedState != null) return _cachedState;
|
var tokenResult = await _storageService.GetSecureString(StorageKeys.AuthToken);
|
||||||
|
|
||||||
var tokenResult = await _storageService.GetSecureString(TokenKey);
|
|
||||||
var token = tokenResult.IsSuccess ? tokenResult.Value : null;
|
var token = tokenResult.IsSuccess ? tokenResult.Value : null;
|
||||||
|
|
||||||
// 1. Try Token-based auth
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
if (!string.IsNullOrWhiteSpace(token))
|
|
||||||
{
|
{
|
||||||
var emailResult = await _storageService.GetSecureString(StorageKeys.UserEmail);
|
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||||
var tenantIdResult = await _storageService.GetSecureString(StorageKeys.UserTenant);
|
}
|
||||||
var rolesResult = await _storageService.GetSecureString(StorageKeys.UserRoles);
|
|
||||||
|
|
||||||
if (emailResult.IsSuccess && !string.IsNullOrEmpty(emailResult.Value))
|
var emailResult = await _storageService.GetSecureString(StorageKeys.UserEmail);
|
||||||
|
var tenantResult = await _storageService.GetSecureString(StorageKeys.UserTenant);
|
||||||
|
var rolesResult = await _storageService.GetSecureString(StorageKeys.UserRoles);
|
||||||
|
|
||||||
|
var email = emailResult.IsSuccess ? emailResult.Value : "unknown";
|
||||||
|
var tenantId = tenantResult.IsSuccess ? tenantResult.Value : "default";
|
||||||
|
var roles = rolesResult.IsSuccess ? rolesResult.Value : "";
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(new[]
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.Name, email),
|
||||||
|
new Claim("TenantId", tenantId)
|
||||||
|
}, "api");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(roles))
|
||||||
|
{
|
||||||
|
foreach (var role in roles.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||||
{
|
{
|
||||||
_cachedState = CreateState(
|
identity.AddClaim(new Claim(ClaimTypes.Role, role.Trim()));
|
||||||
emailResult.Value,
|
|
||||||
tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown",
|
|
||||||
"OpaqueBearer",
|
|
||||||
rolesResult.IsSuccess ? rolesResult.Value! : "");
|
|
||||||
return _cachedState;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Try Cookie-based auth indicators
|
_cachedState = new AuthenticationState(new ClaimsPrincipal(identity));
|
||||||
var storedEmailResult = await _storageService.GetSecureString(StorageKeys.UserEmail);
|
return _cachedState;
|
||||||
if (storedEmailResult.IsSuccess && !string.IsNullOrEmpty(storedEmailResult.Value))
|
|
||||||
{
|
|
||||||
var tenantIdResult = await _storageService.GetSecureString(StorageKeys.UserTenant);
|
|
||||||
var rolesResult = await _storageService.GetSecureString(StorageKeys.UserRoles);
|
|
||||||
_cachedState = CreateState(
|
|
||||||
storedEmailResult.Value,
|
|
||||||
tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown",
|
|
||||||
"CookieAuth",
|
|
||||||
rolesResult.IsSuccess ? rolesResult.Value! : "");
|
|
||||||
return _cachedState;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Fallback: If we have no local info, we might still have a cookie (e.g. after refresh or Google login).
|
|
||||||
// We should return anonymous for now but trigger a background check if we're in WASM.
|
|
||||||
// Wait! In WASM, the first GetAuthenticationStateAsync is awaited.
|
|
||||||
// We can do a quick check here if it's the first time.
|
|
||||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch
|
||||||
{
|
{
|
||||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthenticationState CreateState(string email, string tenantId, string authType, string rolesStr = "")
|
public void NotifyUserAuthentication(string email, string tenantId, string roles = "")
|
||||||
{
|
{
|
||||||
var claims = new List<Claim>
|
var identity = new ClaimsIdentity(new[]
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.Name, email),
|
new Claim(ClaimTypes.Name, email),
|
||||||
new Claim(ClaimTypes.Email, email),
|
|
||||||
new Claim("TenantId", tenantId)
|
new Claim("TenantId", tenantId)
|
||||||
};
|
}, "api");
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(rolesStr))
|
if (!string.IsNullOrEmpty(roles))
|
||||||
{
|
{
|
||||||
var roles = rolesStr.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
foreach (var role in roles.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||||
foreach (var role in roles)
|
|
||||||
{
|
{
|
||||||
claims.Add(new Claim(ClaimTypes.Role, role.Trim()));
|
identity.AddClaim(new Claim(ClaimTypes.Role, role.Trim()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var identity = new ClaimsIdentity(claims, authType);
|
|
||||||
return new AuthenticationState(new ClaimsPrincipal(identity));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void NotifyUserAuthentication(string email, string tenantId, string rolesStr = "")
|
var user = new ClaimsPrincipal(identity);
|
||||||
{
|
_cachedState = new AuthenticationState(user);
|
||||||
_cachedState = CreateState(email, tenantId, "OpaqueBearer", rolesStr);
|
var authState = Task.FromResult(_cachedState);
|
||||||
NotifyAuthenticationStateChanged(Task.FromResult(_cachedState));
|
NotifyAuthenticationStateChanged(authState);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void NotifyUserLogout()
|
public void NotifyUserLogout()
|
||||||
|
{
|
||||||
|
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
|
||||||
|
_cachedState = new AuthenticationState(anonymousUser);
|
||||||
|
var authState = Task.FromResult(_cachedState);
|
||||||
|
NotifyAuthenticationStateChanged(authState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearCache()
|
||||||
{
|
{
|
||||||
_cachedState = null;
|
_cachedState = null;
|
||||||
var guest = new ClaimsPrincipal(new ClaimsIdentity());
|
|
||||||
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(guest)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user