ASP.NET Core IdentityOptions 기본 예제

✨ ASP.NET Core IdentityOptions 개요

https://github.com/dotnet/aspnetcore/blob/3f1acb59718cadf111a0a796681e3d3509bb3381/src/Identity/Extensions.Core/src/IdentityOptions.cs#L17C65-L65C66

ASP.NET Core의 IdentityOptions는 시스템의 설정을 한 곳에 몰아서 관리하는 클래스입니다. (중앙 집중식 구성)

이를 통해 패스워드 정책, 사용자 설정, 로그인 정책, 잠금 정책 등을 세밀하게 제어할 수 있습니다.

🔥 주요 구성 요소

learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.identity.identityoptions?view=aspnetcore-8.0

1️⃣ Password 옵션 (PasswordOptions)

패스워드 복잡성 요구사항을 설정합니다.

csharp
// Program.cs 또는 Startup.cs

// 개발 환경용 - 간단한 패스워드
public void ConfigureDevelopmentPassword(IdentityOptions options)
{
    options.Password.RequireDigit = false;
    options.Password.RequireLowercase = false;
    options.Password.RequireUppercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequiredLength = 4;
}

// 프로덕션 환경용 - 강력한 패스워드
public void ConfigureProductionPassword(IdentityOptions options)
{
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequiredLength = 12;
    options.Password.RequiredUniqueChars = 4;
}

2️⃣ Lockout 옵션 (LockoutOptions)

계정 잠금 정책을 설정합니다.

csharp
public void ConfigureProgressiveLockout(IdentityOptions options)
{
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(1);   // 1분부터 시작
    options.Lockout.MaxFailedAccessAttempts = 3;                        // 3회 실패시 잠금
    options.Lockout.AllowedForNewUsers = true;
}

// 커스텀 잠금 시간 증가 로직 (서비스에서 구현)
public class CustomLockoutService
{
    public TimeSpan CalculateLockoutDuration(int failedAttempts)
    {
        return failedAttempts switch
        {
            <= 3 => TimeSpan.FromMinutes(5),
            <= 6 => TimeSpan.FromMinutes(15),
            <= 10 => TimeSpan.FromHours(1),
            _ => TimeSpan.FromHours(24)
        };
    }
}

public void ConfigureProgressiveLockout(IdentityOptions options)
{
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(1);   // 1분부터 시작
    options.Lockout.MaxFailedAccessAttempts = 3;                        // 3회 실패시 잠금
    options.Lockout.AllowedForNewUsers = true;
}

// 커스텀 잠금 시간 증가 로직 (서비스에서 구현)
public class CustomLockoutService
{
    public TimeSpan CalculateLockoutDuration(int failedAttempts)
    {
        return failedAttempts switch
        {
            <= 3 => TimeSpan.FromMinutes(5),
            <= 6 => TimeSpan.FromMinutes(15),
            <= 10 => TimeSpan.FromHours(1),
            _ => TimeSpan.FromHours(24)
        };
    }
}

3️⃣ SignIn 옵션 (SignInOptions)

로그인 관련 설정을 구성합니다.

csharp
// 이메일 인증 필수 설정
public void ConfigureEmailConfirmation(IdentityOptions options)
{
    options.SignIn.RequireConfirmedEmail = true;
}

// 회원가입 컨트롤러
[HttpPost]
public async Task<IActionResult> Register(RegisterViewModel model)
{
    var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
    var result = await _userManager.CreateAsync(user, model.Password);
    
    if (result.Succeeded)
    {
        // 이메일 인증 토큰 생성
        var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
        var confirmationLink = Url.Action("ConfirmEmail", "Account", 
            new { userId = user.Id, token = token }, Request.Scheme);
        
        // 이메일 발송
        await _emailSender.SendEmailAsync(model.Email, "이메일 인증", 
            $"다음 링크를 클릭하여 계정을 활성화하세요: {confirmationLink}");
        
        return View("EmailConfirmationSent");
    }
    
    return View(model);
}

// 이메일 인증 필수 설정
public void ConfigureEmailConfirmation(IdentityOptions options)
{
    options.SignIn.RequireConfirmedEmail = true;
}

// 회원가입 컨트롤러
[HttpPost]
public async Task<IActionResult> Register(RegisterViewModel model)
{
    var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
    var result = await _userManager.CreateAsync(user, model.Password);
    
    if (result.Succeeded)
    {
        // 이메일 인증 토큰 생성
        var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
        var confirmationLink = Url.Action("ConfirmEmail", "Account", 
            new { userId = user.Id, token = token }, Request.Scheme);
        
        // 이메일 발송
        await _emailSender.SendEmailAsync(model.Email, "이메일 인증", 
            $"다음 링크를 클릭하여 계정을 활성화하세요: {confirmationLink}");
        
        return View("EmailConfirmationSent");
    }
    
    return View(model);
}

4️⃣ User 옵션 (UserOptions)

사용자 계정 관련 설정을 구성합니다.

csharp
builder.Services.Configure<IdentityOptions>(options =>
{
    // 사용자 설정
    options.User.AllowedUserNameCharacters = 
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
    options.User.RequireUniqueEmail = true;  // 중복 이메일 방지
});


