ASP.NET Identity : 二要素認証 (2FA) の実装


環境 : Visual Studio 2013 Update 2 および Update 3

ASP.NET Identity に関する補足

こんにちは。

月曜に Azure を使用した Twilio ハンズオンが実施されましたが、いまや電話 API は、モバイル開発や認証など、クラウドで利用されるベース技術の 1 つとなっています。次回は 3 時間コースとして、後半は Remote Debug など Azure を活用したプログラミングの話なども計画中ですので、今後も、是非 ご期待ください !

さて、そんな電話 API の使い方の 1 つとして、今回は、すっかり更新をすっぽかしてしまっていた、ASP.NET Identity と Twilio API を組み合わせた二要素認証 (Two-Factor Authentication, 2FA) のプログラミングを紹介したいと思います。(すみません、前回投稿して以来、長い間 放置していました。。。)
かなり昔に Azure Activre Directory における Multi-Factor Authentication (多要素認証) を紹介しましたが、ASP.NET Identity 2.0 以降では、ASP.NET の認証機構自体に二要素認証を組み込むためのメソッドやクラスが提供されています。

なお、実装の概念を紹介する前に「二要素認証」について簡単に紹介すると、クラウドやモビリティー環境が充実した昨今では、みなさん、当たり前のように、電車の中で SNS を見たり、メールを確認したりしていることでしょう。そして、もちろん お勧めはしないものの、電車の中でパスワードの入力などが必要になるシーンもあるでしょう。
セキュリティ強化のために、使用できるパスワード文字列や組み合わせを制限したり、定期的にパスワード変更をさせたり、場合によっては複数回のパスワード入力をさせるなどの多くの対策が思いつくかもしれませんが、ショルダー ハックに代表されるように、結局、「ばれてしまえば、皆、同じ」、文字列を使ったセキュリティ強化をおこなう以上は、この手の強化をしても管理が煩雑になるばかりで、"情報" そのものの特性から逃げることはできません。
しかし、今回紹介する二要素認証 (多要素認証) は、悪意のあるユーザーから見れば、例えば、「デバイスそのものを盗む」などの手段が必要です。つまり、文字列の搾取だけではユーザーに成り代わることはできません。「認証方法が 1 つ増えただけ」ですが、上述のような強化方法とは次元が異なっていることがお分かりいただけるでしょう。

ここでは、スマートフォン (電話) を使った二要素認証を紹介しますが、前回解説した E-Mail との組み合わせや、指紋認証との組み合わせなども、こうした Multi-Factor Authentication (多要素認証) の 1 つです。

 

プログラミングの考え方 (概要)

全体の流れや考え方は、前回解説した E-Mail Confirmation に似ています。
例えば、ASP.NET Identity では、Microsoft.AspNet.Identity.UserManager に二要素認証のための処理が実装されており、実際の電話 API による SMS 送信などは、開発者自身でこのフレームワークに組み込みます。そして、このメソッドを使った UI 構築などは、このフレームワークが提供するクラスやメソッドを使って、開発者自身が組み立てていきます。(後述しますが、Visual Studio 2013 Update 3 以降のプロジェクト テンプレートでは、二要素認証を使った UI 実装などが容易になっています。)

ただし、E-Mail Confirmation のプログラミングと異なる点もあります。
例えば、二要素認証で発行する Code (Token) や状態は一時的なもの (次回ログイン時には、また新しく発行される揮発的なもの) ですので、Cookie 設定など、E-Mail Confirmation のときにはなかった操作もいくつか必要です。

 

準備

まず、Twilio API を使って SMS 送信をおこなうためのセットアップをします。

あらかじめ、Twilio のアカウントを取得しておいてください。

そして、Visual Studio 2013 Update 2 以上を使って、今回は ASP.NET MVC のプロジェクトを新規作成します。
この際、認証方法として個人ユーザー アカウント (Individual User Accounts) を選択します (下図)。今回は、この個人ユーザー アカウントを使ったログインの処理で、二要素認証を使用します。

プロジェクトが作成されたら、NuGet から Twilio のパッケージをインストールしておきましょう。(下図)

Visual Studio 2013 Update 2 の ASP.NET のプロジェクト テンプレート (ASP.NET Identity 2.0 使用) では、既に、二要素認証 (Two-Factor Authentication) のためのコードがサンプルとして記述されています。
App_Start/IdentityConfig.cs を見ると、下記の通り記述されています。

