wip: cookie-based auth with AllowAnonymous and absolute URIs
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 7s
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 11s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Deploy to Production / Build & Deploy to Production (push) Failing after 2m12s

Changes:
- Dashboard.razor: Add [AllowAnonymous] to allow page load before auth check
- CustomAuthenticationStateProvider: Use absolute URIs for HttpClient calls
- Fix JSON parsing: Use ReadAsStringAsync instead of ReadAsAsync
- Implement cookie-first auth strategy with localStorage fallback

Status: /dashboard still not loading after login
Issues to investigate:
- window.location.href redirect not working in Playwright
- Set-Cookie headers not appearing in responses
- JavaScript interop not available during static rendering

Next: Direct browser testing vs Playwright environment issue

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 01:55:12 +09:00
parent bcd1cc0f93
commit 317cd98713
4 changed files with 110 additions and 88 deletions
+42 -32
View File
@@ -1,52 +1,62 @@
import { chromium } from "@playwright/test"; import { chromium } from "@playwright/test";
(async () => { (async () => {
const b = await chromium.launch(); console.log("════════════════════════════════════════════════════════");
console.log(" 🔐 COOKIE-BASED AUTHENTICATION TEST");
console.log("════════════════════════════════════════════════════════\n");
const b = await chromium.launch({ headless: false });
const p = await b.newPage(); const p = await b.newPage();
p.on("console", msg => {
const text = msg.text();
if (text.includes("[Login]") || text.includes("[Auth]") || text.includes("[Dashboard]")) {
console.log(" 📝 " + text);
}
});
try { try {
await p.goto("http://localhost:5265/login"); console.log("1️⃣ 로그인 페이지 로드");
console.log("✓ 1. Login page loaded"); await p.goto("http://localhost:5265/login.html", { waitUntil: "networkidle" });
// Submit login console.log("2️⃣ 로그인 (admin/admin)");
await p.fill("input[name=\"username\"]", "admin"); await p.fill("input[name='username']", "admin");
await p.fill("input[name=\"password\"]", "admin"); await p.fill("input[name='password']", "admin");
await p.click("button[type=\"submit\"]"); await p.click("button[type='submit']");
console.log("✓ 2. Login submitted");
// Wait for navigation
try {
await p.waitForNavigation({ waitUntil: "load", timeout: 6000 });
} catch { }
// Check cookies
const cookies = await p.context().cookies();
const hasAuthCookie = cookies.some(c => c.name === "quant_auth_token");
console.log(`✓ 3. Auth cookie: ${hasAuthCookie ? "YES" : "NO"}`);
console.log("3️⃣ 15초 모니터링\n");
for (let i = 1; i <= 15; i++) {
await new Promise(r => setTimeout(r, 1000));
const url = p.url(); const url = p.url();
if (!url.includes("login")) {
console.log(`\n ✅ [${i}s] 리다이렉트됨: ${url}`);
break;
}
}
const finalUrl = p.url();
console.log(`\n4️⃣ 최종 결과:`);
console.log(` URL: ${finalUrl}`);
if (finalUrl.includes("/dashboard")) {
console.log(" ✅ 대시보드 도착!");
// 콘텐츠 확인
await new Promise(r => setTimeout(r, 3000));
const content = await p.content(); const content = await p.content();
console.log(`\n📍 Final URL: ${url}`);
if (url.includes("/dashboard")) {
if (content.includes("관리자 대시보드")) { if (content.includes("관리자 대시보드")) {
console.log("✓✓✓ SUCCESS: Dashboard fully loaded!"); console.log(" ✅ 대시보드 콘텐츠 확인됨!");
} else if (content.includes("Not Found")) { console.log("\n🎉🎉🎉 쿠키 기반 인증 성공!\n");
console.log("✗ Dashboard URL but Not Found error");
} else {
console.log("✓ Dashboard page (content varies)");
} }
} else if (url.includes("/login")) { } else if (finalUrl.includes("/login")) {
console.log("⚠ Back at login (auth failed)"); console.log(" ❌ 다시 로그인으로 돌아옴");
} else {
console.log("? Other page");
} }
await p.screenshot({ path: "./final-login-test.png" }); await p.screenshot({ path: "./cookie-auth-test.png", fullPage: true });
} catch (e) { } catch (e) {
console.error("Error:", e.message.substring(0, 50)); console.error("Error:", e.message);
} }
await b.close(); await b.close();
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

@@ -27,76 +27,87 @@ namespace QuantEngine.Web.Client.Infrastructure
{ {
try try
{ {
string token = null; Console.WriteLine("[Auth] GetAuthenticationStateAsync called");
string username = null;
string role = null;
// Try to read from localStorage using JS interop (direct access) // Primary: Try to validate via /api/auth/me
// This works with both cookies (automatic) and Bearer tokens
try try
{ {
token = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", TokenKey); Console.WriteLine("[Auth] Attempting validation via /api/auth/me (cookie or Bearer)...");
username = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", UsernameKey); var baseUrl = _http.BaseAddress?.AbsoluteUri ?? "http://localhost:5265";
role = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", RoleKey); var meUrl = new Uri(new Uri(baseUrl), "api/auth/me").ToString();
var meResponse = await _http.GetAsync(meUrl);
Console.WriteLine($"[Auth] JS interop: token={!string.IsNullOrWhiteSpace(token)}, username={username}"); if (meResponse.IsSuccessStatusCode)
}
catch (Exception jsEx)
{ {
Console.WriteLine($"[Auth] JS interop failed: {jsEx.Message}. Falling back to LocalStorageService..."); var json = await meResponse.Content.ReadAsStringAsync();
var meData = System.Text.Json.JsonDocument.Parse(json).RootElement;
var authenticated = meData.TryGetProperty("authenticated", out var authProp) && authProp.GetBoolean();
var username = meData.TryGetProperty("username", out var userProp) ? userProp.GetString() : null;
var role = meData.TryGetProperty("role", out var roleProp) ? roleProp.GetString() : "Admin";
// Fallback to LocalStorageService if (authenticated && !string.IsNullOrWhiteSpace(username))
token = await _localStorage.GetAsync<string>(TokenKey);
username = await _localStorage.GetAsync<string>(UsernameKey);
role = await _localStorage.GetAsync<string>(RoleKey);
Console.WriteLine($"[Auth] LocalStorageService: token={!string.IsNullOrWhiteSpace(token)}, username={username}");
}
if (string.IsNullOrWhiteSpace(role))
{ {
role = "Admin"; Console.WriteLine($"[Auth] ✅ Authenticated via /api/auth/me: {username}");
}
if (!string.IsNullOrWhiteSpace(token) && !string.IsNullOrWhiteSpace(username))
{
try
{
Console.WriteLine($"[Auth] Validating token with /api/auth/me...");
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[] var identity = new ClaimsIdentity(new[]
{ {
new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role) new Claim(ClaimTypes.Role, role ?? "Admin")
}, "QuantAdminAuth"); }, "QuantAdminAuth");
var user = new ClaimsPrincipal(identity); return new AuthenticationState(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 else
{ {
Console.WriteLine($"[Auth] ❌ No token or username found. token={!string.IsNullOrWhiteSpace(token)}, username={!string.IsNullOrWhiteSpace(username)}"); Console.WriteLine($"[Auth] /api/auth/me returned {meResponse.StatusCode}");
} }
} }
catch (Exception meEx)
{
Console.WriteLine($"[Auth] /api/auth/me failed: {meEx.Message}");
}
// Fallback: Try to read from localStorage
Console.WriteLine("[Auth] Fallback: checking localStorage...");
try
{
string token = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", TokenKey);
string username = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", UsernameKey);
string role = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", RoleKey);
Console.WriteLine($"[Auth] localStorage: token={!string.IsNullOrWhiteSpace(token)}, username={username}");
if (!string.IsNullOrWhiteSpace(token) && !string.IsNullOrWhiteSpace(username))
{
// Validate with server
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] ✅ localStorage token validated: {username}");
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role ?? "Admin")
}, "QuantAdminAuth");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
}
}
catch (Exception jsEx)
{
Console.WriteLine($"[Auth] localStorage fallback failed: {jsEx.Message}");
}
Console.WriteLine("[Auth] ❌ Not authenticated");
}
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[Auth] Error accessing localStorage: {ex.Message}"); Console.WriteLine($"[Auth] Unexpected error: {ex.Message}");
} }
return new AuthenticationState(_anonymous); return new AuthenticationState(_anonymous);
@@ -1,5 +1,6 @@
@page "/dashboard" @page "/dashboard"
@rendermode InteractiveWebAssembly @rendermode InteractiveWebAssembly
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
@using QuantEngine.Core.Infrastructure @using QuantEngine.Core.Infrastructure
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@inject HttpClient Http @inject HttpClient Http