From a7d883da841059cf95f075b3b5958567b4578a15 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Mon, 11 May 2026 18:06:48 +0000 Subject: [PATCH] refactor: use Application layer constants and add security documentation to auth provider --- .../NexusAuthenticationStateProvider.cs | 124 +++++++++--------- 1 file changed, 59 insertions(+), 65 deletions(-) diff --git a/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs b/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs index de299ee..82c03ea 100644 --- a/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs +++ b/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs @@ -1,114 +1,108 @@ using System.Security.Claims; -using System.Text.Json; using Microsoft.AspNetCore.Components.Authorization; using NexusReader.Application.Abstractions.Services; -using NexusReader.UI.Shared.Constants; +using NexusReader.Application.Constants; namespace NexusReader.UI.Shared.Services; +/** + * + * Custom AuthenticationStateProvider that manages user sessions using local storage. + * + * + * 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. + * + */ public class NexusAuthenticationStateProvider : AuthenticationStateProvider { private readonly INativeStorageService _storageService; - private const string TokenKey = StorageKeys.AuthToken; + private AuthenticationState? _cachedState; public NexusAuthenticationStateProvider(INativeStorageService storageService) { _storageService = storageService; } - public void ClearCache() - { - _cachedState = null; - NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); - } - - private AuthenticationState? _cachedState; - public override async Task GetAuthenticationStateAsync() { + if (_cachedState != null) return _cachedState; + try { - if (_cachedState != null) return _cachedState; - - var tokenResult = await _storageService.GetSecureString(TokenKey); + var tokenResult = await _storageService.GetSecureString(StorageKeys.AuthToken); 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); - var tenantIdResult = await _storageService.GetSecureString(StorageKeys.UserTenant); - var rolesResult = await _storageService.GetSecureString(StorageKeys.UserRoles); + return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); + } - 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( - emailResult.Value, - tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown", - "OpaqueBearer", - rolesResult.IsSuccess ? rolesResult.Value! : ""); - return _cachedState; + identity.AddClaim(new Claim(ClaimTypes.Role, role.Trim())); } } - // 2. Try Cookie-based auth indicators - var storedEmailResult = await _storageService.GetSecureString(StorageKeys.UserEmail); - 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())); + _cachedState = new AuthenticationState(new ClaimsPrincipal(identity)); + return _cachedState; } - catch (Exception) + catch { 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 + var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, email), - new Claim(ClaimTypes.Email, email), new Claim("TenantId", tenantId) - }; - - if (!string.IsNullOrEmpty(rolesStr)) + }, "api"); + + if (!string.IsNullOrEmpty(roles)) { - var roles = rolesStr.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var role in roles) + foreach (var role in roles.Split(',', StringSplitOptions.RemoveEmptyEntries)) { - 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 = "") - { - _cachedState = CreateState(email, tenantId, "OpaqueBearer", rolesStr); - NotifyAuthenticationStateChanged(Task.FromResult(_cachedState)); + var user = new ClaimsPrincipal(identity); + _cachedState = new AuthenticationState(user); + var authState = Task.FromResult(_cachedState); + NotifyAuthenticationStateChanged(authState); } 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; - var guest = new ClaimsPrincipal(new ClaimsIdentity()); - NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(guest))); } }