ASP.NET Core での多要素認証

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

作成者: Damien Bowden

サンプル コードを表示またはダウンロードする (damienbod/AspNetCoreHybridFlowWithApi GitHub リポジトリ)

多要素認証 (MFA) は、追加の形式の身元証明のためのサインイン イベント中にユーザーに求められるプロセスです。 携帯電話からのコードの入力、FIDO2 キーの使用、あるいは指紋スキャンの提供を求められる場合があります。 2 つ目の形式の認証を要求すれば、セキュリティが強化されます。 追加の要素は、攻撃者によって簡単に取得されたり複製されたりすることはありません。

この記事では、次の領域について説明します。

  • MFA とは何か、および推奨される MFA フロー
  • ASP.NET Core Identity を使用して管理ページの MFA を構成する
  • OpenID Connect サーバーに MFA サインイン要件を送信する
  • ASP.NET Core OpenID Connect クライアントで MFA を要求するように強制する

MFA、2FA

MFA では、ユーザーの認証を行うために、知っていること、所有しているもの、生体認証など、少なくとも 2 種類以上の身元証明が必要です。

2 要素認証 (2FA) は MFA のサブセットのようなものですが、MFA では身元を証明するために 2 つ以上の要素を要求することができる点が異なります。

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 のサポートを無効にするには、AddDefaultIdentity ではなく AddIdentity を使って認証を構成します。 内部的には 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 SMS

SMS を使用した MFA では、パスワード認証 (単一要素) と比較してセキュリティが大幅に向上します。 しかし、第 2 要素として SMS を使用することは推奨されなくなりました。 この種の実装の場合、存在する既知の攻撃ベクトルが多すぎます。

NIST のガイドライン

ASP.NET Core Identity を使用して管理ページの MFA を構成する

MFA は、ASP.NET Core Identity アプリ内の機密性の高いページにアクセスするユーザーに対して強制できます。 これは、異なる ID に対して異なるレベルのアクセスが存在するアプリに役立つ場合があります。 たとえば、ユーザーはパスワード ログインを使用してプロファイル データを表示できる場合がありますが、管理者は MFA を使用して管理ページにアクセスする必要があります。

MFA 要求を使用してログインを拡張する

デモ コードは、Identity および Razor ページで ASP.NET Core を使用してセットアップされています。 AddIdentity メソッドが AddDefaultIdentity の代わりに使用されています。そのため、IUserClaimsPrincipalFactory 実装を使用して、ログインが成功した後に ID に要求を追加できます。

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

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

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

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

builder.Services.AddRazorPages();

AdditionalUserClaimsPrincipalFactory クラスでは、ログインが成功した後にのみ、ユーザー要求に amr 要求を追加します。 要求の値はデータベースから読み取られます。 ユーザーは、ID で MFA を使用してログインしている場合にのみ、より高く保護されたビューにアクセスする必要があるため、要求がここに追加されます。 データベース ビューが、要求を使用する代わりにデータベースから直接読み取られた場合は、MFA をアクティブ化した後で直接 MFA なしでビューにアクセスできます。

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

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

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

            var claims = new List<Claim>();

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

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

Startup クラスで Identity サービスのセットアップが変更されたため、Identity のレイアウトを更新する必要があります。 Identity ページをアプリにスキャフォールディングします。 Identity/Account/Manage/_Layout.cshtml ファイルでレイアウトを定義します。

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

また、Identity ページのすべての管理ページのレイアウトを割り当てます。

@{
    Layout = "_Layout.cshtml";
}

管理ページで MFA 要件を検証する

管理 Razor ページでは、ユーザーが MFA を使用してログインしたことが検証されます。 OnGet メソッドでは、ユーザー要求にアクセスするために ID が使用されます。 amr 要求は、mfa 値に対して確認されます。 ID にこの要求がないか、false の場合、ページは [MFA の有効化] ページにリダイレクトされます。 これは、ユーザーが既にログインしているものの、MFA を使用していないために発生する可能性があります。

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

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

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

            return Page();
        }
    }
}

ユーザー ログイン情報を切り替えるための UI ロジック

起動時に承認ポリシーが追加されました。 そのポリシーでは、値が amrmfa 要求が求められます。

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

その後、このポリシーを _Layout ビューで使用して、次の警告が示された [管理者] メニューを表示または非表示にすることができます。

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

ID で MFA を使用してログインしている場合は、ツールヒントの警告なしで [管理者] メニューが表示されます。 ユーザーが MFA を使用せずにログインした場合、[Admin (Not Enabled)]\(管理者 (無効)\) メニューは、ユーザーに知らせるヒント (警告の説明) と共に表示されます。

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

