ASP.NET Core 中的多重身份验证Multi-factor authentication in ASP.NET Core

本文内容

作者: Damien Bowden

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

本文涵盖以下几个方面:

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

MFA,2FAMFA, 2FA

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

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

MFA TOTP (基于时间的一次性密码算法)MFA TOTP (Time-based One-time Password Algorithm)

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

  • Microsoft Authenticator 应用
  • Google 验证器应用

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

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

MFA FIDO2 或无密码MFA FIDO2 or passwordless

FIDO2 目前:

  • 实现 MFA 的最安全方法。
  • 唯一防止仿冒攻击的 MFA 流。

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

Azure Active Directory 提供对 FIDO2 和无密码流的支持。有关详细信息,请参阅无密码 authentication options for Azure Active Directory

MFA 短信MFA SMS

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

NIST 指导原则

使用 ASP.NET Core 为管理页配置 MFA IdentityConfigure MFA for administration pages using ASP.NET Core Identity

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

使用 MFA 声明扩展登录名Extend the login with an MFA claim

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

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddDbContext<ApplicationDbContext>(options =>
  4. options.UseSqlite(
  5. Configuration.GetConnectionString("DefaultConnection")));
  6. services.AddIdentity<IdentityUser, IdentityRole>(
  7. options => options.SignIn.RequireConfirmedAccount = false)
  8. .AddEntityFrameworkStores<ApplicationDbContext>()
  9. .AddDefaultTokenProviders();
  10. services.AddSingleton<IEmailSender, EmailSender>();
  11. services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>,
  12. AdditionalUserClaimsPrincipalFactory>();
  13. services.AddAuthorization(options =>
  14. options.AddPolicy("TwoFactorEnabled",
  15. x => x.RequireClaim("amr", "mfa")));
  16. services.AddRazorPages();
  17. }

仅在成功登录后,AdditionalUserClaimsPrincipalFactory 类才将 amr 声明添加到用户声明。将从数据库中读取声明的值。此处添加了声明,因为如果该标识已使用 MFA 登录,则该用户只应访问受保护的视图。如果直接从数据库中读取数据库视图而不是使用声明,则在激活 MFA 后,可以直接访问该视图,而无需进行 MFA。

  1. using Microsoft.AspNetCore.Identity;
  2. using Microsoft.Extensions.Options;
  3. using System.Collections.Generic;
  4. using System.Security.Claims;
  5. using System.Threading.Tasks;
  6. namespace IdentityStandaloneMfa
  7. {
  8. public class AdditionalUserClaimsPrincipalFactory :
  9. UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
  10. {
  11. public AdditionalUserClaimsPrincipalFactory(
  12. UserManager<IdentityUser> userManager,
  13. RoleManager<IdentityRole> roleManager,
  14. IOptions<IdentityOptions> optionsAccessor)
  15. : base(userManager, roleManager, optionsAccessor)
  16. {
  17. }
  18. public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
  19. {
  20. var principal = await base.CreateAsync(user);
  21. var identity = (ClaimsIdentity)principal.Identity;
  22. var claims = new List<Claim>();
  23. if (user.TwoFactorEnabled)
  24. {
  25. claims.Add(new Claim("amr", "mfa"));
  26. }
  27. else
  28. {
  29. claims.Add(new Claim("amr", "pwd"));
  30. }
  31. identity.AddClaims(claims);
  32. return principal;
  33. }
  34. }
  35. }

由于 Identity 服务设置在 Startup 类中发生了更改,因此需要更新 Identity 的布局。将 Identity 页基架到应用程序中。Identity/Account/Manage/_Layout的文件中定义布局。

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

同时为 "Identity" 页中的所有 "管理" 页指定布局:

  1. @{
  2. Layout = "_Layout.cshtml";
  3. }

在管理页中验证 MFA 要求Validate the MFA requirement in the administration page

"管理" Razor 页面验证用户是否已使用 MFA 登录。OnGet 方法中,标识用于访问用户声明。检查 amr 声明的值 mfa如果标识缺少此声明或 false,则页面将重定向到 "启用 MFA" 页。这是可能的,因为用户已登录,但没有 MFA。

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. using Microsoft.AspNetCore.Mvc;
  6. using Microsoft.AspNetCore.Mvc.RazorPages;
  7. namespace IdentityStandaloneMfa
  8. {
  9. public class AdminModel : PageModel
  10. {
  11. public IActionResult OnGet()
  12. {
  13. var claimTwoFactorEnabled =
  14. User.Claims.FirstOrDefault(t => t.Type == "amr");
  15. if (claimTwoFactorEnabled != null &&
  16. "mfa".Equals(claimTwoFactorEnabled.Value))
  17. {
  18. // You logged in with MFA, do the administrative stuff
  19. }
  20. else
  21. {
  22. return Redirect(
  23. "/Identity/Account/Manage/TwoFactorAuthentication");
  24. }
  25. return Page();
  26. }
  27. }
  28. }

