다음을 통해 공유


ASP.NET Core의 다단계 인증

참고 항목

이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

Important

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.

현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

작성자: Damien Bowden

샘플 코드 보기 또는 다운로드(damienbod/AspNetCoreHybridFlowWithApi GitHub 리포지토리)

MFA(다단계 인증)는 추가 형태의 식별을 위해 로그인 이벤트 중에 사용자에게 요청되는 프로세스입니다. 이는 휴대폰에서 코드를 입력하거나, FIDO2 키를 사용하거나, 지문 검색을 제공하라는 메시지로 표시될 수 있습니다. 두 번째 인증 형식이 필요한 경우 보안이 강화됩니다. 공격자가 추가 요소를 쉽게 얻거나 복제하지 못합니다.

이 문서에서는 다음 영역에 대해 설명합니다.

  • MFA의 정의 및 권장되는 MFA 흐름
  • ASP.NET Core Identity를 사용하여 관리 페이지에 대한 MFA 구성
  • OpenID Connect 서버에 MFA 로그인 요구 사항 보내기
  • 강제로 ASP.NET Core OpenID Connect 클라이언트에 MFA 요구

MFA, 2FA

MFA를 사용하려면 사용자가 인증하기 위해 알고 있는 것, 소유하고 있는 것 또는 생체 인증과 같이 ID에 대해 적어도 두 가지 이상의 증명 형식이 필요합니다.

2FA(2단계 인증)는 MFA의 하위 집합과 유사하지만 MFA가 ID를 증명하기 위해 두 개 이상의 요소를 요구할 수 있다는 점에서 다릅니다.

2FA는 ASP.NET Core Identity를 사용하는 경우 기본적으로 지원됩니다. 특정 사용자에 대해 2FA를 사용하거나 사용하지 않도록 설정하려면 IdentityUser<TKey>.TwoFactorEnabled 속성을 설정합니다. ASP.NET Core Identity 기본 UI에는 2FA를 구성하기 위한 페이지가 포함되어 있습니다.

MFA TOTP(시간 기반 일회용 암호 알고리즘)

TOTP를 사용하는 MFA는 ASP.NET Core Identity를 사용하는 경우 기본적으로 지원됩니다. 이 방법은 다음을 비롯한 모든 규격 인증자 앱과 함께 사용할 수 있습니다.

  • Microsoft Authenticator
  • Google Authenticator

구현 세부 정보는 ASP.NET Core에서 TOTP 인증자 앱에 대한 QR 코드 생성 사용을 참조하세요.

MFA TOTP에 대한 지원을 사용하지 않도록 설정하려면 AddDefaultIdentity 대신 AddIdentity를 사용하여 인증을 구성합니다. AddDefaultIdentity는 MFA TOTP용 토큰 공급자를 포함하여 여러 토큰 공급자를 등록하는 AddDefaultTokenProviders를 내부적으로 호출합니다. 특정 토큰 공급자만 등록하려면 각 필수 공급자에 대해 AddTokenProvider를 호출합니다. 사용 가능한 토큰 공급자에 대한 자세한 내용은 GitHub의 AddDefaultTokenProviders 원본을 참조하세요.

MFA 암호/FIDO2 또는 암호 없는

passkeys/FIDO2는 현재 다음과 같습니다.

  • MFA를 가장 안전하게 달성하는 방법입니다.
  • 피싱 공격으로부터 보호하는 MFA입니다. (뿐만 아니라 인증서 인증 및 비즈니스용 Windows)

현재 ASP.NET Core는 passkeys/FIDO2를 직접 지원하지 않습니다. Passkeys/FIDO2는 MFA 또는 암호 없는 흐름에 사용할 수 있습니다.

Microsoft Entra ID는 passkeys/FIDO2 및 암호 없는 흐름을 지원합니다. 자세한 내용은 암호 없는 인증 옵션을 참조 하세요.

다른 형태의 암호 없는 MFA는 피싱으로부터 보호하지 않거나 보호하지 않을 수 있습니다.

MFA SMS

SMS를 사용하는 MFA는 암호 인증(단일 요소)에 비해 보안성을 대폭 강화합니다. 그러나 SMS를 두 번째 요소로 사용하는 것은 더 이상 권장되지 않습니다. 이 구현 형식에 대해 알려진 공격 벡터가 너무 많습니다.

NIST 지침

ASP.NET Core Identity를 사용하여 관리 페이지에 대한 MFA 구성

MFA는 사용자가 ASP.NET Core Identity 앱 내에서 중요한 페이지에 액세스하도록 강제할 수 있습니다. 이는 다양한 ID에 대해 서로 다른 수준의 액세스 권한이 있는 앱에 유용할 수 있습니다. 예를 들어 사용자는 암호 로그인을 사용하여 프로필 데이터를 볼 수 있지만, 관리자는 관리 페이지에 액세스하기 위해서는 MFA를 사용해야 합니다.

MFA 클레임을 사용하여 로그인 확장

데모 코드는 Identity 및 Razor Pages와 ASP.NET Core를 사용하여 설정됩니다. AddIdentity 메서드는 하나의 AddDefaultIdentity 대신 사용되므로 성공적인 로그인 후에는 IUserClaimsPrincipalFactory 구현을 사용하여 클레임을 ID에 추가할 수 있습니다.

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(
        Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
		options.SignIn.RequireConfirmedAccount = false)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

builder.Services.AddSingleton<IEmailSender, EmailSender>();
builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, 
    AdditionalUserClaimsPrincipalFactory>();

builder.Services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled", x => x.RequireClaim("amr", "mfa")));

builder.Services.AddRazorPages();

AdditionalUserClaimsPrincipalFactory 클래스는 로그인에 성공한 후에만 사용자 클레임에 amr 클레임을 추가합니다. 클레임의 값을 데이터베이스에서 읽습니다. ID가 MFA로 로그인된 경우에는 사용자가 더 높은 수준으로 보호된 보기에만 액세스해야 하므로 클레임이 여기에 추가됩니다. 클레임을 사용하는 대신 데이터베이스에서 직접 데이터베이스 보기를 읽는 경우 MFA를 활성화한 후 MFA 없이 보기에 직접 액세스할 수 있습니다.

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityStandaloneMfa
{
    public class AdditionalUserClaimsPrincipalFactory : 
        UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
    {
        public AdditionalUserClaimsPrincipalFactory( 
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager, 
            IOptions<IdentityOptions> optionsAccessor) 
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
        {
            var principal = await base.CreateAsync(user);
            var identity = (ClaimsIdentity)principal.Identity;

            var claims = new List<Claim>();

            if (user.TwoFactorEnabled)
            {
                claims.Add(new Claim("amr", "mfa"));
            }
            else
            {
                claims.Add(new Claim("amr", "pwd"));
            }

            identity.AddClaims(claims);
            return principal;
        }
    }
}

Startup 클래스에서 Identity 서비스 설정이 변경되었으므로 Identity의 레이아웃을 업데이트해야 합니다. Identity 페이지를 앱으로 스캐폴드합니다. Identity/Account/Manage/_Layout.cshtml 파일에서 레이아웃을 정의합니다.

@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}

또한 Identity 페이지에서 모든 관리 페이지에 대한 레이아웃을 할당합니다.

@{
    Layout = "_Layout.cshtml";
}