. . .
public class ApplicationUserManager : UserManager<ApplicationUser>
{
  . . .

  public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
  {
    var manager = new ApplicationUserManager(
      new UserStore<ApplicationUser>(
        context.Get<ApplicationDbContext>()));
    . . .

    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();
    . . .

    return manager;
  }
}

public class EmailService : IIdentityMessageService
{
  public Task SendAsync(IdentityMessage message)
  {
    // Plug in your email service here to send an email.
    return Task.FromResult(0);
  }
}

public class SmsService : IIdentityMessageService
{
  public Task SendAsync(IdentityMessage message)
  {
    // Plug in your sms service here to send a text message.
    return Task.FromResult(0);
  }
}

上記の通り、この既定のコードでは、E-Mail や SMS の処理のためのひな形が設定されており、内部の実装は空になっています。
今回は、Twilio を使って SMS 送信をおこなうので、ここに、下記太字の通り処理を追記します。(SID, AuthToken, From は、取得した Twilio アカウントの設定を入力してください。)
なお、今回は SMS を使用するので、Twilio で US の電話番号を取得しておいてください。(さきほど昼休みに試してみたのですが、見事、ここにハマりました。。。宋さん いわく、現在、日本の電話番号では対応していないそうです。)

. . .
using Twilio;
. . .

public class ApplicationUserManager : UserManager<ApplicationUser>
{
  . . .

  public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
  {
    var manager = new ApplicationUserManager(
      new UserStore<ApplicationUser>(
        context.Get<ApplicationDbContext>()));
    . . .

    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();
    . . .

    return manager;
  }
}
. . .

public class SmsService : IIdentityMessageService
{
  public Task SendAsync(IdentityMessage message)
  {
    var cl = new TwilioRestClient(
      "AC33f...",      // SID
      "0616d...");     // AuthToken
    var result = cl.SendMessage(
      "+131xxxxxxxx",  // From number
      message.Destination,
      message.Body);

    return Task.FromResult(0);
  }
}

よくやる方法として、上記の通り、Phone Provider と E-Mail Provider の 2 つを登録しておき、二要素認証の際、利用者 (ユーザー) に、どちらの Provider を使うか選択させます。(E-Mail Provider を選択した際は、
SendGrid などを使って E-Mail を送信するようにしておきます。)
しかし、今回のサンプルでは、サンプルを煩雑にしないために、Phone Provider のみを使用し、プログラムでこれを自動選択するようにします。
このため、今回、下記の通り、E-Mail Provider の箇所はコメント アウトしておきましょう。

public class ApplicationUserManager : UserManager<ApplicationUser>
{
  . . .

  public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
  {
    var manager = new ApplicationUserManager(
      new UserStore<ApplicationUser>(
        context.Get<ApplicationDbContext>()));
    . . .

    manager.RegisterTwoFactorProvider(
      "PhoneCode",
      new PhoneNumberTokenProvider<ApplicationUser>
    {
      MessageFormat = "Your security code is: {0}"
    });
    //// This time, we set only one provider
    //manager.RegisterTwoFactorProvider(
    //  "EmailCode",
    //  new EmailTokenProvider<ApplicationUser>
    //{
    //  Subject = "Security Code",
    //  BodyFormat = "Your security code is: {0}"
    //});
    manager.EmailService = new EmailService();
    manager.SmsService = new SmsService();
    . . .

    return manager;
  }
}
. . .

後述しますが、ログイン時に、パスワード認証だけでなく、二要素認証のための Cookie を使用します。(どのようなユーザーが二要素認証を処理しているか、などの基本的な情報を管理しておくためです。)

このため、App_Start/Startup.Auth.cs を開き、app.UseCookieAuthentication の後に、下記の app.UseTwoFactorSignInCookie を設定しておきます。
なお、app.UseTwoFactorRememberBrowserCookie を設定すると、同じブラウザーを使った場合に 2 回目以降は二要素認証が不要になります (Cookie がおぼえています) が、今回は、この設定はおこないません。

public partial class Startup
{
  public void ConfigureAuth(IAppBuilder app)
  {
    . . .

    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
      . . .
    });
    app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));

    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
    . . .
  }
}

以上で、二要素認証を使用するための準備 (設定) は完了です。

 

二要素認証 (2FA) のプログラミング