用于切换用户登录信息的 UI 逻辑UI logic to toggle user login information

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

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

然后,可以在 _Layout 视图中使用此策略来显示或隐藏带有警告的 "管理" 菜单:

  1. @using Microsoft.AspNetCore.Authorization
  2. @using Microsoft.AspNetCore.Identity
  3. @inject SignInManager<IdentityUser> SignInManager
  4. @inject UserManager<IdentityUser> UserManager
  5. @inject IAuthorizationService AuthorizationService

如果标识已使用 MFA 登录,则会显示 "管理" 菜单而不显示工具提示警告。如果用户已登录而没有 MFA,则会显示 "管理员(未启用) " 菜单以及通知用户的工具提示(说明警告)。

  1. @if (SignInManager.IsSignedIn(User))
  2. {
  3. @if ((AuthorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
  4. {
  5. <li class="nav-item">
  6. <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
  7. </li>
  8. }
  9. else
  10. {
  11. <li class="nav-item">
  12. <a class="nav-link text-dark" asp-area="" asp-page="/Admin"
  13. id="tooltip-demo"
  14. data-toggle="tooltip"
  15. data-placement="bottom"
  16. title="MFA is NOT enabled. This is required for the Admin Page. If you have activated MFA, then logout, login again.">
  17. Admin (Not Enabled)
  18. </a>
  19. </li>
  20. }
  21. }

如果用户在没有 MFA 的情况下登录,将显示警告:

管理员 MFA 身份验证

单击 "管理" 链接时,用户将重定向到 "MFA 启用" 视图:

管理员激活 MFA 身份验证

将 MFA 登录要求发送到 OpenID Connect 服务器Send MFA sign-in requirement to OpenID Connect server

acr_values 参数可用于将客户端的 mfa 必需值传递到身份验证请求中的服务器。

备注

需要在 Open ID Connect 服务器上处理 acr_values 参数,此操作才有效。

OpenID Connect ASP.NET Core 客户端OpenID Connect ASP.NET Core client

ASP.NET Core Razor Pages Open ID Connect 客户端应用程序使用 AddOpenIdConnect 方法登录到 Open ID Connect 服务器。acr_values 参数设置为 mfa 值,并随身份验证请求一起发送。OpenIdConnectEvents 用于添加此。

有关建议 acr_values 参数值,请参阅身份验证方法引用值

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddAuthentication(options =>
  4. {
  5. options.DefaultScheme =
  6. CookieAuthenticationDefaults.AuthenticationScheme;
  7. options.DefaultChallengeScheme =
  8. OpenIdConnectDefaults.AuthenticationScheme;
  9. })
  10. .AddCookie()
  11. .AddOpenIdConnect(options =>
  12. {
  13. options.SignInScheme =
  14. CookieAuthenticationDefaults.AuthenticationScheme;
  15. options.Authority = "<OpenID Connect server URL>";
  16. options.RequireHttpsMetadata = true;
  17. options.ClientId = "<OpenID Connect client ID>";
  18. options.ClientSecret = "<>";
  19. // Code with PKCE can also be used here
  20. options.ResponseType = "code id_token";
  21. options.Scope.Add("profile");
  22. options.Scope.Add("offline_access");
  23. options.SaveTokens = true;
  24. options.Events = new OpenIdConnectEvents
  25. {
  26. OnRedirectToIdentityProvider = context =>
  27. {
  28. context.ProtocolMessage.SetParameter("acr_values", "mfa");
  29. return Task.FromResult(0);
  30. }
  31. };
  32. });

示例 OpenID Connect IdentityServer 4 服务器与 ASP.NET Core IdentityExample OpenID Connect IdentityServer 4 server with ASP.NET Core Identity

在使用 ASP.NET Core Identity 和 MVC 视图实现的 OpenID Connect 服务器上,将创建一个名为ErrorEnable2FA的新视图。视图:

  • 显示 Identity 是否来自需要 MFA 但用户未在 Identity中激活此应用程序的应用程序。
  • 通知用户并添加一个用于激活此的链接。
  1. @{
  2. ViewData["Title"] = "ErrorEnable2FA";
  3. }
  4. <h1>The client application requires you to have MFA enabled. Enable this, try login again.</h1>
  5. <br />
  6. You can enable MFA to login here:
  7. <br />
  8. <a asp-controller="Manage" asp-action="TwoFactorAuthentication">Enable MFA</a>

Login 方法中,IIdentityServerInteractionService 接口实现 _interaction 用于访问 Open ID Connect 请求参数。使用 AcrValues 属性访问 acr_values 参数。当客户端发送此 mfa 集时,可以检查此情况。

如果需要 MFA,并且 ASP.NET Core Identity 中的用户启用了 MFA,则登录将继续。如果用户未启用 MFA,则会将用户重定向到自定义视图ErrorEnable2FA然后 ASP.NET Core Identity 对用户进行签名。

  1. //
  2. // POST: /Account/Login
  3. [HttpPost]
  4. [AllowAnonymous]
  5. [ValidateAntiForgeryToken]
  6. public async Task<IActionResult> Login(LoginInputModel model)
  7. {
  8. var returnUrl = model.ReturnUrl;
  9. var context =
  10. await _interaction.GetAuthorizationContextAsync(returnUrl);
  11. var requires2Fa =
  12. context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;
  13. var user = await _userManager.FindByNameAsync(model.Email);
  14. if (user != null && !user.TwoFactorEnabled && requires2Fa)
  15. {
  16. return RedirectToAction(nameof(ErrorEnable2FA));
  17. }
  18. // code omitted for brevity

ExternalLoginCallback 方法的工作方式类似于本地 Identity 登录名。检查 mfa 值的 AcrValues 属性。如果 mfa 值存在,则会在登录完成之前强制执行 MFA (例如,重定向到 ErrorEnable2FA 视图)。

  1. //
  2. // GET: /Account/ExternalLoginCallback
  3. [HttpGet]
  4. [AllowAnonymous]
  5. public async Task<IActionResult> ExternalLoginCallback(
  6. string returnUrl = null,
  7. string remoteError = null)
  8. {
  9. var context =
  10. await _interaction.GetAuthorizationContextAsync(returnUrl);
  11. var requires2Fa =
  12. context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;
  13. if (remoteError != null)
  14. {
  15. ModelState.AddModelError(
  16. string.Empty,
  17. _sharedLocalizer["EXTERNAL_PROVIDER_ERROR",
  18. remoteError]);
  19. return View(nameof(Login));
  20. }
  21. var info = await _signInManager.GetExternalLoginInfoAsync();
  22. if (info == null)
  23. {
  24. return RedirectToAction(nameof(Login));
  25. }
  26. var email = info.Principal.FindFirstValue(ClaimTypes.Email);
  27. if (!string.IsNullOrEmpty(email))
  28. {
  29. var user = await _userManager.FindByNameAsync(email);
  30. if (user != null && !user.TwoFactorEnabled && requires2Fa)
  31. {
  32. return RedirectToAction(nameof(ErrorEnable2FA));
  33. }
  34. }
  35. // Sign in the user with this external login provider if the user already has a login.
  36. var result = await _signInManager
  37. .ExternalLoginSignInAsync(
  38. info.LoginProvider,
  39. info.ProviderKey,
  40. isPersistent:
  41. false);
  42. // code omitted for brevity

如果用户已登录,则客户端应用:

  • 仍验证 amr 声明。
  • 可以使用指向 ASP.NET Core Identity 视图的链接来设置 MFA。

acr_values-1

强制 ASP.NET Core OpenID Connect 客户端要求 MFAForce ASP.NET Core OpenID Connect client to require MFA

此示例演示如何使用 OpenID Connect 登录的 ASP.NET Core Razor 页面应用程序可能要求用户使用 MFA 进行身份验证。

为了验证 MFA 要求,将创建一个 IAuthorizationRequirement 要求。这将使用需要 MFA 的策略添加到页面中。

  1. using Microsoft.AspNetCore.Authorization;
  2. namespace AspNetCoreRequireMfaOidc
  3. {
  4. public class RequireMfa : IAuthorizationRequirement{}
  5. }

实现的 AuthorizationHandler 将使用 amr 声明并检查值 mfaamr 在身份验证成功的 id_token 中返回,并且可以有许多不同的值,如身份验证方法引用值规范中所定义。

返回的值取决于身份如何进行身份验证以及打开 ID 连接服务器实现。

AuthorizationHandler 使用 RequireMfa 要求并验证 amr 声明。可以使用 IdentityServer4 和 ASP.NET Core Identity来实现 OpenID Connect 服务器。当用户使用 TOTP 登录时,将使用 MFA 值返回 amr 声明。如果使用不同的 OpenID Connect 服务器实现或不同的 MFA 类型,则 amr 声明将具有不同的值,也可以具有不同的值。要接受此代码,还必须对代码进行扩展。

  1. using Microsoft.AspNetCore.Authorization;
  2. using System;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. namespace AspNetCoreRequireMfaOidc
  6. {
  7. public class RequireMfaHandler : AuthorizationHandler<RequireMfa>
  8. {
  9. protected override Task HandleRequirementAsync(
  10. AuthorizationHandlerContext context,
  11. RequireMfa requirement)
  12. {
  13. if (context == null)
  14. throw new ArgumentNullException(nameof(context));
  15. if (requirement == null)
  16. throw new ArgumentNullException(nameof(requirement));
  17. var amrClaim =
  18. context.User.Claims.FirstOrDefault(t => t.Type == "amr");
  19. if (amrClaim != null && amrClaim.Value == Amr.Mfa)
  20. {
  21. context.Succeed(requirement);
  22. }
  23. return Task.CompletedTask;
  24. }
  25. }
  26. }

Startup.ConfigureServices 方法中,AddOpenIdConnect 方法用作默认质询方案。用于检查 amr 声明的授权处理程序将添加到控制容器的反转。然后,将创建一个策略来添加 RequireMfa 要求。

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.ConfigureApplicationCookie(options =>
  4. options.Cookie.SecurePolicy =
  5. CookieSecurePolicy.Always);
  6. services.AddSingleton<IAuthorizationHandler, RequireMfaHandler>();
  7. services.AddAuthentication(options =>
  8. {
  9. options.DefaultScheme =
  10. CookieAuthenticationDefaults.AuthenticationScheme;
  11. options.DefaultChallengeScheme =
  12. OpenIdConnectDefaults.AuthenticationScheme;
  13. })
  14. .AddCookie()
  15. .AddOpenIdConnect(options =>
  16. {
  17. options.SignInScheme =
  18. CookieAuthenticationDefaults.AuthenticationScheme;
  19. options.Authority = "https://localhost:44352";
  20. options.RequireHttpsMetadata = true;
  21. options.ClientId = "AspNetCoreRequireMfaOidc";
  22. options.ClientSecret = "AspNetCoreRequireMfaOidcSecret";
  23. options.ResponseType = "code id_token";
  24. options.Scope.Add("profile");
  25. options.Scope.Add("offline_access");
  26. options.SaveTokens = true;
  27. });
  28. services.AddAuthorization(options =>
  29. {
  30. options.AddPolicy("RequireMfa", policyIsAdminRequirement =>
  31. {
  32. policyIsAdminRequirement.Requirements.Add(new RequireMfa());
  33. });
  34. });
  35. services.AddRazorPages();
  36. }

然后,将在 Razor 页面中根据需要使用此策略。也可以全局为整个应用程序添加策略。

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. using Microsoft.AspNetCore.Authorization;
  6. using Microsoft.AspNetCore.Mvc;
  7. using Microsoft.AspNetCore.Mvc.RazorPages;
  8. using Microsoft.Extensions.Logging;
  9. namespace AspNetCoreRequireMfaOidc.Pages
  10. {
  11. [Authorize(Policy= "RequireMfa")]
  12. public class IndexModel : PageModel
  13. {
  14. private readonly ILogger<IndexModel> _logger;
  15. public IndexModel(ILogger<IndexModel> logger)
  16. {
  17. _logger = logger;
  18. }
  19. public void OnGet()
  20. {
  21. }
  22. }
  23. }

如果用户在没有 MFA 的情况下进行身份验证,则 amr 声明可能会有一个 pwd 值。请求不会被授权访问此页。如果使用默认值,则用户将被重定向到Account/AccessDenied页。此行为可以更改,也可以在此处实现自己的自定义逻辑。在此示例中,添加了一个链接,以便有效的用户可以为其帐户设置 MFA。

  1. @page
  2. @model AspNetCoreRequireMfaOidc.AccessDeniedModel
  3. @{
  4. ViewData["Title"] = "AccessDenied";
  5. Layout = "~/Pages/Shared/_Layout.cshtml";
  6. }
  7. <h1>AccessDenied</h1>
  8. You require MFA to login here
  9. <a href="https://localhost:44352/Manage/TwoFactorAuthentication">Enable MFA</a>

现在只有通过 MFA 进行身份验证的用户才能访问该页面或网站。如果使用不同的 MFA 类型,或者如果2FA 为正常,则 amr 声明将具有不同的值,并且需要正确处理。不同的打开 ID 连接服务器也会为此声明返回不同的值,并且可能不遵循身份验证方法引用值规范。

在没有 MFA 的情况下登录时(例如只使用密码):

  • amr 具有 pwd 值:

require_mfa_oidc_02 .png

  • 拒绝访问:

require_mfa_oidc_03 .png

或者,使用 OTP Identity登录:

require_mfa_oidc_01 .png

其他资源Additional resources