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 驗證器
  • Google Authenticator

如需實作詳細資料,請參閱在 ASP.NET Core 中為 TOTP 驗證器應用程式啟用 QR 代碼產生

若要停用對 MFA TOTP 的支援,請使用 AddIdentity 來設定身份驗證,而不是 AddDefaultIdentityAddDefaultIdentity 在內部呼叫 AddDefaultTokenProviders,以註冊多個權杖提供者,包括一個用於 MFA TOTP 的權杖提供者。 若要只註冊特定的權杖提供者,請針對每個必要提供者呼叫 AddTokenProvider。 如需可用權杖提供者的詳細資訊,請參閱 GitHub 上的 AddDefaultTokenProviders 來源

MFA 通行金鑰/FIDO2 或無密碼

通行金鑰/FIDO2 目前為:

  • 達成 MFA 的最安全方式。
  • 防止網路釣魚攻擊的 MFA。 (以及憑證驗證和企業版 Windows)

目前,ASP.NET Core 不能直接支援通行金鑰/FIDO2。 通行金鑰/FIDO2 可用於 MFA 或無密碼流程。

Microsoft Entra ID 支援通行金鑰/FIDO2 和無密碼流程。 如需詳細資訊,請參閱無密碼驗證選項

其他形式的無密碼 MFA 不一定能夠防範網路釣魚。

MFA SMS

相較於密碼驗證 (單一要素),採用 SMS 的 MFA 會大幅提高安全性。 不過,不再建議使用 SMS 作為第二個要素。 這種類型的實作存在太多已知的攻擊媒介。

NIST 指導方針

使用 ASP.NET Core Identity 設定管理頁面的 MFA

在存取 ASP.NET Core Identity 應用程式內的敏感性頁面時,可以強制使用者採用 MFA。 對於不同身分識別存在不同存取層級的應用程式而言,這種做法很有用。 例如,使用者可以使用密碼登入來檢視設定檔資料,但系統管理員必須使用 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;
        }
    }
}

因為 Startup 類別中的 Identity 服務設定已變更,因此必須更新 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 邏輯

啟動時已新增授權原則。 此原則需要值為 mfaamr 宣告。

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 的情況下登入,則會顯示如下警告:

管理員 istrator MFA 驗證

按一下 [系統管理] 連結時,使用者會重新導向至 MFA 啟用檢視表:

管理員 istrator 會啟用 MFA 驗證

將 MFA 登入需求傳送至 OpenID Connect 伺服器

可使用 acr_values 參數將 mfa 所需值從用戶端傳遞至驗證要求中包含的伺服器。

注意

acr_values 參數必須在 OpenID Connect 伺服器上處理才能運作。

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 在中啟用 MFA,則會顯示此頁面。
  • 通知使用者並新增連結以啟用 MFA。
@{
    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 要求參數。 acr_values 參數可使用 AcrValues 屬性進行存取。 當用戶端在設置了 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{}

實作 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 值。 要求將不會獲得存取頁面的授權。 使用預設值,使用者將會重新導向至 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 的情況下登入時 (例如,只使用密碼):

  • 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 驗證器
  • Google Authenticator

如需實作詳細資料,請參閱在 ASP.NET Core 中為 TOTP 驗證器應用程式啟用 QR 代碼產生

若要停用對 MFA TOTP 的支援,請使用 AddIdentity 來設定身份驗證,而不是 AddDefaultIdentityAddDefaultIdentity 在內部呼叫 AddDefaultTokenProviders,以註冊多個權杖提供者,包括一個用於 MFA TOTP 的權杖提供者。 若要只註冊特定的權杖提供者,請針對每個必要提供者呼叫 AddTokenProvider。 如需可用權杖提供者的詳細資訊,請參閱 GitHub 上的 AddDefaultTokenProviders 來源

MFA 通行金鑰/FIDO2 或無密碼

通行金鑰/FIDO2 目前為:

  • 達成 MFA 的最安全方式。
  • 防止網路釣魚攻擊的 MFA。 (以及憑證驗證和企業版 Windows)

目前,ASP.NET Core 不能直接支援通行金鑰/FIDO2。 通行金鑰/FIDO2 可用於 MFA 或無密碼流程。

Microsoft Entra ID 支援通行金鑰/FIDO2 和無密碼流程。 如需詳細資訊,請參閱無密碼驗證選項

其他形式的無密碼 MFA 不一定能夠防範網路釣魚。

MFA SMS

相較於密碼驗證 (單一要素),採用 SMS 的 MFA 會大幅提高安全性。 不過,不再建議使用 SMS 作為第二個要素。 這種類型的實作存在太多已知的攻擊媒介。

NIST 指導方針

使用 ASP.NET Core Identity 設定管理頁面的 MFA

在存取 ASP.NET Core Identity 應用程式內的敏感性頁面時,可以強制使用者採用 MFA。 對於不同身分識別存在不同存取層級的應用程式而言,這種做法很有用。 例如,使用者可以使用密碼登入來檢視設定檔資料,但系統管理員必須使用 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;
        }
    }
}

