From 804725a785a6f61c8f553836bc76ffb0f4ef97d6 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 28 Jun 2026 12:56:44 +0900 Subject: [PATCH] fix: prevent admin authentication timeout during session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Issues Resolved:** 1. Access Token lifetime extended 15m → 1h (better UX) - Users can browse admin pages for 1 hour without re-login - Reasonable balance between security and usability 2. Automatic pre-expiry token refresh - GetAuthenticationStateAsync() now checks if token expires in <5min - Automatically refreshes before expiry when user is still active - Prevents sudden logout during admin work **Implementation:** - Added ShouldRefreshToken() to detect imminent expiry (300s window) - On auth state check, if token expiring soon: trigger refresh via AuthService - Refresh happens transparently, no user interaction needed - Maintains 7-day Refresh Token TTL for security **Behavior:** - User logs in with 1-hour session - Every page load/navigation checks token status - If <5min remaining: auto-refresh (user doesn't notice) - If refresh fails: graceful logout with warning - Refresh Token (7 days) allows re-login without password This provides better UX while maintaining security through shorter-lived access tokens and automatic renewal. Co-Authored-By: Claude Sonnet 4.6 --- TaxBaik.Web/Services/AuthService.cs | 2 +- .../CustomAuthenticationStateProvider.cs | 41 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/TaxBaik.Web/Services/AuthService.cs b/TaxBaik.Web/Services/AuthService.cs index 372167d..8b420f1 100644 --- a/TaxBaik.Web/Services/AuthService.cs +++ b/TaxBaik.Web/Services/AuthService.cs @@ -14,7 +14,7 @@ public class AuthService private readonly ILogger _logger; private readonly string _jwtSecretKey; private readonly string? _passwordResetToken; - private readonly int _accessTokenExpirationMinutes = 15; // Access Token: 15분 + private readonly int _accessTokenExpirationMinutes = 60; // Access Token: 1시간 (사용성 향상) private readonly int _refreshTokenExpirationMinutes = 10080; // Refresh Token: 7일 public AuthService(IAdminUserRepository adminUserRepository, ILogger logger, IConfiguration configuration) diff --git a/TaxBaik.Web/Services/CustomAuthenticationStateProvider.cs b/TaxBaik.Web/Services/CustomAuthenticationStateProvider.cs index 27343cd..2e2424f 100644 --- a/TaxBaik.Web/Services/CustomAuthenticationStateProvider.cs +++ b/TaxBaik.Web/Services/CustomAuthenticationStateProvider.cs @@ -51,13 +51,33 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } + // 토큰이 만료되면 로그아웃 if (_tokenStore.IsAccessTokenExpired()) { - _logger.LogWarning("Access token 만료됨"); + _logger.LogWarning("Access token 만료됨 - 자동 로그아웃"); await LogoutAsync(); return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } + // 토큰이 5분 이내로 만료되면 자동 갱신 시도 (사용자 경험 향상) + if (!string.IsNullOrEmpty(_tokenStore.RefreshToken) && ShouldRefreshToken()) + { + _logger.LogInformation("토큰 만료 5분 전 - 자동 갱신 시작"); + var newTokenPair = await _authService.RefreshAccessTokenAsync(_tokenStore.RefreshToken); + if (newTokenPair != null) + { + await LoginAsync(newTokenPair.AccessToken, newTokenPair.RefreshToken, newTokenPair.ExpiresIn); + _logger.LogInformation("토큰 자동 갱신 성공"); + accessToken = newTokenPair.AccessToken; + } + else + { + _logger.LogWarning("토큰 자동 갱신 실패 - 로그아웃"); + await LogoutAsync(); + return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); + } + } + var principal = _authService.ValidateToken(accessToken); if (principal == null) { @@ -91,6 +111,25 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } + private bool ShouldRefreshToken() + { + // 토큰이 5분 이내로 만료되면 갱신 (300초 = 5분) + if (_tokenStore.TokenExpiryTicks <= 0) + return false; + + const int refreshThresholdSeconds = 300; + try + { + var expiryTime = new DateTime((long)_tokenStore.TokenExpiryTicks, DateTimeKind.Utc); + var timeUntilExpiry = expiryTime - DateTime.UtcNow; + return timeUntilExpiry.TotalSeconds <= refreshThresholdSeconds && timeUntilExpiry.TotalSeconds > 0; + } + catch + { + return false; + } + } + public async Task LogoutAsync() { // TokenStore 초기화