ASP.NET Core 中的多重身份验证

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

作者:Damien Bowden

查看或下载示例代码(damienbod/AspNetCoreHybridFlowWithApi GitHub 存储库)

多重身份验证 (MFA) 是在登录事件期间请求用户进行其他形式的身份验证的过程。 此提示可以是输入手机中的代码、使用 FIDO2 密钥或提供指纹扫描。 需要进行另一种形式的身份验证时,安全性便得到了增强。 攻击者无法轻松获取或复制额外的因素。

本文涵盖以下几个方面:

  • 什么是 MFA 以及建议使用哪些 MFA 流
  • 使用 ASP.NET Core Identity 为管理页面配置 MFA
  • 将 MFA 登录要求发送到 OpenID Connect 服务器
  • 强制 ASP.NET Core OpenID Connect 客户端要求 MFA

MFA、2FA

MFA 至少需要两种或更多类型的身份验证,例如你知道的东西、你拥有的内容或对用户进行身份验证的生物识别验证。

双因素身份验证 (2FA) 类似于 MFA 的子集,但不同之处在于,MFA 可能需要两个或多个因素来证明身份。

使用 ASP.NET Core Identity 时,默认支持 2FA。 若要为特定用户启用或禁用 2FA,请设置 IdentityUser<TKey>.TwoFactorEnabled 属性。 ASP.NET Core Identity 默认 UI 包括用于配置 2FA 的页面。

MFA TOTP(基于时间的一次性密码算法)

使用 ASP.NET Core Identity 时,默认支持使用 TOTP 的 MFA。 此方法可以与任何兼容的验证器应用一起使用,包括:

  • Microsoft Authenticator
  • Google Authenticator

有关实现详细信息,请参阅在 ASP.NET Core 中为 TOTP 验证器应用启用 QR 码生成

若要禁用对 MFA TOTP 的支持,请使用 AddIdentity 而不是 AddDefaultIdentity 配置身份验证。 AddDefaultIdentity 在内部调用 AddDefaultTokenProviders,其注册多个令牌提供程序,包括一个用于 MFA TOTP 的令牌提供程序。 若要仅注册特定的令牌提供程序,请为每个必需的提供程序调用 AddTokenProvider。 有关可用令牌提供程序的详细信息,请参阅 GitHub 上的 AddDefaultTokenProviders 源

MFA 密钥/FIDO2 或无密码

密钥/FIDO2 目前是:

  • 实现 MFA 的最安全方法。
  • 可防范钓鱼攻击的 MFA。 (以及证书身份验证和适用于企业的 Windows)

目前,ASP.NET Core 不能直接支持 密钥/FIDO2。 密钥/FIDO2 可用于 MFA 或无密码流。

Microsoft Entra ID 提供对密钥/FIDO2 和无密码流的支持。 有关详细信息,请参阅无密码身份验证选项

其他形式的无密码 MFA 不会或无法防范钓鱼。

MFA 短信

与密码身份验证(单因素)相比,使用短信的 MFA 大大提高了安全性。 但是,不再建议使用短信作为第二个因素。 此类型的实现存在太多已知攻击媒介。

NIST 指南

使用 ASP.NET Core Identity 为管理页面配置 MFA

MFA 可能会强制用户访问 ASP.NET Core Identity 应用中的敏感页面。 对于不同标识存在不同级别访问权限的应用,这可能很有用。 例如,用户可以使用密码登录来查看配置文件数据,但管理员需要使用 MFA 才能访问管理页面。

使用 MFA 声明扩展登录名

演示代码是使用 ASP.NET Core 与 Identity 和 Razor Pages 设置的。 使用的是 AddIdentity 方法而不是 AddDefaultIdentity,因此在成功登录后可以使用 IUserClaimsPrincipalFactory 实现向标识添加声明。

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 声明。 声明的值是从数据库中读取的。 此处添加声明是因为如果标识已使用 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;
        }
    }
}

由于 Identity 服务设置在 Startup 类中发生了变化,需要更新 Identity 的布局。 将 Identity 页面植入应用。 定义 Identity/Account/Manage/_Layout.cshtml 文件中的布局。

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

同时从 Identity 页面中为所有的管理页面分配布局:

@{
    Layout = "_Layout.cshtml";
}

在管理页中验证 MFA 要求

管理 Razor 页将验证用户是否已使用 MFA 登录。 在 OnGet 方法中,标识用于访问用户声明。 系统将检查 amr 声明的值 mfa。 如果标识缺少此声明或为 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 逻辑

系统在启动时添加了授权策略。 策略要求 amr 声明的值为 mfa

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

