ASP.NET Identity で SMS と電子メールを利用して 2 要素認証を行う

著者: Hao KungPranav RastogiRick AndersonSuhas Joshi

このチュートリアルでは、SMS とメールを使用して 2 要素認証 (2FA) を設定する方法について説明します。

この記事の著者は Rick Anderson (@RickAndMSFT)、Pranav Rastogi (@rustd)、Hao Kung、Suhas Joshi です。 NuGet サンプルは、主に Hao Kung によって記述されました。

このトピックでは、以下の内容を説明します。

Identity サンプルのビルド

このセクションでは、NuGet を使用して、これから先で扱うサンプルをダウンロードします。 まず、Visual Studio Express 2013 for Web または Visual Studio 2013 をインストールして実行します。 Visual Studio 2013 Update 2 以上をインストールします。

Note

警告: このチュートリアルを完了するには、Visual Studio 2013 Update 2 をインストールする必要があります。

  1. 新しい空の ASP.NET Web プロジェクトを作成します。

  2. パッケージ マネージャー コンソールで、以下のコマンドを入力します。

    Install-Package SendGrid
    Install-Package -Prerelease Microsoft.AspNet.Identity.Samples

    このチュートリアルでは、メール送信には SendGrid を、SMS テキスト送信には Twilio または ASPSMS を使用します。 Identity.Samples パッケージは、使用するコードをインストールします。

  3. SSL を使用するようにプロジェクトを設定します。

  4. "省略可能": 「メール確認チュートリアル」の指示に従って SendGrid をフックした後、アプリを実行してメール アカウントを登録します。

  5. "省略可能:" サンプルからデモのメール リンク確認コードを削除します (アカウント コントローラー内の ViewBag.Link コードです。DisplayEmailForgotPasswordConfirmation の各アクション メソッドと Razor ビューを参照してください)。

  6. "省略可能:" Manage および Account コントローラー、そして Views\Account\VerifyCode.cshtml および Views\Manage\VerifyPhoneNumber.cshtml razor ビューから ViewBag.Status コードを削除します。 代わりに、フックを行わずにこのアプリがどのように動作しているかをローカルでテストしてメールと SMS メッセージを送信するために ViewBag.Status 表示を残しても構いません。

Note

警告: このサンプルのいずれかのセキュリティ設定を変更した場合、運用アプリは、加えられた変更を明示的に呼び出すセキュリティ監査を受ける必要があります。

2 要素認証用に SMS を設定する

