ASP.NET Identity でのアカウントの確認とパスワードの回復 (C#)

このチュートリアルを実行する前に、まず「ログイン、電子メール確認、パスワード リセットを使用して安全な ASP.NET MVC 5 Web アプリを作成する」を完了する必要があります。 このチュートリアルでは、ローカル アカウントの確認用に電子メールを設定し、ユーザーが忘れたパスワードを ASP.NET Identity でリセットできるようにする方法を示し、その詳細を説明します。

ローカル ユーザー アカウントでは、ユーザーがアカウントのパスワードを作成する必要があり、そのパスワードは Web アプリに (安全に) 保存されます。 ASP.NET Identity はソーシャル アカウントもサポートしています。ソーシャル アカウントでは、ユーザーがアプリ用のパスワードを作成する必要がありません。 ソーシャル アカウントは、サード パーティ (Google、Twitter、Facebook、Microsoft など) を使用してユーザーを認証します。 このトピックでは、以下の内容を説明します。

新しいユーザーは、ローカル アカウントを作成するメール エイリアスを登録します。

Image of the account register window

[登録] ボタンを選択すると、検証トークンを含む確認メールがメール アドレスに送信されます。

Image showing email sent confirmation

ユーザーに対し、アカウントの確認トークンを含む電子メールが送信されます。

Image of confirmation token

そのリンクを選択すると、アカウントが確認されます。

Image confirming email address

パスワードの回復/リセット

パスワードを忘れたローカル ユーザーは、メール アカウントにセキュリティ トークンを送信して、パスワードをリセットできます。

Image of forgot password reset window

ユーザーは間もなく、パスワードのリセットを許可するリンクが記載されたメールを受け取ります。

Image showing reset password email
リンクを選択すると、[リセット] ページに移動します。

Image showing user password reset window

[リセット] ボタンを選択すると、パスワードがリセットされたことが確認されます。

Image showing password reset confirmation

ASP.NET Web アプリを作成する

まず、Visual Studio 2017 をインストールして実行します。

  1. 新しい ASP.NET Web プロジェクトを作成し、MVC テンプレートを選択します。 Web Forms では ASP.NET Identity もサポートされているため、Web フォーム アプリでも同様の手順を実行できます。

  2. 認証を [個別のユーザー アカウント] に変更します。

  3. アプリを実行し、[登録] リンクを選んで、ユーザーを登録します。 この時点で、メール アドレスに対する検証は、[EmailAddress] 属性によるもののみです。

  4. サーバー エクスプローラーで、Data Connections\DefaultConnection\Tables\AspNetUsers に移動し、右クリックして [テーブル定義を開く] を選択します。

    次の図は AspNetUsers スキーマを示しています。

    Image showing A s p Net Users schema

  5. AspNetUsers テーブルを右クリックし、[テーブル データの表示] を選択します。

    Image showing table data

    この時点で、メールは確認されていません。

ASP.NET Identity の既定のデータ ストアは Entity Framework ですが、他のデータ ストアを使用してフィールドを追加するように構成できます。 このチュートリアルの最後にある「その他のリソース」セクションを参照してください。

OWIN スタートアップ クラス (Startup.cs) は、アプリの起動時に呼び出され、App_Start\Startup.Auth.csConfigureAuth メソッドを呼び出します。このメソッドは、OWIN パイプラインを構成し、ASP.NET Identity を初期化します。 ConfigureAuth メソッドを調べます。 各 CreatePerOwinContext 呼び出しは、指定された型のインスタンスを作成するために、要求ごとに 1 回呼び出されるコールバック (OwinContext に保存) を登録します。 各型 (ApplicationDbContext, ApplicationUserManager) のコンストラクターと Create メソッドにブレーク ポイントを設定すると、各要求でこれらが呼び出されることを確認できます。 ApplicationDbContext のインスタンスと ApplicationUserManager は OWIN コンテキストに格納され、このコンテキストには、アプリケーション全体でアクセスできます。 ASP.NET Identity は、Cookie ミドルウェアを介して OWIN パイプラインにフックされます。 詳細については、「ASP.NET Identity での UserManager クラスの要求ごとの有効期間管理」を参照してください。

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

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

コード内のコメントにあるとおり、UseCookieAuthentication メソッドは Cookie 認証をサポートします。 SecurityStamp フィールドと関連するコードにより、アプリにセキュリティの追加レイヤーが提供されます。パスワードを変更すると、ログインしたブラウザーからログアウトされます。 SecurityStampValidator.OnValidateIdentity メソッドを使用すると、ユーザーがログインするときにアプリでセキュリティ トークンを検証できます。このトークンは、パスワードを変更するとき、または外部ログインを使用するときに使用されます。 これは、古いパスワードで生成されたすべてのトークン (Cookie) が無効になるようにするために必要です。 このサンプル プロジェクトでは、ユーザーのパスワードを変更すると、そのユーザーに対して新しいトークンが生成され、以前のトークンはすべて無効になり、SecurityStamp フィールドが更新されます。

ID システムを使用すると、ユーザーのセキュリティ プロファイルが変更されたときに (たとえば、ユーザーがパスワードを変更したり、関連するログイン (Facebook、Google、Microsoft アカウントなど) を変更したりしたときに)、ユーザーがすべてのブラウザー インスタンスからログアウトされるようにアプリを構成できます。 たとえば、次の図はシングル サインアウト サンプル アプリを示しています。このアプリを使用すると、ユーザーは 1 つのボタンを選択してすべてのブラウザー インスタンス (この場合は IE、Firefox、Chrome) からサインアウトできます。 または、このサンプルでは、特定のブラウザー インスタンスからのみログアウトできます。

Image showing the single sign-out sample app window

このシングル サインアウト サンプル アプリでは、ASP.NET Identity を使用してセキュリティ トークンを再生成する方法を示します。 これは、古いパスワードで生成されたすべてのトークン (Cookie) が無効になるようにするために必要です。 この機能は、アプリケーションにセキュリティの追加レイヤーを提供します。パスワードを変更すると、ログアウトし、このアプリケーションにログインした場所に戻ります。

App_Start\IdentityConfig.cs ファイルには、ApplicationUserManagerEmailServiceSmsService の各クラスが含まれています。 EmailServiceSmsService の各クラスは、それぞれ IIdentityMessageService インターフェイスを実装するため、電子メールと SMS を構成するための共通の方法が各クラスに用意されています。 このチュートリアルでは SendGrid を使ってメール通知を追加する方法のみを示しますが、SMTP や他のメカニズムを使ってメールを送信することもできます。

Startup クラスには、ソーシャル ログイン (Facebook、Twitter など) を追加するためのボイラー プレートも含まれています。詳細については、筆者の Facebook、Twitter、LinkedIn、Google OAuth2 のサインオンを使用した MVC 5 アプリに関するチュートリアルを参照してください。

ユーザー ID 情報を含む ApplicationUserManager クラスを調べ、次の機能を構成します。

  • パスワード強度の要件。
  • ユーザーのロックアウト (試行回数と時間)。
  • 2 要素認証 (2FA)。 2FA と SMS については、筆者の別のチュートリアルで扱います。
  • 電子メールと SMS サービスの接続。 (SMS については、筆者の別のチュートリアルで扱います)。

ApplicationUserManager クラスは、ジェネリック UserManager<ApplicationUser> クラスから派生します。 ApplicationUser は、IdentityUser から派生します。 IdentityUser は、ジェネリック IdentityUser クラスから派生します。

//     Default EntityFramework IUser implementation
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
   where TLogin : IdentityUserLogin<TKey>
   where TRole : IdentityUserRole<TKey>
   where TClaim : IdentityUserClaim<TKey>
{
   public IdentityUser()
   {
      Claims = new List<TClaim>();
      Roles = new List<TRole>();
      Logins = new List<TLogin>();
   }

   ///     User ID (Primary Key)
   public virtual TKey Id { get; set; }

   public virtual string Email { get; set; }
   public virtual bool EmailConfirmed { get; set; }

   public virtual string PasswordHash { get; set; }

   ///     A random value that should change whenever a users credentials have changed (password changed, login removed)
   public virtual string SecurityStamp { get; set; }

   public virtual string PhoneNumber { get; set; }
   public virtual bool PhoneNumberConfirmed { get; set; }

   public virtual bool TwoFactorEnabled { get; set; }

   ///     DateTime in UTC when lockout ends, any time in the past is considered not locked out.
   public virtual DateTime? LockoutEndDateUtc { get; set; }

   public virtual bool LockoutEnabled { get; set; }

   ///     Used to record failures for the purposes of lockout
   public virtual int AccessFailedCount { get; set; }
   
   ///     Navigation property for user roles
   public virtual ICollection<TRole> Roles { get; private set; }

   ///     Navigation property for user claims
   public virtual ICollection<TClaim> Claims { get; private set; }

   ///     Navigation property for user logins
   public virtual ICollection<TLogin> Logins { get; private set; }
   
   public virtual string UserName { get; set; }
}

上記のプロパティは、上記の AspNetUsers 表のプロパティと一致します。

IUser にジェネリック引数を使用すると、主キーに異なる型を使用してクラスを派生できます。 主キーを文字列から整数または GUID に変更する方法を示す ChangePK サンプルを参照してください。

ApplicationUser

ApplicationUser (public class ApplicationUserManager : UserManager<ApplicationUser>) は、Models\IdentityModels.cs 内で次のように定義されます。

public class ApplicationUser : IdentityUser
{
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
        UserManager<ApplicationUser> manager)
    {
        // Note the authenticationType must match the one defined in 
       //   CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, 
    DefaultAuthenticationTypes.ApplicationCookie);
        // Add custom user claims here
        return userIdentity;
    }
}