如果标识已使用 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 身份验证

将 MFA 登录要求发送到 OpenID Connect 服务器

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 Identity 的示例 OpenID Connect Duende IdentityServer 服务器

在使用 ASP.NET Core Identity 与 Razor Pages 一起实现的 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 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 页面应用如何要求用户使用 MFA 进行身份验证。

若要验证 MFA 要求,请创建一个 IAuthorizationRequirement 要求。 系统会使用需要 MFA 的策略将它添加到页面中。

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc;

public class RequireMfa : IAuthorizationRequirement{}

将实现 AuthorizationHandler,它将使用 amr 声明并检查值 mfa。 成功的身份验证的 id_token 中会返回 amr,并且它可能有许多不同的值,如身份验证方法参考值规范中所述。

返回的值取决于标识进行身份验证的方式以及 OpenID Connect 服务器实现的方式。

AuthorizationHandler 使用 RequireMfa 要求并验证 amr 声明。 可以通过将 Duende Identity Server 与 ASP.NET Core Identity 结合使用来实现 OpenID Connect 服务器。 用户使用 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 页面中根据需要使用此策略。 也可以全局性地为整个应用添加策略。

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

如果用户不使用 MFA 进行身份验证,则 amr 声明可能具有 pwd 值。 不会授权该请求访问页面。 如果使用默认值,用户将被重定向到“帐户/拒绝访问”页。 可以更改此行为,也可以在此处实现自己的自定义逻辑。 本示例中添加了一个链接,以便有效的用户可以为其帐户设置 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 登录(例如,只使用密码):

  • amr 具有 pwd 值:

    amr 具有 pwd 值

  • 拒绝访问:

    访问被拒绝

或者,使用 OTP 通过 Identity 进行登录:

使用 OTP 通过 Identity 进行登录

其他资源

作者:Damien Bowden

查看或下载示例代码(damienbod/AspNetCoreHybridFlowWithApi GitHub 存储库)

多重身份验证 (MFA) 是在登录事件期间请求用户进行其他形式的身份验证的过程。 此提示可以是输入手机中的代码、使用 FIDO2 密钥或提供指纹扫描。 需要进行另一种形式的身份验证时,安全性便得到了增强。 攻击者无法轻松获取或复制额外的因素。

本文涵盖以下几个方面:

  • 什么是 MFA 以及建议使用哪些 MFA 流
  • 使用 ASP.NET Core Identity 为管理页面配置 MFA
  • 将 MFA 登录要求发送到 OpenID Connect 服务器
  • 强制 ASP.NET Core OpenID Connect 客户端要求 MFA

MFA、2FA

MFA 至少需要两种或更多类型的身份验证,例如你知道的东西、你拥有的内容或对用户进行身份验证的生物识别验证。

双因素身份验证 (2FA) 类似于 MFA 的子集,但不同之处在于,MFA 可能需要两个或多个因素来证明身份。

使用 ASP.NET Core Identity 时,默认支持 2FA。 若要为特定用户启用或禁用 2FA,请设置 IdentityUser<TKey>.TwoFactorEnabled 属性。 ASP.NET Core Identity 默认 UI 包括用于配置 2FA 的页面。

MFA TOTP(基于时间的一次性密码算法)

使用 ASP.NET Core Identity 时,默认支持使用 TOTP 的 MFA。 此方法可以与任何兼容的验证器应用一起使用,包括:

  • Microsoft Authenticator
  • Google Authenticator

有关实现详细信息,请参阅在 ASP.NET Core 中为 TOTP 验证器应用启用 QR 码生成

若要禁用对 MFA TOTP 的支持,请使用 AddIdentity 而不是 AddDefaultIdentity 配置身份验证。 AddDefaultIdentity 在内部调用 AddDefaultTokenProviders,其注册多个令牌提供程序,包括一个用于 MFA TOTP 的令牌提供程序。 若要仅注册特定的令牌提供程序,请为每个必需的提供程序调用 AddTokenProvider。 有关可用令牌提供程序的详细信息,请参阅 GitHub 上的 AddDefaultTokenProviders 源

MFA 密钥/FIDO2 或无密码

密钥/FIDO2 目前是:

  • 实现 MFA 的最安全方法。
  • 可防范钓鱼攻击的 MFA。 (以及证书身份验证和适用于企业的 Windows)

目前,ASP.NET Core 不能直接支持 密钥/FIDO2。 密钥/FIDO2 可用于 MFA 或无密码流。

Microsoft Entra ID 提供对密钥/FIDO2 和无密码流的支持。 有关详细信息,请参阅无密码身份验证选项

