diff --git a/auth-test.png b/auth-test.png new file mode 100644 index 0000000..a04d81e Binary files /dev/null and b/auth-test.png differ diff --git a/cookie-auth-test.mjs b/cookie-auth-test.mjs new file mode 100644 index 0000000..7780030 --- /dev/null +++ b/cookie-auth-test.mjs @@ -0,0 +1,53 @@ +import { chromium } from "@playwright/test"; + +(async () => { + const b = await chromium.launch(); + const p = await b.newPage(); + + try { + await p.goto("http://localhost:5265/login"); + console.log("โœ“ 1. Login page loaded"); + + // 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"); + + // 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)"); + } + } 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" }); + + } catch (e) { + console.error("Error:", e.message.substring(0, 50)); + } + + await b.close(); +})(); diff --git a/final-login-test.png b/final-login-test.png new file mode 100644 index 0000000..da8a76f Binary files /dev/null and b/final-login-test.png differ diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs index 13c619e..203ab22 100644 --- a/src/dotnet/QuantEngine.Web/Program.cs +++ b/src/dotnet/QuantEngine.Web/Program.cs @@ -204,7 +204,7 @@ app.MapGet("/login", (HttpContext ctx) => app.MapCollectionEndpoints(); // Login API (API-First for Blazor WASM client authentication) -app.MapPost("/api/auth/login", async (JsonElement payload, IWorkspaceRepository workspaceRepo) => +app.MapPost("/api/auth/login", async (JsonElement payload, HttpContext httpContext, IWorkspaceRepository workspaceRepo, IWebHostEnvironment env) => { static string? ReadString(JsonElement root, params string[] names) { @@ -240,6 +240,21 @@ app.MapPost("/api/auth/login", async (JsonElement payload, IWorkspaceRepository { var devToken = Guid.NewGuid().ToString("N"); var devExpiresAt = DateTimeOffset.UtcNow.AddDays(7); + + // Set HTTP-only cookie for dev fallback too + httpContext.Response.Cookies.Append( + "quant_auth_token", + devToken, + new Microsoft.AspNetCore.Http.CookieOptions + { + HttpOnly = true, + Secure = httpContext.Request.IsHttps, + SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, + Expires = devExpiresAt, + Path = "/" + } + ); + return Results.Ok(new { success = true, @@ -278,6 +293,22 @@ app.MapPost("/api/auth/login", async (JsonElement payload, IWorkspaceRepository RevokedAt = null }); + // Set HTTP-only cookie for server-side authentication + // Note: Secure=true only in production (HTTPS); localhost uses HTTP + httpContext.Response.Cookies.Append( + "quant_auth_token", + rawToken, + new Microsoft.AspNetCore.Http.CookieOptions + { + HttpOnly = true, + Secure = httpContext.Request.IsHttps, // Only secure on HTTPS + SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Lax for localhost + Expires = expiresAt, + Path = "/" + } + ); + + // Also return token for localStorage backup (for SPA navigation) return Results.Ok(new { success = true, @@ -290,13 +321,19 @@ app.MapPost("/api/auth/login", async (JsonElement payload, IWorkspaceRepository app.MapGet("/api/auth/me", async (HttpContext context, IWorkspaceRepository workspaceRepo) => { + // Try to get token from Bearer header first, then fall back to cookie + var token = ""; + var authHeader = context.Request.Headers.Authorization.ToString(); - if (string.IsNullOrWhiteSpace(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { - return Results.Unauthorized(); + token = authHeader["Bearer ".Length..].Trim(); + } + else if (context.Request.Cookies.TryGetValue("quant_auth_token", out var cookieToken)) + { + token = cookieToken; } - var token = authHeader["Bearer ".Length..].Trim(); if (string.IsNullOrWhiteSpace(token)) { return Results.Unauthorized(); @@ -328,6 +365,10 @@ app.MapPost("/api/auth/logout", async (HttpContext context, IWorkspaceRepository var tokenHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token))); await workspaceRepo.RevokeSessionAsync(tokenHash, DateTimeOffset.UtcNow.ToString("O")); + + // Clear authentication cookie + context.Response.Cookies.Delete("quant_auth_token"); + return Results.Ok(new { success = true }); }).DisableAntiforgery(); diff --git a/src/dotnet/QuantEngine.Web/wwwroot/login.html b/src/dotnet/QuantEngine.Web/wwwroot/login.html index b143fc7..bdc258f 100644 --- a/src/dotnet/QuantEngine.Web/wwwroot/login.html +++ b/src/dotnet/QuantEngine.Web/wwwroot/login.html @@ -320,13 +320,13 @@ // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ alertDiv.className = 'alert alert-success show'; - alertDiv.textContent = '๋กœ๊ทธ์ธ ์„ฑ๊ณต! ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค...'; + alertDiv.textContent = '๋กœ๊ทธ์ธ ์„ฑ๊ณต! ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.'; - // Blazor ์•ฑ์ด localStorage์—์„œ ํ† ํฐ์„ ์ฝ๊ณ  ์ดˆ๊ธฐํ™”ํ•  ์‹œ๊ฐ„์„ ์ถฉ๋ถ„ํžˆ ์ค€๋‹ค - // 4์ดˆ ๋Œ€๊ธฐ ํ›„ ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ + // ์ฟ ํ‚ค๊ฐ€ ์„ค์ •๋˜์—ˆ์œผ๋ฏ€๋กœ ์งง์€ ๋Œ€๊ธฐ ์‹œ๊ฐ„๋งŒ ํ•„์š” + // (Blazor๊ฐ€ ์ฟ ํ‚ค๋ฅผ ์ž๋™์œผ๋กœ ์ธ์‹ํ•จ) setTimeout(() => { window.location.href = '/dashboard'; - }, 4000); + }, 1000); } else { const data = await response.json(); alertDiv.className = 'alert alert-error show'; diff --git a/test-cookie-auth.mjs b/test-cookie-auth.mjs new file mode 100644 index 0000000..8394987 --- /dev/null +++ b/test-cookie-auth.mjs @@ -0,0 +1,38 @@ +import { chromium } from "@playwright/test"; + +(async () => { + const b = await chromium.launch(); + const p = await b.newPage(); + + try { + await p.goto("http://localhost:5265/login"); + console.log("โœ“ Login loaded"); + + // Submit + 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({ timeout: 5000 }); } catch { } + + // Check cookie and URL + const cookies = await p.context().cookies(); + const hasCookie = cookies.some(c => c.name === "quant_auth_token"); + const url = p.url(); + + console.log(`Auth cookie: ${hasCookie ? "YES" : "NO"}`); + console.log(`URL: ${url}`); + + if (url.includes("/dashboard") && !url.includes("/not-found")) { + console.log("โœ“โœ“โœ“ SUCCESS!"); + } + + await p.screenshot({ path: "./auth-test.png" }); + + } catch (e) { + console.error(e.message); + } + + await b.close(); +})();