このチュートリアルでは、Twilio または ASPSMS のいずれかを使用する手順について説明しますが、他の SMS プロバイダーを使用することもできます。

  1. SMS プロバイダーを使用したユーザー アカウントの作成

    Twilio アカウントまたは ASPSMS アカウントを作成します。

  2. 追加パッケージのインストールまたはサービス参照の追加

    Twilio:
    パッケージ マネージャー コンソールで、次のコマンドを入力します。
    Install-Package Twilio

    ASPSMS:
    次のサービス参照を追加する必要があります。

    Image of add service reference window

    [アドレス]:
    https://webservice.aspsms.com/aspsmsx2.asmx?WSDL

    名前空間:
    ASPSMSX2

  3. SMS プロバイダーのユーザーの資格情報を確認する

    Twilio:
    Twilio アカウントの [ダッシュボード] タブの [Account SID]\(アカウント SID\)[Auth token]\(セキュリティ トークン\) をコピーします。

    ASPSMS:
    アカウント設定から [Userkey] に移動し、それを自分で設定したパスワードと共にコピーします。

    これらの値は後で変数 SMSAccountIdentificationSMSAccountPassword 内に保存します。

  4. SenderID または Originator の指定

    Twilio:
    [Numbers]\(数値\) タブで、Twilio の電話番号をコピーします。

    ASPSMS:
    [Unlock Originators]\(発信元のロック解除\) メニューで、1 つまたは複数の発信元のロックを解除するか、英数字の発信元を選択します (すべてのネットワークではサポートされていません)。

    この値は後で変数 SMSAccountFrom 内に保存します。

  5. SMS プロバイダーの資格情報をアプリへ転送

    以下のようにアプリが資格情報と送信者の電話番号を利用できるようにします。

    public static class Keys
    {
       public static string SMSAccountIdentification = "My Idenfitication";
       public static string SMSAccountPassword = "My Password";
       public static string SMSAccountFrom = "+15555551234";
    }
    

    警告

    セキュリティ - 機密データをソース コード内に保存しないでください。 アカウントと資格情報を上記のコードに追加したのは、サンプルをわかりやすくするためです。 Jon Atten による「ASP.NET MVC: プライベート設定をソース管理に含めない」を参照してください。

  6. SMS プロバイダーへのデータ転送の実装

    App_Start\IdentityConfig.cs ファイルで SmsService クラスを構成します。

    使用する SMS プロバイダーに応じて、Twilio または ASPSMS セクションをアクティブにします。

    public class SmsService : IIdentityMessageService
    {
        public Task SendAsync(IdentityMessage message)
        {
            // Twilio Begin
            // var Twilio = new TwilioRestClient(
            //   Keys.SMSAccountIdentification,
            //   Keys.SMSAccountPassword);
            // var result = Twilio.SendMessage(
            //   Keys.SMSAccountFrom,
            //   message.Destination, message.Body
            // );
            // Status is one of Queued, Sending, Sent, Failed or null if the number is not valid
            // Trace.TraceInformation(result.Status);
            // Twilio doesn't currently have an async API, so return success.
            // return Task.FromResult(0);
            // Twilio End
    
            // ASPSMS Begin 
            // var soapSms = new WebApplication1.ASPSMSX2.ASPSMSX2SoapClient("ASPSMSX2Soap");
            // soapSms.SendSimpleTextSMS(
            //   Keys.SMSAccountIdentification,
            //   Keys.SMSAccountPassword,
            //   message.Destination,
            //   Keys.SMSAccountFrom,
            //   message.Body);
            // soapSms.Close();
            // return Task.FromResult(0);
            // ASPSMS End
        }
    }
    
  7. アプリを実行し、以前に登録したアカウントでログインします。

  8. ユーザー ID をクリックすると、Manage コントローラーで Index アクション メソッドがアクティブになります。

    Image of registered account logged into the app

  9. 追加をクリックします。

    Image of add phone number link

  10. 電話番号を入力すると数秒後に、確認コードを含むテキスト メッセージが送信されます。 確認コードを入力し、[送信] を押します。

    Image showing phone verification code entry

  11. 管理ビューには、電話番号が追加されたと表示されます。

    Image of manage view window showing phone number

コードを確認する

// GET: /Account/Index
public async Task<ActionResult> Index(ManageMessageId? message)
{
    ViewBag.StatusMessage =
        message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed."
        : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set."
        : message == ManageMessageId.SetTwoFactorSuccess ? "Your two factor provider has been set."
        : message == ManageMessageId.Error ? "An error has occurred."
        : message == ManageMessageId.AddPhoneSuccess ? "The phone number was added."
        : message == ManageMessageId.RemovePhoneSuccess ? "Your phone number was removed."
        : "";

    var model = new IndexViewModel
    {
        HasPassword = HasPassword(),
        PhoneNumber = await UserManager.GetPhoneNumberAsync(User.Identity.GetUserId()),
        TwoFactor = await UserManager.GetTwoFactorEnabledAsync(User.Identity.GetUserId()),
        Logins = await UserManager.GetLoginsAsync(User.Identity.GetUserId()),
        BrowserRemembered = await AuthenticationManager.TwoFactorBrowserRememberedAsync(User.Identity.GetUserId())
    };
    return View(model);
}

Manage コントローラーの Index アクション メソッドは、前回のアクションに基づいて状態メッセージを設定し、ローカル パスワードを変更したり、ローカル アカウントを追加したりするためのリンクを提供します。 Index メソッドは、2FA 電話番号、外部ログイン、2FA の有効化、および "このブラウザーで 2FA を記憶する" (これについては後で説明します) の状態も表示します。 タイトル バーでユーザー ID (メール) をクリックしても、メッセージは渡されません。 [電話番号 : 削除] リンクをクリックすると、Message=RemovePhoneSuccess がクエリ文字列として渡されます。

https://localhost:44300/Manage?Message=RemovePhoneSuccess

[Image of phone number removed]

AddPhoneNumber アクション メソッドは、SMS メッセージを受信できる電話番号を入力するダイアログ ボックスを表示します。

// GET: /Account/AddPhoneNumber
public ActionResult AddPhoneNumber()
{
   return View();
}

Image of add phone number action dialog box

[確認コードの送信] ボタンをクリックすると、電話番号が HTTP POST AddPhoneNumber アクション メソッドにポストされます。