ユーザーが MFA を使用せずにログインすると、次の警告が表示されます。

管理者 MFA 認証

[管理者] リンクをクリックすると、ユーザーは MFA の有効化ビューにリダイレクトされます。

管理者が MFA 認証をアクティブ化する

OpenID Connect サーバーに MFA サインイン要件を送信する

acr_values パラメーターを使用して、認証要求でクライアントからサーバーに必要な mfa 値を渡すことができます。

メモ

これを機能させるには、OpenID Connect サーバーで acr_values パラメーターを処理する必要があります。

OpenID Connect ASP.NET Core クライアント

ASP.NET Core Razor ページの OpenID Connect クライアントでは、AddOpenIdConnect メソッドを使用して OpenID Connec サーバーにログインします。 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 サーバーの例

Razor Pages で ASP.NET Core Identity を使用して実装される OpenID Connect サーバーでは、ErrorEnable2FA.cshtml という名前の新しいページが作成されます。 そのビューは次のようになります。

  • Identity は MFA を必要とするアプリからのものであるものの、ユーザーが Identity でこれをアクティブ化していない場合に表示される。
  • ユーザーに通知し、これをアクティブ化するためのリンクを追加する。
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

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

<br />

You can enable MFA to login here:

<br />

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

Login メソッドでは、OpenID Connect 要求パラメーターにアクセスするために IIdentityServerInteractionService インターフェイスの実装 _interaction が使用されます。 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 ページ アプリで、MFA を使用してユーザーを認証することをどのように要求できるかを示します。

MFA 要件を検証するために、IAuthorizationRequirement 要件が作成されます。 これは、MFA を要求するポリシーを使用してページに追加されます。

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc;

public class RequireMfa : IAuthorizationRequirement{}

amr 要求を使用し、値 mfa を確認する AuthorizationHandler が実装されています。 amr は正常な認証の id_token で返され、認証方法の参照値の仕様で定義されているさまざまな値を持つ場合があります。

返される値は、ID の認証方法と OpenID Connect サーバーの実装によって異なります。

AuthorizationHandler では RequireMfa 要件を使用して、amr 要求を検証します。 OpenID Connect サーバーは、ASP.NET Core Identity で Duende Identity Server を使用して実装できます。 ユーザーが TOTP を使用してログインすると、amr 要求は MFA 値と共に返されます。 異なる 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 で認証されるユーザーのみが、ページまたは Web サイトにアクセスできるようになりました。 異なる種類の MFA が使用されている場合、または 2FA が正常な場合は、amr 要求に異なる値が設定されるようになり、正しく処理する必要があります。 また、OpenID Connect サーバーが異なると、この要求に対して異なる値が返され、認証方法の参照値の仕様に従っていない可能性があります。

MFA を使用せずにログインする場合 (たとえば、パスワードのみを使用する場合):

  • amr の値は pwd となります。

    amr に pwd 値がある

  • アクセスは拒否されます。

    アクセスが拒否されました

または、Identity で OTP を使用してログインします。

Identity で OTP を使用するログイン

その他の技術情報

作成者: Damien Bowden

サンプル コードを表示またはダウンロードする (damienbod/AspNetCoreHybridFlowWithApi GitHub リポジトリ)

多要素認証 (MFA) は、追加の形式の身元証明のためのサインイン イベント中にユーザーに求められるプロセスです。 携帯電話からのコードの入力、FIDO2 キーの使用、あるいは指紋スキャンの提供を求められる場合があります。 2 つ目の形式の認証を要求すれば、セキュリティが強化されます。 追加の要素は、攻撃者によって簡単に取得されたり複製されたりすることはありません。

この記事では、次の領域について説明します。

  • MFA とは何か、および推奨される MFA フロー
  • ASP.NET Core Identity を使用して管理ページの MFA を構成する
  • OpenID Connect サーバーに MFA サインイン要件を送信する
  • ASP.NET Core OpenID Connect クライアントで MFA を要求するように強制する

MFA、2FA

MFA では、ユーザーの認証を行うために、知っていること、所有しているもの、生体認証など、少なくとも 2 種類以上の身元証明が必要です。

2 要素認証 (2FA) は MFA のサブセットのようなものですが、MFA では身元を証明するために 2 つ以上の要素を要求することができる点が異なります。

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 のサポートを無効にするには、AddDefaultIdentity ではなく AddIdentity を使って認証を構成します。 内部的には 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 SMS