因為 Startup 類別中的 Identity 服務設定已變更,因此必須更新 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 邏輯

啟動時已新增授權原則。 此原則需要值為 mfaamr 宣告。

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 的情況下登入,則會顯示如下警告:

管理員 istrator MFA 驗證

按一下 [系統管理] 連結時,使用者會重新導向至 MFA 啟用檢視表:

管理員 istrator 會啟用 MFA 驗證

將 MFA 登入需求傳送至 OpenID Connect 伺服器

可使用 acr_values 參數將 mfa 所需值從用戶端傳遞至驗證要求中包含的伺服器。

注意

acr_values 參數必須在 OpenID Connect 伺服器上處理才能運作。

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 在中啟用 MFA,則會顯示此頁面。
  • 通知使用者並新增連結以啟用 MFA。
@{
    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 要求參數。 acr_values 參數可使用 AcrValues 屬性進行存取。 當用戶端在設置了 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{}

實作 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 值。 要求將不會獲得存取頁面的授權。 使用預設值,使用者將會重新導向至 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 的情況下登入時 (例如,只使用密碼):

  • 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 (以時間為基礎的單次密碼演算法)

使用 ASP.NET Core Identity 時,支持使用 TOTP 進行 MFA 的實作方式。 這可與任何符合規範的驗證器應用程式搭配使用,包括:

  • 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 SMS

相較於密碼驗證 (單一要素),採用 SMS 的 MFA 會大幅提高安全性。 不過,不再建議使用 SMS 作為第二個要素。 這種類型的實作存在太多已知的攻擊媒介。

NIST 指導方針

使用 ASP.NET Core Identity 設定管理頁面的 MFA

在存取 ASP.NET Core Identity 應用程式內的敏感性頁面時,可以強制使用者採用 MFA。 對於不同身分識別存在不同存取層級的應用程式而言,這種做法很有用。 例如,使用者可以使用密碼登入來檢視設定檔資料,但系統管理員必須使用 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;
        }
    }
}

因為 Startup 類別中的 Identity 服務設定已變更,因此必須更新 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 邏輯

已在應用程式檔案中新增授權原則。 此原則需要值為 mfaamr 宣告。

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 的情況下登入,則會顯示如下警告:

管理員 istrator MFA 驗證

按一下 [系統管理] 連結時,使用者會重新導向至 MFA 啟用檢視表:

管理員 istrator 會啟用 MFA 驗證

將 MFA 登入需求傳送至 OpenID Connect 伺服器

可使用 acr_values 參數將 mfa 所需值從用戶端傳遞至驗證要求中包含的伺服器。

注意

acr_values 參數必須在 OpenID Connect 伺服器上處理才能運作。

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);
            }
        };
    });

Identity Server 4 伺服器與 ASP.NET Core Identity 搭配使用的 OpenID Connect 伺服器

透過 ASP.NET Core Identity 與 MVC 檢視表的搭配使用而實作的 OpenID Connect 伺服器上,將會建立一個名為 ErrorEnable2FA.cshtml 的新檢視表。 檢視:

  • 如果 Identity 來自一個要求 MFA 的應用程式,但使用者沒有 Identity 在中啟用 MFA,則會顯示此頁面。
  • 通知使用者並新增連結以啟用 MFA。
@{
    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 要求參數。 acr_values 參數可使用 AcrValues 屬性進行存取。 當用戶端在設置了 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{}
}

實作 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 值。 要求將不會獲得存取頁面的授權。 使用預設值,使用者將會重新導向至 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 的情況下登入時 (例如,只使用密碼):

  • amr 具有值 pwd

    amr 具有 pwd 值

  • 存取遭到拒絕:

    存取遭拒

或者,使用 OTP 搭配 Identity 登入:

使用 OTP 搭配登入 Identity

其他資源