ここでは、Account/Login で、二要素認証が必要かどうか確認をおこない、もし必要な場合はコードの送信をおこなう画面 (MVC の Action) を表示します。
そして、この画面で、SMS を使って Code の送信をおこない、Code を受け取ったユーザーは、その送られた Code をアプリケーションに登録します。
アプリケーションは、その受け取った Code の妥当性を検証して、問題なければログインを実行します。

さて、この実装 (プログラミング) をちゃんとおこなうには、実は、面倒な事前準備をいくつか実施しておく必要があります。
例えば、ユーザーの電話番号を登録する画面を用意して、あらかじめユーザー情報の一部として設定しておく必要があります。そして、この電話番号を登録する際も、スマートフォンへの Code の送信と、Code のチェックをおこなって、正しい電話番号かチェックする必要があるでしょう。また、ユーザーごとに二要素認証の有効化 / 無効化なども設定できるようにしておき、その内容を設定する必要もあるでしょう。
これらすべての実装 (プログラミング) を書いていると、どんどん長くなってしまうため、今回は、以下の通り、ユーザーと電話番号の登録は、あらかじめプログラムを使って設定しておきます。(以降では、ログイン以降の処理を解説します。)

補足 : 今回はプログラム (コード) を省略しますが、電話番号の登録 / 変更時の確認 (スマートフォンへの送信) の際は、UserManager.GenerateChangePhoneNumberToken (GenerateChangePhoneNumberTokenAsync) で Code を生成し、UserManager.ChangePhoneNumber (ChangePhoneNumberAsync) で Code を確認します。

[Authorize]
public class AccountController : Controller
{
  . . .

  [AllowAnonymous]
  public ActionResult TestStart()
  {
    // Register test user ...
    var user = new ApplicationUser()
    {
      UserName = "tsmatsuz@microsoft.com",
      Email = "tsmatsuz@microsoft.com",
      PhoneNumber = "+8190xxxxxxxx"
    };
    IdentityResult result = UserManager.Create(user, "P@ssw0rd");
    return RedirectToAction("Index", "Home");
  }
  . . .

では、Login の処理を実装していきます。

まず、ログイン完了後の処理 (Account/Login) として、下記の通り、もし二要素認証が必要であった場合、「SendCode」という Action にリダイレクトします。(この Action は、このあとでプログラミングします。)
ここが二要素認証の出発点となりますが、下記のコードにより、.AspNet.TwoFactorCookie という Cookie が内部で設定されます。

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
  if (ModelState.IsValid)
  {
    var user = await UserManager.FindAsync(model.Email, model.Password);
    if (user != null)
    {
      // This time, we alyways enable 2FA
      // (Usually, please check this user whether to enable or disable ...)
      await UserManager.SetTwoFactorEnabledAsync(user.Id, true);

      if (await UserManager.GetTwoFactorEnabledAsync(user.Id) &&
        !await AuthenticationManager.TwoFactorBrowserRememberedAsync(user.Id))
      {
        // If 2FA is needed
        var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
        AuthenticationManager.SignIn(identity);
        return RedirectToAction(
          "SendCode",
          new { ReturnUrl = returnUrl });
      }

      // If 2FA is not needed
      await SignInAsync(user, model.RememberMe);
      return RedirectToLocal(returnUrl);
    }
    else
    {
      ModelState.AddModelError("", "Invalid username or password");
    }
  }

  return View(model);
}

つぎに、遷移先の SendCode アクションと画面 (UI) を実装します。
今回作成する画面 (UI) では、ボタンを押すと、Code を生成して SMS で送信します。このため、下図のような画面をデザインしておきます。(画面の実装コードは省略します。)

上図で [Send Code] ボタンを押すと、Code を作成して (UserManager.GenerateTwoFactorToken)、作成した Code をユーザーの電話番号に送信して (UserManager.NotifyTwoFactorToken)、さいごに、Code をユーザーが入力して確認するための画面 (アクション) に遷移します。(今回、この遷移先のアクションを VerifyCode とします。)
このため、下記のようなプログラミングになります。
なお、前述の通り、普通なら Phone provider か E-Mail provider かユーザーに選択させ、その選択内容によって送信するメッセージの種類を変えますが、今回は、単一の Provider しか設定していないので、これを使用しています。