// POST: /Account/AddPhoneNumber
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> AddPhoneNumber(AddPhoneNumberViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    // Generate the token 
    var code = await UserManager.GenerateChangePhoneNumberTokenAsync(
                               User.Identity.GetUserId(), model.Number);
    if (UserManager.SmsService != null)
    {
        var message = new IdentityMessage
        {
            Destination = model.Number,
            Body = "Your security code is: " + code
        };
        // Send token
        await UserManager.SmsService.SendAsync(message);
    }
    return RedirectToAction("VerifyPhoneNumber", new { PhoneNumber = model.Number });
}

GenerateChangePhoneNumberTokenAsync メソッドは、SMS メッセージ内に設定されることになるセキュリティ トークンを生成します。 SMS サービスが構成済みである場合、トークンは "Your security code is <token>" という文字列として送信されます。 SmsService.SendAsync メソッドが非同期的に呼び出された後、アプリは (次のダイアログを表示する) VerifyPhoneNumber アクション メソッドにリダイレクトされ、そこでユーザーは確認コードを入力することができます。

Image of verify phone number action method dialog box

コードを入力して送信をクリックすると、コードは HTTP POST VerifyPhoneNumber アクション メソッドにポストされます。

// POST: /Account/VerifyPhoneNumber
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> VerifyPhoneNumber(VerifyPhoneNumberViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    var result = await UserManager.ChangePhoneNumberAsync(User.Identity.GetUserId(), model.PhoneNumber, model.Code);
    if (result.Succeeded)
    {
        var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
        if (user != null)
        {
            await SignInAsync(user, isPersistent: false);
        }
        return RedirectToAction("Index", new { Message = ManageMessageId.AddPhoneSuccess });
    }
    // If we got this far, something failed, redisplay form
    ModelState.AddModelError("", "Failed to verify phone");
    return View(model);
}

ChangePhoneNumberAsync メソッドは、ポストされたセキュリティ コードをチェックします。 コードが正しい場合は、その電話番号が AspNetUsers テーブルの PhoneNumber フィールドに追加されます。 その呼び出しが成功すると、以下の SignInAsync メソッドが呼び出されます。

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
   // Clear the temporary cookies used for external and two factor sign ins
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie, 
       DefaultAuthenticationTypes.TwoFactorCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties
    {
       IsPersistent = isPersistent 
    }, 
       await user.GenerateUserIdentityAsync(UserManager));
}

isPersistent パラメーターは、認証セッションが複数の要求にわたって保持されるかどうかを設定します。

セキュリティ プロファイルを変更すると、新しいセキュリティ スタンプが生成され、AspNetUsers テーブルの SecurityStamp フィールドに格納されます。 SecurityStamp フィールドはセキュリティ Cookie と異なることに注意してください。 セキュリティ Cookie は、AspNetUsers テーブル (または Identity DB 内の他の場所) には格納されません。 セキュリティ Cookie トークンは DPAPI を使用して自己署名され、UserId, SecurityStamp と有効期限情報と共に作成されます。

Cookie ミドルウェアは、各要求で Cookie をチェックします。 Startup クラス内の SecurityStampValidator メソッドは DB にヒットし、セキュリティ スタンプを validateInterval に指定されたとおりに定期的にチェックします。 これは、セキュリティ プロファイルを変更しない限り、(このサンプルでは) 30 分ごとに行われます。 この 30 分間隔は、データベースとの往復を最小限に抑えるために選択されました。

