58edbd9c8f
TaxBaik CI/CD / build-and-deploy (push) Successful in 48s
**Implementation:**
- AuthService: Split token generation
* AccessToken: 15 minutes
* RefreshToken: 7 days (10080 minutes)
* New: GenerateTokenPair() method
* New: RefreshAccessTokenAsync() method
- AuthTokenPair: New record (accessToken, refreshToken, expiresIn)
- AuthController: New /api/auth/refresh endpoint
* POST /api/auth/refresh?refreshToken=...
* Response: { accessToken, refreshToken, expiresIn }
* RefreshTokenRequest DTO
- TokenRefreshHandler: New DelegatingHandler
* Automatic Bearer token injection
* 401 response handling
* Auto-refresh with retry
* localStorage sync (accessToken, refreshToken, tokenExpiry)
- CustomAuthenticationStateProvider: Token storage split
* Before: auth_token (single)
* After: accessToken, refreshToken, tokenExpiry
* LoginAsync signature updated
- Login.razor: Handle token pair
* LoginResponse: { accessToken, refreshToken, expiresIn }
* Call new LoginAsync(accessToken, refreshToken, expiresIn)
- Program.cs: TokenRefreshHandler registration
* AddScoped<TokenRefreshHandler>()
* AdminDashboardClient pipeline: .AddHttpMessageHandler<TokenRefreshHandler>()
**SOLID Principles:**
✓ S (Single Responsibility): TokenRefreshHandler handles only token refresh
✓ D (Dependency Inversion): DelegatingHandler abstracts HTTP concerns
✓ O (Open/Closed): Token lifetime extension without code changes
**Security Pattern:**
- Short-lived access tokens (15min) reduce theft window
- Refresh tokens (7d) enable persistence without storing secrets
- Automatic refresh is transparent to components
**Flow:**
Blazor → AdminDashboardClient → TokenRefreshHandler (auto-add Bearer)
→ 401 → RefreshTokenAsync() → POST /api/auth/refresh
→ Store new pair → Retry original request
Status: Token lifecycle complete, ready for SignalR integration (Phase 6)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
85 lines
2.9 KiB
C#
85 lines
2.9 KiB
C#
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Security.Claims;
|
|
using Microsoft.AspNetCore.Components.Authorization;
|
|
|
|
namespace TaxBaik.Web.Services;
|
|
|
|
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
|
{
|
|
private readonly ILocalStorageService _localStorage;
|
|
private readonly AuthService _authService;
|
|
private readonly ILogger<CustomAuthenticationStateProvider> _logger;
|
|
|
|
public CustomAuthenticationStateProvider(ILocalStorageService localStorage, AuthService authService, ILogger<CustomAuthenticationStateProvider> logger)
|
|
{
|
|
_localStorage = localStorage;
|
|
_authService = authService;
|
|
_logger = logger;
|
|
}
|
|
|
|
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
|
{
|
|
try
|
|
{
|
|
var accessToken = await _localStorage.GetItemAsStringAsync("accessToken");
|
|
|
|
if (string.IsNullOrEmpty(accessToken))
|
|
{
|
|
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
|
}
|
|
|
|
if (IsTokenExpired(accessToken))
|
|
{
|
|
_logger.LogWarning("Access token 만료됨");
|
|
await LogoutAsync();
|
|
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
|
}
|
|
|
|
var principal = _authService.ValidateToken(accessToken);
|
|
if (principal == null)
|
|
{
|
|
await LogoutAsync();
|
|
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
|
}
|
|
|
|
return new AuthenticationState(principal);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "인증 상태 조회 중 오류 발생");
|
|
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
|
}
|
|
}
|
|
|
|
public async Task LoginAsync(string accessToken, string refreshToken, int expiresIn)
|
|
{
|
|
await _localStorage.SetItemAsStringAsync("accessToken", accessToken);
|
|
await _localStorage.SetItemAsStringAsync("refreshToken", refreshToken);
|
|
await _localStorage.SetItemAsStringAsync("tokenExpiry",
|
|
DateTime.UtcNow.AddSeconds(expiresIn).Ticks.ToString());
|
|
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
|
}
|
|
|
|
public async Task LogoutAsync()
|
|
{
|
|
await _localStorage.RemoveItemAsync("accessToken");
|
|
await _localStorage.RemoveItemAsync("refreshToken");
|
|
await _localStorage.RemoveItemAsync("tokenExpiry");
|
|
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
|
}
|
|
|
|
private bool IsTokenExpired(string token)
|
|
{
|
|
try
|
|
{
|
|
var handler = new JwtSecurityTokenHandler();
|
|
var jwtToken = handler.ReadJwtToken(token);
|
|
return jwtToken.ValidTo < DateTime.UtcNow;
|
|
}
|
|
catch
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|