# 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 { 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([]) 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 ``` #### 구현 난이도: ⭐⭐ (보통) **예상 작업량**: 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 { 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 { public override async Task HandleAsync(VerifyMfaRequest req, CancellationToken ct) { var setup = await cache.GetAsync($"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 { 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 ``` #### 구현 난이도: ⭐⭐⭐ (복잡) **예상 작업량**: 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 _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(); ``` **Step 3: 감사 로그 조회 & 보고** ```csharp // GetAuditLogsEndpoint.cs public class GetAuditLogsEndpoint : Endpoint { 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) ✅ 규제 기관 감사 지원