セキュリティ プロファイルに何らかの変更が加えられた場合は、SignInAsync メソッドを呼び出す必要があります。 セキュリティ プロファイルが変更されると、データベースは SecurityStamp フィールドを更新するため、SignInAsync メソッドを呼び出さずにログインしたままでいられるのは、次に OWIN パイプラインがデータベースに到達するまで (つまり validateInterval の間) "だけ" です。 以下のように、すぐに return するように SignInAsync メソッドを変更し、Cookie の validateInterval プロパティを 30 分から 5 秒に設定することで、これをテストすることができます。

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
   return;

   // Clear any partial cookies from external or two factor partial sign ins
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie, 
       DefaultAuthenticationTypes.TwoFactorCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties
    {
       IsPersistent = isPersistent 
    }, 
       await user.GenerateUserIdentityAsync(UserManager));
}
public void ConfigureAuth(IAppBuilder app) {
    // Configure the db context, user manager and role manager to use a single instance per request
    app.CreatePerOwinContext(ApplicationDbContext.Create);
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
    app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
    app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

    // Enable the application to use a cookie to store information for the signed in user
    // and to use a cookie to temporarily store information about a user logging in with a 
    // third party login provider
    // Configure the sign in cookie
    app.UseCookieAuthentication(new CookieAuthenticationOptions {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider {
            // Enables the application to validate the security stamp when the user logs in.
            // This is a security feature which is used when you change a password or add 
            // an external login to your account.  
            OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                //validateInterval: TimeSpan.FromMinutes(30),
                validateInterval: TimeSpan.FromSeconds(5),
                regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
        }
    });
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

上記のようにコードを変更した上で、(たとえば、[2 要素の有効化] の状態を変更することで) セキュリティ プロファイルを変更すると、SecurityStampValidator.OnValidateIdentity メソッドが失敗してから 5 秒後にログアウトされることになります。 SignInAsync メソッド内の return 行を削除すると、別のセキュリティ プロファイルの変更を行っても、ログアウトはされなくなります。SignInAsync メソッドは、新しいセキュリティ Cookie を生成します。

2 要素認証を有効化する

サンプル アプリでは、UI を使用して 2 要素認証 (2FA) を有効にする必要があります。 2FA を有効にするには、ナビゲーション バーでユーザー ID (メール エイリアス) をクリックします。Image of U I to enable two-factor authentication
[2FA を有効にする] をクリックします。Image after clicking user I D showing two-factor authentication enable link ログアウトしてから、ログインし直します。 メールを有効にしている場合 (前回のチュートリアルを参照)、2FA に使用する SMS またはメールを選択できます。Image displaying verification send options (SMS またはメールからの) コードを入力できる [コードの確認] ページが表示されます。Image of verify code page[このブラウザーを記憶する] チェック ボックスをクリックすることで、そのコンピューターとブラウザーでログオンするために 2FA を使用する必要がなくなります。 2FA を有効にし、[このブラウザーを記憶する] をクリックすると、あなたのアカウントにアクセスしようとしている悪意のあるユーザーからの 2FA による強力な保護が提供されます (これは、そのユーザーがあなたのコンピューターへのアクセス権を持っていない場合に限ります)。 これは、定期的に使用するすべてのプライベート マシンで実行できます。 [このブラウザーを記憶する] を設定することで、普段は使用しないコンピューターに 2FA というセキュリティを追加すると同時に、自身のコンピューターで 2FA を行う必要がないという利便性も達成できます。

2 要素認証プロバイダーを登録する方法

新しい MVC プロジェクトを作成する際には、IdentityConfig.cs ファイルには、2 要素認証プロバイダーを登録するための次のコードが含まれることになります。

public static ApplicationUserManager Create(
   IdentityFactoryOptions<ApplicationUserManager> options, 
   IOwinContext context) 
{
    var manager = new ApplicationUserManager(
       new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
    // Configure validation logic for usernames
    manager.UserValidator = new UserValidator<ApplicationUser>(manager)
    {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true
    };
    // Configure validation logic for passwords
    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 6,
        RequireNonLetterOrDigit = true,
        RequireDigit = true,
        RequireLowercase = true,
        RequireUppercase = true,
    };
    // Register two factor authentication providers. This application uses Phone and Emails as a 
    // step of receiving a code for verifying the user
    // You can write your own provider and plug it in here.
    manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser>
    {
        MessageFormat = "Your security code is: {0}"
    });
    manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser>
    {
        Subject = "Security Code",
        BodyFormat = "Your security code is: {0}"
    });
    manager.EmailService = new EmailService();
    manager.SmsService = new SmsService();

    var dataProtectionProvider = options.DataProtectionProvider;
    if (dataProtectionProvider != null)
    {
        manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>
           (dataProtectionProvider.Create("ASP.NET Identity"));
    }
    return manager;
}

2FA 用の電話番号を追加する

Manage コントローラー内の AddPhoneNumber アクション メソッドは、セキュリティ トークンを生成し、それをユーザーが指定した電話番号に送信します。

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> AddPhoneNumber(AddPhoneNumberViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    // Generate the token and send it
    var code = await UserManager.GenerateChangePhoneNumberTokenAsync(
       User.Identity.GetUserId(), model.Number);
    if (UserManager.SmsService != null)
    {
        var message = new IdentityMessage
        {
            Destination = model.Number,
            Body = "Your security code is: " + code
        };
        await UserManager.SmsService.SendAsync(message);
    }
    return RedirectToAction("VerifyPhoneNumber", new { PhoneNumber = model.Number });
}