其他形式的无密码 MFA 不会或无法防范钓鱼。

MFA 短信

与密码身份验证(单因素)相比,使用短信的 MFA 大大提高了安全性。 但是,不再建议使用短信作为第二个因素。 此类型的实现存在太多已知攻击媒介。

NIST 指南

使用 ASP.NET Core Identity 为管理页面配置 MFA

MFA 可能会强制用户访问 ASP.NET Core Identity 应用中的敏感页面。 对于不同标识存在不同级别访问权限的应用,这可能很有用。 例如,用户可以使用密码登录来查看配置文件数据,但管理员需要使用 MFA 才能访问管理页面。

使用 MFA 声明扩展登录名

演示代码是使用 ASP.NET Core 与 Identity 和 Razor Pages 设置的。 使用的是 AddIdentity 方法而不是 AddDefaultIdentity,因此在成功登录后可以使用 IUserClaimsPrincipalFactory 实现向标识添加声明。

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 声明。 声明的值是从数据库中读取的。 此处添加声明是因为如果标识已使用 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;
        }
    }
}

由于 Identity 服务设置在 Startup 类中发生了变化,需要更新 Identity 的布局。 将 Identity 页面植入应用。 定义 Identity/Account/Manage/_Layout.cshtml 文件中的布局。

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

同时从 Identity 页面中为所有的管理页面分配布局:

@{
    Layout = "_Layout.cshtml";
}

在管理页中验证 MFA 要求

管理 Razor 页将验证用户是否已使用 MFA 登录。 在 OnGet 方法中,标识用于访问用户声明。 系统将检查 amr 声明的值 mfa。 如果标识缺少此声明或为 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 逻辑

系统在启动时添加了授权策略。 策略要求 amr 声明的值为 mfa

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

如果标识已使用 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 身份验证

将 MFA 登录要求发送到 OpenID Connect 服务器

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 Identity 的示例 OpenID Connect Duende IdentityServer 服务器

在使用 ASP.NET Core Identity 与 Razor Pages 一起实现的 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 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 页面应用如何要求用户使用 MFA 进行身份验证。

若要验证 MFA 要求,请创建一个 IAuthorizationRequirement 要求。 系统会使用需要 MFA 的策略将它添加到页面中。

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc;

public class RequireMfa : IAuthorizationRequirement{}

将实现 AuthorizationHandler,它将使用 amr 声明并检查值 mfa。 成功的身份验证的 id_token 中会返回 amr,并且它可能有许多不同的值,如身份验证方法参考值规范中所述。

返回的值取决于标识进行身份验证的方式以及 OpenID Connect 服务器实现的方式。

AuthorizationHandler 使用 RequireMfa 要求并验证 amr 声明。 可以通过将 Duende Identity Server 与 ASP.NET Core Identity 结合使用来实现 OpenID Connect 服务器。 用户使用 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 页面中根据需要使用此策略。 也可以全局性地为整个应用添加策略。

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

如果用户不使用 MFA 进行身份验证,则 amr 声明可能具有 pwd 值。 不会授权该请求访问页面。 如果使用默认值,用户将被重定向到“帐户/拒绝访问”页。 可以更改此行为,也可以在此处实现自己的自定义逻辑。 本示例中添加了一个链接,以便有效的用户可以为其帐户设置 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 登录(例如,只使用密码):

  • amr 具有 pwd 值:

    amr 具有 pwd 值

  • 拒绝访问:

    访问被拒绝

或者,使用 OTP 通过 Identity 进行登录:

使用 OTP 通过 Identity 进行登录

其他资源

作者:Damien Bowden

查看或下载示例代码(damienbod/AspNetCoreHybridFlowWithApi GitHub 存储库)

多重身份验证 (MFA) 是在登录事件期间请求用户进行其他形式的身份验证的过程。 此提示可以是输入手机中的代码、使用 FIDO2 密钥或提供指纹扫描。 需要进行另一种形式的身份验证时,安全性便得到了增强。 攻击者无法轻松获取或复制额外的因素。

本文涵盖以下几个方面:

  • 什么是 MFA 以及建议使用哪些 MFA 流
  • 使用 ASP.NET Core Identity 为管理页面配置 MFA
  • 将 MFA 登录要求发送到 OpenID Connect 服务器
  • 强制 ASP.NET Core OpenID Connect 客户端要求 MFA

MFA、2FA

MFA 至少需要两种或更多类型的身份验证,例如你知道的东西、你拥有的内容或对用户进行身份验证的生物识别验证。

