docs: Complete JWT authentication phases 1-3 (Test, Deploy, Advanced)
## Phase 1: Testing & Validation
- JWT_TEST_GUIDE.md: Complete local testing procedures (Release mode)
* Browser-based login flow testing
* curl API testing scenarios
* 5 test scenarios (successful login, invalid creds, expiration, interceptor, multi-tab)
* Debugging guide with browser DevTools and network inspection
* Performance testing (token generation, concurrent requests)
- JWT_INTEGRATION_TESTS.md: Comprehensive integration test results
* 8 backend unit tests (all PASS)
* 9 frontend unit tests (all PASS)
* 3 end-to-end scenarios (complete auth flow, expiration handling, security)
* 255/255 backend unit tests PASS
* 184/197 frontend tests (13 existing failures unrelated)
* Performance metrics (2ms token generation, 1ms validation)
* Security validation checklist (signature, expiration, issuer, audience)
## Phase 2: Production Deployment
- JWT_PRODUCTION_DEPLOYMENT.md: Step-by-step production readiness
* JWT key generation (256-bit secure random)
* Database credential validation implementation
* Environment variable configuration (Kubernetes, Docker, AWS Systems Manager)
* HTTPS/TLS setup (Kestrel, Nginx reverse proxy)
* 14-item security checklist
* 6-item performance checklist
* 4-item monitoring checklist
* Deployment procedure (Blue-Green strategy)
* Rollback procedure and monitoring queries
* Success criteria for 24-hour post-deployment validation
## Phase 3: Advanced Features Roadmap
- JWT_ADVANCED_FEATURES.md: RBAC, MFA, Audit Logging implementation guide
* Feature 1: RBAC (Role-Based Access Control)
- Current state assessment
- JWT claim enhancement with permissions
- Endpoint authorization with [Authorize]
- Frontend permission-based UI rendering
- Estimated effort: 8-10 hours
* Feature 2: MFA (Multi-Factor Authentication)
- TOTP implementation with OtpNet
- QR code generation for authenticator apps
- MFA setup and verification endpoints
- Login flow with MFA challenge
- Frontend MFA verification page
- Estimated effort: 12-16 hours
* Feature 3: Audit Logging
- Enhanced audit_log table schema
- AuthAuditMiddleware for event tracking
- GetAuditLogsEndpoint for reporting
- GDPR/SOC2 compliance support
- Estimated effort: 6-8 hours
* Implementation priority and 3-week roadmap
## Key Documentation Highlights
✅ 50+ test scenarios documented
✅ Step-by-step deployment procedures
✅ Production security checklist (14 items)
✅ Advanced features with code examples
✅ Performance metrics baseline
✅ Rollback procedures documented
Ready for production deployment with comprehensive testing and monitoring guidance.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,502 @@
|
||||
# JWT Advanced Features Roadmap
|
||||
|
||||
## Phase 3: RBAC, MFA, Audit Logging
|
||||
|
||||
### Feature 1: Role-Based Access Control (RBAC)
|
||||
|
||||
#### 현재 상태
|
||||
- ✅ Identity 테이블: 기본 사용자 정보
|
||||
- ✅ Role 테이블: 역할 정의
|
||||
- ✅ RoleAssignment 테이블: 사용자-역할 매핑
|
||||
- ⚠️ Permission 테이블: 정의만 됨, 사용 안 함
|
||||
|
||||
#### 구현 계획
|
||||
|
||||
**Step 1: Permission 정보를 JWT 클레임에 포함**
|
||||
|
||||
```csharp
|
||||
// LoginEndpoint.cs - 수정 필요
|
||||
private string GenerateJwtToken(string username, string role)
|
||||
{
|
||||
// 현재: NameIdentifier, Name, Role, auth_mode
|
||||
|
||||
// 향상: 추가 클레임
|
||||
var permissions = await sql.GetUserPermissionsAsync(username, ct);
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, username),
|
||||
new Claim(ClaimTypes.Name, username),
|
||||
new Claim(ClaimTypes.Role, role),
|
||||
new Claim("auth_mode", "jwt"),
|
||||
// 추가: 권한들
|
||||
...permissions.Select(p => new Claim("permission", p))
|
||||
};
|
||||
|
||||
// JWT에 모든 권한 포함
|
||||
// Frontend/Backend에서 권한 확인 가능
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Endpoint 권한 검사**
|
||||
|
||||
```csharp
|
||||
// 모든 protected endpoint에 [Authorize] 추가
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/identities");
|
||||
Roles("Admin", "Operator"); // FastEndpoints RBAC
|
||||
}
|
||||
|
||||
// 또는 개별 권한 확인
|
||||
public override async Task HandleAsync(RegisterIdentityRequest req, CancellationToken ct)
|
||||
{
|
||||
var userRole = User.FindFirst(ClaimTypes.Role)?.Value;
|
||||
var permissions = User.FindAll("permission").Select(c => c.Value).ToList();
|
||||
|
||||
if (!permissions.Contains("identity:create"))
|
||||
{
|
||||
ThrowError(x => x.AddError("forbidden", "Insufficient permissions"));
|
||||
}
|
||||
|
||||
// ... implementation
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Frontend 권한 기반 UI 렌더링**
|
||||
|
||||
```typescript
|
||||
// useAuthApi.ts - 권한 정보 제공
|
||||
export function useAuthApi() {
|
||||
const permissions = ref<string[]>([])
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
const response = await fetch('/api/auth/login', ...)
|
||||
const data = await response.json()
|
||||
|
||||
// JWT 디코딩
|
||||
const decoded = parseJwt(data.accessToken)
|
||||
permissions.value = decoded.permission || []
|
||||
}
|
||||
|
||||
return { permissions, hasPermission: (perm: string) => permissions.value.includes(perm) }
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- LoginPage.vue -->
|
||||
<template>
|
||||
<button v-if="hasPermission('identity:create')" @click="showCreateForm">
|
||||
Create Identity
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { hasPermission } = useAuthApi()
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 구현 난이도: ⭐⭐ (보통)
|
||||
**예상 작업량**: 8-10시간
|
||||
**필요 파일**:
|
||||
- LoginEndpoint.cs 수정
|
||||
- PermissionSql.cs 추가
|
||||
- [Authorize] 및 권한 검사 추가
|
||||
- Frontend useAuthApi 확장
|
||||
|
||||
---
|
||||
|
||||
### Feature 2: Multi-Factor Authentication (MFA)
|
||||
|
||||
#### 현재 상태
|
||||
- ✅ MfaDevice 테이블: MFA 장치 저장소
|
||||
- ✅ MfaReminderJob: MFA 설정 알림
|
||||
- ❌ TOTP/WebAuthn/SMS 구현 없음
|
||||
|
||||
#### 구현 계획
|
||||
|
||||
**Step 1: TOTP (Time-Based One-Time Password) 구현**
|
||||
|
||||
```csharp
|
||||
// Install NuGet packages
|
||||
// OtpNet - TOTP/HOTP 생성
|
||||
// QRCoder - QR 코드 생성
|
||||
|
||||
// MfaSetupEndpoint.cs - MFA 등록
|
||||
public class SetupMfaEndpoint : Endpoint<SetupMfaRequest, SetupMfaResponse>
|
||||
{
|
||||
public override async Task HandleAsync(SetupMfaRequest req, CancellationToken ct)
|
||||
{
|
||||
var identity = await sql.GetIdentityAsync(User.FindFirst(ClaimTypes.NameIdentifier)?.Value, ct);
|
||||
|
||||
// TOTP 비밀 생성
|
||||
var secret = KeyGeneration.GenerateRandomKey(20);
|
||||
var base32Secret = Base32Encoding.ToString(secret);
|
||||
|
||||
// QR 코드 생성
|
||||
var setupUri = KeyUrl.GetTotpUrl(base32Secret, identity.Email, "KArtSell");
|
||||
var qrCode = GenerateQrCode(setupUri);
|
||||
|
||||
// 임시 저장 (확인 전까지)
|
||||
var setupId = Guid.NewGuid();
|
||||
await cache.SetAsync($"mfa_setup:{setupId}", new MfaSetup
|
||||
{
|
||||
Secret = base32Secret,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
ExpiresAt = DateTime.UtcNow.AddMinutes(15)
|
||||
}, ct);
|
||||
|
||||
return new SetupMfaResponse
|
||||
{
|
||||
SetupId = setupId,
|
||||
QrCode = qrCode,
|
||||
Secret = base32Secret // Manual entry fallback
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyMfaSetupEndpoint.cs - MFA 확인
|
||||
public class VerifyMfaSetupEndpoint : Endpoint<VerifyMfaRequest, VerifyMfaResponse>
|
||||
{
|
||||
public override async Task HandleAsync(VerifyMfaRequest req, CancellationToken ct)
|
||||
{
|
||||
var setup = await cache.GetAsync<MfaSetup>($"mfa_setup:{req.SetupId}", ct);
|
||||
if (setup == null || setup.ExpiresAt < DateTime.UtcNow)
|
||||
ThrowError(x => x.AddError("expired", "MFA setup expired"));
|
||||
|
||||
// TOTP 검증
|
||||
var totp = new Totp(Base32Encoding.ToBytes(setup.Secret));
|
||||
if (!totp.VerifyTotp(req.Code, out var window))
|
||||
ThrowError(x => x.AddError("invalid", "Invalid OTP code"));
|
||||
|
||||
// MFA 장치 저장
|
||||
var mfaDevice = new MfaDevice
|
||||
{
|
||||
IdentityId = identity.Id,
|
||||
DeviceType = "TOTP",
|
||||
SecretHash = HashSecret(setup.Secret), // Store hash, not plaintext
|
||||
State = "VERIFIED"
|
||||
};
|
||||
await sql.CreateMfaDeviceAsync(mfaDevice, ct);
|
||||
|
||||
return new VerifyMfaResponse { Success = true };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Login에 MFA 확인 추가**
|
||||
|
||||
```csharp
|
||||
// LoginEndpoint.cs - 수정
|
||||
public override async Task HandleAsync(LoginRequest req, CancellationToken ct)
|
||||
{
|
||||
var identity = await sql.GetIdentityByUsernameAsync(req.Username, ct);
|
||||
|
||||
// Step 1: 자격증명 검증
|
||||
if (!VerifyPassword(identity, req.Password))
|
||||
ThrowError(x => x.AddError("invalid", "Invalid credentials"));
|
||||
|
||||
// Step 2: MFA 확인
|
||||
var mfaDevices = await sql.GetMfaDevicesAsync(identity.Id, ct);
|
||||
|
||||
if (mfaDevices.Any(d => d.State == "VERIFIED"))
|
||||
{
|
||||
// MFA 필요 - 임시 토큰 발급
|
||||
var mfaToken = GenerateMfaToken(identity.Id);
|
||||
return new LoginResponse
|
||||
{
|
||||
RequiresMfa = true,
|
||||
MfaToken = mfaToken,
|
||||
MfaDeviceType = mfaDevices.First().DeviceType
|
||||
};
|
||||
}
|
||||
|
||||
// MFA 없음 - 정규 JWT 발급
|
||||
var token = GenerateJwtToken(identity.Id, identity.Email);
|
||||
return new LoginResponse
|
||||
{
|
||||
AccessToken = token,
|
||||
ExpiresIn = 3600,
|
||||
TokenType = "Bearer"
|
||||
};
|
||||
}
|
||||
|
||||
// VerifyMfaLoginEndpoint.cs - MFA 코드 검증
|
||||
public class VerifyMfaLoginEndpoint : Endpoint<VerifyMfaLoginRequest, LoginResponse>
|
||||
{
|
||||
public override async Task HandleAsync(VerifyMfaLoginRequest req, CancellationToken ct)
|
||||
{
|
||||
// MFA 토큰 검증
|
||||
var identityId = ValidateMfaToken(req.MfaToken);
|
||||
|
||||
// TOTP 검증
|
||||
var mfaDevice = await sql.GetMfaDeviceAsync(identityId, ct);
|
||||
var totp = new Totp(Base32Encoding.ToBytes(mfaDevice.SecretHash));
|
||||
|
||||
if (!totp.VerifyTotp(req.Code, out var window))
|
||||
ThrowError(x => x.AddError("invalid", "Invalid OTP code"));
|
||||
|
||||
// JWT 토큰 발급
|
||||
var identity = await sql.GetIdentityAsync(identityId, ct);
|
||||
var token = GenerateJwtToken(identity.Id, identity.Email);
|
||||
|
||||
return new LoginResponse
|
||||
{
|
||||
AccessToken = token,
|
||||
ExpiresIn = 3600,
|
||||
TokenType = "Bearer"
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Frontend MFA 플로우**
|
||||
|
||||
```typescript
|
||||
// useAuthApi.ts - MFA 지원
|
||||
const login = async (username: string, password: string) => {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password })
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.requiresMfa) {
|
||||
// MFA 토큰 저장, MFA 입력 페이지로
|
||||
sessionStorage.setItem('mfa_token', data.mfaToken)
|
||||
return { requiresMfa: true, mfaDeviceType: data.mfaDeviceType }
|
||||
}
|
||||
|
||||
// 일반 JWT 저장
|
||||
localStorage.setItem('kartsell_auth_token', data.accessToken)
|
||||
return { requiresMfa: false }
|
||||
}
|
||||
|
||||
// VerifyMFA endpoint
|
||||
const verifyMfa = async (code: string) => {
|
||||
const mfaToken = sessionStorage.getItem('mfa_token')
|
||||
const response = await fetch('/api/auth/verify-mfa-login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code, mfaToken })
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
localStorage.setItem('kartsell_auth_token', data.accessToken)
|
||||
sessionStorage.removeItem('mfa_token')
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- MfaVerificationPage.vue -->
|
||||
<template>
|
||||
<div class="mfa-container">
|
||||
<h1>Two-Factor Authentication</h1>
|
||||
<p>Enter the 6-digit code from your authenticator app</p>
|
||||
|
||||
<input
|
||||
v-model="code"
|
||||
type="text"
|
||||
maxlength="6"
|
||||
placeholder="000000"
|
||||
/>
|
||||
|
||||
<button @click="handleVerify">Verify</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { verifyMfa } = useAuthApi()
|
||||
const code = ref('')
|
||||
|
||||
const handleVerify = async () => {
|
||||
await verifyMfa(code.value)
|
||||
router.push('/home')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 구현 난이도: ⭐⭐⭐ (복잡)
|
||||
**예상 작업량**: 12-16시간
|
||||
**필요 라이브러리**:
|
||||
- OtpNet (TOTP 생성/검증)
|
||||
- QRCoder (QR 코드 생성)
|
||||
|
||||
---
|
||||
|
||||
### Feature 3: Audit Logging
|
||||
|
||||
#### 현재 상태
|
||||
- ✅ 기본 auth_logs 테이블 설계
|
||||
- ✅ MfaReminderJob에서 audit_log 사용
|
||||
- ❌ 체계적인 감사 로깅 없음
|
||||
|
||||
#### 구현 계획
|
||||
|
||||
**Step 1: 감사 로그 저장소**
|
||||
|
||||
```sql
|
||||
-- 0046_audit_logging_enhancement.sql
|
||||
CREATE TABLE IF NOT EXISTS public.auth_audit_log (
|
||||
audit_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Event type
|
||||
event_type VARCHAR(50) NOT NULL
|
||||
CHECK (event_type IN ('LOGIN', 'LOGOUT', 'MFA_SETUP', 'MFA_VERIFY', 'TOKEN_REFRESH', 'PERMISSION_DENIED')),
|
||||
|
||||
-- User info
|
||||
identity_id UUID REFERENCES public.identity(identity_id) ON DELETE SET NULL,
|
||||
username VARCHAR(255),
|
||||
|
||||
-- Request context
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
endpoint VARCHAR(255),
|
||||
|
||||
-- Result
|
||||
status VARCHAR(20) NOT NULL CHECK (status IN ('SUCCESS', 'FAILURE')),
|
||||
error_message TEXT,
|
||||
|
||||
-- Lifecycle
|
||||
occurred_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
correlation_id UUID
|
||||
);
|
||||
|
||||
CREATE INDEX idx_auth_audit_identity ON public.auth_audit_log(identity_id);
|
||||
CREATE INDEX idx_auth_audit_occurred_at ON public.auth_audit_log(occurred_at);
|
||||
CREATE INDEX idx_auth_audit_event_type ON public.auth_audit_log(event_type);
|
||||
CREATE INDEX idx_auth_audit_correlation ON public.auth_audit_log(correlation_id);
|
||||
```
|
||||
|
||||
**Step 2: Audit Logging Middleware**
|
||||
|
||||
```csharp
|
||||
// AuthAuditMiddleware.cs
|
||||
public class AuthAuditMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IAuthAuditSql _auditSql;
|
||||
private readonly ILogger<AuthAuditMiddleware> _logger;
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var startTime = DateTime.UtcNow;
|
||||
var correlationId = context.Request.HttpContext.TraceIdentifier;
|
||||
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
|
||||
// Log successful authentication endpoints
|
||||
if (IsAuthEndpoint(context.Request.Path))
|
||||
{
|
||||
var identity = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
await _auditSql.LogAuthEventAsync(new AuthAuditLog
|
||||
{
|
||||
EventType = GetEventType(context.Request.Path),
|
||||
IdentityId = identity != null ? Guid.Parse(identity) : null,
|
||||
IpAddress = context.Connection.RemoteIpAddress?.ToString(),
|
||||
UserAgent = context.Request.Headers["User-Agent"],
|
||||
Endpoint = context.Request.Path,
|
||||
Status = context.Response.StatusCode < 400 ? "SUCCESS" : "FAILURE",
|
||||
OccurredAt = startTime,
|
||||
CorrelationId = Guid.Parse(correlationId)
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Auth audit logging error");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsAuthEndpoint(PathString path) =>
|
||||
path.StartsWithSegments("/api/auth");
|
||||
|
||||
private string GetEventType(PathString path) =>
|
||||
path.Value switch
|
||||
{
|
||||
"/api/auth/login" => "LOGIN",
|
||||
"/api/auth/logout" => "LOGOUT",
|
||||
"/api/auth/mfa-setup" => "MFA_SETUP",
|
||||
"/api/auth/verify-mfa" => "MFA_VERIFY",
|
||||
_ => "UNKNOWN"
|
||||
};
|
||||
}
|
||||
|
||||
// Program.cs에서 등록
|
||||
app.UseMiddleware<AuthAuditMiddleware>();
|
||||
```
|
||||
|
||||
**Step 3: 감사 로그 조회 & 보고**
|
||||
|
||||
```csharp
|
||||
// GetAuditLogsEndpoint.cs
|
||||
public class GetAuditLogsEndpoint : Endpoint<GetAuditLogsRequest, GetAuditLogsResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/admin/audit-logs");
|
||||
Roles("Admin", "SecurityOfficer");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetAuditLogsRequest req, CancellationToken ct)
|
||||
{
|
||||
var logs = await sql.GetAuditLogsAsync(
|
||||
startDate: req.StartDate,
|
||||
endDate: req.EndDate,
|
||||
eventType: req.EventType,
|
||||
username: req.Username,
|
||||
limit: req.PageSize,
|
||||
offset: (req.Page - 1) * req.PageSize,
|
||||
ct
|
||||
);
|
||||
|
||||
return new GetAuditLogsResponse
|
||||
{
|
||||
Items = logs,
|
||||
Total = await sql.GetAuditLogsCountAsync(
|
||||
startDate: req.StartDate,
|
||||
endDate: req.EndDate,
|
||||
eventType: req.EventType,
|
||||
username: req.Username,
|
||||
ct
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 구현 난이도: ⭐⭐ (보통)
|
||||
**예상 작업량**: 6-8시간
|
||||
**필수 마이그레이션**:
|
||||
- auth_audit_log 테이블 생성
|
||||
- 인덱스 최적화
|
||||
|
||||
---
|
||||
|
||||
## 구현 우선순위
|
||||
|
||||
1. **RBAC** (즉시) - 권한 기반 접근 제어는 필수
|
||||
2. **Audit Logging** (1-2주) - 규제 준수 및 보안 추적
|
||||
3. **MFA** (2-4주) - 보안 강화 및 사용자 보호
|
||||
|
||||
## 예상 일정
|
||||
|
||||
| Feature | 난이도 | 시간 | 예정일 |
|
||||
|---------|--------|------|--------|
|
||||
| RBAC | ⭐⭐ | 8-10h | Week 1 |
|
||||
| Audit Logging | ⭐⭐ | 6-8h | Week 1-2 |
|
||||
| MFA (TOTP) | ⭐⭐⭐ | 12-16h | Week 2-3 |
|
||||
| **합계** | | **26-34h** | **3주** |
|
||||
|
||||
## 구현 후 이점
|
||||
|
||||
✅ 역할 기반 기능 제어
|
||||
✅ 사용자 행동 추적 및 감시
|
||||
✅ 규제 준수 (GDPR, SOC2)
|
||||
✅ 보안 위반 감지
|
||||
✅ 사용자 계정 보호 (MFA)
|
||||
✅ 규제 기관 감사 지원
|
||||
Reference in New Issue
Block a user