관리 페이지에서 MFA 요구 사항의 유효성을 검사합니다.

관리 Razor Page에서는 사용자가 MFA를 사용하여 로그인했는지 확인합니다. OnGet 메서드에서 ID는 사용자 클레임에 액세스하는 데 사용됩니다. amr 클레임이 mfa 값에 대해 확인됩니다. ID가 이 클레임을 누락하거나 false인 경우 페이지는 MFA 사용 페이지로 리디렉션됩니다. 사용자가 이미 로그인했지만 MFA를 사용하지 않았기 때문에 이를 수행할 수 있습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IdentityStandaloneMfa
{
    public class AdminModel : PageModel
    {
        public IActionResult OnGet()
        {
            var claimTwoFactorEnabled = 
                User.Claims.FirstOrDefault(t => t.Type == "amr");

            if (claimTwoFactorEnabled != null && 
                "mfa".Equals(claimTwoFactorEnabled.Value))
            {
                // You logged in with MFA, do the administrative stuff
            }
            else
            {
                return Redirect(
                    "/Identity/Account/Manage/TwoFactorAuthentication");
            }

            return Page();
        }
    }
}

사용자 로그인 정보를 설정/해제하는 UI 논리

시작 시 권한 부여 정책이 추가되었습니다. 정책에는 mfa 값이 포함된 amr 클레임이 필요합니다.

services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled",
        x => x.RequireClaim("amr", "mfa")));

그러면 _Layout 보기에서 이 정책을 사용하여 경고와 함께 관리 메뉴를 표시하거나 숨길 수 있습니다.

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@inject IAuthorizationService AuthorizationService

ID가 MFA를 사용하여 로그인한 경우에는 도구 설명 경고 없이 관리 메뉴가 표시됩니다. 사용자가 MFA 없이 로그인한 경우 관리자(사용 안 함) 메뉴가 사용자에게 알리는(경고 설명) 도구 설명과 함께 표시됩니다.

@if (SignInManager.IsSignedIn(User))
{
    @if ((AuthorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin" 
               id="tooltip-demo"  
               data-toggle="tooltip" 
               data-placement="bottom" 
               title="MFA is NOT enabled. This is required for the Admin Page. If you have activated MFA, then logout, login again.">
                Admin (Not Enabled)
            </a>
        </li>
    }
}

사용자가 MFA 없이 로그인하는 경우 경고가 표시됩니다.

관리자 MFA 인증

관리 링크를 클릭하면 사용자가 MFA 사용 보기로 리디렉션됩니다.

관리자가 MFA 인증 활성화

OpenID Connect 서버에 MFA 로그인 요구 사항 보내기

acr_values 매개 변수를 사용하여 인증 요청에서 클라이언트의 mfa 필수 값을 서버에 전달할 수 있습니다.

참고 항목

이 작업을 수행하려면 OpenID Connect 서버에서 acr_values 매개 변수를 처리해야 합니다.

OpenID Connect ASP.NET Core 클라이언트

ASP.NET Core Razor Pages OpenID Connect 클라이언트 앱은 AddOpenIdConnect 메서드를 사용하여 OpenID Connect 서버에 로그인합니다. acr_values 매개 변수는 mfa 값으로 설정되고 인증 요청과 함께 전송됩니다. OpenIdConnectEvents는 이를 추가하는 데 사용됩니다.

권장되는 acr_values 매개 변수 값은 인증 방법 참조 값을 참조하세요.

build.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "<OpenID Connect server URL>";
	options.RequireHttpsMetadata = true;
	options.ClientId = "<OpenID Connect client ID>";
	options.ClientSecret = "<>";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
	options.AdditionalAuthorizationParameters.Add("acr_values", "mfa");
});

ASP.NET Core를 사용하여 Duende IdentityServer 서버에 커넥트 OpenID 예제Identity

Pages와 함께 Razor ASP.NET Core Identity 를 사용하여 구현되는 OpenID 커넥트 서버에서 새 페이지가 ErrorEnable2FA.cshtml 만들어집니다. 보기는 다음과 같습니다.

  • Identity가 MFA를 필요로 하는 앱에서 제공되지만 사용자가 Identity에서 이를 활성화하지 않은 경우를 표시합니다.
  • 사용자에게 알리고 이를 활성화하는 링크를 추가합니다.
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

<h1>The client application requires you to have MFA enabled. Enable this, try login again.</h1>

<br />

You can enable MFA to login here:

<br />

<a href="~/Identity/Account/Manage/TwoFactorAuthentication">Enable MFA</a>

Login 메서드에서 IIdentityServerInteractionService 인터페이스 구현 _interaction은 OpenID Connect 요청 매개 변수에 액세스하는 데 사용됩니다. AcrValues 속성을 사용하여 acr_values 매개 변수에 액세스합니다. 클라이언트에서 mfa 세트를 사용하여 이를 보내면 확인할 수 있습니다.

MFA가 필요하고 ASP.NET Core Identity 사용자가 MFA를 사용하도록 설정한 경우 로그인이 계속됩니다. 사용자가 MFA를 사용하도록 설정하지 않은 경우 사용자는 사용자 지정 보기 ErrorEnable2FA.cshtml로 리디렉션됩니다. 그런 다음 ASP.NET Core Identity 사용자를 로그인합니다.

Fido2Store는 사용자가 사용자 지정 FIDO2 토큰 공급자를 사용하여 MFA를 활성화한 경우 검사 데 사용됩니다.

public async Task<IActionResult> OnPost()
{
	// check if we are in the context of an authorization request
	var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);

	var requires2Fa = context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

	var user = await _userManager.FindByNameAsync(Input.Username);
	if (user != null && !user.TwoFactorEnabled && requires2Fa)
	{
		return RedirectToPage("/Home/ErrorEnable2FA/Index");
	}

	// code omitted for brevity

	if (ModelState.IsValid)
	{
		var result = await _signInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberLogin, lockoutOnFailure: true);
		if (result.Succeeded)
		{
			// code omitted for brevity
		}
		if (result.RequiresTwoFactor)
		{
			var fido2ItemExistsForUser = await _fido2Store.GetCredentialsByUserNameAsync(user.UserName);
			if (fido2ItemExistsForUser.Count > 0)
			{
				return RedirectToPage("/Account/LoginFido2Mfa", new { area = "Identity", Input.ReturnUrl, Input.RememberLogin });
			}

			return RedirectToPage("/Account/LoginWith2fa", new { area = "Identity", Input.ReturnUrl, RememberMe = Input.RememberLogin });
		}

		await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, "invalid credentials", clientId: context?.Client.ClientId));
		ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
	}

	// something went wrong, show form with error
	await BuildModelAsync(Input.ReturnUrl);
	return Page();
}

사용자가 이미 로그인한 경우 클라이언트 앱은 다음과 같습니다.

  • 여전히 amr 클레임의 유효성을 검사합니다.
  • ASP.NET Core Identity 보기에 대한 링크를 사용하여 MFA를 설정할 수 있습니다.

acr_values-1 이미지

강제로 ASP.NET Core OpenID Connect 클라이언트에 MFA 요구

이 예제에서는 OpenID Connect를 사용하여 로그인하는 ASP.NET Core Razor Page 앱에서 사용자가 MFA를 사용하여 인증해야 하는 방법을 보여 줍니다.

