feat: implement server-side cookie-based authentication
- Add HTTP-only cookie setting in /api/auth/login endpoint - Support both Bearer token and cookie auth in /api/auth/me - Clear cookie on /api/auth/logout - Handle admin:admin dev fallback with cookie support - Update login.html to use 1 second redirect (cookie-based auth faster) Cookie configuration: - Name: quant_auth_token - HttpOnly: true (prevents JavaScript access) - Secure: based on HTTPS status - SameSite: Lax (for localhost compatibility) - Expires: 7 days Status: Cookie auth framework complete, testing in progress Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user