diff --git a/SPMS.API/Controllers/AuthController.cs b/SPMS.API/Controllers/AuthController.cs index 7eac9f4..fdc41e0 100644 --- a/SPMS.API/Controllers/AuthController.cs +++ b/SPMS.API/Controllers/AuthController.cs @@ -132,9 +132,10 @@ public class AuthController : ControllerBase [Authorize] [SwaggerOperation( Summary = "비밀번호 변경", - Description = "현재 로그인된 관리자의 비밀번호를 변경합니다.")] - [SwaggerResponse(200, "비밀번호 변경 성공")] - [SwaggerResponse(400, "현재 비밀번호 불일치")] + Description = "현재 로그인된 관리자의 비밀번호를 변경합니다. 변경 성공 시 모든 세션이 무효화되며 재로그인이 필요합니다. " + + "비밀번호 정책: 8자 이상, 영문 대/소문자·숫자·특수문자 각 1자 이상, 현재 비밀번호와 동일 불가.")] + [SwaggerResponse(200, "비밀번호 변경 성공", typeof(ApiResponse))] + [SwaggerResponse(400, "현재 비밀번호 불일치 / 정책 위반 / 동일 비밀번호 재사용")] [SwaggerResponse(401, "인증되지 않은 요청")] public async Task ChangePasswordAsync([FromBody] ChangePasswordRequestDto request) { @@ -142,7 +143,7 @@ public class AuthController : ControllerBase if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId)) throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다."); - await _authService.ChangePasswordAsync(adminId, request); - return Ok(ApiResponse.Success()); + var result = await _authService.ChangePasswordAsync(adminId, request); + return Ok(ApiResponse.Success(result)); } } diff --git a/SPMS.Application/DTOs/Auth/ChangePasswordRequestDto.cs b/SPMS.Application/DTOs/Auth/ChangePasswordRequestDto.cs index 0707e04..7691b7f 100644 --- a/SPMS.Application/DTOs/Auth/ChangePasswordRequestDto.cs +++ b/SPMS.Application/DTOs/Auth/ChangePasswordRequestDto.cs @@ -9,5 +9,9 @@ public class ChangePasswordRequestDto [Required(ErrorMessage = "새 비밀번호를 입력해주세요.")] [MinLength(8, ErrorMessage = "비밀번호는 8자 이상이어야 합니다.")] + [MaxLength(64, ErrorMessage = "비밀번호는 64자 이하여야 합니다.")] + [RegularExpression( + @"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':""\\|,.<>\/?]).{8,}$", + ErrorMessage = "비밀번호는 영문 대/소문자, 숫자, 특수문자를 각각 1자 이상 포함해야 합니다.")] public string NewPassword { get; set; } = string.Empty; } diff --git a/SPMS.Application/DTOs/Auth/ChangePasswordResponseDto.cs b/SPMS.Application/DTOs/Auth/ChangePasswordResponseDto.cs new file mode 100644 index 0000000..58efa4f --- /dev/null +++ b/SPMS.Application/DTOs/Auth/ChangePasswordResponseDto.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace SPMS.Application.DTOs.Auth; + +public class ChangePasswordResponseDto +{ + [JsonPropertyName("re_login_required")] + public bool ReLoginRequired { get; set; } +} diff --git a/SPMS.Application/Interfaces/IAuthService.cs b/SPMS.Application/Interfaces/IAuthService.cs index cf663c5..a41299a 100644 --- a/SPMS.Application/Interfaces/IAuthService.cs +++ b/SPMS.Application/Interfaces/IAuthService.cs @@ -9,7 +9,7 @@ public interface IAuthService Task LoginAsync(LoginRequestDto request); Task RefreshTokenAsync(TokenRefreshRequestDto request); Task LogoutAsync(long adminId, string accessToken); - Task ChangePasswordAsync(long adminId, ChangePasswordRequestDto request); + Task ChangePasswordAsync(long adminId, ChangePasswordRequestDto request); Task CheckEmailAsync(EmailCheckRequestDto request); Task VerifyEmailAsync(EmailVerifyRequestDto request); Task ResendVerificationAsync(EmailResendRequestDto request); diff --git a/SPMS.Application/Services/AuthService.cs b/SPMS.Application/Services/AuthService.cs index d05b3a3..cb82514 100644 --- a/SPMS.Application/Services/AuthService.cs +++ b/SPMS.Application/Services/AuthService.cs @@ -315,7 +315,7 @@ public class AuthService : IAuthService } } - public async Task ChangePasswordAsync(long adminId, ChangePasswordRequestDto request) + public async Task ChangePasswordAsync(long adminId, ChangePasswordRequestDto request) { // 1. 관리자 조회 var admin = await _adminRepository.GetByIdAsync(adminId); @@ -336,19 +336,37 @@ public class AuthService : IAuthService 400); } - // 3. 새 비밀번호 해싱 및 저장 + // 3. 동일 비밀번호 재사용 금지 + if (BCrypt.Net.BCrypt.Verify(request.NewPassword, admin.Password)) + { + throw new SpmsException( + ErrorCodes.PasswordReuseForbidden, + "현재 비밀번호와 동일한 비밀번호는 사용할 수 없습니다.", + 400); + } + + // 4. 새 비밀번호 해싱 및 저장 admin.Password = BCrypt.Net.BCrypt.HashPassword(request.NewPassword); - // 4. 강제변경 플래그 해제 + // 5. 강제변경 플래그 해제 if (admin.MustChangePassword) { admin.MustChangePassword = false; admin.TempPasswordIssuedAt = null; } + // 6. 세션 무효화 — Refresh Token 삭제 + admin.RefreshToken = null; + admin.RefreshTokenExpiresAt = null; + _adminRepository.Update(admin); await _unitOfWork.SaveChangesAsync(); - _logger.LogInformation("[AUTH] 비밀번호 변경 — AdminId: {AdminId}", adminId); + _logger.LogInformation("[AUTH] 비밀번호 변경 + 세션 무효화 — AdminId: {AdminId}", adminId); + + return new ChangePasswordResponseDto + { + ReLoginRequired = true + }; } public async Task CheckEmailAsync(EmailCheckRequestDto request) diff --git a/SPMS.Domain/Common/ErrorCodes.cs b/SPMS.Domain/Common/ErrorCodes.cs index 0fb55d5..9f0f670 100644 --- a/SPMS.Domain/Common/ErrorCodes.cs +++ b/SPMS.Domain/Common/ErrorCodes.cs @@ -31,6 +31,8 @@ public static class ErrorCodes // === Account (2) === public const string PasswordValidationFailed = "121"; public const string ResetTokenError = "122"; + public const string PasswordPolicyViolation = "123"; + public const string PasswordReuseForbidden = "124"; // === Service (3) === public const string DecryptionFailed = "131";