ログイン、電子メール確認、パスワード リセットを使用して安全な ASP.NET MVC 5 Web アプリを作成する (C#)

作成者 : Rick Anderson

このチュートリアルでは、ASP.NET ID メンバーシップ システムを使用して、電子メールの確認とパスワードリセットを使用して ASP.NET MVC 5 Web アプリを構築する方法について説明します。

.NET Core を使用するこのチュートリアルの更新バージョンについては、「ASP.NET Coreでのアカウントの確認とパスワードの回復」を参照してください。

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

まず、Visual Studio Express 2013 for Web または Visual Studio 2013をインストールして実行します。 Update 3 以降Visual Studio 2013インストールします。

Note

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

  1. 新しい ASP.NET Web プロジェクトを作成し、MVC テンプレートを選択します。 Web Formsでは ASP.NET ID もサポートされているため、Web フォーム アプリでも同様の手順を実行できます。
    [New A S P dot Net Project]\(新しい A S P ドット Net プロジェクト\) ページを示すスクリーンショット。[M V C] テンプレートが選択され、[個々のユーザー アカウント] が強調表示されています。

  2. 既定の認証は [個々のユーザー アカウント] のままにします。 Azure でアプリをホストする場合は、[チェック] チェック ボックスをオンのままにします。 チュートリアルの後半では、Azure にデプロイします。 Azure アカウントは無料で開くことができます。

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

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

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

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

    サーバー エクスプローラーの [A S P Net Users スクリプト ファイル] タブを示すスクリーンショット。

  6. AspNetUsers テーブルを右クリックし、[テーブル データの表示] を選択します。
    A S P Net Users スキーマを示すスクリーンショット。[Email確認済み] 列に False というラベルが付けられます。
    この時点で、メールは確認されていません。

  7. 行をクリックし、[削除] を選択します。 次の手順でこのメールをもう一度追加し、確認メールを送信します。

Email確認

新しいユーザー登録の電子メールを確認して、他のユーザーを偽装していないことを確認することをお勧めします (つまり、他のユーザーのメールに登録していません)。 ディスカッション フォーラムがあるとします。これは、 として"joe@contoso.com"登録できないように"bob@example.com"したいとします。 電子メールの確認がない場合は、 "joe@contoso.com" アプリから不要なメールを受け取る可能性があります。 Bob が誤って として "bib@example.com" 登録され、それに気付かなかったとします。アプリに正しいメールがないため、パスワードの回復を使用できないとします。 Email確認では、ボットからの保護が制限され、決定されたスパマーからの保護は提供されません。登録に使用できる多くの動作する電子メール エイリアスがあります。

一般に、新しいユーザーが電子メール、SMS テキスト メッセージ、または別のメカニズムによって確認される前に、Web サイトにデータを投稿できないようにする必要があります。 以下のセクションでは、電子メールの確認を有効にし、新しく登録されたユーザーがメールが確認されるまでログインできないようにコードを変更します。

SendGrid をフックする

このセクションの手順は最新ではありません。 更新された手順については 、「SendGrid 電子メール プロバイダーの構成」 を参照してください。

このチュートリアルでは SendGrid を介してメール通知を追加する方法のみを示しますが、SMTP やその他のメカニズムを使用してメールを送信できます ( その他のリソースを参照してください)。

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

    Install-Package SendGrid
    
  2. Azure SendGrid サインアップ ページに移動し、無料の SendGrid アカウントに登録します。 sendGrid を構成するには、 App_Start/IdentityConfig.cs で次のようなコードを追加します。

    public class EmailService : IIdentityMessageService
    {
       public async Task SendAsync(IdentityMessage message)
       {
          await configSendGridasync(message);
       }
    
       // Use NuGet to install SendGrid (Basic C# client lib) 
       private async 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)
          {
             await transportWeb.DeliverAsync(myMessage);
          }
          else
          {
             Trace.TraceError("Failed to create Web transport.");
             await Task.FromResult(0);
          }
       }
    }
    

次の内容を追加する必要があります。

using SendGrid;
using System.Net;
using System.Configuration;
using System.Diagnostics;

このサンプルをシンプルにするために、アプリ設定を web.config ファイルに格納します。

</connectionStrings>
   <appSettings>
      <add key="webpages:Version" value="3.0.0.0" />
      <!-- Markup removed for clarity. -->
      
      <add key="mailAccount" value="xyz" />
      <add key="mailPassword" value="password" />
   </appSettings>
  <system.web>

警告

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

アカウント コントローラーで電子メールの確認を有効にする

