ASP.NET ID を使用したアカウントの確認とパスワードの回復 (C#)

このチュートリアルを実行する前に、まず 、ログイン、電子メールの確認、パスワードリセットを使用して、セキュリティで保護された ASP.NET MVC 5 Web アプリを作成する必要があります。 このチュートリアルには詳細が含まれており、ローカル アカウントの確認用にメールを設定し、ユーザーが忘れたパスワードを ASP.NET ID でリセットできるようにする方法について説明します。

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

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

アカウント登録ウィンドウの画像

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

電子メールの送信確認を示す画像

ユーザーは、自分のアカウントの確認トークンを含む電子メールを送信されます。

確認トークンの画像

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

電子メール アドレスを確認する画像

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

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

パスワードを忘れた場合のリセット ウィンドウの画像

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

パスワードのリセット電子メールを示す画像
リンクを選択すると、[リセット] ページに移動します。

ユーザー パスワード リセット ウィンドウを示す画像

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

パスワード リセットの確認を示す画像

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

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

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

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

  3. アプリを実行し、[ 登録 ] リンクを選択してユーザーを登録します。 この時点で、電子メールの検証は [EmailAddress] 属性のみです。

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

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

    A s p Net Users スキーマを示す画像

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

    テーブル データを示す画像

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

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

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

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

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

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

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

シングル サインアウトのサンプル アプリ ウィンドウを示す画像

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

App_Start\IdentityConfig.cs ファイルには、 クラスと SmsService クラスがApplicationUserManagerEmailService含まれています。 EmailServiceクラスと SmsService クラスはそれぞれ インターフェイスを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> クラスから派生します。 ApplicationUserIdentityUser から派生します。 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 ジェネリック引数を使用すると、主キーに異なる型を使用してクラスを派生できます。 主キーを文字列から int または 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 ID と 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 示されています。

Email確認

新しいユーザーが登録したメールを確認して、他のユーザーを偽装していないことを確認することをお勧めします (つまり、他のユーザーのメールに登録していません)。 ディスカッション フォーラムがあるとします。これは、 として"joe@contoso.com"登録できないように"bob@example.com"したいとします。 電子メールの確認がない場合は、 "joe@contoso.com" アプリから不要なメールを受け取る可能性があります。 Bob が誤って として "bib@example.com" 登録され、それに気付かなかったとします。アプリに正しいメールがないため、パスワードの回復を使用できないとします。 Email確認では、ボットからの保護が制限され、決定されたスパマーからの保護は提供されません。登録に使用できる多くの動作する電子メール エイリアスがあります。次のサンプルでは、ユーザーは自分のアカウントが確認されるまでパスワードを変更できません (登録したメール アカウントで受信した確認リンクを選択することによって)。この作業フローを他のシナリオに適用できます。たとえば、管理者によって作成された新しいアカウントのパスワードを確認およびリセットするためのリンクの送信、プロファイルの変更時にユーザーに電子メールを送信するなどです。 一般に、新しいユーザーが電子メール、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 アカウント コントローラーのコード。 DisplayEmail および ForgotPasswordConfirmation アクション メソッドと 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 ID データ ストアに格納します。 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);
      }
   }
}

注意

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

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

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 の C# SMTP Configuration for Outlook.Com SMTP Host」および「ASP.NET Identity 2.0: Setting Up Account Validation and Two-Factor Authorization posts」を参照してください。

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

メール送信確認ウィンドウの画像

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

受信したメールの画像

コードを確認する

次のコードは 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 ではTwo-Factor認証 (2FA) がサポートされています。 「ASP.NET ID 2.0: アカウント検証の設定」と「John Atten による承認のTwo-Factor」を参照してください。 ログイン パスワードの試行エラーにアカウント ロックアウトを設定できますが、この方法では、ログインが DOS ロックアウトの影響を受けやすくなります。 アカウントロックアウトは 2FA でのみ使用することをお勧めします。

その他のリソース