refactor: switch to cookie-based auth flow with JS interop fallback
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 7s
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 15s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Deploy to Production / Build & Deploy to Production (push) Failing after 3m14s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 7s
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 15s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Deploy to Production / Build & Deploy to Production (push) Failing after 3m14s
Architecture shift: - Primary: HTTP-only cookie authentication (server-side) - Fallback: localStorage with JS interop for SPA Changes: 1. CustomAuthenticationStateProvider: - Add IJSRuntime for direct localStorage access - Try JS interop first, fallback to LocalStorageService - Added detailed logging for auth debugging 2. Dashboard.razor: - Add @rendermode InteractiveWebAssembly (CLAUDE.md compliance) - Restore auth check with logging - Redirect to /login.html if not authenticated 3. Program.cs: - Reorder MapRazorComponents: WebAssembly first (default) - Add detailed logging to /api/auth/login cookie setup - Verify Set-Cookie headers are sent correctly 4. login.html: - Simplified to 2-second wait before redirect - localStorage as backup storage - Ready for cookie-based auth Next steps: - Verify Set-Cookie headers appear in responses - Confirm cookie-based auth works end-to-end - Test dashboard loads with cookie authentication Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
@@ -0,0 +1,70 @@
|
||||
import { chromium } from "@playwright/test";
|
||||
|
||||
(async () => {
|
||||
console.log("════════════════════════════════════════════════════════");
|
||||
console.log(" ✅ FINAL INTEGRATED TEST (JS Interop Enabled)");
|
||||
console.log("════════════════════════════════════════════════════════\n");
|
||||
|
||||
const b = await chromium.launch({ headless: false });
|
||||
const p = await b.newPage();
|
||||
|
||||
p.on("console", msg => {
|
||||
const text = msg.text();
|
||||
if (text.includes("[Auth]") || text.includes("[Dashboard]") || text.includes("[Login]")) {
|
||||
console.log(" 📝 " + text);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log("1️⃣ 로그인 페이지 로드");
|
||||
await p.goto("http://localhost:5265/login.html", { waitUntil: "networkidle" });
|
||||
|
||||
console.log("2️⃣ 로그인 (admin/admin)");
|
||||
await p.fill("input[name='username']", "admin");
|
||||
await p.fill("input[name='password']", "admin");
|
||||
await p.click("button[type='submit']");
|
||||
|
||||
console.log("3️⃣ 대기 및 모니터링 (12초)\n");
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
const url = p.url();
|
||||
if (!url.includes("login")) {
|
||||
console.log(`\n ✅ [${i}s] 리다이렉트됨!`);
|
||||
console.log(` URL: ${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, 2000));
|
||||
const content = await p.content();
|
||||
|
||||
if (content.includes("관리자 대시보드")) {
|
||||
console.log(" ✅ 대시보드 콘텐츠 확인됨!");
|
||||
console.log("\n🎉🎉🎉 로그인 시스템 완전 성공!\n");
|
||||
} else {
|
||||
console.log(" ⚠️ 콘텐츠 미확인");
|
||||
}
|
||||
} else if (finalUrl.includes("/login")) {
|
||||
console.log(" ❌ 다시 로그인으로 돌아옴");
|
||||
console.log(" → 인증 체크에서 실패했거나, JS interop이 작동하지 않음");
|
||||
} else {
|
||||
console.log(" ❓ 예상치 못한 페이지");
|
||||
}
|
||||
|
||||
await p.screenshot({ path: "./final-integrated-test.png", fullPage: true });
|
||||
console.log("📷 스크린샷: final-integrated-test.png");
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error:", e.message);
|
||||
}
|
||||
|
||||
await b.close();
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
@@ -0,0 +1,88 @@
|
||||
import { chromium } from "@playwright/test";
|
||||
|
||||
(async () => {
|
||||
console.log("════════════════════════════════════════════════════════");
|
||||
console.log(" 🔬 PRECISION DEBUG TEST (Auth Check Disabled)");
|
||||
console.log("════════════════════════════════════════════════════════\n");
|
||||
|
||||
const b = await chromium.launch({ headless: false });
|
||||
const p = await b.newPage();
|
||||
|
||||
const allLogs = [];
|
||||
p.on("console", msg => {
|
||||
const text = msg.text();
|
||||
allLogs.push(text);
|
||||
if (text.includes("[") || text.includes("dashboard") || text.includes("login")) {
|
||||
console.log(" 📝 " + text);
|
||||
}
|
||||
});
|
||||
|
||||
// Network events
|
||||
p.on("response", res => {
|
||||
const url = res.url();
|
||||
if (url.includes("dashboard") || url.includes("login") || url.includes("api")) {
|
||||
console.log(` 📡 ${res.status()} ${url.split('/').pop() || 'root'}`);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log("1️⃣ 로그인 페이지 로드");
|
||||
await p.goto("http://localhost:5265/login.html", { waitUntil: "networkidle" });
|
||||
|
||||
console.log("2️⃣ 로그인 제출");
|
||||
await p.fill("input[name='username']", "admin");
|
||||
await p.fill("input[name='password']", "admin");
|
||||
await p.click("button[type='submit']");
|
||||
|
||||
console.log("3️⃣ 12초 동안 모니터링\n");
|
||||
let urlHistory = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
const url = p.url();
|
||||
if (!urlHistory.includes(url)) {
|
||||
urlHistory.push(url);
|
||||
console.log(` [${i+1}s] → ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n4️⃣ 최종 상태:");
|
||||
const finalUrl = p.url();
|
||||
const finalContent = await p.content();
|
||||
|
||||
console.log(` URL: ${finalUrl}`);
|
||||
|
||||
if (finalUrl.includes("/dashboard")) {
|
||||
console.log(" ✅ /dashboard 도착!");
|
||||
|
||||
if (finalContent.includes("관리자 대시보드")) {
|
||||
console.log(" ✅ 대시보드 콘텐츠 로드됨!");
|
||||
console.log("\n🎉 SUCCESS!\n");
|
||||
} else {
|
||||
console.log(" ⚠️ URL은 dashboard인데 콘텐츠가 없음");
|
||||
}
|
||||
} else if (finalUrl.includes("/login")) {
|
||||
console.log(" ❌ 다시 login으로 리다이렉트됨");
|
||||
console.log("\n 분석:");
|
||||
console.log(" - 이것은 Dashboard.razor에서 redirect되는 뜻");
|
||||
console.log(" - localStorage에서 토큰을 읽지 못했을 가능성");
|
||||
} else {
|
||||
console.log(" ❓ 예상치 못한 URL");
|
||||
}
|
||||
|
||||
console.log("\n5️⃣ 콘솔 로그 분석:");
|
||||
const dashboardLogs = allLogs.filter(l => l.includes("[Dashboard]"));
|
||||
if (dashboardLogs.length > 0) {
|
||||
console.log(" Dashboard 로그:");
|
||||
dashboardLogs.forEach(l => console.log(" - " + l));
|
||||
} else {
|
||||
console.log(" ⚠️ Dashboard 로그 없음 (페이지가 로드되지 않음?)");
|
||||
}
|
||||
|
||||
await p.screenshot({ path: "./precision-test-result.png", fullPage: true });
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error:", e.message);
|
||||
}
|
||||
|
||||
await b.close();
|
||||
})();
|
||||
+36
-7
@@ -1,5 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.JSInterop;
|
||||
using QuantEngine.Web.Client.Services;
|
||||
|
||||
namespace QuantEngine.Web.Client.Infrastructure
|
||||
@@ -8,30 +9,59 @@ namespace QuantEngine.Web.Client.Infrastructure
|
||||
{
|
||||
private readonly LocalStorageService _localStorage;
|
||||
private readonly HttpClient _http;
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
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)
|
||||
public CustomAuthenticationStateProvider(LocalStorageService localStorage, HttpClient http, IJSRuntime jsRuntime)
|
||||
{
|
||||
_localStorage = localStorage;
|
||||
_http = http;
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
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";
|
||||
string token = null;
|
||||
string username = null;
|
||||
string role = null;
|
||||
|
||||
// Try to read from localStorage using JS interop (direct access)
|
||||
try
|
||||
{
|
||||
token = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", TokenKey);
|
||||
username = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", UsernameKey);
|
||||
role = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", RoleKey);
|
||||
|
||||
Console.WriteLine($"[Auth] JS interop: token={!string.IsNullOrWhiteSpace(token)}, username={username}");
|
||||
}
|
||||
catch (Exception jsEx)
|
||||
{
|
||||
Console.WriteLine($"[Auth] JS interop failed: {jsEx.Message}. Falling back to LocalStorageService...");
|
||||
|
||||
// Fallback to LocalStorageService
|
||||
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";
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -43,7 +73,7 @@ namespace QuantEngine.Web.Client.Infrastructure
|
||||
return new AuthenticationState(_anonymous);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[Auth] User authenticated: {username}");
|
||||
Console.WriteLine($"[Auth] ✅ User authenticated: {username}");
|
||||
var identity = new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim(ClaimTypes.Name, username),
|
||||
@@ -61,13 +91,12 @@ namespace QuantEngine.Web.Client.Infrastructure
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[Auth] No token or username found in localStorage");
|
||||
Console.WriteLine($"[Auth] ❌ No token or username found. token={!string.IsNullOrWhiteSpace(token)}, username={!string.IsNullOrWhiteSpace(username)}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Auth] Error accessing localStorage: {ex.Message}");
|
||||
// Return anonymous if localStorage isn't ready
|
||||
}
|
||||
|
||||
return new AuthenticationState(_anonymous);
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
{
|
||||
// Check authentication
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
Console.WriteLine($"[Dashboard] Auth state received. IsAuthenticated: {authState.User.Identity?.IsAuthenticated}");
|
||||
Console.WriteLine($"[Dashboard] Auth state: IsAuthenticated={authState.User.Identity?.IsAuthenticated}, Name={authState.User.Identity?.Name}");
|
||||
|
||||
if (!authState.User.Identity?.IsAuthenticated ?? true)
|
||||
{
|
||||
@@ -255,7 +255,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[Dashboard] Authenticated as: {authState.User.Identity?.Name}");
|
||||
Console.WriteLine($"[Dashboard] ✅ Authenticated as: {authState.User.Identity?.Name}");
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -294,7 +294,9 @@ app.MapPost("/api/auth/login", async (JsonElement payload, HttpContext httpConte
|
||||
});
|
||||
|
||||
// Set HTTP-only cookie for server-side authentication
|
||||
// Note: Secure=true only in production (HTTPS); localhost uses HTTP
|
||||
Console.WriteLine($"[Auth/Login] Setting cookie 'quant_auth_token'");
|
||||
Console.WriteLine($"[Auth/Login] IsHttps: {httpContext.Request.IsHttps}");
|
||||
|
||||
httpContext.Response.Cookies.Append(
|
||||
"quant_auth_token",
|
||||
rawToken,
|
||||
@@ -308,8 +310,11 @@ app.MapPost("/api/auth/login", async (JsonElement payload, HttpContext httpConte
|
||||
}
|
||||
);
|
||||
|
||||
Console.WriteLine($"[Auth/Login] Cookie append completed");
|
||||
Console.WriteLine($"[Auth/Login] Response headers count: {httpContext.Response.Headers.Count}");
|
||||
|
||||
// Also return token for localStorage backup (for SPA navigation)
|
||||
return Results.Ok(new
|
||||
var result = Results.Ok(new
|
||||
{
|
||||
success = true,
|
||||
username = account.Username,
|
||||
@@ -317,6 +322,9 @@ app.MapPost("/api/auth/login", async (JsonElement payload, HttpContext httpConte
|
||||
accessToken = rawToken,
|
||||
expiresAt = expiresAt.ToString("O")
|
||||
});
|
||||
|
||||
Console.WriteLine($"[Auth/Login] About to return 200 OK response");
|
||||
return result;
|
||||
}).DisableAntiforgery();
|
||||
|
||||
app.MapGet("/api/auth/me", async (HttpContext context, IWorkspaceRepository workspaceRepo) =>
|
||||
|
||||
Reference in New Issue
Block a user