SMS を使用した MFA では、パスワード認証 (単一要素) と比較してセキュリティが大幅に向上します。 しかし、第 2 要素として SMS を使用することは推奨されなくなりました。 この種の実装の場合、存在する既知の攻撃ベクトルが多すぎます。

NIST のガイドライン

ASP.NET Core Identity を使用して管理ページの MFA を構成する

MFA は、ASP.NET Core Identity アプリ内の機密性の高いページにアクセスするユーザーに対して強制できます。 これは、異なる ID に対して異なるレベルのアクセスが存在するアプリに役立つ場合があります。 たとえば、ユーザーはパスワード ログインを使用してプロファイル データを表示できる場合がありますが、管理者は MFA を使用して管理ページにアクセスする必要があります。

MFA 要求を使用してログインを拡張する

デモ コードは、Identity および Razor ページで ASP.NET Core を使用してセットアップされています。 AddIdentity メソッドが AddDefaultIdentity の代わりに使用されています。そのため、IUserClaimsPrincipalFactory 実装を使用して、ログインが成功した後に ID に要求を追加できます。

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

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

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

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

builder.Services.AddRazorPages();

AdditionalUserClaimsPrincipalFactory クラスでは、ログインが成功した後にのみ、ユーザー要求に amr 要求を追加します。 要求の値はデータベースから読み取られます。 ユーザーは、ID で MFA を使用してログインしている場合にのみ、より高く保護されたビューにアクセスする必要があるため、要求がここに追加されます。 データベース ビューが、要求を使用する代わりにデータベースから直接読み取られた場合は、MFA をアクティブ化した後で直接 MFA なしでビューにアクセスできます。

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

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

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

            var claims = new List<Claim>();

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

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

Startup クラスで Identity サービスのセットアップが変更されたため、Identity のレイアウトを更新する必要があります。 Identity ページをアプリにスキャフォールディングします。 Identity/Account/Manage/_Layout.cshtml ファイルでレイアウトを定義します。

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

また、Identity ページのすべての管理ページのレイアウトを割り当てます。

@{
    Layout = "_Layout.cshtml";
}

管理ページで MFA 要件を検証する

管理 Razor ページでは、ユーザーが MFA を使用してログインしたことが検証されます。 OnGet メソッドでは、ユーザー要求にアクセスするために ID が使用されます。 amr 要求は、mfa 値に対して確認されます。 ID にこの要求がないか、false の場合、ページは [MFA の有効化] ページにリダイレクトされます。 これは、ユーザーが既にログインしているものの、MFA を使用していないために発生する可能性があります。

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

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

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

            return Page();
        }
    }
}

ユーザー ログイン情報を切り替えるための UI ロジック

起動時に承認ポリシーが追加されました。 そのポリシーでは、値が amrmfa 要求が求められます。

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

その後、このポリシーを _Layout ビューで使用して、次の警告が示された [管理者] メニューを表示または非表示にすることができます。

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

ID で MFA を使用してログインしている場合は、ツールヒントの警告なしで [管理者] メニューが表示されます。 ユーザーが MFA を使用せずにログインした場合、[Admin (Not Enabled)]\(管理者 (無効)\) メニューは、ユーザーに知らせるヒント (警告の説明) と共に表示されます。

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

ユーザーが MFA を使用せずにログインすると、次の警告が表示されます。

管理者 MFA 認証

[管理者] リンクをクリックすると、ユーザーは MFA の有効化ビューにリダイレクトされます。

管理者が MFA 認証をアクティブ化する

OpenID Connect サーバーに MFA サインイン要件を送信する

acr_values パラメーターを使用して、認証要求でクライアントからサーバーに必要な mfa 値を渡すことができます。

メモ

これを機能させるには、OpenID Connect サーバーで acr_values パラメーターを処理する必要があります。

OpenID Connect ASP.NET Core クライアント

ASP.NET Core Razor ページの OpenID Connect クライアントでは、AddOpenIdConnect メソッドを使用して OpenID Connec サーバーにログインします。 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 サーバーの例

Razor Pages で ASP.NET Core Identity を使用して実装される OpenID Connect サーバーでは、ErrorEnable2FA.cshtml という名前の新しいページが作成されます。 そのビューは次のようになります。

  • Identity は MFA を必要とするアプリからのものであるものの、ユーザーが Identity でこれをアクティブ化していない場合に表示される。
  • ユーザーに通知し、これをアクティブ化するためのリンクを追加する。
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

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

<br />

You can enable MFA to login here:

<br />

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

Login メソッドでは、OpenID Connect 要求パラメーターにアクセスするために IIdentityServerInteractionService インターフェイスの実装 _interaction が使用されます。 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 ページ アプリで、MFA を使用してユーザーを認証することをどのように要求できるかを示します。