トークンの送信後には、これは VerifyPhoneNumber メソッドへのリダイレクトを行い、そこではコードを入力することで 2FA 用の SMS を登録できます。 SMS 2FA は、電話番号の確認が完了するまで使用されません。

2FA の有効化

EnableTFA アクション メソッドは、以下のように 2FA を有効にします。

// POST: /Manage/EnableTFA
[HttpPost]
public async Task<ActionResult> EnableTFA()
{
    await UserManager.SetTwoFactorEnabledAsync(User.Identity.GetUserId(), true);
    var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
    if (user != null)
    {
        await SignInAsync(user, isPersistent: false);
    }
    return RedirectToAction("Index", "Manage");
}

2FA の有効化はセキュリティ プロファイルに対する変更であるため、SignInAsync を呼び出す必要があることに注意してください。 2FA が有効にされると、ユーザーは 2FA を使用してログインしなくてはならなくなり、自分が登録した 2FA の手法 (サンプルでは SMS とメール) を使用することになります。

QR コード ジェネレーターなどの 2FA プロバイダーをさらに追加することも、独自のプロバイダーを記述することもできます。

Note

2FA コードは時間ベースのワンタイム パスワード アルゴリズムを使用して生成され、そのコードは 6 分間有効です。 コードの入力に 6 分以上かかった場合は、無効なコード エラー メッセージが表示されます。

ソーシャルとローカルのログイン アカウントを結合する

メールのリンクをクリックして、ローカル アカウントとソーシャル アカウントを組み合わせることができます。 以下のシーケンスでは、"RickAndMSFT@gmail.com" は最初からローカル ログインとして作成されますが、まずはアカウントをソーシャル ログインとして作成してから、ローカル ログインを追加することもできます。

Image selecting email link

[管理] リンクをクリックします。 このアカウントに関連付けられている外部 (ソーシャル ログイン) が 0 であることに注意してください。

Image displaying next page and selecting manage

別のログイン サービスへのリンクを選択し、アプリの要求を受け入れます。 2 つのアカウントが組み合わされているため、どちらのアカウントでもログオンできます。 ソーシャル ログイン認証サービスがダウンしたときのため、またはさらに可能性が高いのはソーシャル アカウントにアクセスできなくなったときのために、ユーザーにローカル アカウントを追加させたいことがあります。

次の図では、Tom はソーシャル ログインです (ページに表示されている [外部ログイン: 1] から確認できます)。

Image showing external logins and location of pick a password

[パスワードの選択] を選択すると、同じアカウントに関連付けられているローカル ログオンを追加できます。

Image of pick a password page

ブルート フォース攻撃からのアカウント ロックアウト

ユーザー ロックアウトを有効にすることで、アプリ上のアカウントを辞書攻撃から保護できます。 ApplicationUserManager Create メソッド内の次のコードは、ロックアウトを構成します。

// Configure user lockout defaults
manager.UserLockoutEnabledByDefault = true;
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
manager.MaxFailedAccessAttemptsBeforeLockout = 5;

上記のコードは、2 要素認証に対してのみロックアウトを有効にします。 アカウント コントローラーの Login メソッド内で shouldLockout を true に変更することで、ログインに対してロックアウトを有効にできますが、それによってアカウントが DOS ログイン攻撃を受けやすくなるので、ログインに対してはロックアウトを有効にしないことをお勧めします。 サンプル コードでは、以下のように ApplicationDbInitializer Seed メソッド内で作成された管理者アカウントに対してロックアウトを無効にしています。

public static void InitializeIdentityForEF(ApplicationDbContext db)
{
    var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
    var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();
    const string name = "admin@example.com";
    const string roleName = "Admin";

    //Create Role Admin if it does not exist
    var role = roleManager.FindByName(roleName);
    if (role == null)
    {
        role = new IdentityRole(roleName);
        var roleresult = roleManager.Create(role);
    }

    var user = userManager.FindByName(name);
    if (user == null)
    {
        user = new ApplicationUser { UserName = name, Email = name };
        var result = userManager.Create(user, GetSecurePassword());
        result = userManager.SetLockoutEnabled(user.Id, false);
    }

    // Add user admin to Role Admin if not already added
    var rolesForUser = userManager.GetRoles(user.Id);
    if (!rolesForUser.Contains(role.Name))
    {
        var result = userManager.AddToRole(user.Id, role.Name);
    }
}