上記で強調表示されているコードでは、ClaimsIdentity が生成されます。 ASP.NET Identity と OWIN Cookie 認証はクレーム ベースであるため、フレームワークではアプリがユーザー用に ClaimsIdentity を生成する必要があります。 ClaimsIdentity には、ユーザーの名前、年齢、ユーザーが属するロールなど、ユーザーのすべてのクレームに関する情報があります。 この段階で、ユーザーのクレームをさらに追加することもできます。

OWIN AuthenticationManager.SignIn メソッドは、ClaimsIdentity で渡され、ユーザーをサインインさせます。

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties(){
       IsPersistent = isPersistent }, 
       await user.GenerateUserIdentityAsync(UserManager));
}

Facebook、Twitter、LinkedIn、Google OAuth2 のサインオンを使用した MVC 5 アプリでは、ApplicationUser クラスに追加のプロパティを追加する方法を示します。

メールの確認

新しいユーザーが登録に使用したメールを確認して、そのユーザーが他のユーザーを偽装していない (つまり、同じユーザーが他のユーザーのメール アドレスで登録していない) ことを確認するのをお勧めします。 ディスカッション フォーラムを管理していて、"bob@example.com""joe@contoso.com" として登録できないようにしたいとします。 電子メールによる確認を行わないと、"joe@contoso.com" はアプリから不要なメールを受け取る可能性があります。 Bob が誤って "bib@example.com" として登録し、これに気付かなかったとします。アプリに自分の正しいメール アドレスがないため、パスワード回復を使用できません。 電子メール確認では、ボットからの保護が制限され、しつこいスパム送信元からの保護は提供されず、このような送信元は、登録に使用できる多数の稼働中の電子メール エイリアスを持っています。次のサンプルでは、ユーザーは自分のアカウントが確認される (登録した電子メール アカウントで受信した確認リンクを選択することによって行う) までは、パスワードを変更できません。このワークフローは、他のシナリオに適用できます。たとえば、管理者が作成した新しいアカウントのパスワードを確認およびリセットするためのリンクを送信する場合や、ユーザーがプロファイルを変更したときに電子メールを送信する場合です。 一般に、新しいユーザーがメール、SMS テキスト メッセージ、または別のメカニズムによって確認される前に、Web サイトにデータを投稿できないようにする必要があります。