MFA 要件を検証するために、IAuthorizationRequirement 要件が作成されます。 これは、MFA を要求するポリシーを使用してページに追加されます。

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc;

public class RequireMfa : IAuthorizationRequirement{}

amr 要求を使用し、値 mfa を確認する AuthorizationHandler が実装されています。 amr は正常な認証の id_token で返され、認証方法の参照値の仕様で定義されているさまざまな値を持つ場合があります。

返される値は、ID の認証方法と OpenID Connect サーバーの実装によって異なります。

AuthorizationHandler では RequireMfa 要件を使用して、amr 要求を検証します。 OpenID Connect サーバーは、ASP.NET Core Identity で Duende Identity Server を使用して実装できます。 ユーザーが TOTP を使用してログインすると、amr 要求は MFA 値と共に返されます。 異なる 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 で認証されるユーザーのみが、ページまたは Web サイトにアクセスできるようになりました。 異なる種類の MFA が使用されている場合、または 2FA が正常な場合は、amr 要求に異なる値が設定されるようになり、正しく処理する必要があります。 また、OpenID Connect サーバーが異なると、この要求に対して異なる値が返され、認証方法の参照値の仕様に従っていない可能性があります。

MFA を使用せずにログインする場合 (たとえば、パスワードのみを使用する場合):

  • amr の値は pwd となります。

    amr に pwd 値がある

  • アクセスは拒否されます。

    アクセスが拒否されました

または、Identity で OTP を使用してログインします。

Identity で OTP を使用するログイン

その他の技術情報

作成者: Damien Bowden

サンプル コードを表示またはダウンロードする (damienbod/AspNetCoreHybridFlowWithApi GitHub リポジトリ)

多要素認証 (MFA) は、追加の形式の身元証明のためのサインイン イベント中にユーザーに求められるプロセスです。 携帯電話からのコードの入力、FIDO2 キーの使用、あるいは指紋スキャンの提供を求められる場合があります。 2 つ目の形式の認証を要求すれば、セキュリティが強化されます。 追加の要素は、攻撃者によって簡単に取得されたり複製されたりすることはありません。

この記事では、次の領域について説明します。

  • MFA とは何か、および推奨される MFA フロー
  • ASP.NET Core Identity を使用して管理ページの MFA を構成する
  • OpenID Connect サーバーに MFA サインイン要件を送信する
  • ASP.NET Core OpenID Connect クライアントで MFA を要求するように強制する

MFA、2FA

MFA では、ユーザーの認証を行うために、知っていること、所有しているもの、生体認証など、少なくとも 2 種類以上の身元証明が必要です。

2 要素認証 (2FA) は MFA のサブセットのようなものですが、MFA では身元を証明するために 2 つ以上の要素を要求することができる点が異なります。

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 SMS

SMS を使用した MFA では、パスワード認証 (単一要素) と比較してセキュリティが大幅に向上します。 しかし、第 2 要素として SMS を使用することは推奨されなくなりました。 この種の実装の場合、存在する既知の攻撃ベクトルが多すぎます。

NIST のガイドライン

ASP.NET Core Identity を使用して管理ページの MFA を構成する

MFA は、ASP.NET Core Identity アプリ内の機密性の高いページにアクセスするユーザーに対して強制できます。 これは、異なる ID に対して異なるレベルのアクセスが存在するアプリに役立つ場合があります。 たとえば、ユーザーはパスワード ログインを使用してプロファイル データを表示できる場合がありますが、管理者は MFA を使用して管理ページにアクセスする必要があります。

MFA 要求を使用してログインを拡張する

デモ コードは、Identity および Razor ページで ASP.NET Core を使用してセットアップされています。 AddIdentity メソッドが AddDefaultIdentity の代わりに使用されています。そのため、IUserClaimsPrincipalFactory 実装を使用して、ログインが成功した後に ID に要求を追加できます。

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

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

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

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

    services.AddRazorPages();
}

AdditionalUserClaimsPrincipalFactory クラスでは、ログインが成功した後にのみ、ユーザー要求に amr 要求を追加します。 要求の値はデータベースから読み取られます。 ユーザーは、ID で MFA を使用してログインしている場合にのみ、より高く保護されたビューにアクセスする必要があるため、要求がここに追加されます。 データベース ビューが、要求を使用する代わりにデータベースから直接読み取られた場合は、MFA をアクティブ化した後で直接 MFA なしでビューにアクセスできます。

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

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

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

            var claims = new List<Claim>();

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

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

