Refactor admin login to cookie auth
TaxBaik CI/CD / build-and-deploy (push) Failing after 2m23s

This commit is contained in:
2026-07-09 11:07:09 +09:00
parent 88c1d835b5
commit 0545964468
7 changed files with 177 additions and 54 deletions
-30
View File
@@ -64,33 +64,3 @@
</div>
</section>
@if (Model.LoginSucceeded && Model.TokenPair is not null)
{
<script>
(function () {
const tokenPair = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(
Model.TokenPair,
new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase }));
const expiryTicks = 621355968000000000 + ((Date.now() + (tokenPair.expiresIn || 3600) * 1000) * 10000);
localStorage.setItem('accessToken', tokenPair.accessToken || '');
localStorage.setItem('refreshToken', tokenPair.refreshToken || '');
localStorage.setItem('tokenExpiry', String(expiryTicks));
@if (Model.RememberMe)
{
<text>
localStorage.setItem('admin-remembered-username', @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Username)));
localStorage.setItem('admin-remember-checkbox', 'true');
</text>
}
else
{
<text>
localStorage.removeItem('admin-remembered-username');
localStorage.removeItem('admin-remember-checkbox');
</text>
}
window.location.replace(@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.RedirectUrl)));
})();
</script>
}
+20 -20
View File
@@ -1,7 +1,8 @@
using System.ComponentModel.DataAnnotations;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Web.Endpoints.Auth;
using Microsoft.AspNetCore.Authentication;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin;
@@ -27,11 +28,6 @@ public class LoginModel(AuthService authService) : PageModel
public string? ReturnUrl { get; set; }
public string? ErrorMessage { get; set; }
public bool LoginSucceeded { get; set; }
public TokenPairResponse? TokenPair { get; set; }
public string RedirectUrl { get; set; } = "/admin/dashboard";
public void OnGet()
@@ -49,28 +45,32 @@ public class LoginModel(AuthService authService) : PageModel
return Page();
}
var authResult = await authService.AuthenticateAndGenerateTokenPairAsync(Username, Password);
if (!authResult.IsSuccess || authResult.TokenPair is null)
var user = await authService.AuthenticateAsync(Username, Password);
if (user is null)
{
ErrorMessage = authResult.FailureReason switch
{
AuthFailureReason.DatabaseUnavailable => "로그인 서비스를 일시적으로 사용할 수 없습니다. 잠시 후 다시 시도하세요.",
_ => "아이디 또는 비밀번호가 올바르지 않습니다."
};
ErrorMessage = "아이디 또는 비밀번호가 올바르지 않습니다.";
return Page();
}
LoginSucceeded = true;
RedirectUrl = NormalizeRedirectUrl(ReturnUrl);
TokenPair = new TokenPairResponse
var claims = new List<Claim>
{
AccessToken = authResult.TokenPair.AccessToken,
RefreshToken = authResult.TokenPair.RefreshToken,
ExpiresIn = authResult.TokenPair.ExpiresIn,
Token = authResult.TokenPair.AccessToken
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.Username),
new(ClaimTypes.Role, "Admin")
};
return Page();
var identity = new ClaimsIdentity(claims, AdminAuthDefaults.Scheme);
var principal = new ClaimsPrincipal(identity);
var properties = new AuthenticationProperties
{
IsPersistent = RememberMe,
RedirectUri = RedirectUrl
};
await HttpContext.SignInAsync(AdminAuthDefaults.Scheme, principal, properties);
return LocalRedirect(RedirectUrl);
}
private static string NormalizeRedirectUrl(string? returnUrl)
+12 -1
View File
@@ -115,7 +115,7 @@ builder.Services.AddSession(options =>
builder.Services.AddDistributedMemoryCache();
// TempData는 기본적으로 쿠키 저장소 사용 (위 세션 설정 상속)
// JWT 인증
// 인증: 관리자 쿠키 + 포털 쿠키 + 기존 JWT(과도기 API)
var connectionString = builder.Configuration.GetConnectionString("Default")
?? throw new InvalidOperationException("Missing connection string");
var jwtKey = builder.Configuration["Jwt:SecretKey"] ?? throw new InvalidOperationException("Missing JWT SecretKey");
@@ -153,6 +153,17 @@ var authenticationBuilder = builder.Services.AddAuthentication(opts =>
opts.SlidingExpiration = true;
opts.ExpireTimeSpan = TimeSpan.FromDays(7);
})
.AddCookie(AdminAuthDefaults.Scheme, opts =>
{
opts.Cookie.Name = AdminAuthDefaults.CookieName;
opts.Cookie.HttpOnly = true;
opts.Cookie.SameSite = SameSiteMode.Lax;
opts.Cookie.SecurePolicy = isProduction ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
opts.LoginPath = "/admin/login";
opts.AccessDeniedPath = "/admin/login";
opts.SlidingExpiration = true;
opts.ExpireTimeSpan = TimeSpan.FromHours(12);
})
.AddCookie(PortalOAuthDefaults.ExternalScheme, opts =>
{
opts.Cookie.Name = "TaxBaik.Portal.External";
@@ -0,0 +1,8 @@
namespace TaxBaik.Web.Services;
public static class AdminAuthDefaults
{
public const string Scheme = "AdminCookie";
public const string CookieName = "TaxBaik.Admin.Auth";
}
+29
View File
@@ -88,6 +88,35 @@ public class AuthService
return AuthResult.Success(GenerateTokenPair(user));
}
public async Task<AdminUser?> AuthenticateAsync(string username, string password)
{
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
return null;
try
{
var user = await _adminUserRepository.GetByUsernameAsync(username);
if (user == null || string.IsNullOrWhiteSpace(user.PasswordHash))
return null;
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
return null;
await _adminUserRepository.UpdateLastLoginAtAsync(user.Id);
return user;
}
catch (Exception ex) when (_environment.IsDevelopment())
{
_logger.LogError(ex, "개발 환경 관리자 인증 실패: {Username}", username);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "관리자 인증 중 오류: {Username}", username);
return null;
}
}
public async Task<AuthTokenPair?> RefreshAccessTokenAsync(string refreshToken)
{
try