[HttpPost]
[AllowAnonymous]
public async Task<ActionResult> SendCode()
{
  // Get 2FA processing UserId from 2FA cookie
  var authres = await AuthenticationManager.AuthenticateAsync(
    DefaultAuthenticationTypes.TwoFactorCookie);
  var userId = authres.Identity.GetUserId();

  // Get 2FA provider
  // (This time, we set the only one provider)
  var providers = await UserManager.GetValidTwoFactorProvidersAsync(userId);
  var provider = providers.SingleOrDefault();

  // Create 2FA token (code) and Send !
  var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider);
  await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token);

  // Move to Verify
  return RedirectToAction("VerifyCode");

  // (This time, we ignore rerturnUrl...)
}

遷移先の VerifyCode アクションでは、電話 (SMS) で受け取った Code を入力して検証します。
下図のような画面をデザインします。(今回、画面のコード実装は省略します。)

上図の [Verify] ボタンを押すと、下記の通り、UserManager.VerifyTwoFactorToken を使用して、Code の正当性を検証します。

[HttpPost]
[AllowAnonymous]
public async Task<ActionResult> VerifyCode(string code)
{
  // Get 2FA processing UserId from 2FA cookie
  var authres = await AuthenticationManager.AuthenticateAsync(
    DefaultAuthenticationTypes.TwoFactorCookie);
  var userId = authres.Identity.GetUserId();

  // Get 2FA provider
  // (This time, we set only one provider !)
  var providers = await UserManager.GetValidTwoFactorProvidersAsync(userId);
  var provider = providers.SingleOrDefault();

  // Check code !
  var isOk = await UserManager.VerifyTwoFactorTokenAsync(userId, provider, code);
  if (!isOk)
  {
    // If it's not valid code
    return View("Error");
  }

  // SignIn
  var user = await UserManager.FindByIdAsync(userId);
  await SignInAsync(user, false); // (This time, we ignore "rememberme" ...)

  // Always go to Home/Index
  // (This time, we ignore rerturnUrl...)
  return RedirectToAction("Index", "Home");
}

以上で完了です。

この Web アプリケーション (ASP.NET MVC のアプリケーション) を実行して、上記の TestStart() アクションで事前準備 (ユーザー作成) をおこない、ログインをおこなってみてください。
スマートフォンに Code の記載された SMS メッセージが送信され、そこに書かれた Code を画面に入力するとログインが完了します。

 

ASP.NET Identity 2.1 (Visual Studio 2013 Update 3) における二要素認証の実装

上述の通り、二要素認証をちゃんと実装する場合、いろいろとやることがあるのですが、実は、Visual Studio 2013 Update 3 (その中で使用されている ASP.NET Identity 2.1) とそのプロジェクト テンプレートを使うと、もっと容易に実装 (プログラミング) できます。(すみません、「今更言うなよ」と言われるかもしれませんが、まあでも、仕組みを理解しておくことは大事なんです !)

例えば、上記で、ユーザー登録のためのカスタマイズもそこそこ必要であると説明しましたが、Visual Studio 2013 Update 3 のプロジェクト テンプレートを使用すると、上述した電話番号の登録画面なども、あらかじめ、数行のコメント アウトで実装できるようになっています。(二要素認証の有効化 / 無効化の設定も実装できます。)
また、SignInManager クラスが提供されていて、これを使用すると、下記の通り、ログインの際に、まずはパスワードのみを使った SignIn をおこなって (下記の PasswordSignInAsync)、その結果、二要素認証が必要か否かを判断できるようになっています。(このクラスは、実は、以前、Sample の中で提供されていた alpha 版のライブラリーに含まれていましたが、Update 3 で正式に標準クラスに含まれました。)

補足 : 下記の Lockout は、パスワードを誤って 5 回入力した場合に設定されます。Visual Studio 2013 Update 2 をお使いの方は、UserManager.IsLockedOut で確認できます。

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

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

 

ASP.NET vNext (Visual Studio "14") においても、ASP.NET Identity は使用されます。(細かな実装は、まだ見えないところですが。。。)

 

参考情報

ASP.NET Identity : Two-factor authentication using SMS and email with ASP.NET Identity

http://www.asp.net/identity/overview/features-api/two-factor-authentication-using-sms-and-email-with-aspnet-identity

Scott Hanselman Blog : Adding Two-Factor authentication to an ASP.NET application

http://www.hanselman.com/blog/AddingTwoFactorAuthenticationToAnASPNETApplication.aspx

 

Comments (0)

Skip to main content