MFA 요구 사항의 유효성을 검사하기 위해 IAuthorizationRequirement 요구 사항이 생성됩니다. 이는 MFA를 요구하는 정책을 사용하여 페이지에 추가됩니다.

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc;

public class RequireMfa : IAuthorizationRequirement{}

amr 클레임을 사용하고 mfa 값에 대해 확인하는 AuthorizationHandler가 구현됩니다. amr은 성공적인 인증의 id_token에서 반환되며 인증 방법 참조 값 사양에 정의된 대로 다양한 값을 가질 수 있습니다.

반환되는 값은 ID가 인증되는 방법 및 OpenID Connect 서버 구현에 따라 달라집니다.

AuthorizationHandlerRequireMfa 요구 사항을 사용하고 amr 클레임의 유효성을 검사합니다. OpenID Connect 서버는 ASP.NET Core Identity와 함께 Duende Identity Server를 사용하여 구현할 수 있습니다. 사용자가 TOTP를 사용하여 로그인하는 경우에는 MFA 값을 사용하여 amr 클레임이 반환됩니다. 다른 OpenID Connect 서버 구현 또는 다른 MFA 유형을 사용하는 경우 amr 클레임에 다른 값이 있을 수 있습니다. 또한 이를 허용하려면 코드를 확장해야 합니다.

public class RequireMfaHandler : AuthorizationHandler<RequireMfa>
{
	protected override Task HandleRequirementAsync(
		AuthorizationHandlerContext context, 
		RequireMfa requirement)
	{
		if (context == null)
			throw new ArgumentNullException(nameof(context));
		if (requirement == null)
			throw new ArgumentNullException(nameof(requirement));

		var amrClaim =
			context.User.Claims.FirstOrDefault(t => t.Type == "amr");

		if (amrClaim != null && amrClaim.Value == Amr.Mfa)
		{
			context.Succeed(requirement);
		}

		return Task.CompletedTask;
	}
}

프로그램 파일에서 AddOpenIdConnect 메서드는 기본 챌린지 스키마로 사용됩니다. amr 클레임을 확인하는 데 사용되는 권한 부여 처리기는 제어 컨테이너의 반전에 추가됩니다. 그런 다음, RequireMfa 요구 사항을 추가하는 정책이 생성됩니다.

builder.Services.ConfigureApplicationCookie(options =>
        options.Cookie.SecurePolicy =
            CookieSecurePolicy.Always);

builder.Services.AddSingleton<IAuthorizationHandler, RequireMfaHandler>();

builder.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "https://localhost:44352";
	options.RequireHttpsMetadata = true;
	options.ClientId = "AspNetCoreRequireMfaOidc";
	options.ClientSecret = "AspNetCoreRequireMfaOidcSecret";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
});

builder.Services.AddAuthorization(options =>
{
	options.AddPolicy("RequireMfa", policyIsAdminRequirement =>
	{
		policyIsAdminRequirement.Requirements.Add(new RequireMfa());
	});
});

builder.Services.AddRazorPages();

이 정책은 필요에 따라 Razor Page에서 사용됩니다. 정책은 전체 앱에 대해서도 전역적으로 추가할 수 있습니다.

[Authorize(Policy= "RequireMfa")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}

사용자가 MFA 없이 인증하는 경우 amr 클레임에 pwd 값이 있을 수 있습니다. 이 요청에는 페이지에 대한 액세스 권한이 부여되지 않습니다. 기본값을 사용하여 사용자가 Account/AccessDenied 페이지로 리디렉션됩니다. 이 동작은 변경할 수 있습니다. 또는 사용자 고유의 사용자 지정 논리를 여기서 구현할 수 있습니다. 이 예제에서는 유효한 사용자가 계정에 대해 MFA를 설정할 수 있도록 링크가 추가됩니다.