より完全なサンプルを構築する

このセクションでは、NuGet を使用して、ここで操作するより完全なサンプルをダウンロードします。

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

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

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

    このチュートリアルでは、SendGrid を使用して電子メールを送信します。 Identity.Samples パッケージは、使用するコードをインストールします。

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

  4. アプリを実行し、[登録] リンクを選択して登録フォームを投稿して、ローカル アカウントの作成をテストします。

  5. 電子メール確認をシミュレートするデモ電子メール リンクを選択します。

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

警告

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

App_Start\IdentityConfig.cs のコードを調べる

このサンプルでは、アカウントを作成し、管理者ロールに追加する方法を示します。 サンプルのメールは、管理者アカウントに使用するメールに置き換える必要があります。 現在、管理者アカウントを作成する最も簡単な方法は、Seed メソッド内でプログラムによって行います。 今後、ユーザーとロールを作成および管理できるツールを提供したいと考えています。 サンプル コードでは、ユーザーとロールを作成および管理できますが、ロールとユーザー管理者ページを実行するには、まず管理者アカウントが必要です。 このサンプルでは、DB のシード処理時に管理者アカウントが作成されます。

パスワードを変更し、メール通知を受信できるアカウントに名前を変更します。

警告

セキュリティ - 機密データをソース コード内に保存しないでください。

