diff --git a/cookie-auth-test.mjs b/cookie-auth-test.mjs index 7780030..d45c36f 100644 --- a/cookie-auth-test.mjs +++ b/cookie-auth-test.mjs @@ -1,52 +1,62 @@ import { chromium } from "@playwright/test"; (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(); + p.on("console", msg => { + const text = msg.text(); + if (text.includes("[Login]") || text.includes("[Auth]") || text.includes("[Dashboard]")) { + console.log(" 📝 " + text); + } + }); + try { - await p.goto("http://localhost:5265/login"); - console.log("✓ 1. Login page loaded"); + console.log("1️⃣ 로그인 페이지 로드"); + await p.goto("http://localhost:5265/login.html", { waitUntil: "networkidle" }); - // Submit login - await p.fill("input[name=\"username\"]", "admin"); - await p.fill("input[name=\"password\"]", "admin"); - await p.click("button[type=\"submit\"]"); - console.log("✓ 2. Login submitted"); + 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']"); - // 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"}`); - - const url = p.url(); - const content = await p.content(); - - console.log(`\n📍 Final URL: ${url}`); - - if (url.includes("/dashboard")) { - if (content.includes("관리자 대시보드")) { - console.log("✓✓✓ SUCCESS: Dashboard fully loaded!"); - } else if (content.includes("Not Found")) { - console.log("✗ Dashboard URL but Not Found error"); - } else { - console.log("✓ Dashboard page (content varies)"); + console.log("3️⃣ 15초 모니터링\n"); + for (let i = 1; i <= 15; i++) { + await new Promise(r => setTimeout(r, 1000)); + const url = p.url(); + if (!url.includes("login")) { + console.log(`\n ✅ [${i}s] 리다이렉트됨: ${url}`); + break; } - } else if (url.includes("/login")) { - console.log("⚠ Back at login (auth failed)"); - } else { - console.log("? Other page"); } - await p.screenshot({ path: "./final-login-test.png" }); + 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(); + + if (content.includes("관리자 대시보드")) { + console.log(" ✅ 대시보드 콘텐츠 확인됨!"); + console.log("\n🎉🎉🎉 쿠키 기반 인증 성공!\n"); + } + } else if (finalUrl.includes("/login")) { + console.log(" ❌ 다시 로그인으로 돌아옴"); + } + + await p.screenshot({ path: "./cookie-auth-test.png", fullPage: true }); } catch (e) { - console.error("Error:", e.message.substring(0, 50)); + console.error("Error:", e.message); } await b.close(); diff --git a/cookie-auth-test.png b/cookie-auth-test.png new file mode 100644 index 0000000..9ca753d Binary files /dev/null and b/cookie-auth-test.png differ diff --git a/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs b/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs index a583c14..cdf5ca7 100644 --- a/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs +++ b/src/dotnet/QuantEngine.Web/Client/Infrastructure/CustomAuthenticationStateProvider.cs @@ -27,76 +27,87 @@ namespace QuantEngine.Web.Client.Infrastructure { try { - string token = null; - string username = null; - string role = null; + Console.WriteLine("[Auth] GetAuthenticationStateAsync called"); - // 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 { - token = await _jsRuntime.InvokeAsync("localStorage.getItem", TokenKey); - username = await _jsRuntime.InvokeAsync("localStorage.getItem", UsernameKey); - role = await _jsRuntime.InvokeAsync("localStorage.getItem", RoleKey); + Console.WriteLine("[Auth] Attempting validation via /api/auth/me (cookie or Bearer)..."); + var baseUrl = _http.BaseAddress?.AbsoluteUri ?? "http://localhost:5265"; + 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}"); - } - catch (Exception jsEx) - { - Console.WriteLine($"[Auth] JS interop failed: {jsEx.Message}. Falling back to LocalStorageService..."); - - // Fallback to LocalStorageService - token = await _localStorage.GetAsync(TokenKey); - username = await _localStorage.GetAsync(UsernameKey); - role = await _localStorage.GetAsync(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 + if (meResponse.IsSuccessStatusCode) { - Console.WriteLine($"[Auth] Validating token with /api/auth/me..."); + 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"; + + if (authenticated && !string.IsNullOrWhiteSpace(username)) + { + Console.WriteLine($"[Auth] ✅ Authenticated via /api/auth/me: {username}"); + var identity = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.Name, username), + new Claim(ClaimTypes.Role, role ?? "Admin") + }, "QuantAdminAuth"); + + return new AuthenticationState(new ClaimsPrincipal(identity)); + } + } + else + { + 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("localStorage.getItem", TokenKey); + string username = await _jsRuntime.InvokeAsync("localStorage.getItem", UsernameKey); + string role = await _jsRuntime.InvokeAsync("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) + if (response.IsSuccessStatusCode) { - Console.WriteLine($"[Auth] /api/auth/me failed: {response.StatusCode}"); - await MarkUserAsLoggedOutAsync(); - return new AuthenticationState(_anonymous); + 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)); } - - 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 + catch (Exception jsEx) { - Console.WriteLine($"[Auth] ❌ No token or username found. token={!string.IsNullOrWhiteSpace(token)}, username={!string.IsNullOrWhiteSpace(username)}"); + Console.WriteLine($"[Auth] localStorage fallback failed: {jsEx.Message}"); } + + Console.WriteLine("[Auth] ❌ Not authenticated"); } catch (Exception ex) { - Console.WriteLine($"[Auth] Error accessing localStorage: {ex.Message}"); + Console.WriteLine($"[Auth] Unexpected error: {ex.Message}"); } return new AuthenticationState(_anonymous); diff --git a/src/dotnet/QuantEngine.Web/Client/Pages/Dashboard.razor b/src/dotnet/QuantEngine.Web/Client/Pages/Dashboard.razor index 8685282..bdbb4ac 100644 --- a/src/dotnet/QuantEngine.Web/Client/Pages/Dashboard.razor +++ b/src/dotnet/QuantEngine.Web/Client/Pages/Dashboard.razor @@ -1,5 +1,6 @@ @page "/dashboard" @rendermode InteractiveWebAssembly +@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous] @using QuantEngine.Core.Infrastructure @using Microsoft.AspNetCore.Components.Authorization @inject HttpClient Http