using System.Security.Claims; using System.Text.Json; using Microsoft.AspNetCore.Components.Authorization; using NexusReader.Application.Abstractions.Services; using NexusReader.Application.Constants; namespace NexusReader.UI.Shared.Services; public class NexusAuthenticationStateProvider : AuthenticationStateProvider { private readonly INativeStorageService _storageService; // SECURITY NOTE: We currently store roles in local storage to persist state across refreshes. // In a production SaaS environment, consider using ProtectedBrowserStorage (Blazor Server) // or encrypted storage/JWT claims validation to prevent client-side role tampering. private const string TokenKey = StorageKeys.AuthToken; public NexusAuthenticationStateProvider(INativeStorageService storageService) { _storageService = storageService; } public void ClearCache() { _cachedState = null; NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } private AuthenticationState? _cachedState; public override async Task GetAuthenticationStateAsync() { try { if (_cachedState != null) return _cachedState; var tokenResult = await _storageService.GetSecureString(TokenKey); var token = tokenResult.IsSuccess ? tokenResult.Value : null; // 1. Try Token-based auth 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); if (emailResult.IsSuccess && !string.IsNullOrEmpty(emailResult.Value)) { _cachedState = CreateState( emailResult.Value, tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown", "OpaqueBearer", rolesResult.IsSuccess ? rolesResult.Value! : ""); return _cachedState; } } // 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())); } catch (Exception) { return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } } private AuthenticationState CreateState(string email, string tenantId, string authType, string rolesStr = "") { var claims = new List { new Claim(ClaimTypes.Name, email), new Claim(ClaimTypes.Email, email), new Claim("TenantId", tenantId) }; if (!string.IsNullOrEmpty(rolesStr)) { var roles = rolesStr.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (var role in roles) { claims.Add(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)); } public void NotifyUserLogout() { _cachedState = null; var guest = new ClaimsPrincipal(new ClaimsIdentity()); NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(guest))); } }