//
// POST: /Account/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)
        {
            await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);

            string 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 <a href=\"" 
               + callbackUrl + "\">here</a>");

            return RedirectToAction("Index", "Home");
        }
        AddErrors(result);
    }

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

Views\Account\ConfirmEmail.cshtml ファイルに正しい razor 構文があることを確認します。 ( 最初の行の @ 文字が見つからない可能性があります。

@{
    ViewBag.Title = "Confirm Email";
}

<h2>@ViewBag.Title.</h2>
<div>
    <p>
        Thank you for confirming your email. Please @Html.ActionLink("Click here to Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })
    </p>
</div>

アプリを実行し、[登録] リンクをクリックします。 登録フォームを送信すると、ログインします。

[My A S P dot NET Log In Home]\(MY A S P ドット NET ログイン\) ホーム ページを示すスクリーンショット。

メール アカウントを確認し、リンクをクリックしてメールを確認します。

ログイン前に電子メールの確認を要求する

現在、ユーザーが登録フォームを完了すると、ユーザーはログインします。 通常、ログインする前にメールを確認する必要があります。 以下のセクションでは、新しいユーザーがログイン (認証) される前に確認済みのメールを受け取る必要があるコードを変更します。 次の HttpPost Register 強調表示された変更を使用して メソッドを更新します。

//
// POST: /Account/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)
      {
         //  Comment the following line to prevent log in until the user is confirmed.
         //  await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);

         string 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 <a href=\"" + callbackUrl + "\">here</a>");

         // Uncomment to debug locally 
         // TempData["ViewBagLink"] = callbackUrl;

         ViewBag.Message = "Check your email and confirm your account, you must be confirmed "
                         + "before you can log in.";

         return View("Info");
         //return RedirectToAction("Index", "Home");
      }
      AddErrors(result);
   }

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

メソッドをコメントアウト SignInAsync すると、ユーザーは登録によってサインインされません。 この TempData["ViewBagLink"] = callbackUrl; 行を使用すると、メールを送信せずに アプリをデバッグ し、登録をテストできます。 ViewBag.Message は、確認命令を表示するために使用されます。 ダウンロード サンプルには、電子メールを設定せずに電子メールの確認をテストするコードが含まれており、アプリケーションのデバッグにも使用できます。

ファイルを Views\Shared\Info.cshtml 作成し、次の Razor マークアップを追加します。

@{
   ViewBag.Title = "Info";
}
<h2>@ViewBag.Title.</h2>
<h3>@ViewBag.Message</h3>

Authorize 属性を Home コントローラーの Contact action メソッドに追加します。 [ 連絡先 ] リンクをクリックすると、匿名ユーザーにアクセス権がなく、認証済みユーザーがアクセス権を持っていることを確認できます。

[Authorize]
public ActionResult Contact()
{
   ViewBag.Message = "Your contact page.";

   return View();
}

アクション メソッドも更新する HttpPost Login 必要があります。

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
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 doesn't count login failures towards account lockout
    // To enable password failures to trigger account 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, RememberMe = model.RememberMe });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}

Views\Shared\Error.cshtml ビューを更新して、エラー メッセージを表示します。

@model System.Web.Mvc.HandleErrorInfo

@{
    ViewBag.Title = "Error";
}

<h1 class="text-danger">Error.</h1>
@{
   if (String.IsNullOrEmpty(ViewBag.errorMessage))
   {
      <h2 class="text-danger">An error occurred while processing your request.</h2>
   }
   else
   {
      <h2 class="text-danger">@ViewBag.errorMessage</h2>
   }
}

テスト対象の電子メール エイリアスを含む AspNetUsers テーブル内のすべてのアカウントを削除します。 アプリを実行し、メール アドレスを確認するまでログインできないことを確認します。 メール アドレスを確認したら、[ 連絡先 ] リンクをクリックします。

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

アカウント コントローラーのアクション メソッドから HttpPost ForgotPassword コメント文字を削除します。

//
// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
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");
        }

        string 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 <a href=\"" + callbackUrl + "\">here</a>");
        return RedirectToAction("ForgotPasswordConfirmation", "Account");
    }

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

Views\Account\Login.cshtml razor ビュー ファイルの ActionLink からForgotPasswordコメント文字を削除します。

@using MvcPWy.Models
@model LoginViewModel
@{
   ViewBag.Title = "Log in";
}