Startup クラスで Identity サービスのセットアップが変更されたため、Identity のレイアウトを更新する必要があります。 Identity ページをアプリにスキャフォールディングします。 Identity/Account/Manage/_Layout.cshtml ファイルでレイアウトを定義します。

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

また、Identity ページのすべての管理ページのレイアウトを割り当てます。

@{
    Layout = "_Layout.cshtml";
}

管理ページで MFA 要件を検証する

管理 Razor ページでは、ユーザーが MFA を使用してログインしたことが検証されます。 OnGet メソッドでは、ユーザー要求にアクセスするために ID が使用されます。 amr 要求は、mfa 値に対して確認されます。 ID にこの要求がないか、false の場合、ページは [MFA の有効化] ページにリダイレクトされます。 これは、ユーザーが既にログインしているものの、MFA を使用していないために発生する可能性があります。

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

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

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

            return Page();
        }
    }
}

ユーザー ログイン情報を切り替えるための UI ロジック

認可ポリシーがプログラム ファイルに追加されました。 そのポリシーでは、値が amrmfa 要求が求められます。

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

その後、このポリシーを _Layout ビューで使用して、次の警告が示された [管理者] メニューを表示または非表示にすることができます。

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

ID で MFA を使用してログインしている場合は、ツールヒントの警告なしで [管理者] メニューが表示されます。 ユーザーが MFA を使用せずにログインした場合、[Admin (Not Enabled)]\(管理者 (無効)\) メニューは、ユーザーに知らせるヒント (警告の説明) と共に表示されます。

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

ユーザーが MFA を使用せずにログインすると、次の警告が表示されます。

管理者 MFA 認証

[管理者] リンクをクリックすると、ユーザーは MFA の有効化ビューにリダイレクトされます。

管理者が MFA 認証をアクティブ化する

OpenID Connect サーバーに MFA サインイン要件を送信する

acr_values パラメーターを使用して、認証要求でクライアントからサーバーに必要な mfa 値を渡すことができます。

メモ

これを機能させるには、OpenID Connect サーバーで acr_values パラメーターを処理する必要があります。

OpenID Connect ASP.NET Core クライアント

ASP.NET Core Razor ページの OpenID Connect クライアントでは、AddOpenIdConnect メソッドを使用して OpenID Connec サーバーにログインします。 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 サーバーの例

MVC ビューで ASP.NET Core Identity を使用して実装される OpenID Connect サーバーでは、ErrorEnable2FA.cshtml という名前の新しいビューが作成されます。 そのビューは次のようになります。

  • Identity は MFA を必要とするアプリからのものであるものの、ユーザーが Identity でこれをアクティブ化していない場合に表示される。
  • ユーザーに通知し、これをアクティブ化するためのリンクを追加する。
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

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

<br />

You can enable MFA to login here:

<br />

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

Login メソッドでは、OpenID Connect 要求パラメーターにアクセスするために IIdentityServerInteractionService インターフェイスの実装 _interaction が使用されます。 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 ページ アプリで、MFA を使用してユーザーを認証することをどのように要求できるかを示します。

MFA 要件を検証するために、IAuthorizationRequirement 要件が作成されます。 これは、MFA を要求するポリシーを使用してページに追加されます。

using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc
{
    public class RequireMfa : IAuthorizationRequirement{}
}

amr 要求を使用し、値 mfa を確認する AuthorizationHandler が実装されています。 amr は正常な認証の id_token で返され、認証方法の参照値の仕様で定義されているさまざまな値を持つ場合があります。

返される値は、ID の認証方法と OpenID Connect サーバーの実装によって異なります。

AuthorizationHandler では RequireMfa 要件を使用して、amr 要求を検証します。 OpenID Connect サーバーは、ASP.NET Core Identity で IdentityServer4 を使用して実装できます。 ユーザーが TOTP を使用してログインすると、amr 要求は MFA 値と共に返されます。 異なる 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 で認証されるユーザーのみが、ページまたは Web サイトにアクセスできるようになりました。 異なる種類の MFA が使用されている場合、または 2FA が正常な場合は、amr 要求に異なる値が設定されるようになり、正しく処理する必要があります。 また、OpenID Connect サーバーが異なると、この要求に対して異なる値が返され、認証方法の参照値の仕様に従っていない可能性があります。

MFA を使用せずにログインする場合 (たとえば、パスワードのみを使用する場合):

  • amr の値は pwd となります。

    amr に pwd 値がある

  • アクセスは拒否されます。

    アクセスが拒否されました

または、Identity で OTP を使用してログインします。

Identity で OTP を使用するログイン

その他の技術情報