refactor: Phase 5 - JWT token lifecycle (Access + Refresh + Auto-refresh)
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>
This commit is contained in:
2026-06-28 10:51:24 +09:00
parent 0334a5f607
commit 58edbd9c8f
7 changed files with 276 additions and 40 deletions
@@ -78,15 +78,15 @@
var request = new { model.Username, model.Password };
var response = await ApiClient.PostAsync<LoginResponse>("auth/login", request);
if (response?.Token == null)
if (response?.AccessToken == null || response?.RefreshToken == null)
{
errorMessage = "사용자명 또는 비밀번호가 올바르지 않습니다.";
isLoading = false;
return;
}
await ApiClient.SetAuthToken(response.Token);
await AuthStateProvider.LoginAsync(response.Token);
await ApiClient.SetAuthToken(response.AccessToken);
await AuthStateProvider.LoginAsync(response.AccessToken, response.RefreshToken, response.ExpiresIn);
NavigationManager.NavigateTo(GetReturnUrl(), forceLoad: false);
}
catch
@@ -98,7 +98,8 @@
private class LoginResponse
{
public string Token { get; set; } = "";
public string AccessToken { get; set; } = "";
public string RefreshToken { get; set; } = "";
public int ExpiresIn { get; set; }
}