ASP.NET MVC 4 での WebMatrix で提供されているユーザー認証の利用(その2)

ASP.NET MVC 4 での WebMatrix で提供されているユーザー認証の利用(その1)の続きです。
前回でひととおり動くものができていますが、ユーザー名の表示がメールアドレス固定となっているので、ユーザーがユーザー名を変更できるようにします。

最初に、できあがりの画面を掲載します。

最初に、ユーザー登録後にメールで送付したトークンの確認が完了したときの画面です。
WithWebMatrixAuthentication004T

次にユーザー名をクリックして表示するアカウントの管理画面です。入力項目の一番上にユーザー名の入力ボックスを配置しています。
アカウントの管理画面

パスワード変更のフォームは、もともと部分ビューで作られていますが、ユーザー名変更のフォームも部分ビューで作っています。

次にモデルの検証エラーの通知です。ちなみに、JavaScript の実行を抑止して無入力で「ユーザー名変更ボタン」をクリックしてサーバー側で検証エラーにしています。
モデルの検証エラー

次にユーザー名変更の完了通知です。
ユーザ名変更の完了通知

まず最初にユーザー名を格納する場所ですね。認証 DB 側でもいいんですけど、アプリケーションデータ格納用の DB を作ってそこにユーザー情報格納用のテーブルを設けることにします。
ということで、モデルです。Models フォルダに UserAttrib クラスを作ります。

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace WithWebMatrixAuthentication.Models
{
    public class UserAttrib
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int Id { get; set; }

        [Required]
        [Display(Name = "ユーザー名")]
        public string Name { get; set; }
    }
}

Id はユーザー登録時に採番されたユーザー ID を使うので、自動採番されないようにしています。

Name プロパティでユーザー名にアクセスするわけですが、このままだとユーザー名の重複がありえます。DB 側で UNIQUE 制約を付けたいところですが、現時点(2013/06/27)では属性で UNIQUE 制約を指定することはできなさそうです。アプリケーション側でやってもいいのですが、やっぱり DB 側でチェックするのが筋だよねぇ。。。ということで、Entity Framework Code First Migrations の機能を使って UNIQUE 制約 をセットすることにします(この話は(その3)で書きます)。マイグレーションで UNIQUE 制約 をセットするモデル変更を行うため、この時点では最大長を指定しません(なので、このあとに書く UserNameModel クラスの属性と不一致になります 😛 )。

次にデータコンテキストです。プロジェクトに DAL フォルダを作成し、DAL フォルダに AppDataContext クラスを作ります。なお、今回はリポジトリは実装しません(ユーザー名にアクセスするだけですし) 🙂

using System.Data.Entity;

using WithWebMatrixAuthentication.Models;

namespace WithWebMatrixAuthentication.DAL
{
    public class AppDataContext : DbContext
    {
        public AppDataContext() : base("AppDatabase") { }

        public DbSet<UserAttrib> UserAttribs { get; set; }
    }
}

次に、Web.Config に接続文字列を追加します。

<?xml version="1.0" encoding="utf-8"?>
~省略~
  <connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-WithWebMatrixAuthentication-20130622144212;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\aspnet-WithWebMatrixAuthentication-20130622144212.mdf" providerName="System.Data.SqlClient" />
    <add name="AppDatabase" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=AppDatabase-WithWebMatrixAuthentication-20130623144212;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\AppDatabase-WithWebMatrixAuthentication-20130623144212.mdf" providerName="System.Data.SqlClient" />
  </connectionStrings>
~省略~

名前は AppDataContext クラスで指定している AppDatabase になります。

以上で DB アクセス関係の設定ができました。Database.SetInitializer メソッドを設定していませんが、コードファーストで DB を作るだけなら無くても動きます。(その3)で Code First Migrations を使うので、モデル変更系の設定はそちらで行います。

次は、ユーザー名のハンドリングを行う部分です。先ほどの画面のとおり、AccountController のアクションで、次のような操作を行います。

  • ローカルアカウントを追加するときには、入力されたメールアドレスをユーザー名として登録
  • 外部の認証サービスで初めてログインしてきたときには、ExternalLoginConfirmation アクションで入力されたメールアドレスをユーザー名として登録
  • ユーザー名の変更は「アカウントの管理画面」で行えるようにする

まずはモデルから。Models フォルダの AccountModels.cs ファイルです。追加や変更の情報提示が煩雑になるので、すべて掲載します(以下同様)。

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;

namespace WithWebMatrixAuthentication.Models
{
    public class UsersContext : DbContext
    {
        public UsersContext()
            : base("DefaultConnection")
        {
        }

        public DbSet<UserProfile> UserProfiles { get; set; }
    }