前に言及したように、スタートアップ クラスでの app.CreatePerOwinContext 呼び出しは、アプリ DB コンテンツ、ユーザー マネージャー、ロール マネージャーの各クラスの Create メソッドにコールバックを追加します。 OWIN パイプラインは、要求ごとにこれらのクラスで Create メソッドを呼び出し、各クラスのコンテキストを格納します。 アカウント コントローラーは、HTTP コンテキスト (OWIN コンテキストを含む) からユーザー マネージャーを公開します。

public ApplicationUserManager UserManager
{
    get
    {
        return _userManager ?? 
    HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
    }
    private set
    {
        _userManager = value;
    }
}

ユーザーがローカル アカウントを登録すると、HTTP Post Register メソッドが呼び出されます。

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
            var callbackUrl = Url.Action(
               "ConfirmEmail", "Account", 
               new { userId = user.Id, code = code }, 
               protocol: Request.Url.Scheme);

            await UserManager.SendEmailAsync(user.Id, 
               "Confirm your account", 
               "Please confirm your account by clicking this link: <a href=\"" 
                                               + callbackUrl + "\">link</a>");
            // ViewBag.Link = callbackUrl;   // Used only for initial demo.
            return View("DisplayEmail");
        }
        AddErrors(result);
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

上記のコードでは、モデル データを使用して、入力された電子メールとパスワードを使用して新しいユーザー アカウントを作成します。 電子メール エイリアスがデータ ストア内にある場合、アカウントの作成は失敗し、フォームが再び表示されます。 GenerateEmailConfirmationTokenAsync メソッドは、セキュリティで保護された確認トークンを作成し、ASP.NET Identity データ ストアに格納します。 Url.Action メソッドは、UserId と確認トークンを含むリンクを作成します。 このリンクはユーザーに電子メールで送信され、ユーザーはメール アプリのリンクを選択してアカウントを確認できます。

電子メールによる確認を設定する

SendGrid のサインアップ ページに移動し、無料アカウントに登録します。 SendGrid を構成するには、次のようなコードを追加します。

public class EmailService : IIdentityMessageService
{
   public Task SendAsync(IdentityMessage message)
   {
      return configSendGridasync(message);
   }

   private Task configSendGridasync(IdentityMessage message)
   {
      var myMessage = new SendGridMessage();
      myMessage.AddTo(message.Destination);
      myMessage.From = new System.Net.Mail.MailAddress(
                          "Joe@contoso.com", "Joe S.");
      myMessage.Subject = message.Subject;
      myMessage.Text = message.Body;
      myMessage.Html = message.Body;

      var credentials = new NetworkCredential(
                 ConfigurationManager.AppSettings["mailAccount"],
                 ConfigurationManager.AppSettings["mailPassword"]
                 );

      // Create a Web transport for sending email.
      var transportWeb = new Web(credentials);

      // Send the email.
      if (transportWeb != null)
      {
         return transportWeb.DeliverAsync(myMessage);
      }
      else
      {
         return Task.FromResult(0);
      }
   }
}

Note

電子メール クライアントは、テキスト メッセージのみを受け入れることがよくあります (HTML は受け入れない)。 メッセージはテキストと HTML で指定する必要があります。 上記の SendGrid サンプルでは、上記の myMessage.TextmyMessage.Html コードを使用してこれを行います。

次のコードは、message.Body がリンクのみを返す MailMessage クラスを使用して電子メールを送信する方法を示しています。

void sendMail(Message message)
{
#region formatter
   string text = string.Format("Please click on this link to {0}: {1}", message.Subject, message.Body);
   string html = "Please confirm your account by clicking this link: <a href=\"" + message.Body + "\">link</a><br/>";

   html += HttpUtility.HtmlEncode(@"Or click on the copy the following link on the browser:" + message.Body);
#endregion

   MailMessage msg = new MailMessage();
   msg.From = new MailAddress("joe@contoso.com");
   msg.To.Add(new MailAddress(message.Destination));
   msg.Subject = message.Subject;
   msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(text, null, MediaTypeNames.Text.Plain));
   msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(html, null, MediaTypeNames.Text.Html));

   SmtpClient smtpClient = new SmtpClient("smtp.gmail.com", Convert.ToInt32(587));
   System.Net.NetworkCredential credentials = new System.Net.NetworkCredential("joe@contoso.com", "XXXXXX");
   smtpClient.Credentials = credentials;
   smtpClient.EnableSsl = true;
   smtpClient.Send(msg);
}