사용자명 정책 예제:
public void ConfigureUserPolicies(IdentityOptions options)
{
    // 한국어 사용자명 허용
    options.User.AllowedUserNameCharacters = 
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+가-힣";
    
    // 이메일을 고유하게 설정
    options.User.RequireUniqueEmail = true;
}

// 커스텀 사용자명 검증
public class CustomUserValidator<TUser> : IUserValidator<TUser> 
    where TUser : class
{
    public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user)
    {
        var userName = manager.GetUserNameAsync(user).Result;
        
        // 사용자명이 숫자로만 구성되면 안됨
        if (userName.All(char.IsDigit))
        {
            return Task.FromResult(
                IdentityResult.Failed(new IdentityError
                {
                    Code = "NumericUserName",
                    Description = "사용자명은 숫자로만 구성될 수 없습니다."
                }));
        }
        
        return Task.FromResult(IdentityResult.Success);
    }
}

5️⃣ Stores 옵션 (StoreOptions)

사용자 계정 관련 설정을 구성합니다.

csharp
builder.Services.Configure<IdentityOptions>(options =>
{
    // 저장소 설정
    options.Stores.MaxLengthForKeys = 128;              // 키 최대 길이
    options.Stores.ProtectPersonalData = false;         // 개인정보 암호화 여부
});

🐱 실제 활용 시나리오

1️⃣ 다단계 인증 설정

csharp
public void ConfigureTwoFactorAuthentication(IdentityOptions options)
{
    options.SignIn.RequireConfirmedPhoneNumber = true;  // 전화번호 인증 필수
    options.Tokens.AuthenticatorTokenProvider = TokenOptions.DefaultAuthenticatorProvider;
}

// 2FA 활성화 코드
[HttpPost]
public async Task<IActionResult> EnableTwoFactorAuthentication()
{
    var user = await _userManager.GetUserAsync(User);
    await _userManager.SetTwoFactorEnabledAsync(user, true);
    
    return RedirectToAction("ShowRecoveryCodes");
}

2️⃣ 소셜 로그인과 연계

csharp
public void ConfigureExternalLogins(IdentityOptions options)
{
    // 외부 로그인 시에는 이메일 인증 건너뛰기
    options.SignIn.RequireConfirmedEmail = false;
    
    // 하지만 고유 이메일은 필수
    options.User.RequireUniqueEmail = true;
}

// 외부 로그인 처리
[HttpPost]
public IActionResult ExternalLogin(string provider)
{
    var redirectUrl = Url.Action("ExternalLoginCallback");
    var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
    return Challenge(properties, provider);
}

3️⃣ 커스텀 검증 규칙 적용

csharp
// 커스텀 패스워드 검증기
public class CustomPasswordValidator : IPasswordValidator<ApplicationUser>
{
    public Task<IdentityResult> ValidateAsync(UserManager<ApplicationUser> manager, 
        ApplicationUser user, string password)
    {
        // 사용자명과 패스워드가 너무 유사하면 안됨
        if (password.Contains(user.UserName, StringComparison.OrdinalIgnoreCase))
        {
            return Task.FromResult(IdentityResult.Failed(new IdentityError
            {
                Code = "PasswordContainsUserName",
                Description = "패스워드에 사용자명이 포함될 수 없습니다."
            }));
        }
        
        return Task.FromResult(IdentityResult.Success);
    }
}

// 서비스 등록
builder.Services.AddTransient<IPasswordValidator<ApplicationUser>, CustomPasswordValidator>();

❇️ 참고 사항

1️⃣ 모니터링 및 로깅

보안 이벤트 로깅

csharp
public class SecurityEventLogger
{
    private readonly ILogger<SecurityEventLogger> _logger;
    
    public SecurityEventLogger(ILogger<SecurityEventLogger> logger)
    {
        _logger = logger;
    }
    
    public void LogFailedLogin(string userName, string ipAddress)
    {
        _logger.LogWarning("Failed login attempt for user {UserName} from IP {IpAddress}", 
            userName, ipAddress);
    }
    
    public void LogAccountLocked(string userName)
    {
        _logger.LogWarning("Account locked for user {UserName}", userName);
    }
}

2️⃣ 환경별 설정 관리

appsettings.json을 활용한 설정

json
{
  "Identity": {
    "Password": {
      "RequiredLength": 8,
      "RequireDigit": true,
      "RequireUppercase": true,
      "RequireNonAlphanumeric": false
    },
    "Lockout": {
      "MaxFailedAccessAttempts": 5,
      "DefaultLockoutTimeSpanMinutes": 15
    },
    "SignIn": {
      "RequireConfirmedEmail": true
    }
  }
}
csharp
// 설정 바인딩
public class IdentityConfig
{
    public PasswordConfig Password { get; set; }
    public LockoutConfig Lockout { get; set; }
    public SignInConfig SignIn { get; set; }
}

// Program.cs에서 적용
var identityConfig = builder.Configuration.GetSection("Identity").Get<IdentityConfig>();

builder.Services.Configure<IdentityOptions>(options =>
{
    options.Password.RequiredLength = identityConfig.Password.RequiredLength;
    options.Password.RequireDigit = identityConfig.Password.RequireDigit;
    // ... 기타 설정
});