Files
QuantEngineByItz/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs
T
kjh2064 84e5784b66 feat: complete localStorage-based auth with API fallback support
- Enhanced login.html with 3-second Blazor init wait + console logging
- Added database fallback to /api/auth/me for development
- Improved CustomAuthenticationStateProvider with detailed logging
- Complete auth API chain: login → token → /api/auth/me → Blazor auth state

Auth flow:
1. login.html POST /api/auth/login (admin/admin)
2. API returns token + sets fallback for /api/auth/me
3. Token stored in localStorage
4. Redirect to /dashboard (3 second wait)
5. Blazor loads, CustomAuthenticationStateProvider reads token
6. Calls /api/auth/me with Bearer token
7. Sets authenticated state

Status: Auth APIs validated and working
- Login API: ✓ Returns token
- /api/auth/me: ✓ Accepts Bearer token
- localStorage: ✓ Token persists
- Blazor auth: ✓ Console logging added

Next: Manual browser testing needed (Playwright environment has limitations)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-06 01:14:22 +09:00

150 lines
6.0 KiB
C#

using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using QuantEngine.Web.Client.Services;
namespace QuantEngine.Web.Client.Infrastructure
{
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly LocalStorageService _localStorage;
private readonly HttpClient _http;
private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());
private const string TokenKey = "quant_admin_access_token";
private const string UsernameKey = "quant_admin_username";
private const string RoleKey = "quant_admin_role";
private const string RememberUsernameKey = "quant_admin_remember_username";
public CustomAuthenticationStateProvider(LocalStorageService localStorage, HttpClient http)
{
_localStorage = localStorage;
_http = http;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
var token = await _localStorage.GetAsync<string>(TokenKey);
var username = await _localStorage.GetAsync<string>(UsernameKey);
var role = await _localStorage.GetAsync<string>(RoleKey) ?? "Admin";
if (!string.IsNullOrWhiteSpace(token) && !string.IsNullOrWhiteSpace(username))
{
try
{
var request = new HttpRequestMessage(HttpMethod.Get, "api/auth/me");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var response = await _http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"[Auth] /api/auth/me failed: {response.StatusCode}");
await MarkUserAsLoggedOutAsync();
return new AuthenticationState(_anonymous);
}
Console.WriteLine($"[Auth] User authenticated: {username}");
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role)
}, "QuantAdminAuth");
var user = new ClaimsPrincipal(identity);
return new AuthenticationState(user);
}
catch (Exception ex)
{
Console.WriteLine($"[Auth] Error during /api/auth/me call: {ex.Message}");
// Fall through to anonymous
}
}
else
{
Console.WriteLine("[Auth] No token or username found in localStorage");
}
}
catch (Exception ex)
{
Console.WriteLine($"[Auth] Error accessing localStorage: {ex.Message}");
// Return anonymous if localStorage isn't ready
}
return new AuthenticationState(_anonymous);
}
public async Task MarkUserAsAuthenticatedAsync(string username, string accessToken, string role)
{
await MarkUserAsAuthenticatedAsync(username, accessToken, role, rememberUsername: true);
}
public async Task MarkUserAsAuthenticatedAsync(string username, string accessToken, string role, bool rememberUsername)
{
await _localStorage.SetAsync(TokenKey, accessToken);
if (rememberUsername)
{
await _localStorage.SetAsync(UsernameKey, username);
await _localStorage.SetAsync(RememberUsernameKey, true);
}
else
{
await _localStorage.DeleteAsync(UsernameKey);
await _localStorage.SetAsync(RememberUsernameKey, false);
}
await _localStorage.SetAsync(RoleKey, role);
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role)
}, "QuantAdminAuth");
var user = new ClaimsPrincipal(identity);
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
}
public async Task MarkUserAsLoggedOutAsync()
{
await _localStorage.DeleteAsync(TokenKey);
await _localStorage.DeleteAsync(RoleKey);
var rememberUsername = await _localStorage.GetAsync<bool>(RememberUsernameKey);
if (!rememberUsername)
{
await _localStorage.DeleteAsync(UsernameKey);
}
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_anonymous)));
}
public async Task LogoutFromServerAsync()
{
var token = await _localStorage.GetAsync<string>(TokenKey);
if (!string.IsNullOrWhiteSpace(token))
{
try
{
var request = new HttpRequestMessage(HttpMethod.Post, "api/auth/logout");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
await _http.SendAsync(request);
}
catch
{
// Best-effort server revocation; always clear local state.
}
}
await MarkUserAsLoggedOutAsync();
}
public async Task<string?> GetRememberedUsernameAsync()
{
var rememberUsername = await _localStorage.GetAsync<bool>(RememberUsernameKey);
if (!rememberUsername)
{
return null;
}
return await _localStorage.GetAsync<string>(UsernameKey);
}
}
}