@page
@model AspNetCoreRequireMfaOidc.AccessDeniedModel
@{
    ViewData["Title"] = "AccessDenied";
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

<h1>AccessDenied</h1>

You require MFA to login here

<a href="https://localhost:44352/Manage/TwoFactorAuthentication">Enable MFA</a>

이제 MFA로 인증하는 사용자만 페이지 또는 웹 사이트에 액세스할 수 있습니다. 다른 MFA 형식을 사용하거나 2FA가 양호하면 amr 클레임은 다른 값을 가지며 올바르게 처리되어야 합니다. 다른 OpenID Connect 서버는 이 클레임에 대해 서로 다른 값을 반환하고 인증 방법 참조 값 사양을 따르지 않을 수도 있습니다.

MFA 없이 로그인하는 경우(예: 암호만 사용):

  • amrpwd 값은 다음과 같습니다.

    amr에 있는 pwd 값

  • 액세스가 거부되었습니다.

    액세스 거부됨

또는 Identity를 통한 OTP를 사용하여 로그인합니다.

다음으로 OTP를 사용하여 로그인 Identity

추가 리소스

작성자: Damien Bowden

샘플 코드 보기 또는 다운로드(damienbod/AspNetCoreHybridFlowWithApi GitHub 리포지토리)

MFA(다단계 인증)는 추가 형태의 식별을 위해 로그인 이벤트 중에 사용자에게 요청되는 프로세스입니다. 이는 휴대폰에서 코드를 입력하거나, FIDO2 키를 사용하거나, 지문 검색을 제공하라는 메시지로 표시될 수 있습니다. 두 번째 인증 형식이 필요한 경우 보안이 강화됩니다. 공격자가 추가 요소를 쉽게 얻거나 복제하지 못합니다.

이 문서에서는 다음 영역에 대해 설명합니다.

  • MFA의 정의 및 권장되는 MFA 흐름
  • ASP.NET Core Identity를 사용하여 관리 페이지에 대한 MFA 구성
  • OpenID Connect 서버에 MFA 로그인 요구 사항 보내기
  • 강제로 ASP.NET Core OpenID Connect 클라이언트에 MFA 요구

MFA, 2FA

MFA를 사용하려면 사용자가 인증하기 위해 알고 있는 것, 소유하고 있는 것 또는 생체 인증과 같이 ID에 대해 적어도 두 가지 이상의 증명 형식이 필요합니다.

2FA(2단계 인증)는 MFA의 하위 집합과 유사하지만 MFA가 ID를 증명하기 위해 두 개 이상의 요소를 요구할 수 있다는 점에서 다릅니다.

2FA는 ASP.NET Core Identity를 사용하는 경우 기본적으로 지원됩니다. 특정 사용자에 대해 2FA를 사용하거나 사용하지 않도록 설정하려면 IdentityUser<TKey>.TwoFactorEnabled 속성을 설정합니다. ASP.NET Core Identity 기본 UI에는 2FA를 구성하기 위한 페이지가 포함되어 있습니다.

MFA TOTP(시간 기반 일회용 암호 알고리즘)

TOTP를 사용하는 MFA는 ASP.NET Core Identity를 사용하는 경우 기본적으로 지원됩니다. 이 방법은 다음을 비롯한 모든 규격 인증자 앱과 함께 사용할 수 있습니다.

  • Microsoft Authenticator
  • Google Authenticator

구현 세부 정보는 ASP.NET Core에서 TOTP 인증자 앱에 대한 QR 코드 생성 사용을 참조하세요.

MFA TOTP에 대한 지원을 사용하지 않도록 설정하려면 AddDefaultIdentity 대신 AddIdentity를 사용하여 인증을 구성합니다. AddDefaultIdentity는 MFA TOTP용 토큰 공급자를 포함하여 여러 토큰 공급자를 등록하는 AddDefaultTokenProviders를 내부적으로 호출합니다. 특정 토큰 공급자만 등록하려면 각 필수 공급자에 대해 AddTokenProvider를 호출합니다. 사용 가능한 토큰 공급자에 대한 자세한 내용은 GitHub의 AddDefaultTokenProviders 원본을 참조하세요.

MFA 암호/FIDO2 또는 암호 없는

passkeys/FIDO2는 현재 다음과 같습니다.

  • MFA를 가장 안전하게 달성하는 방법입니다.
  • 피싱 공격으로부터 보호하는 MFA입니다. (뿐만 아니라 인증서 인증 및 비즈니스용 Windows)

현재 ASP.NET Core는 passkeys/FIDO2를 직접 지원하지 않습니다. Passkeys/FIDO2는 MFA 또는 암호 없는 흐름에 사용할 수 있습니다.

Microsoft Entra ID는 passkeys/FIDO2 및 암호 없는 흐름을 지원합니다. 자세한 내용은 암호 없는 인증 옵션을 참조 하세요.

다른 형태의 암호 없는 MFA는 피싱으로부터 보호하지 않거나 보호하지 않을 수 있습니다.

MFA SMS

SMS를 사용하는 MFA는 암호 인증(단일 요소)에 비해 보안성을 대폭 강화합니다. 그러나 SMS를 두 번째 요소로 사용하는 것은 더 이상 권장되지 않습니다. 이 구현 형식에 대해 알려진 공격 벡터가 너무 많습니다.

NIST 지침

ASP.NET Core Identity를 사용하여 관리 페이지에 대한 MFA 구성

MFA는 사용자가 ASP.NET Core Identity 앱 내에서 중요한 페이지에 액세스하도록 강제할 수 있습니다. 이는 다양한 ID에 대해 서로 다른 수준의 액세스 권한이 있는 앱에 유용할 수 있습니다. 예를 들어 사용자는 암호 로그인을 사용하여 프로필 데이터를 볼 수 있지만, 관리자는 관리 페이지에 액세스하기 위해서는 MFA를 사용해야 합니다.

MFA 클레임을 사용하여 로그인 확장

데모 코드는 Identity 및 Razor Pages와 ASP.NET Core를 사용하여 설정됩니다. AddIdentity 메서드는 하나의 AddDefaultIdentity 대신 사용되므로 성공적인 로그인 후에는 IUserClaimsPrincipalFactory 구현을 사용하여 클레임을 ID에 추가할 수 있습니다.

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(
        Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
		options.SignIn.RequireConfirmedAccount = false)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

builder.Services.AddSingleton<IEmailSender, EmailSender>();
builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, 
    AdditionalUserClaimsPrincipalFactory>();

builder.Services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled", x => x.RequireClaim("amr", "mfa")));

builder.Services.AddRazorPages();

AdditionalUserClaimsPrincipalFactory 클래스는 로그인에 성공한 후에만 사용자 클레임에 amr 클레임을 추가합니다. 클레임의 값을 데이터베이스에서 읽습니다. ID가 MFA로 로그인된 경우에는 사용자가 더 높은 수준으로 보호된 보기에만 액세스해야 하므로 클레임이 여기에 추가됩니다. 클레임을 사용하는 대신 데이터베이스에서 직접 데이터베이스 보기를 읽는 경우 MFA를 활성화한 후 MFA 없이 보기에 직접 액세스할 수 있습니다.

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityStandaloneMfa
{
    public class AdditionalUserClaimsPrincipalFactory : 
        UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
    {
        public AdditionalUserClaimsPrincipalFactory( 
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager, 
            IOptions<IdentityOptions> optionsAccessor) 
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
        {
            var principal = await base.CreateAsync(user);
            var identity = (ClaimsIdentity)principal.Identity;

            var claims = new List<Claim>();

            if (user.TwoFactorEnabled)
            {
                claims.Add(new Claim("amr", "mfa"));
            }
            else
            {
                claims.Add(new Claim("amr", "pwd"));
            }

            identity.AddClaims(claims);
            return principal;
        }
    }
}

Startup 클래스에서 Identity 서비스 설정이 변경되었으므로 Identity의 레이아웃을 업데이트해야 합니다. Identity 페이지를 앱으로 스캐폴드합니다. Identity/Account/Manage/_Layout.cshtml 파일에서 레이아웃을 정의합니다.

@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}

또한 Identity 페이지에서 모든 관리 페이지에 대한 레이아웃을 할당합니다.

@{
    Layout = "_Layout.cshtml";
}

관리 페이지에서 MFA 요구 사항의 유효성을 검사합니다.

관리 Razor Page에서는 사용자가 MFA를 사용하여 로그인했는지 확인합니다. OnGet 메서드에서 ID는 사용자 클레임에 액세스하는 데 사용됩니다. amr 클레임이 mfa 값에 대해 확인됩니다. ID가 이 클레임을 누락하거나 false인 경우 페이지는 MFA 사용 페이지로 리디렉션됩니다. 사용자가 이미 로그인했지만 MFA를 사용하지 않았기 때문에 이를 수행할 수 있습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IdentityStandaloneMfa
{
    public class AdminModel : PageModel
    {
        public IActionResult OnGet()
        {
            var claimTwoFactorEnabled = 
                User.Claims.FirstOrDefault(t => t.Type == "amr");

            if (claimTwoFactorEnabled != null && 
                "mfa".Equals(claimTwoFactorEnabled.Value))
            {
                // You logged in with MFA, do the administrative stuff
            }
            else
            {
                return Redirect(
                    "/Identity/Account/Manage/TwoFactorAuthentication");
            }

            return Page();
        }
    }
}

사용자 로그인 정보를 설정/해제하는 UI 논리

시작 시 권한 부여 정책이 추가되었습니다. 정책에는 mfa 값이 포함된 amr 클레임이 필요합니다.

services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled",
        x => x.RequireClaim("amr", "mfa")));

그러면 _Layout 보기에서 이 정책을 사용하여 경고와 함께 관리 메뉴를 표시하거나 숨길 수 있습니다.

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@inject IAuthorizationService AuthorizationService

ID가 MFA를 사용하여 로그인한 경우에는 도구 설명 경고 없이 관리 메뉴가 표시됩니다. 사용자가 MFA 없이 로그인한 경우 관리자(사용 안 함) 메뉴가 사용자에게 알리는(경고 설명) 도구 설명과 함께 표시됩니다.