双因素身份验证 (2FA) 类似于 MFA 的子集,但不同之处在于,MFA 可能需要两个或多个因素来证明身份。

MFA TOTP(基于时间的一次性密码算法)

使用 TOTP 的 MFA 是使用 ASP.NET Core Identity 的一种受支持的实现。 这可以与任何兼容的验证器应用一起使用,包括:

  • Microsoft Authenticator 应用
  • Google Authenticator 应用

有关实现的详细信息,请参阅以下链接:

为 ASP.NET Core 中的 TOTP 验证器应用启用 QR 码生成

MFA 密钥/FIDO2 或无密码

密钥/FIDO2 目前是:

  • 实现 MFA 的最安全方法。
  • 可防范钓鱼攻击的 MFA。 (以及证书身份验证和适用于企业的 Windows)

目前,ASP.NET Core 不能直接支持 密钥/FIDO2。 密钥/FIDO2 可用于 MFA 或无密码流。

Microsoft Entra ID 提供对密钥/FIDO2 和无密码流的支持。 有关详细信息,请参阅无密码身份验证选项

其他形式的无密码 MFA 不会或无法防范钓鱼。

MFA 短信

与密码身份验证(单因素)相比,使用短信的 MFA 大大提高了安全性。 但是,不再建议使用短信作为第二个因素。 此类型的实现存在太多已知攻击媒介。

NIST 指南

使用 ASP.NET Core Identity 为管理页面配置 MFA

MFA 可能会强制用户访问 ASP.NET Core Identity 应用中的敏感页面。 对于不同标识存在不同级别访问权限的应用,这可能很有用。 例如,用户可以使用密码登录来查看配置文件数据,但管理员需要使用 MFA 才能访问管理页面。

使用 MFA 声明扩展登录名

演示代码是使用 ASP.NET Core 与 Identity 和 Razor Pages 设置的。 使用的是 AddIdentity 方法而不是 AddDefaultIdentity,因此在成功登录后可以使用 IUserClaimsPrincipalFactory 实现向标识添加声明。

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 声明。 声明的值是从数据库中读取的。 此处添加声明是因为如果标识已使用 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;
        }
    }
}

由于 Identity 服务设置在 Startup 类中发生了变化,需要更新 Identity 的布局。 将 Identity 页面植入应用。 定义 Identity/Account/Manage/_Layout.cshtml 文件中的布局。

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

同时从 Identity 页面中为所有的管理页面分配布局:

@{
    Layout = "_Layout.cshtml";
}

在管理页中验证 MFA 要求

管理 Razor 页将验证用户是否已使用 MFA 登录。 在 OnGet 方法中,标识用于访问用户声明。 系统将检查 amr 声明的值 mfa。 如果标识缺少此声明或为 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 逻辑

授权策略已添加到程序文件中。 策略要求 amr 声明的值为 mfa

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

如果标识已使用 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 身份验证

将 MFA 登录要求发送到 OpenID Connect 服务器

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 IdentityServer 4 服务器

在使用 ASP.NET Core Identity 与 MVC 视图一起实现的 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 页面应用如何要求用户使用 MFA 进行身份验证。

若要验证 MFA 要求,请创建一个 IAuthorizationRequirement 要求。 系统会使用需要 MFA 的策略将它添加到页面中。

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc
{
    public class RequireMfa : IAuthorizationRequirement{}
}

将实现 AuthorizationHandler,它将使用 amr 声明并检查值 mfa。 成功的身份验证的 id_token 中会返回 amr,并且它可能有许多不同的值,如身份验证方法参考值规范中所述。

返回的值取决于标识进行身份验证的方式以及 OpenID Connect 服务器实现的方式。

AuthorizationHandler 使用 RequireMfa 要求并验证 amr 声明。 可以通过将 IdentityServer4 与 ASP.NET Core Identity 结合使用来实现 OpenID Connect 服务器。 用户使用 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 页面中根据需要使用此策略。 也可以全局性地为整个应用添加策略。

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

如果用户不使用 MFA 进行身份验证,则 amr 声明可能具有 pwd 值。 不会授权该请求访问页面。 如果使用默认值,用户将被重定向到“帐户/拒绝访问”页。 可以更改此行为,也可以在此处实现自己的自定义逻辑。 本示例中添加了一个链接,以便有效的用户可以为其帐户设置 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 登录(例如,只使用密码):

  • amr 具有 pwd 值:

    amr 具有 pwd 值

  • 拒绝访问:

    访问被拒绝

或者,使用 OTP 通过 Identity 进行登录:

使用 OTP 通过 Identity 进行登录

其他资源