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)));
}
}