@if (SignInManager.IsSignedIn(User))
{
    @if ((AuthorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin" 
               id="tooltip-demo"  
               data-toggle="tooltip" 
               data-placement="bottom" 
               title="MFA is NOT enabled. This is required for the Admin Page. If you have activated MFA, then logout, login again.">
                Admin (Not Enabled)
            </a>
        </li>
    }
}

사용자가 MFA 없이 로그인하는 경우 경고가 표시됩니다.

관리자 MFA 인증

관리 링크를 클릭하면 사용자가 MFA 사용 보기로 리디렉션됩니다.

관리자가 MFA 인증 활성화

OpenID Connect 서버에 MFA 로그인 요구 사항 보내기

acr_values 매개 변수를 사용하여 인증 요청에서 클라이언트의 mfa 필수 값을 서버에 전달할 수 있습니다.

참고 항목

이 작업을 수행하려면 OpenID Connect 서버에서 acr_values 매개 변수를 처리해야 합니다.

OpenID Connect ASP.NET Core 클라이언트

ASP.NET Core Razor Pages OpenID Connect 클라이언트 앱은 AddOpenIdConnect 메서드를 사용하여 OpenID Connect 서버에 로그인합니다. acr_values 매개 변수는 mfa 값으로 설정되고 인증 요청과 함께 전송됩니다. OpenIdConnectEvents는 이를 추가하는 데 사용됩니다.

권장되는 acr_values 매개 변수 값은 인증 방법 참조 값을 참조하세요.

build.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "<OpenID Connect server URL>";
	options.RequireHttpsMetadata = true;
	options.ClientId = "<OpenID Connect client ID>";
	options.ClientSecret = "<>";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
	options.Events = new OpenIdConnectEvents
	{
		OnRedirectToIdentityProvider = context =>
		{
			context.ProtocolMessage.SetParameter("acr_values", "mfa");
			return Task.FromResult(0);
		}
	};
});

ASP.NET Core를 사용하여 Duende IdentityServer 서버에 커넥트 OpenID 예제Identity

Pages와 함께 Razor ASP.NET Core Identity 를 사용하여 구현되는 OpenID 커넥트 서버에서 새 페이지가 ErrorEnable2FA.cshtml 만들어집니다. 보기는 다음과 같습니다.

  • Identity가 MFA를 필요로 하는 앱에서 제공되지만 사용자가 Identity에서 이를 활성화하지 않은 경우를 표시합니다.
  • 사용자에게 알리고 이를 활성화하는 링크를 추가합니다.
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

<h1>The client application requires you to have MFA enabled. Enable this, try login again.</h1>

<br />

You can enable MFA to login here:

<br />

<a href="~/Identity/Account/Manage/TwoFactorAuthentication">Enable MFA</a>

Login 메서드에서 IIdentityServerInteractionService 인터페이스 구현 _interaction은 OpenID Connect 요청 매개 변수에 액세스하는 데 사용됩니다. AcrValues 속성을 사용하여 acr_values 매개 변수에 액세스합니다. 클라이언트에서 mfa 세트를 사용하여 이를 보내면 확인할 수 있습니다.

MFA가 필요하고 ASP.NET Core Identity 사용자가 MFA를 사용하도록 설정한 경우 로그인이 계속됩니다. 사용자가 MFA를 사용하도록 설정하지 않은 경우 사용자는 사용자 지정 보기 ErrorEnable2FA.cshtml로 리디렉션됩니다. 그런 다음 ASP.NET Core Identity 사용자를 로그인합니다.

Fido2Store는 사용자가 사용자 지정 FIDO2 토큰 공급자를 사용하여 MFA를 활성화한 경우 검사 데 사용됩니다.

public async Task<IActionResult> OnPost()
{
	// check if we are in the context of an authorization request
	var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);

	var requires2Fa = context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

	var user = await _userManager.FindByNameAsync(Input.Username);
	if (user != null && !user.TwoFactorEnabled && requires2Fa)
	{
		return RedirectToPage("/Home/ErrorEnable2FA/Index");
	}

	// code omitted for brevity

	if (ModelState.IsValid)
	{
		var result = await _signInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberLogin, lockoutOnFailure: true);
		if (result.Succeeded)
		{
			// code omitted for brevity
		}
		if (result.RequiresTwoFactor)
		{
			var fido2ItemExistsForUser = await _fido2Store.GetCredentialsByUserNameAsync(user.UserName);
			if (fido2ItemExistsForUser.Count > 0)
			{
				return RedirectToPage("/Account/LoginFido2Mfa", new { area = "Identity", Input.ReturnUrl, Input.RememberLogin });
			}

			return RedirectToPage("/Account/LoginWith2fa", new { area = "Identity", Input.ReturnUrl, RememberMe = Input.RememberLogin });
		}

		await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, "invalid credentials", clientId: context?.Client.ClientId));
		ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
	}

	// something went wrong, show form with error
	await BuildModelAsync(Input.ReturnUrl);
	return Page();
}

사용자가 이미 로그인한 경우 클라이언트 앱은 다음과 같습니다.

  • 여전히 amr 클레임의 유효성을 검사합니다.
  • ASP.NET Core Identity 보기에 대한 링크를 사용하여 MFA를 설정할 수 있습니다.

acr_values-1 이미지

강제로 ASP.NET Core OpenID Connect 클라이언트에 MFA 요구

이 예제에서는 OpenID Connect를 사용하여 로그인하는 ASP.NET Core Razor Page 앱에서 사용자가 MFA를 사용하여 인증해야 하는 방법을 보여 줍니다.

MFA 요구 사항의 유효성을 검사하기 위해 IAuthorizationRequirement 요구 사항이 생성됩니다. 이는 MFA를 요구하는 정책을 사용하여 페이지에 추가됩니다.

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc;

public class RequireMfa : IAuthorizationRequirement{}

amr 클레임을 사용하고 mfa 값에 대해 확인하는 AuthorizationHandler가 구현됩니다. amr은 성공적인 인증의 id_token에서 반환되며 인증 방법 참조 값 사양에 정의된 대로 다양한 값을 가질 수 있습니다.

반환되는 값은 ID가 인증되는 방법 및 OpenID Connect 서버 구현에 따라 달라집니다.

AuthorizationHandlerRequireMfa 요구 사항을 사용하고 amr 클레임의 유효성을 검사합니다. OpenID Connect 서버는 ASP.NET Core Identity와 함께 Duende Identity Server를 사용하여 구현할 수 있습니다. 사용자가 TOTP를 사용하여 로그인하는 경우에는 MFA 값을 사용하여 amr 클레임이 반환됩니다. 다른 OpenID Connect 서버 구현 또는 다른 MFA 유형을 사용하는 경우 amr 클레임에 다른 값이 있을 수 있습니다. 또한 이를 허용하려면 코드를 확장해야 합니다.

public class RequireMfaHandler : AuthorizationHandler<RequireMfa>
{
	protected override Task HandleRequirementAsync(
		AuthorizationHandlerContext context, 
		RequireMfa requirement)
	{
		if (context == null)
			throw new ArgumentNullException(nameof(context));
		if (requirement == null)
			throw new ArgumentNullException(nameof(requirement));

		var amrClaim =
			context.User.Claims.FirstOrDefault(t => t.Type == "amr");

		if (amrClaim != null && amrClaim.Value == Amr.Mfa)
		{
			context.Succeed(requirement);
		}

		return Task.CompletedTask;
	}
}

프로그램 파일에서 AddOpenIdConnect 메서드는 기본 챌린지 스키마로 사용됩니다. amr 클레임을 확인하는 데 사용되는 권한 부여 처리기는 제어 컨테이너의 반전에 추가됩니다. 그런 다음, RequireMfa 요구 사항을 추가하는 정책이 생성됩니다.

builder.Services.ConfigureApplicationCookie(options =>
        options.Cookie.SecurePolicy =
            CookieSecurePolicy.Always);