<h2>@ViewBag.Title.</h2>
<div class="row">
   <div class="col-md-8">
      <section id="loginForm">
         @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
         {
            @Html.AntiForgeryToken()
            <h4>Use a local account to log in.</h4>
            <hr />
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })
            <div class="form-group">
               @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
               <div class="col-md-10">
                  @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
                  @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
               </div>
            </div>
            <div class="form-group">
               @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
               <div class="col-md-10">
                  @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                  @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
               </div>
            </div>
            <div class="form-group">
               <div class="col-md-offset-2 col-md-10">
                  <div class="checkbox">
                     @Html.CheckBoxFor(m => m.RememberMe)
                     @Html.LabelFor(m => m.RememberMe)
                  </div>
               </div>
            </div>
            <div class="form-group">
               <div class="col-md-offset-2 col-md-10">
                  <input type="submit" value="Log in" class="btn btn-default" />
               </div>
            </div>
            <p>
               @Html.ActionLink("Register as a new user", "Register")
            </p>
            @* Enable this once you have account confirmation enabled for password reset functionality *@
            <p>
               @Html.ActionLink("Forgot your password?", "ForgotPassword")
            </p>
         }
      </section>
   </div>
   <div class="col-md-4">
      <section id="socialLoginForm">
         @Html.Partial("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl })
      </section>
   </div>
</div>

@section Scripts {
   @Scripts.Render("~/bundles/jqueryval")
}

[ログイン] ページに、パスワードをリセットするためのリンクが表示されます。

ユーザーが新しいローカル アカウントを作成すると、ログオンする前に使用する必要がある確認リンクが電子メールで送信されます。 ユーザーが誤って確認メールを削除した場合、または電子メールが届かなかった場合は、もう一度確認リンクを送信する必要があります。 次のコード変更は、これを有効にする方法を示しています。

Controllers\AccountController.cs ファイルの下部に次のヘルパー メソッドを追加します。

private async Task<string> SendEmailConfirmationTokenAsync(string userID, string subject)
{
   string code = await UserManager.GenerateEmailConfirmationTokenAsync(userID);
   var callbackUrl = Url.Action("ConfirmEmail", "Account",
      new { userId = userID, code = code }, protocol: Request.Url.Scheme);
   await UserManager.SendEmailAsync(userID, subject,
      "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");

   return callbackUrl;
}

新しいヘルパーを使用するように Register メソッドを更新します。

//
// POST: /Account/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)
      {
         //  Comment the following line to prevent log in until the user is confirmed.
         //  await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);

         string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account");

         ViewBag.Message = "Check your email and confirm your account, you must be confirmed "
                         + "before you can log in.";

         return View("Info");
         //return RedirectToAction("Index", "Home");
      }
      AddErrors(result);
   }

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

ユーザー アカウントが確認されていない場合は、Login メソッドを更新してパスワードを再送信します。

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
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);
   var user =  UserManager.Find(model.Email, model.Password);
   if (user != null)
   {
      if (!await UserManager.IsEmailConfirmedAsync(user.Id))
      {
         string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account-Resend");

          // Uncomment to debug locally  
          // ViewBag.Link = callbackUrl;
         ViewBag.errorMessage = "You must have a confirmed email to log on. "
                              + "The confirmation token has been resent to your email account.";
         return View("Error");
      }
   }

   // This doesn't count login failures towards account lockout
   // To enable password failures to trigger account 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, RememberMe = model.RememberMe });
      case SignInStatus.Failure:
      default:
         ModelState.AddModelError("", "Invalid login attempt.");
         return View(model);
   }
}

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

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

My A S P ドットの [Net Log In Home]\(ホーム\) ページを示すスクリーンショット。サンプルのユーザー ID が強調表示されています。

[管理] リンクをクリックします。 このアカウントに関連付けられている 外部ログイン: 0 に注意してください。

[My A S P] ドット [Net Manage your account]\(アカウントの管理\) ページを示すスクリーンショット。[外部ログイン] 行の横にある [0] と [管理] リンクが強調表示されています。

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

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

[My A S P] ドット [Net Manage your account]\(アカウントの管理\) ページを示すスクリーンショット。[パスワードの選択] と [外部ログイン] 行が強調表示されています。

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

[My A S P dot Net Create Local Login]\(マイ A S P ドット Net ローカル ログインの作成\) ページを示すスクリーンショット。[新しいパスワード] フィールドと [新しいパスワードの確認] テキスト フィールドにサンプル パスワードが入力されています。

Email確認の詳細

ASP.NET ID を使用したアカウントの確認とパスワードの回復 に関するチュートリアルでは、このトピックで詳しく説明します。

アプリのデバッグ

リンクを含むメールが届かない場合:

  • 迷惑メールフォルダーまたはスパム フォルダーを確認します。
  • SendGrid アカウントにログインし、[Email アクティビティ] リンクをクリックします。

電子メールなしで検証リンクをテストするには、 完成したサンプルをダウンロードします。 確認リンクと確認コードがページに表示されます。

その他のリソース