検証済みのメール アカウントを作成することをユーザーに要求する

次のコードは、ログインの前に検証済みのメール アカウントを作成することをユーザーに要求しています。

public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

   // Require the user to have a confirmed email before they can log on.
    var user = await UserManager.FindByNameAsync(model.Email);
    if (user != null)
    {
       if (!await UserManager.IsEmailConfirmedAsync(user.Id))
       {
          ViewBag.errorMessage = "You must have a confirmed email to log on.";
          return View("Error");
       }         
    }
    // This doen't count login failures towards lockout only two factor authentication
    // To enable password failures to trigger lockout, change to shouldLockout: true
    var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, 
       model.RememberMe, shouldLockout: false);
    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}

SignInManager が 2FA 要件を確認する方法

ローカル ログインとソーシャル ログインは両方とも、2FA が有効になっているかどうかを確認するためのチェックを行います。 2FA が有効になっている場合、SignInManager ログオン メソッドは SignInStatus.RequiresVerification を返し、ユーザーは SendCode アクション メソッドにリダイレクトされます。そこでユーザーはログイン シーケンスを完了するためにコードを入力しなくてはならなくなります。 ユーザーがユーザー ローカル Cookie に RememberMe を設定している場合、SignInManagerSignInStatus.Success を返すため、ユーザーは 2FA を実行する必要がなくなります。

public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

   // Require the user to have a confirmed email before they can log on.
    var user = await UserManager.FindByNameAsync(model.Email);
    if (user != null)
    {
       if (!await UserManager.IsEmailConfirmedAsync(user.Id))
       {
          ViewBag.errorMessage = "You must have a confirmed email to log on.";
          return View("Error");
       }         
    }
    // This doen't count login failures towards lockout only two factor authentication
    // To enable password failures to trigger lockout, change to shouldLockout: true
    var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, 
       model.RememberMe, shouldLockout: false);
    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (loginInfo == null)
    {
        return RedirectToAction("Login");
    }

    // Sign in the user with this external login provider if the user already has a login
    var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
        case SignInStatus.Failure:
        default:
            // If the user does not have an account, then prompt the user to create an account
            ViewBag.ReturnUrl = returnUrl;
            ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
            return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
    }
}

次のコードは SendCode アクション メソッドを示しています。 SelectListItem は、ユーザーに対して有効になっているすべての 2FA メソッドを持つように作成されます。 この SelectListItemDropDownListFor ヘルパーに渡されることで、ユーザーは 2FA の手法 (通常はメールと SMS) を選択できるようになります。

public async Task<ActionResult> SendCode(string returnUrl)
{
    var userId = await SignInManager.GetVerifiedUserIdAsync();
    if (userId == null)
    {
        return View("Error");
    }
    var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(userId);
    var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList();
    return View(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl });
}

ユーザーが 2FA の手法をポストすると、HTTP POST SendCode アクション メソッドが呼び出され、SignInManager が 2FA コードを送信し、ユーザーは VerifyCode アクション メソッドにリダイレクトされ、そこでコードを入力してログインを完了できます。

//
// POST: /Account/SendCode
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> SendCode(SendCodeViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View();
    }

    // Generate the token and send it
    if (!await SignInManager.SendTwoFactorCodeAsync(model.SelectedProvider))
    {
        return View("Error");
    }
    return RedirectToAction("VerifyCode", new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl });
}

2FA ロックアウト

ログイン パスワードの試行エラーにアカウント ロックアウトを設定することはできますが、この方法では、ログインが DOS ロックアウトの影響を受けやすくなります。 アカウント ロックアウトは 2FA でのみ使用することをお勧めします。 ApplicationUserManager が作成されると、サンプル コードは 2FA ロックアウトを設定し、MaxFailedAccessAttemptsBeforeLockout を 5 に設定します。 ユーザーが (ローカル アカウントまたはソーシャル アカウントを通して) ログインを行うと、2FA での失敗した試行がすべて保存され、最大試行回数に達した場合、ユーザーは 5 分間ロックアウトされます (ロックアウト時間は DefaultAccountLockoutTimeSpan で設定できます)。

その他のリソース