builder.Services.AddSingleton<IAuthorizationHandler, RequireMfaHandler>();

builder.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "https://localhost:44352";
	options.RequireHttpsMetadata = true;
	options.ClientId = "AspNetCoreRequireMfaOidc";
	options.ClientSecret = "AspNetCoreRequireMfaOidcSecret";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
});

builder.Services.AddAuthorization(options =>
{
	options.AddPolicy("RequireMfa", policyIsAdminRequirement =>
	{
		policyIsAdminRequirement.Requirements.Add(new RequireMfa());
	});
});

builder.Services.AddRazorPages();

이 정책은 필요에 따라 Razor Page에서 사용됩니다. 정책은 전체 앱에 대해서도 전역적으로 추가할 수 있습니다.

[Authorize(Policy= "RequireMfa")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}

사용자가 MFA 없이 인증하는 경우 amr 클레임에 pwd 값이 있을 수 있습니다. 이 요청에는 페이지에 대한 액세스 권한이 부여되지 않습니다. 기본값을 사용하여 사용자가 Account/AccessDenied 페이지로 리디렉션됩니다. 이 동작은 변경할 수 있습니다. 또는 사용자 고유의 사용자 지정 논리를 여기서 구현할 수 있습니다. 이 예제에서는 유효한 사용자가 계정에 대해 MFA를 설정할 수 있도록 링크가 추가됩니다.

@page
@model AspNetCoreRequireMfaOidc.AccessDeniedModel
@{
    ViewData["Title"] = "AccessDenied";
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

<h1>AccessDenied</h1>

You require MFA to login here

<a href="https://localhost:44352/Manage/TwoFactorAuthentication">Enable MFA</a>

이제 MFA로 인증하는 사용자만 페이지 또는 웹 사이트에 액세스할 수 있습니다. 다른 MFA 형식을 사용하거나 2FA가 양호하면 amr 클레임은 다른 값을 가지며 올바르게 처리되어야 합니다. 다른 OpenID Connect 서버는 이 클레임에 대해 서로 다른 값을 반환하고 인증 방법 참조 값 사양을 따르지 않을 수도 있습니다.

MFA 없이 로그인하는 경우(예: 암호만 사용):

  • amrpwd 값은 다음과 같습니다.

    amr에 있는 pwd 값

  • 액세스가 거부되었습니다.

    액세스 거부됨

또는 Identity를 통한 OTP를 사용하여 로그인합니다.

다음으로 OTP를 사용하여 로그인 Identity

추가 리소스

작성자: Damien Bowden

샘플 코드 보기 또는 다운로드(damienbod/AspNetCoreHybridFlowWithApi GitHub 리포지토리)

MFA(다단계 인증)는 추가 형태의 식별을 위해 로그인 이벤트 중에 사용자에게 요청되는 프로세스입니다. 이는 휴대폰에서 코드를 입력하거나, FIDO2 키를 사용하거나, 지문 검색을 제공하라는 메시지로 표시될 수 있습니다. 두 번째 인증 형식이 필요한 경우 보안이 강화됩니다. 공격자가 추가 요소를 쉽게 얻거나 복제하지 못합니다.

이 문서에서는 다음 영역에 대해 설명합니다.

  • MFA의 정의 및 권장되는 MFA 흐름
  • ASP.NET Core Identity를 사용하여 관리 페이지에 대한 MFA 구성
  • OpenID Connect 서버에 MFA 로그인 요구 사항 보내기
  • 강제로 ASP.NET Core OpenID Connect 클라이언트에 MFA 요구

MFA, 2FA

MFA를 사용하려면 사용자가 인증하기 위해 알고 있는 것, 소유하고 있는 것 또는 생체 인증과 같이 ID에 대해 적어도 두 가지 이상의 증명 형식이 필요합니다.

2FA(2단계 인증)는 MFA의 하위 집합과 유사하지만 MFA가 ID를 증명하기 위해 두 개 이상의 요소를 요구할 수 있다는 점에서 다릅니다.

MFA TOTP(시간 기반 일회용 암호 알고리즘)

TOTP를 사용하는 MFA는 ASP.NET Core Identity를 사용하여 지원되는 구현입니다. 다음을 포함한 모든 규격 인증 앱과 함께 사용할 수 있습니다.

  • Microsoft Authenticator 앱
  • Google 인증 앱

구현 세부 정보는 다음 링크를 참조하세요.

ASP.NET Core에서 TOTP 인증 앱에 QR 코드 생성 사용

MFA 암호/FIDO2 또는 암호 없는

passkeys/FIDO2는 현재 다음과 같습니다.

  • MFA를 가장 안전하게 달성하는 방법입니다.
  • 피싱 공격으로부터 보호하는 MFA입니다. (뿐만 아니라 인증서 인증 및 비즈니스용 Windows)

현재 ASP.NET Core는 passkeys/FIDO2를 직접 지원하지 않습니다. Passkeys/FIDO2는 MFA 또는 암호 없는 흐름에 사용할 수 있습니다.

Microsoft Entra ID는 passkeys/FIDO2 및 암호 없는 흐름을 지원합니다. 자세한 내용은 암호 없는 인증 옵션을 참조 하세요.

다른 형태의 암호 없는 MFA는 피싱으로부터 보호하지 않거나 보호하지 않을 수 있습니다.

MFA SMS

SMS를 사용하는 MFA는 암호 인증(단일 요소)에 비해 보안성을 대폭 강화합니다. 그러나 SMS를 두 번째 요소로 사용하는 것은 더 이상 권장되지 않습니다. 이 구현 형식에 대해 알려진 공격 벡터가 너무 많습니다.

NIST 지침

ASP.NET Core Identity를 사용하여 관리 페이지에 대한 MFA 구성

MFA는 사용자가 ASP.NET Core Identity 앱 내에서 중요한 페이지에 액세스하도록 강제할 수 있습니다. 이는 다양한 ID에 대해 서로 다른 수준의 액세스 권한이 있는 앱에 유용할 수 있습니다. 예를 들어 사용자는 암호 로그인을 사용하여 프로필 데이터를 볼 수 있지만, 관리자는 관리 페이지에 액세스하기 위해서는 MFA를 사용해야 합니다.

MFA 클레임을 사용하여 로그인 확장

데모 코드는 Identity 및 Razor Pages와 ASP.NET Core를 사용하여 설정됩니다. AddIdentity 메서드는 하나의 AddDefaultIdentity 대신 사용되므로 성공적인 로그인 후에는 IUserClaimsPrincipalFactory 구현을 사용하여 클레임을 ID에 추가할 수 있습니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlite(
            Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<IdentityUser, IdentityRole>(
            options => options.SignIn.RequireConfirmedAccount = false)
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddSingleton<IEmailSender, EmailSender>();
    services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, 
        AdditionalUserClaimsPrincipalFactory>();

    services.AddAuthorization(options =>
        options.AddPolicy("TwoFactorEnabled",
            x => x.RequireClaim("amr", "mfa")));

    services.AddRazorPages();
}

AdditionalUserClaimsPrincipalFactory 클래스는 로그인에 성공한 후에만 사용자 클레임에 amr 클레임을 추가합니다. 클레임의 값을 데이터베이스에서 읽습니다. ID가 MFA로 로그인된 경우에는 사용자가 더 높은 수준으로 보호된 보기에만 액세스해야 하므로 클레임이 여기에 추가됩니다. 클레임을 사용하는 대신 데이터베이스에서 직접 데이터베이스 보기를 읽는 경우 MFA를 활성화한 후 MFA 없이 보기에 직접 액세스할 수 있습니다.

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityStandaloneMfa
{
    public class AdditionalUserClaimsPrincipalFactory : 
        UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
    {
        public AdditionalUserClaimsPrincipalFactory( 
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager, 
            IOptions<IdentityOptions> optionsAccessor) 
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
        {
            var principal = await base.CreateAsync(user);
            var identity = (ClaimsIdentity)principal.Identity;

            var claims = new List<Claim>();

            if (user.TwoFactorEnabled)
            {
                claims.Add(new Claim("amr", "mfa"));
            }
            else
            {
                claims.Add(new Claim("amr", "pwd"));
            }

            identity.AddClaims(claims);
            return principal;
        }
    }
}

Startup 클래스에서 Identity 서비스 설정이 변경되었으므로 Identity의 레이아웃을 업데이트해야 합니다. Identity 페이지를 앱으로 스캐폴드합니다. Identity/Account/Manage/_Layout.cshtml 파일에서 레이아웃을 정의합니다.

@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}