    [Table("UserProfile")]
    public class UserProfile
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int UserId { get; set; }
        public string Email { get; set; }
    }

    public class RegisterExternalLoginModel
    {
        [Required]
        [Display(Name = "メールアドレス")]
        public string Email { get; set; }

        public string ExternalLoginData { get; set; }
    }

    public class ManageModel
    {
        public LocalPasswordModel LocalPasswordChangeModel { get; set; }
        public UserNameModel UserNameChangeModel { get; set; }
    }

    public class LocalPasswordModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "現在のパスワード")]
        public string OldPassword { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "{0} の長さは {2} 文字以上である必要があります。", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "新しいパスワード")]
        public string NewPassword { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "新しいパスワードの確認入力")]
        [Compare("NewPassword", ErrorMessage = "新しいパスワードと確認のパスワードが一致しません。")]
        public string ConfirmPassword { get; set; }
    }

    public class UserNameModel
    {
        [Required]
        [StringLength(50, ErrorMessage = "{0} の長さは {2} 文字以上である必要があります。", MinimumLength = 1)]
        [Display(Name = "新しいユーザー名")]
        public string NewUserName { get; set; }
    }

    public class LoginModel
    {
        [Required]
        [Display(Name = "メールアドレス")]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "パスワード")]
        public string Password { get; set; }

        [Display(Name = "このアカウントを記憶する")]
        public bool RememberMe { get; set; }
    }

    public class RegisterModel
    {
        [Required]
        [DataType(DataType.EmailAddress)]
        [Display(Name = "メールアドレス")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "{0} の長さは {2} 文字以上である必要があります。", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "パスワード")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "パスワードの確認入力")]
        [Compare("Password", ErrorMessage = "パスワードと確認のパスワードが一致しません。")]
        public string ConfirmPassword { get; set; }
    }

    public class ExternalLogin
    {
        public string Provider { get; set; }
        public string ProviderDisplayName { get; set; }
        public string ProviderUserId { get; set; }
    }

    public class ConfirmModel
    {
        [Required]
        [Display(Name = "確認コード")]
        public string ConfirmationCode { get; set; }
    }

    public class ForgotPasswordModel
    {
        [Required]
        [DataType(DataType.EmailAddress)]
        [Display(Name = "登録したメールアドレスを入力してください。")]
        public string Email { get; set; }
    }

    public class ResetPasswordModel
    {
        [Required]
        [StringLength(100, ErrorMessage = "{0} の長さは {2} 文字以上である必要があります。", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "パスワード")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "パスワードの確認入力")]
        [Compare("Password", ErrorMessage = "パスワードと確認のパスワードが一致しません。")]
        public string ConfirmPassword { get; set; }

        public string ResetToken { get; set; }
    }
}

次に、コントローラーです。まずは AccountController です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Transactions;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using DotNetOpenAuth.AspNet;
using Microsoft.Web.WebPages.OAuth;
using WebMatrix.WebData;

using MakCraft.SmtpOverSsl;

using WithWebMatrixAuthentication.DAL;
using WithWebMatrixAuthentication.Filters;
using WithWebMatrixAuthentication.Models;

namespace WithWebMatrixAuthentication.Controllers
{
    [Authorize]
    [InitializeSimpleMembership]
    public class AccountController : Controller
    {
        //
        // GET: /Account/Login

        [AllowAnonymous]
        public ActionResult Login(string returnUrl)
        {
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }

        //
        // POST: /Account/Login

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Login(LoginModel model, string returnUrl)
        {
            var limitNumberOfMistake = 4;
            var lockedTime = 60;

            if (ModelState.IsValid)
            {
                // (パスワードの誤入力が 4回を超えている and 前回の誤入力から 60秒以内) であれば、アカウント・ロックを表示する
                if (WebSecurity.UserExists(model.Email) &&
                    WebSecurity.GetPasswordFailuresSinceLastSuccess(model.Email) > limitNumberOfMistake &&
                    WebSecurity.GetLastPasswordFailureDate(model.Email).AddSeconds(lockedTime) > DateTime.UtcNow)
                    return RedirectToAction("AccountLockedOut");

                if (WebSecurity.Login(model.Email, model.Password, persistCookie: model.RememberMe))
                {
                    if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
                        && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
                    {
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        return RedirectToAction("Index", "Home");
                    }
                }

                // ここで問題が発生した場合はフォームを再表示します
                ModelState.AddModelError("", UserCertByEmailStatus2String(UserCertByEmailStatus.NotFoundOrDisagree));
            }

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

        //
        // GET: /Account/AccountLockedOut

        [AllowAnonymous]
        public ActionResult AccountLockedOut()
        {
            return View();
        }

        //
        // POST: /Account/LogOff

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult LogOff()
        {
            WebSecurity.Logout();

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

        //
        // GET: /Account/Register

        [AllowAnonymous]
        public ActionResult Register()
        {
            return View();
        }

        //
        // POST: /Account/Register

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                // ユーザーの登録を試みます
                try
                {
                    var requireEmailConfirmation = !string.IsNullOrEmpty(SmtpMail.ServerName);   // メール送信設定の確認
                    var token = WebSecurity.CreateUserAndAccount(model.Email, model.Password, requireConfirmationToken: requireEmailConfirmation);
                    // メールの設定ができていればメールアドレスの確認を行う。設定できていなければ /Home/Index に移動して終了
                    if (requireEmailConfirmation)
                    {
                        var hostUrl = Request.Url.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
                        var confirmationUrl = hostUrl + VirtualPathUtility.ToAbsolute("~/Account/Confirm?confirmationCode=" + HttpUtility.UrlEncode(token));
                        SnedConfirmMail(MailMode.Register, model.Email, confirmationUrl, token);
                        AddUserAttrib(WebSecurity.GetUserId(model.Email), model.Email);
                        return RedirectToAction("Thanks");
                    }
                    AddUserAttrib(WebSecurity.GetUserId(model.Email), model.Email);
                    WebSecurity.Login(model.Email, model.Password);
                    return RedirectToAction("Index", "Home");
                }
                catch (MembershipCreateUserException e)
                {
                    ModelState.AddModelError("", ErrorCodeToString(e.StatusCode));
                }
            }

            // ここで問題が発生した場合はフォームを再表示します
            return View(model);
        }