警告

セキュリティ - 機密データをソース コード内に保存しないでください。 アカウントと資格情報は appSetting に保存されます。 Azure では、これらの値を Azure portal の [構成] タブに安全に保存できます。 「ASP.NET と Azure にパスワードやその他の機密データを配置するためのベスト プラクティス」を参照してください。

SendGrid の資格情報を入力し、アプリを実行し、電子メール エイリアスに登録すると、メールの確認リンクを選択できます。 Outlook.com 電子メール アカウントでこれを行う方法については、John Atten の Outlook.Com SMTP ホストの C# SMTP 構成に関する投稿と ASP.NET Identity 2.0: アカウント検証と 2 要素認証承認の設定に関する投稿を参照してください。

ユーザーが [登録] ボタンを選択すると、検証トークンを含む確認メールがそのユーザーのメール アドレスに送信されます。

Image of email sent confirmation window

ユーザーに対し、アカウントの確認トークンを含む電子メールが送信されます。

Image of email received

コードを確認する

次のコードは POST ForgotPassword メソッドを示します。

public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindByNameAsync(model.Email);
        if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
        {
            // Don't reveal that the user does not exist or is not confirmed
            return View("ForgotPasswordConfirmation");
        }

        var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
        var callbackUrl = Url.Action("ResetPassword", "Account", 
    new { UserId = user.Id, code = code }, protocol: Request.Url.Scheme);
        await UserManager.SendEmailAsync(user.Id, "Reset Password", 
    "Please reset your password by clicking here: <a href=\"" + callbackUrl + "\">link</a>");        
        return View("ForgotPasswordConfirmation");
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

ユーザーの電子メールが確認されていない場合、メソッドは自動的に失敗します。 無効なメール アドレスに対してエラーが投稿された場合、悪意のあるユーザーはその情報を使用して、攻撃対象の有効な userId (電子メール エイリアス) を見つける可能性があります。

次のコードは、送信された電子メールの中でユーザーが確認リンクを選択したときに呼び出される、アカウント コントローラーの ConfirmEmail メソッドを示しています。

public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
    if (userId == null || code == null)
    {
        return View("Error");
    }
    var result = await UserManager.ConfirmEmailAsync(userId, code);
    if (result.Succeeded)
    {
        return View("ConfirmEmail");
    }
    AddErrors(result);
    return View();
}

パスワードを忘れた場合のトークンは、使用された後は無効になります。 Create メソッド内の次のコード変更 (App_Start\IdentityConfig.cs ファイル内) では、トークンの有効期限が 3 時間に設定されます。

if (dataProtectionProvider != null)
 {
    manager.UserTokenProvider =
       new DataProtectorTokenProvider<ApplicationUser>
          (dataProtectionProvider.Create("ASP.NET Identity"))
          {                    
             TokenLifespan = TimeSpan.FromHours(3)
          };
 }

上記のコードでは、パスワードを忘れた場合のトークンと電子メール確認トークンは 3 時間で期限切れになります。 既定の TokenLifespan は 1 日です。

次のコードは、電子メール確認方法を示しています。

// GET: /Account/ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
   if (userId == null || code == null)
   {
      return View("Error");
   }
   IdentityResult result;
   try
   {
      result = await UserManager.ConfirmEmailAsync(userId, code);
   }
   catch (InvalidOperationException ioe)
   {
      // ConfirmEmailAsync throws when the userId is not found.
      ViewBag.errorMessage = ioe.Message;
      return View("Error");
   }

   if (result.Succeeded)
   {
      return View();
   }

   // If we got this far, something failed.
   AddErrors(result);
   ViewBag.errorMessage = "ConfirmEmail failed";
   return View("Error");
}

ASP.NET Identity では、アプリの安全性を高めるために 2 要素認証 (2FA) がサポートされています。 John Atten 著「ASP.NET Identity 2.0: アカウント検証と 2 要素認証の設定」を参照してください。 ログイン パスワードの試行エラーにアカウント ロックアウトを設定することはできますが、この方法では、ログインが DOS ロックアウトの影響を受けやすくなります。 アカウント ロックアウトは 2FA でのみ使用することをお勧めします。

その他のリソース