또한 Identity 페이지에서 모든 관리 페이지에 대한 레이아웃을 할당합니다.

@{
    Layout = "_Layout.cshtml";
}

관리 페이지에서 MFA 요구 사항의 유효성을 검사합니다.

관리 Razor Page에서는 사용자가 MFA를 사용하여 로그인했는지 확인합니다. OnGet 메서드에서 ID는 사용자 클레임에 액세스하는 데 사용됩니다. amr 클레임이 mfa 값에 대해 확인됩니다. ID가 이 클레임을 누락하거나 false인 경우 페이지는 MFA 사용 페이지로 리디렉션됩니다. 사용자가 이미 로그인했지만 MFA를 사용하지 않았기 때문에 이를 수행할 수 있습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IdentityStandaloneMfa
{
    public class AdminModel : PageModel
    {
        public IActionResult OnGet()
        {
            var claimTwoFactorEnabled = 
                User.Claims.FirstOrDefault(t => t.Type == "amr");

            if (claimTwoFactorEnabled != null && 
                "mfa".Equals(claimTwoFactorEnabled.Value))
            {
                // You logged in with MFA, do the administrative stuff
            }
            else
            {
                return Redirect(
                    "/Identity/Account/Manage/TwoFactorAuthentication");
            }

            return Page();
        }
    }
}

사용자 로그인 정보를 설정/해제하는 UI 논리

권한 부여 정책이 프로그램 파일에 추가되었습니다. 정책에는 mfa 값이 포함된 amr 클레임이 필요합니다.

builder.Services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled",
        x => x.RequireClaim("amr", "mfa")));

그러면 _Layout 보기에서 이 정책을 사용하여 경고와 함께 관리 메뉴를 표시하거나 숨길 수 있습니다.

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@inject IAuthorizationService AuthorizationService

ID가 MFA를 사용하여 로그인한 경우에는 도구 설명 경고 없이 관리 메뉴가 표시됩니다. 사용자가 MFA 없이 로그인한 경우 관리자(사용 안 함) 메뉴가 사용자에게 알리는(경고 설명) 도구 설명과 함께 표시됩니다.

@if (SignInManager.IsSignedIn(User))
{
    @if ((AuthorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin" 
               id="tooltip-demo"  
               data-toggle="tooltip" 
               data-placement="bottom" 
               title="MFA is NOT enabled. This is required for the Admin Page. If you have activated MFA, then logout, login again.">
                Admin (Not Enabled)
            </a>
        </li>
    }
}

사용자가 MFA 없이 로그인하는 경우 경고가 표시됩니다.

관리자 MFA 인증

관리 링크를 클릭하면 사용자가 MFA 사용 보기로 리디렉션됩니다.

관리자가 MFA 인증 활성화

OpenID Connect 서버에 MFA 로그인 요구 사항 보내기

acr_values 매개 변수를 사용하여 인증 요청에서 클라이언트의 mfa 필수 값을 서버에 전달할 수 있습니다.

참고 항목

이 작업을 수행하려면 OpenID Connect 서버에서 acr_values 매개 변수를 처리해야 합니다.

OpenID Connect ASP.NET Core 클라이언트

ASP.NET Core Razor Pages OpenID Connect 클라이언트 앱은 AddOpenIdConnect 메서드를 사용하여 OpenID Connect 서버에 로그인합니다. acr_values 매개 변수는 mfa 값으로 설정되고 인증 요청과 함께 전송됩니다. OpenIdConnectEvents는 이를 추가하는 데 사용됩니다.

권장되는 acr_values 매개 변수 값은 인증 방법 참조 값을 참조하세요.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme =
            OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.SignInScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = "<OpenID Connect server URL>";
        options.RequireHttpsMetadata = true;
        options.ClientId = "<OpenID Connect client ID>";
        options.ClientSecret = "<>";
        options.ResponseType = "code";
        options.UsePkce = true;	
        options.Scope.Add("profile");
        options.Scope.Add("offline_access");
        options.SaveTokens = true;
        options.Events = new OpenIdConnectEvents
        {
            OnRedirectToIdentityProvider = context =>
            {
                context.ProtocolMessage.SetParameter("acr_values", "mfa");
                return Task.FromResult(0);
            }
        };
    });

ASP.NET Core Identity를 사용한 OpenID Connect Identity Server 4 서버 예제

MVC 보기와 함께 ASP.NET Core Identity를 사용하여 구현되는 OpenID Connect 서버에서 ErrorEnable2FA.cshtml이라는 새 보기가 만들어집니다. 보기는 다음과 같습니다.

  • Identity가 MFA를 필요로 하는 앱에서 제공되지만 사용자가 Identity에서 이를 활성화하지 않은 경우를 표시합니다.
  • 사용자에게 알리고 이를 활성화하는 링크를 추가합니다.
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

<h1>The client application requires you to have MFA enabled. Enable this, try login again.</h1>

<br />

You can enable MFA to login here:

<br />

<a asp-controller="Manage" asp-action="TwoFactorAuthentication">Enable MFA</a>

Login 메서드에서 IIdentityServerInteractionService 인터페이스 구현 _interaction은 OpenID Connect 요청 매개 변수에 액세스하는 데 사용됩니다. AcrValues 속성을 사용하여 acr_values 매개 변수에 액세스합니다. 클라이언트에서 mfa 세트를 사용하여 이를 보내면 확인할 수 있습니다.

MFA가 필요하고 ASP.NET Core Identity 사용자가 MFA를 사용하도록 설정한 경우 로그인이 계속됩니다. 사용자가 MFA를 사용하도록 설정하지 않은 경우 사용자는 사용자 지정 보기 ErrorEnable2FA.cshtml로 리디렉션됩니다. 그런 다음 ASP.NET Core Identity 사용자를 로그인합니다.

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
    var returnUrl = model.ReturnUrl;
    var context = 
        await _interaction.GetAuthorizationContextAsync(returnUrl);
    var requires2Fa = 
        context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

    var user = await _userManager.FindByNameAsync(model.Email);
    if (user != null && !user.TwoFactorEnabled && requires2Fa)
    {
        return RedirectToAction(nameof(ErrorEnable2FA));
    }

    // code omitted for brevity

