ログイン、電子メール確認、パスワード リセットを使用して安全な ASP.NET MVC 5 Web アプリを作成する (C#)
作成者: Rick Anderson
このチュートリアルでは、ASP.NET Identity メンバーシップ システムを使って、メール確認とパスワードのリセットを使用して ASP.NET MVC 5 Web アプリを構築する方法について説明します。
.NET Core を使用するこのチュートリアルの更新バージョンについては、「ASP.NET Core でのアカウントの確認とパスワードの回復」を参照してください。
ASP.NET MVC アプリの作成
まず、Visual Studio Express 2013 for Web または Visual Studio 2013 をインストールして実行します。 Visual Studio 2013 Update 3 以降をインストールしてください。
Note
警告: このチュートリアルを完了するには、Visual Studio 2013 Update 3 以降をインストールする必要があります。
新しい ASP.NET Web プロジェクトを作成し、MVC テンプレートを選択します。 Web Forms も ASP.NET ID をサポートしているため、Web フォーム アプリでも同様の手順を実行できます。
既定の認証は個々のユーザー アカウントのままにします。 Azure でアプリをホストする場合は、チェック ボックスをオンのままにします。 チュートリアルの後半では、Azure にデプロイします。 Azure アカウントは無料で開設できます。
SSL を使用するようにプロジェクトを設定します。
アプリを実行し、[登録] リンクを選んで、ユーザーを登録します。 この時点で、メール アドレスに対する検証は、[EmailAddress] 属性によるもののみです。
サーバー エクスプローラーで、Data Connections\DefaultConnection\Tables\AspNetUsers に移動し、右クリックして [テーブル定義を開く] を選択します。
次の図は
AspNetUsers
スキーマを示しています。AspNetUsers テーブルを右クリックし、[テーブル データの表示] を選択します。
この時点で、メールは確認されていません。行をクリックし、[削除] を選択します。 次の手順でこのメール アドレスを再び追加し、確認メールを送信します。
メールの確認
新しいユーザー登録のメール アドレスを確認して、他のユーザーを偽装していない (つまり、他のユーザーのメール アドレスで登録されていない) ことを確認することをお勧めします。 ディスカッション フォーラムを管理していて、"bob@example.com"
が "joe@contoso.com"
として登録できないようにしたいとします。 電子メールによる確認を行わないと、"joe@contoso.com"
はアプリから不要なメールを受け取る可能性があります。 Bob が誤って "bib@example.com"
として登録して、これに気付かなかったとします。アプリに正しいメール アドレスがないため、パスワード回復を使用できません。 メール確認では、ボットからの保護が制限され、決定されたスパム送信者からの保護は提供されず、機能する多くのメール エイリアスを登録に使用できる可能性があります。
一般に、新しいユーザーがメール、SMS テキスト メッセージ、または別のメカニズムによって確認される前に、Web サイトにデータを投稿できないようにする必要があります。 以下のセクションでは、メール確認を有効にし、メールが確認されるまで新しく登録されたユーザーがログインできないようにコードを変更します。
SendGrid をフックする
このセクションの手順は最新ではありません。 更新された手順については、SendGrid メール プロバイダーの構成に関する記事を参照してください。
このチュートリアルでは SendGrid を使用してメール通知を追加する方法のみを示しますが、SMTP やその他のメカニズムを使用してメールを送信できます (その他のリソースに関する記事を参照)。
パッケージ マネージャー コンソールで、次のコマンドを入力します。
Install-Package SendGrid
Azure SendGrid のサインアップ ページに移動し、無料の SendGrid アカウントに登録します。 App_Start/IdentityConfig.cs で次のようなコードを追加して SendGrid を構成します。
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>
アプリを実行し、[登録] リンクを選択します。 登録フォームを送信したら、ログインします。
メール アカウントを確認し、リンクを選択してメール アドレスを確認します。
ログイン前にメール確認を要求する
現在、ユーザーは登録フォームに入力すると、ログインします。 通常は、ログインする前にメール アドレスを確認する必要があります。 以下のセクションでは、新しいユーザーがログイン (認証) される前に確認済みのメール アドレスを持つことを要求するようにコードを変更します。 次の強調表示された変更で 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 属性 をホーム コントローラーの Contact
アクション メソッドに追加します。 [連絡先] リンクを選択すると、匿名ユーザーにはアクセス権がなく、認証されたユーザーがアクセス権を持っていることを確認できます。
[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 ビュー ファイルの ForgotPassword
ActionLink からコメント文字を削除します。
@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 は最初にローカル ログインとして作成されています。ただし、アカウントを最初にソーシャル ログインとして作成してから、ローカル ログインを追加することができます。
[管理] リンクをクリックします。 このアカウントに [外部ログイン: 0] が関連付けられていることに注意してください。
別のログイン サービスへのリンクを選択し、アプリの要求を受け入れます。 2 つのアカウントが組み合わされているため、どちらのアカウントでもログオンできます。 ソーシャル ログイン認証サービスがダウンしたときのため、またはさらに可能性が高いのはソーシャル アカウントにアクセスできなくなったときのために、ユーザーにローカル アカウントを追加させたいことがあります。
次の図では、Tom はソーシャル ログインです (ページに表示されている [外部ログイン: 1] から確認できます)。
[パスワードの選択] を選択すると、同じアカウントに関連付けられているローカル ログオンを追加できます。
メール確認の詳細
ASP.NET Identity でのアカウントの確認とパスワードの回復に関するチュートリアルでは、このトピックの詳細について説明します。
アプリのデバッグ
リンクを含むメールが届かない場合:
- 迷惑メールまたはスパム フォルダーを確認します。
- SendGrid アカウントにログインし、[メール アクティビティ] リンク を選択します。
メールなしで検証リンクをテストするには、完成したサンプル をダウンロードします。 確認リンクと確認コードがページに表示されます。
その他のリソース
- ASP.NET ID の推奨リソースへのリンク
- 「ASP.NET ID を使用したアカウントの確認とパスワードの回復」のパスワードの回復とアカウントの確認について詳しく説明します。
- この「Facebook、Twitter、LinkedIn、Google OAuth2 サインオンを使用した MVC 5 アプリ」 チュートリアルでは、Facebook と Google OAuth 2 の認証を使用してASP.NET MVC 5 アプリを記述する方法について説明します。 また、ID データベースにデータを追加する方法も示します。
- メンバーシップ、OAuth、SQL Database を使用した安全な ASP.NET MVC アプリの Azure へのデプロイ。 このチュートリアルでは、Azure デプロイ、ロールを使用してアプリをセキュリティで保護する方法、メンバーシップ API を使用してユーザーとロールを追加する方法、および追加のセキュリティ機能を追加します。
- OAuth 2 用の Google アプリを作成し、作成したアプリをプロジェクトに接続する
- Facebook でアプリを作成し、作成したアプリをプロジェクトに接続する
- プロジェクトでの SSL の設定