        //
        // GET: /Account/Confirm

        [AllowAnonymous]
        public ActionResult Confirm()
        {
            return View();
        }

        //
        // POST: /Account/Confirm

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Confirm(ConfirmModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            WebSecurity.Logout();

            if (WebSecurity.ConfirmAccount(model.ConfirmationCode))
            {
                TempData["Message"] = UserCertByEmailStatus2String(UserCertByEmailStatus.UserConfirmed);
            }
            else
            {
                ModelState.AddModelError("", UserCertByEmailStatus2String(UserCertByEmailStatus.UserNotConfirmed));
            }

            return View();
        }

        //
        // GET: /Account/Thanks

        [AllowAnonymous]
        public ActionResult Thanks()
        {
            return View();
        }

        //
        // POST: /Account/Disassociate

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Disassociate(string provider, string providerUserId)
        {
            string ownerAccount = OAuthWebSecurity.GetUserName(provider, providerUserId);
            ManageMessageId? message = null;

            // 現在ログインしているユーザーが所有者の場合にのみ、アカウントの関連付けを解除します
            if (ownerAccount == User.Identity.Name)
            {
                // トランザクションを使用して、ユーザーが最後のログイン資格情報を削除しないようにします
                using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.Serializable }))
                {
                    bool hasLocalAccount = OAuthWebSecurity.HasLocalAccount(WebSecurity.GetUserId(User.Identity.Name));
                    if (hasLocalAccount || OAuthWebSecurity.GetAccountsFromUserName(User.Identity.Name).Count > 1)
                    {
                        OAuthWebSecurity.DeleteAccount(provider, providerUserId);
                        scope.Complete();
                        message = ManageMessageId.RemoveLoginSuccess;
                    }
                }
            }

            return RedirectToAction("Manage", new { Message = message });
        }

        //
        // GET: /Account/Manage

        public ActionResult Manage(ManageMessageId? message)
        {
            ViewBag.StatusMessage =
                message == ManageMessageId.ChangePasswordSuccess ? "パスワードが変更されました。"
                : message == ManageMessageId.SetPasswordSuccess ? "パスワードが設定されました。"
                : message == ManageMessageId.RemoveLoginSuccess ? "外部ログインが削除されました。"
                : message == ManageMessageId.ChangeUserNameSuccess ? "ユーザー名が変更されました。"
                : "";
            ViewBag.HasLocalPassword = OAuthWebSecurity.HasLocalAccount(WebSecurity.GetUserId(User.Identity.Name));
            ViewBag.ReturnUrl = Url.Action("Manage");
            return View();
        }

        //
        // POST: /Account/Manage

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Manage(ManageModel model)
        {
            bool hasLocalAccount = OAuthWebSecurity.HasLocalAccount(WebSecurity.GetUserId(User.Identity.Name));
            ViewBag.HasLocalPassword = hasLocalAccount;
            ViewBag.ReturnUrl = Url.Action("Manage");
            if (hasLocalAccount)
            {
                if (ModelState.IsValid)
                {
                    // 特定のエラー シナリオでは、ChangePassword は false を返す代わりに例外をスローします。
                    bool changePasswordSucceeded;
                    try
                    {
                        changePasswordSucceeded = 
                            WebSecurity.ChangePassword(User.Identity.Name, model.LocalPasswordChangeModel.OldPassword, model.LocalPasswordChangeModel.NewPassword);
                    }
                    catch (Exception)
                    {
                        changePasswordSucceeded = false;
                    }

                    if (changePasswordSucceeded)
                    {
                        return RedirectToAction("Manage", new { Message = ManageMessageId.ChangePasswordSuccess });
                    }
                    else
                    {
                        ModelState.AddModelError("", UserCertByEmailStatus2String(UserCertByEmailStatus.CouldNotChangePassword));
                    }
                }
            }
            else
            {
                // ユーザーにローカル パスワードがないため、
                //OldPassword フィールドがないことに原因があるすべての検証エラーを削除します
                ModelState state = ModelState["LocalPasswordChangeModel.OldPassword"];
                if (state != null)
                {
                    state.Errors.Clear();
                }

                if (ModelState.IsValid)
                {
                    try
                    {
                        var requireEmailConfirmation = !string.IsNullOrEmpty(SmtpMail.ServerName);
                        var token = WebSecurity.CreateAccount(User.Identity.Name, 
                            model.LocalPasswordChangeModel.NewPassword, requireEmailConfirmation);
                        if (requireEmailConfirmation)
                        {
                            var hostUrl = Request.Url.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
                            var confirmationUrl = 
                                hostUrl + VirtualPathUtility.ToAbsolute("~/Account/Confirm?confirmationCode=" + HttpUtility.UrlEncode(token));
                            SnedConfirmMail(MailMode.Register, User.Identity.Name, confirmationUrl, token);
                            return RedirectToAction("Thanks");
                        }
                        return RedirectToAction("Manage", new { Message = ManageMessageId.SetPasswordSuccess });
                    }
                    catch (Exception)
                    {
                        ModelState.AddModelError("", String.Format(UserCertByEmailStatus2String(UserCertByEmailStatus.UnableCreateAccount), User.Identity.Name));
                    }
                }
            }

            // ここで問題が発生した場合はフォームを再表示します
            return View(model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult ChangeUserName(ManageModel model)
        {
            bool hasLocalAccount = OAuthWebSecurity.HasLocalAccount(WebSecurity.GetUserId(User.Identity.Name));
            ViewBag.HasLocalPassword = hasLocalAccount;

            if (ModelState.IsValid)
            {
                using (var context = new AppDataContext())
                {
                    var userAttrib = context.UserAttribs.Find(WebSecurity.GetUserId(User.Identity.Name));
                    userAttrib.Name = model.UserNameChangeModel.NewUserName;
                    context.SaveChanges();
                }
                return RedirectToAction("Manage", new { Message = ManageMessageId.ChangeUserNameSuccess });
            }

            // ここで問題が発生した場合はフォームを再表示します
            return View("Manage", model);
        }

        // GET: //Account/ForgotPassword

        [AllowAnonymous]
        public ActionResult ForgotPassword()
        {
            return View();
        }

        //
        // POST: /Account/ForgotPassword

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult ForgotPassword(ForgotPasswordModel model)
        {

            // サーバー側のメール送信設定の確認
            if (!ValidateEmailSetting())
            {
                return RedirectToAction("Error");
            }

            if (!ModelState.IsValid)
            {
                return View(model);
            }

            var resetToken = "";
            if (WebSecurity.GetUserId(model.Email) > -1 && WebSecurity.IsConfirmed(model.Email))
            {
                resetToken = WebSecurity.GeneratePasswordResetToken(model.Email);   // 必要に応じてトークンの有効期限を指定します
            }

            var hostUrl = Request.Url.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
            var resetUrl = hostUrl + VirtualPathUtility.ToAbsolute("~/Account/PasswordReset?resetToken=" + HttpUtility.UrlEncode(resetToken));

            SnedConfirmMail(MailMode.Forgot, model.Email, resetUrl, resetToken);
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            TempData["title"] = "メールを送りました";
            TempData["message"] = "パスワードリセットに必要な情報をメールで送りました。";
            return RedirectToAction("Done");
        }

        //
        // GET: /Account/Done

        [AllowAnonymous]
        public ActionResult Done()
        {
            return View();
        }

        //
        // GET: /Account/Error

        [AllowAnonymous]
        public ActionResult Error()
        {
            return View();
        }


        // GET: /Account/PasswordReset

        [AllowAnonymous]
        public ActionResult PasswordReset(string resetToken)
        {
            return View();
        }

        //
        // POST: /Account/PasswordReset


        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult PasswordReset(ResetPasswordModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            // サーバー側のメール送信設定の確認
            if (!ValidateEmailSetting())
            {
                return RedirectToAction("Error");
            }

            if (!WebSecurity.ResetPassword(model.ResetToken, model.Password))
            {
                TempData["message"] = UserCertByEmailStatus2String(UserCertByEmailStatus.ResetTokenError);
                return RedirectToAction("Error");
            }

            TempData["title"] = "パスワードをリセットしました";
            TempData["message"] = "新しいパスワードでログオンしてください。";
            return RedirectToAction("Done");
        }

        //
        // POST: /Account/ExternalLogin

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult ExternalLogin(string provider, string returnUrl)
        {
            return new ExternalLoginResult(provider, Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
        }

        //
        // GET: /Account/ExternalLoginCallback

        [AllowAnonymous]
        public ActionResult ExternalLoginCallback(string returnUrl)
        {
            AuthenticationResult result = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
            if (!result.IsSuccessful)
            {
                return RedirectToAction("ExternalLoginFailure");
            }

            if (OAuthWebSecurity.Login(result.Provider, result.ProviderUserId, createPersistentCookie: false))
            {
                return RedirectToLocal(returnUrl);
            }

            if (User.Identity.IsAuthenticated)
            {
                // 現在のユーザーがログインしている場合、新しいアカウントを追加します
                OAuthWebSecurity.CreateOrUpdateAccount(result.Provider, result.ProviderUserId, User.Identity.Name);
                return RedirectToLocal(returnUrl);
            }
            else
            {
                // 新規ユーザーの場合は、既定のユーザー名を外部ログイン プロバイダーから取得した値に設定します
                string loginData = OAuthWebSecurity.SerializeProviderUserId(result.Provider, result.ProviderUserId);
                ViewBag.ProviderDisplayName = OAuthWebSecurity.GetOAuthClientData(result.Provider).DisplayName;
                ViewBag.ReturnUrl = returnUrl;
                return View("ExternalLoginConfirmation", new RegisterExternalLoginModel { Email = result.UserName, ExternalLoginData = loginData });
            }
        }

        //
        // POST: /Account/ExternalLoginConfirmation

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult ExternalLoginConfirmation(RegisterExternalLoginModel model, string returnUrl)
        {
            string provider = null;
            string providerUserId = null;

            if (User.Identity.IsAuthenticated || !OAuthWebSecurity.TryDeserializeProviderUserId(model.ExternalLoginData, out provider, out providerUserId))
            {
                return RedirectToAction("Manage");
            }

            if (ModelState.IsValid)
            {
                // データベースに新しいユーザーを挿入します
                using (UsersContext db = new UsersContext())
                {
                    UserProfile user = db.UserProfiles.FirstOrDefault(u => u.Email.ToLower() == model.Email.ToLower());
                    // ユーザーが既に存在するかどうかを確認します
                    if (user == null)
                    {
                        // プロファイル テーブルに名前を挿入します
                        db.UserProfiles.Add(new UserProfile { Email = model.Email });
                        db.SaveChanges();

                        OAuthWebSecurity.CreateOrUpdateAccount(provider, providerUserId, model.Email);
                        OAuthWebSecurity.Login(provider, providerUserId, createPersistentCookie: false);

                        AddUserAttrib(WebSecurity.GetUserId(model.Email), model.Email);

                        return RedirectToLocal(returnUrl);
                    }
                    else
                    {
                        ModelState.AddModelError("Email", UserCertByEmailStatus2String(UserCertByEmailStatus.DuplicateMailAddress));
                    }
                }
            }

            ViewBag.ProviderDisplayName = OAuthWebSecurity.GetOAuthClientData(provider).DisplayName;
            ViewBag.ReturnUrl = returnUrl;
            return View(model);
        }

        //
        // GET: /Account/ExternalLoginFailure

        [AllowAnonymous]
        public ActionResult ExternalLoginFailure()
        {
            return View();
        }

        [AllowAnonymous]
        [ChildActionOnly]
        public ActionResult ExternalLoginsList(string returnUrl)
        {
            ViewBag.ReturnUrl = returnUrl;
            return PartialView("_ExternalLoginsListPartial", OAuthWebSecurity.RegisteredClientData);
        }

        [ChildActionOnly]
        public ActionResult RemoveExternalLogins()
        {
            ICollection<OAuthAccount> accounts = OAuthWebSecurity.GetAccountsFromUserName(User.Identity.Name);
            List<ExternalLogin> externalLogins = new List<ExternalLogin>();
            foreach (OAuthAccount account in accounts)
            {
                AuthenticationClientData clientData = OAuthWebSecurity.GetOAuthClientData(account.Provider);

                externalLogins.Add(new ExternalLogin
                {
                    Provider = account.Provider,
                    ProviderDisplayName = clientData.DisplayName,
                    ProviderUserId = account.ProviderUserId,
                });
            }

            ViewBag.ShowRemoveButton = externalLogins.Count > 1 || OAuthWebSecurity.HasLocalAccount(WebSecurity.GetUserId(User.Identity.Name));
            return PartialView("_RemoveExternalLoginsPartial", externalLogins);
        }

        #region ヘルパー
        private ActionResult RedirectToLocal(string returnUrl)
        {
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }

        private enum MailMode
        {
            Register,
            Forgot,
        }

        private void SnedConfirmMail(MailMode mode, string toAddress, string confirmationUrl, string token)
        {
            try
            {
                var subject = "";
                var body = "";
                switch (mode)
                {
                    case MailMode.Register:
                        subject = "アカウントを確認してください";
                        body = "確認コード: " + token + "\r\n" +
                            "<a href=\"" + confirmationUrl + "\">" + confirmationUrl + "</a> にアクセスしてアカウントを有効にしてください.";
                        break;
                    case MailMode.Forgot:
                        subject = "パスワードをリセットしてください";
                        body = "パスワードをリセットするには、このパスワード リセット トークンを使用します。トークン:" + token + "\r\n" +
                            "パスワード・リセットを行うページ <" + confirmationUrl + "> " +
                            "を訪問して、パスワードのリセットを行ってください。";
                        break;
                    default:
                        throw new ApplicationException(UserCertByEmailStatus2String(UserCertByEmailStatus.UnknownMailMode));
                }

                var mailMessage = new SmtpMailMessage
                {
                    StringFrom = "mak@hosibune.com",
                    StringTo = toAddress,
                    Subject = subject,
                    Body = body
                };
                SmtpMail.Send(mailMessage);
            }
            catch (ApplicationException ex)
            {
                ModelState.AddModelError("", UserCertByEmailStatus2String(UserCertByEmailStatus.FailedSendMail) + ex.Message);
            }
            catch (System.Net.Sockets.SocketException ex)
            {
                ModelState.AddModelError("", UserCertByEmailStatus2String(UserCertByEmailStatus.CannotConnectMailServer) + ex.Message);
            }
            catch (System.Security.Authentication.AuthenticationException ex)
            {
                ModelState.AddModelError("", UserCertByEmailStatus2String(UserCertByEmailStatus.FailedSslConnection) + ex.Message);
            }
            catch (System.Web.Security.MembershipCreateUserException e)
            {
                ModelState.AddModelError("", e.ToString());
            }
        }

        private enum UserCertByEmailStatus
        {
            CannotConnectMailServer,
            CouldNotChangePassword,
            DuplicateMailAddress,
            FailedSendMail,
            FailedSslConnection,
            NotFoundOrDisagree,
            ResetTokenError,
            ServerSetUpError,
            UnableCreateAccount,
            UnknownMailMode,
            UserConfirmed,
            UserNotConfirmed,
        }

        private static string UserCertByEmailStatus2String(UserCertByEmailStatus status)
        {
            switch (status)
            {
                case UserCertByEmailStatus.CannotConnectMailServer:
                    return "メール送信サーバーと接続ができませんでした。エラー内容: ";
                case UserCertByEmailStatus.CouldNotChangePassword:
                    return "現在のパスワードが正しくないか、新しいパスワードが無効です。";
                case UserCertByEmailStatus.DuplicateMailAddress:
                    return "このメールアドレスは既に存在します。当サイトへの登録済み認証手段でログインしてから他サイトアカウントとの関連付けを行なってください。";
                case UserCertByEmailStatus.FailedSendMail:
                    return "メールが送れませんでした。エラー内容: ";
                case UserCertByEmailStatus.FailedSslConnection:
                    return "メールサーバーと SSL 接続ができませんでした。エラー内容: ";
                case UserCertByEmailStatus.NotFoundOrDisagree:
                    return "指定されたユーザー名またはパスワードが正しくありません。";
                case UserCertByEmailStatus.ResetTokenError:
                    return "パスワード リセット トークンが間違っています。または、有効期限が切れている可能性があります。再度パスワードのリセットを行なってみてください。";
                case UserCertByEmailStatus.ServerSetUpError:
                    return "SMTP サーバーが適切に構成されていないため、この Web サイトではパスワードの回復が無効です。パスワードをリセットするには、このサイトの所有者に連絡してください。";
                case UserCertByEmailStatus.UnableCreateAccount:
                    return "ローカル アカウントを作成できません。名前 \"{0}\" のアカウントは既に存在している可能性があります。";
                case UserCertByEmailStatus.UnknownMailMode:
                    return "不明な MailMode が指定されています。";
                case UserCertByEmailStatus.UserConfirmed:
                    return "登録を確認しました。[ログイン] をクリックしてサイトにログインしてください。初期状態ではメールアドレスがユーザー名になっています。ユーザー名をクリックすると変更することができます。";
                case UserCertByEmailStatus.UserNotConfirmed:
                    return "登録情報を確認できませんでした。";

                default:
                    return "不明なエラーが発生しました。入力を確認してやり直してください。問題が解決しない場合は、システム管理者に連絡してください。";
            }
        }

        private bool ValidateEmailSetting()
        {
            if (string.IsNullOrEmpty(SmtpMail.ServerName))
            {
                TempData["message"] = UserCertByEmailStatus2String(UserCertByEmailStatus.ServerSetUpError);
                return false;
            }

            return true;
        }

        private void AddUserAttrib(int userId, string userName)
        {
            using (var context = new AppDataContext())
            {
                context.UserAttribs.Add(new UserAttrib { Id = userId, Name = userName });
                context.SaveChanges();
            }
        }

        public enum ManageMessageId
        {
            ChangePasswordSuccess,
            SetPasswordSuccess,
            RemoveLoginSuccess,
            ChangeUserNameSuccess,
        }

        internal class ExternalLoginResult : ActionResult
        {
            public ExternalLoginResult(string provider, string returnUrl)
            {
                Provider = provider;
                ReturnUrl = returnUrl;
            }

            public string Provider { get; private set; }
            public string ReturnUrl { get; private set; }

            public override void ExecuteResult(ControllerContext context)
            {
                OAuthWebSecurity.RequestAuthentication(Provider, ReturnUrl);
            }
        }

        private static string ErrorCodeToString(MembershipCreateStatus createStatus)
        {
            // すべてのステータス コードの一覧については、http://go.microsoft.com/fwlink/?LinkID=177550 を
            // 参照してください。
            switch (createStatus)
            {
                case MembershipCreateStatus.DuplicateUserName:
                    return "このユーザー名は既に存在します。別のユーザー名を入力してください。";

                case MembershipCreateStatus.DuplicateEmail:
                    return "その電子メール アドレスのユーザー名は既に存在します。別の電子メール アドレスを入力してください。";

                case MembershipCreateStatus.InvalidPassword:
                    return "指定されたパスワードは無効です。有効なパスワードの値を入力してください。";

                case MembershipCreateStatus.InvalidEmail:
                    return "指定された電子メール アドレスは無効です。値を確認してやり直してください。";

                case MembershipCreateStatus.InvalidAnswer:
                    return "パスワードの回復用に指定された回答が無効です。値を確認してやり直してください。";

                case MembershipCreateStatus.InvalidQuestion:
                    return "パスワードの回復用に指定された質問が無効です。値を確認してやり直してください。";

                case MembershipCreateStatus.InvalidUserName:
                    return "指定されたユーザー名は無効です。値を確認してやり直してください。";

                case MembershipCreateStatus.ProviderError:
                    return "認証プロバイダーからエラーが返されました。入力を確認してやり直してください。問題が解決しない場合は、システム管理者に連絡してください。";

                case MembershipCreateStatus.UserRejected:
                    return "ユーザーの作成要求が取り消されました。入力を確認してやり直してください。問題が解決しない場合は、システム管理者に連絡してください。";

                default:
                    return "不明なエラーが発生しました。入力を確認してやり直してください。問題が解決しない場合は、システム管理者に連絡してください。";
            }
        }
        #endregion
    }
}

次に HomeController です。ユーザーがログインした状態で、サーバーが再起動されたときに、ユーザー名表示のために認証システムにアクセスしようとすると、認証システムのイニシャライズが未実施という例外が発生するので、HomeController へのアクセス時にもイニシャライザが走るようにします。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

using WithWebMatrixAuthentication.Filters;

namespace WithWebMatrixAuthentication.Controllers
{
    [InitializeSimpleMembership]
    public class HomeController : Controller
    {
~省略~

次に、ビューでユーザー名を表示するための HTML ヘルパーを作ります。プロジェクトに「Helpers」フォルダを作り、Helpers フォルダに UserNameExtensions クラスを作成します。

using System.Web.Mvc;
using WebMatrix.WebData;

using WithWebMatrixAuthentication.DAL;

namespace WithWebMatrixAuthentication.Helpers
{
    public static class UserNameExtensions
    {
        public static string UserName(this HtmlHelper helper)
        {
            var userName = "";

            using (var context = new AppDataContext())
            {
                userName = context.UserAttribs.Find(WebSecurity.CurrentUserId).Name;
            }

            return userName;
        }
    }
}

次にビューですが、個別のビューに行く前に、作成した HTML ヘルパーへの参照を設定します。Views フォルダにある Web.config に設定します。

<?xml version="1.0" encoding="utf-8"?>

<configuration>
~省略~
  <system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Optimization"/>
        <add namespace="System.Web.Routing" />
        <add namespace="WithWebMatrixAuthentication.Helpers" />
      </namespaces>
    </pages>
  </system.web.webPages.razor>

namespaces タグの一番下に作成した Helpers フォルダへの参照を追加しています。

それでは、変更・追加するビューです。
まずは Shared フォルダの _LoginPartial.cshtml から

@if (Request.IsAuthenticated) {
    <text>
        こんにちは、@Html.ActionLink(@Html.UserName(), "Manage", "Account", routeValues: null, htmlAttributes: new { @class = "username", title = "管理" })!
        @using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm" })) {
            @Html.AntiForgeryToken()
            <a href="javascript:document.getElementById('logoutForm').submit()">ログオフ</a>
        }
    </text>
} else {
    <ul>
        <li>@Html.ActionLink("登録", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
        <li>@Html.ActionLink("ログイン", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
    </ul>
}

ログイン済みユーザーでのユーザー名表示の部分を変更しています。

次は Account フォルダです。
ExternalLoginConfirmation.cshtml

@model WithWebMatrixAuthentication.Models.RegisterExternalLoginModel
@{
    ViewBag.Title = "登録";
}

<hgroup class="title">
    <h1>@ViewBag.Title</h1>
    <h2>@ViewBag.ProviderDisplayName アカウントを関連付けます。</h2>
</hgroup>

@using (Html.BeginForm("ExternalLoginConfirmation", "Account", new { ReturnUrl = ViewBag.ReturnUrl })) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>関連付けフォーム</legend>
        <p>
            <strong> アカウント@{@ViewBag.ProviderDisplayName}</strong> によって正常に認証されました。
次の入力欄にメールアドレスを入力し、[確認] ボタンを押して、ログインを完了してください。
        </p>
        <p>このメールアドレスは、ローカル認証のパスワードを設定するときに、確認コードと確認のアクセス先 URL の送付先になります。</p>
        <p>初期状態ではメールアドレスがユーザー名になっています。ユーザー名をクリックすると変更することができます。</p>
        <ol>
            <li class="name">
                @Html.LabelFor(m => m.Email)
                @Html.TextBoxFor(m => m.Email)
                @Html.ValidationMessageFor(m => m.Email)
            </li>
        </ol>
        @Html.HiddenFor(m => m.ExternalLoginData)
        <input type="submit" value="登録" />
    </fieldset>
}

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

_ChangePasswordPartial.cshtml

@model WithWebMatrixAuthentication.Models.ManageModel

<h3>パスワードの変更</h3>

@using (Html.BeginForm("Manage", "Account")) {
    @Html.AntiForgeryToken()

    <fieldset>
        <legend>パスワードの変更フォーム</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.LocalPasswordChangeModel.OldPassword)
                @Html.PasswordFor(m => m.LocalPasswordChangeModel.OldPassword)
            </li>
            <li>
                @Html.LabelFor(m => m.LocalPasswordChangeModel.NewPassword)
                @Html.PasswordFor(m => m.LocalPasswordChangeModel.NewPassword)
                @Html.ValidationMessageFor(m => m.LocalPasswordChangeModel.NewPassword)
            </li>
            <li>
                @Html.LabelFor(m => m.LocalPasswordChangeModel.ConfirmPassword)
                @Html.PasswordFor(m => m.LocalPasswordChangeModel.ConfirmPassword)
                @Html.ValidationMessageFor(m => m.LocalPasswordChangeModel.ConfirmPassword)
            </li>
        </ol>
        <input type="submit" value="パスワードの変更" />
    </fieldset>
}

Manage.cshtml

@model WithWebMatrixAuthentication.Models.ManageModel
@{
    ViewBag.Title = "アカウントの管理";
}

<hgroup class="title">
    <h1>@ViewBag.Title</h1>
</hgroup>

<p class="message-success">@ViewBag.StatusMessage</p>

<p><strong>@Html.UserName()</strong> としてログインしています。</p>

@Html.ValidationSummary()

@Html.Partial("_ChangeUserNamePartial")

@if (ViewBag.HasLocalPassword)
{
    @Html.Partial("_ChangePasswordPartial")
}
else
{ 
    @Html.Partial("_SetPasswordPartial")
}

<section id="externalLogins">
    @Html.Action("RemoveExternalLogins")

    <h3>外部ログインの追加</h3>
    @Html.Action("ExternalLoginsList", new { ReturnUrl = ViewBag.ReturnUrl })
</section>

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

_SetPasswordPartial.cshtml

@model WithWebMatrixAuthentication.Models.ManageModel

<p>
    このサイトのローカル パスワードがありません。外部ログイン
なしでログインできるように、ローカル パスワードを追加してください。
</p>

@using (Html.BeginForm("Manage", "Account")) {
    @Html.AntiForgeryToken()

    <fieldset>
        <legend>パスワードの設定フォーム</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.LocalPasswordChangeModel.NewPassword)
                @Html.PasswordFor(m => m.LocalPasswordChangeModel.NewPassword)
                @Html.ValidationMessageFor(m => m.LocalPasswordChangeModel.NewPassword)
            </li>
            <li>
                @Html.LabelFor(m => m.LocalPasswordChangeModel.ConfirmPassword)
                @Html.PasswordFor(m => m.LocalPasswordChangeModel.ConfirmPassword)
                @Html.ValidationMessageFor(m => m.LocalPasswordChangeModel.ConfirmPassword)
            </li>
        </ol>
        <input type="submit" value="パスワードの設定" />
    </fieldset>
}

_ChangeUserNamePartial.cshtml

@model WithWebMatrixAuthentication.Models.ManageModel

<h3>ユーザー名変更</h3>

<p>ユーザー名を変更する場合は新しいユーザー名を入力してから [ユーザー名変更] ボタンをクリックしてください。</p>

@using (Html.BeginForm("ChangeUserName", "Account"))
{
    @Html.AntiForgeryToken()

    <fieldset>
        <legend>ユーザー名の変更フォーム</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.UserNameChangeModel.NewUserName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.UserNameChangeModel.NewUserName)
            @Html.ValidationMessageFor(model => model.UserNameChangeModel.NewUserName)
        </div>

        <p>
            <input type="submit" value="ユーザー名変更" />
        </p>
    </fieldset>
}

以上でユーザー名の設定・表示・変更を行うことができるようになります。

次回は、ユーザー名の変更の際にユーザー名の重複チェックを行い(実際にチェックするのは DB 側ですが)、既に同じユーザー名が登録されていたらエラー表示を行うようにします。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です