ExternalLoginCallback 메서드는 로컬 Identity 로그인과 유사하게 작동합니다. AcrValues 속성이 mfa 값에 대해 확인됩니다. mfa 값이 있는 경우 로그인이 완료되기 전에 MFA가 강제로 수행됩니다(예: ErrorEnable2FA 보기로 리디렉션됨).

//
// GET: /Account/ExternalLoginCallback
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(
    string returnUrl = null,
    string remoteError = null)
{
    var context =
        await _interaction.GetAuthorizationContextAsync(returnUrl);
    var requires2Fa =
        context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

    if (remoteError != null)
    {
        ModelState.AddModelError(
            string.Empty,
            _sharedLocalizer["EXTERNAL_PROVIDER_ERROR", 
            remoteError]);
        return View(nameof(Login));
    }
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        return RedirectToAction(nameof(Login));
    }

    var email = info.Principal.FindFirstValue(ClaimTypes.Email);

    if (!string.IsNullOrEmpty(email))
    {
        var user = await _userManager.FindByNameAsync(email);
        if (user != null && !user.TwoFactorEnabled && requires2Fa)
        {
            return RedirectToAction(nameof(ErrorEnable2FA));
        }
    }

    // Sign in the user with this external login provider if the user already has a login.
    var result = await _signInManager
        .ExternalLoginSignInAsync(
            info.LoginProvider, 
            info.ProviderKey, 
            isPersistent: 
            false);

    // code omitted for brevity

사용자가 이미 로그인한 경우 클라이언트 앱은 다음과 같습니다.

  • 여전히 amr 클레임의 유효성을 검사합니다.
  • ASP.NET Core Identity 보기에 대한 링크를 사용하여 MFA를 설정할 수 있습니다.

acr_values-1 이미지

강제로 ASP.NET Core OpenID Connect 클라이언트에 MFA 요구

이 예제에서는 OpenID Connect를 사용하여 로그인하는 ASP.NET Core Razor Page 앱에서 사용자가 MFA를 사용하여 인증해야 하는 방법을 보여 줍니다.

MFA 요구 사항의 유효성을 검사하기 위해 IAuthorizationRequirement 요구 사항이 생성됩니다. 이는 MFA를 요구하는 정책을 사용하여 페이지에 추가됩니다.

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc
{
    public class RequireMfa : IAuthorizationRequirement{}
}

amr 클레임을 사용하고 mfa 값에 대해 확인하는 AuthorizationHandler가 구현됩니다. amr은 성공적인 인증의 id_token에서 반환되며 인증 방법 참조 값 사양에 정의된 대로 다양한 값을 가질 수 있습니다.

반환되는 값은 ID가 인증되는 방법 및 OpenID Connect 서버 구현에 따라 달라집니다.

AuthorizationHandlerRequireMfa 요구 사항을 사용하고 amr 클레임의 유효성을 검사합니다. OpenID Connect 서버는 ASP.NET Core Identity와 함께 IdentityServer4를 사용하여 구현할 수 있습니다. 사용자가 TOTP를 사용하여 로그인하는 경우에는 MFA 값을 사용하여 amr 클레임이 반환됩니다. 다른 OpenID Connect 서버 구현 또는 다른 MFA 유형을 사용하는 경우 amr 클레임에 다른 값이 있을 수 있습니다. 또한 이를 허용하려면 코드를 확장해야 합니다.

public class RequireMfaHandler : AuthorizationHandler<RequireMfa>
{
	protected override Task HandleRequirementAsync(
		AuthorizationHandlerContext context, 
		RequireMfa requirement)
	{
		if (context == null)
			throw new ArgumentNullException(nameof(context));
		if (requirement == null)
			throw new ArgumentNullException(nameof(requirement));

		var amrClaim =
			context.User.Claims.FirstOrDefault(t => t.Type == "amr");

		if (amrClaim != null && amrClaim.Value == Amr.Mfa)
		{
			context.Succeed(requirement);
		}

		return Task.CompletedTask;
	}
}

Startup.ConfigureServices 메서드에서 AddOpenIdConnect 메서드는 기본 챌린지 체계로 사용됩니다. amr 클레임을 확인하는 데 사용되는 권한 부여 처리기는 제어 컨테이너의 반전에 추가됩니다. 그런 다음, RequireMfa 요구 사항을 추가하는 정책이 생성됩니다.

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureApplicationCookie(options =>
        options.Cookie.SecurePolicy =
            CookieSecurePolicy.Always);

    services.AddSingleton<IAuthorizationHandler, RequireMfaHandler>();

    services.AddAuthentication(options =>
    {
        options.DefaultScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme =
            OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.SignInScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = "https://localhost:44352";
        options.RequireHttpsMetadata = true;
        options.ClientId = "AspNetCoreRequireMfaOidc";
        options.ClientSecret = "AspNetCoreRequireMfaOidcSecret";
        options.ResponseType = "code";
        options.UsePkce = true;	
        options.Scope.Add("profile");
        options.Scope.Add("offline_access");
        options.SaveTokens = true;
    });

    services.AddAuthorization(options =>
    {
        options.AddPolicy("RequireMfa", policyIsAdminRequirement =>
        {
            policyIsAdminRequirement.Requirements.Add(new RequireMfa());
        });
    });

    services.AddRazorPages();
}

이 정책은 필요에 따라 Razor Page에서 사용됩니다. 정책은 전체 앱에 대해서도 전역적으로 추가할 수 있습니다.

[Authorize(Policy= "RequireMfa")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}

사용자가 MFA 없이 인증하는 경우 amr 클레임에 pwd 값이 있을 수 있습니다. 이 요청에는 페이지에 대한 액세스 권한이 부여되지 않습니다. 기본값을 사용하여 사용자가 Account/AccessDenied 페이지로 리디렉션됩니다. 이 동작은 변경할 수 있습니다. 또는 사용자 고유의 사용자 지정 논리를 여기서 구현할 수 있습니다. 이 예제에서는 유효한 사용자가 계정에 대해 MFA를 설정할 수 있도록 링크가 추가됩니다.

@page
@model AspNetCoreRequireMfaOidc.AccessDeniedModel
@{
    ViewData["Title"] = "AccessDenied";
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

<h1>AccessDenied</h1>

You require MFA to login here

<a href="https://localhost:44352/Manage/TwoFactorAuthentication">Enable MFA</a>

이제 MFA로 인증하는 사용자만 페이지 또는 웹 사이트에 액세스할 수 있습니다. 다른 MFA 형식을 사용하거나 2FA가 양호하면 amr 클레임은 다른 값을 가지며 올바르게 처리되어야 합니다. 다른 OpenID Connect 서버는 이 클레임에 대해 서로 다른 값을 반환하고 인증 방법 참조 값 사양을 따르지 않을 수도 있습니다.

MFA 없이 로그인하는 경우(예: 암호만 사용):

  • amrpwd 값은 다음과 같습니다.

    amr에 있는 pwd 값

  • 액세스가 거부되었습니다.

    액세스 거부됨

또는 Identity를 통한 OTP를 사용하여 로그인합니다.

다음으로 OTP를 사용하여 로그인 Identity

추가 리소스