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

以前 ASP.NET MVC 3 で WebMatrix で提供されているユーザー認証を利用する話を書きましたが、MVC 4 では OpenID や OAuth を用いた外部の認証サービスへの対応が実装されるなどいろいろと変わっている点があるので、MVC 4 での WebMatrix で提供されているユーザー認証の利用について書いてみます。最初に WebMatrix で提供されているユーザー認証を利用するメリットを書いておきます。

  • ユーザー登録時に提示されたメールアドレスの有効性を検証したあとにユーザーが有効化される
  • ユーザーがパスワードを忘れた時に、トークンをメールで送り、パスワード設定の際にトークンを確認することで安全なパスワードの再設定が行える

デメリットしては、

  • ユーザー名の設定項目がないため、ユーザー名としてメールアドレスを表示せざるを得ない

ということがありますが、これはアプリケーション側で回避することも可能です(「その2」で対応させる予定です)。

MVC 3 の時との変更点は次のようなものになります。

  • 認証用の DB について、 WebMatrix.WebData.WebSecurity.InitializeDatabaseConnection メソッド でメンバーシップ システムの初期化が行えるようになっている(データベース自体は既に存在している必要がある)
  • OpenID や OAuth を用いた外部の認証サービスへの対応

まずは、新しいプロジェクトで ASP.NETMVC 4 Web アプリケーションを作成します(プロジェクト名は「WithWebMatrixAuthentication」としました)。 プロジェクト テンプレートは「インターネット アプリケーション」を選択します。

メールの送信を行う必要があるので、SmtpOverSsl.dll を使います。ソリューション エクスプローラーの「参照設定」を右クリックして DLL への参照を設定しておいてください。

認証で使用する DB ですが、テスト用ということで SQL Server Compact な DB にしたいところですが、ユーザー ID(メールアドレス)と外部の認証サービスとの関連付けを削除する処理でトランザクション スコープを利用していて、そのスコープ内で複数の DB 接続が発生することから「SQL Server Compact では分散トランザクションはサポートされていません。そのため、ローカル トランザクションを、自動的に完全な分散トランザクションに昇格させることはできません。」という SQL Server Compact の制限で利用できません :mrgreen: このため、接続文字列は、自動生成されたものをそのまま使います(Visual Studio Express 2012 for Web では SQL Server LocalDB への接続になります。DB 自体は App_Data フォルダに自動作成されますが、この DB を削除する際には SQL Server 2012 Management Studio で LocalDB へ接続し、オブジェクト エクスプローラーで選択して削除してください。エクスプローラーでファイルだけを削除してしまうと管理情報が残っているので、アプリケーションの利用者認証関係が動かなくなります :mrgreen: )。

それでは、ソースを編集していきます。まずは外部の認証サービスのテスト用に Google 用のクライアントを活性化させます。App_Start フォルダの AuthConfig.cs を修正します。

using Microsoft.Web.WebPages.OAuth;

namespace WithWebMatrixAuthentication
{
    public static class AuthConfig
    {
        public static void RegisterAuth()
        {
            // このサイトのユーザーが、Microsoft、Facebook、および Twitter などの他のサイトのアカウントを使用してログインできるようにするには、
            // このサイトを更新する必要があります。詳細については、http://go.microsoft.com/fwlink/?LinkID=252166 を参照してください

            //OAuthWebSecurity.RegisterMicrosoftClient(
            //    clientId: "",
            //    clientSecret: "");

            //OAuthWebSecurity.RegisterTwitterClient(
            //    consumerKey: "",
            //    consumerSecret: "");

            //OAuthWebSecurity.RegisterFacebookClient(
            //    appId: "",
            //    appSecret: "");

            OAuthWebSecurity.RegisterGoogleClient();
        }
    }
}

次に認証用のモデルを修正します。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 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 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; }
    }
}

次に認証用 DB の初期化のコードを修正します。Filters フォルダの InitializeSimpleMembershipAttribute.cs です。

using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Threading;
using System.Web.Mvc;
using WebMatrix.WebData;
using WithWebMatrixAuthentication.Models;

using MakCraft.SmtpOverSsl;

namespace WithWebMatrixAuthentication.Filters
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public sealed class InitializeSimpleMembershipAttribute : ActionFilterAttribute
    {
        private static SimpleMembershipInitializer _initializer;
        private static object _initializerLock = new object();
        private static bool _isInitialized;

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            // アプリを起動するごとに ASP.NET Simple Membership が一度だけ初期化されるようにします
            LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);
        }

        private class SimpleMembershipInitializer
        {
            public SimpleMembershipInitializer()
            {
                Database.SetInitializer<UsersContext>(null);

                try
                {
                    using (var context = new UsersContext())
                    {
                        if (!context.Database.Exists())
                        {
                            // Entity Framework 移行スキーマを使用しないで SimpleMembership データベースを作成します
                            ((IObjectContextAdapter)context).ObjectContext.CreateDatabase();
                        }
                    }

                    WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "Email", autoCreateTables: true);
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException("ASP.NET Simple Membership データベースを初期化できませんでした。詳細については、http://go.microsoft.com/fwlink/?LinkId=256588 を参照してください。", ex);
                }

                //SmtpMail.ServerName = "SMTP サーバーの FQDN";
                //SmtpMail.ServerPort = SMTP サーバーのポート番号;
                //SmtpMail.EnableSsl = SSL を利用するか;
                //SmtpMail.AuthUserName = "SMTP 接続の認証用 ID";
                //SmtpMail.AuthPassword = "SMTP 接続の認証用パスワード";
                //SmtpMail.AuthMethod = SMTP 接続の認証方式;
                //SmtpMail.MailEncoding = "ISO-2022-JP";
            }
        }
    }
}

コメントアウトしている部分は、利用する SMTP サーバーの利用設定に合わせて設定して有効化してください。

次にコントローラーです。Controllers フォルダの AccountController.cs です。

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 WithWebMatrixAuthentication.Filters;
using WithWebMatrixAuthentication.Models;

using MakCraft.SmtpOverSsl;

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);
                        return RedirectToAction("Thanks");
                    }
                    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 ? "外部ログインが削除されました。"
                : "";
            ViewBag.HasLocalPassword = OAuthWebSecurity.HasLocalAccount(WebSecurity.GetUserId(User.Identity.Name));
            ViewBag.ReturnUrl = Url.Action("Manage");
            return View();
        }

        //
        // POST: /Account/Manage

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Manage(LocalPasswordModel 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.OldPassword, model.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["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.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);
        }

        // 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);

                        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;
        }

        public enum ManageMessageId
        {
            ChangePasswordSuccess,
            SetPasswordSuccess,
            RemoveLoginSuccess,
        }

        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
    }
}

次にビューです。最初に、ここまでのソースでビルドしておきます。
まず、エラー表示用のビューから。Views フォルダ下の Shared フォルダの Error.cshtml です。エラーメッセージ表示用のフォールドを確保します。

@model System.Web.Mvc.HandleErrorInfo

@{
    ViewBag.Title = "エラー";
}

<hgroup class="title">
    <h1 class="error">エラー。</h1>
    <h2 class="error">要求の処理中にエラーが発生しました。</h2>
</hgroup>

<p>@TempData["message"]</p>

次に認証関係のビューです。場所は Views フォルダ下の Account フォルダです。
_ChangePasswordPartial.cshtml

@model WithWebMatrixAuthentication.Models.LocalPasswordModel

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

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

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

_SetPasswordPartial.cshtml

@model WithWebMatrixAuthentication.Models.LocalPasswordModel

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

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

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

AccountLockedOut.cshtml

@{
    ViewBag.Title = "アカウントのロック";
}

<h2>無効なログインが何度も試行されたため、アカウントがロックアウトされました。</h2>

<p>
    アカウントは 60 秒後に自動的にロック解除されます。
    60 秒経過したら再試行してください。
</p>

Confirm.cshtml

@model WithWebMatrixAuthentication.Models.ConfirmModel

@{
    ViewBag.Title = "アカウントの確認";
}

<h2>アカウントの確認</h2>

<div style="color:Red; margin-bottom:2em; font-size:14pt;">@TempData["Message"]</div>

@{
    if (TempData["Message"] == null)
    {
        using (Html.BeginForm()) {
            @Html.AntiForgeryToken()
            @Html.ValidationSummary(true)

            <fieldset>
                <legend>確認コード</legend>

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

                <p>
                    <input type="submit" value="確認" />
                </p>
            </fieldset>
        }
    }
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

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

Done.cshtml

@{
    ViewBag.Title = TempData["title"];
}

<h2>@TempData["title"]</h2>

<p>@Html.Encode(TempData["message"])</p>

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>
        <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")
}

ExternalLoginFailure.cshtml

@{
    ViewBag.Title = "ログインに失敗しました";
}

<hgroup class="title">
    <h1>@ViewBag.Title</h1>
    <h2>サービスによるログインに失敗しました。</h2>
</hgroup>

ForgotPassword.cshtml

@model WithWebMatrixAuthentication.Models.ForgotPasswordModel

@{
    ViewBag.Title = "パスワードのリセット";
}

<h2>パスワードのリセット</h2>

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>パスワード・リセット</legend>

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

        <p>
            <input type="submit" value="リセット" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

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

Login.cshtml

@model WithWebMatrixAuthentication.Models.LoginModel

@{
    ViewBag.Title = "ログイン";
}

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

<section id="loginForm">
<h2>ローカル アカウントを使用してログインします。</h2>
@using (Html.BeginForm(new { ReturnUrl = ViewBag.ReturnUrl })) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>ログイン フォーム</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.Email)
                @Html.TextBoxFor(m => m.Email)
                @Html.ValidationMessageFor(m => m.Email)
            </li>
            <li>
                @Html.LabelFor(m => m.Password)
                @Html.PasswordFor(m => m.Password)
                @Html.ValidationMessageFor(m => m.Password)
            </li>
            <li>
                @Html.CheckBoxFor(m => m.RememberMe)
                @Html.LabelFor(m => m.RememberMe, new { @class = "checkbox" })
            </li>
        </ol>
        <input type="submit" value="ログイン" />
    </fieldset>
    <p>
        アカウントをお持ちでない場合は、@Html.ActionLink("登録", "Register")してください。
        @Html.ActionLink("パスワードを忘れた場合", "ForgotPassword")
    </p>
}
</section>

<section class="social" id="socialLoginForm">
    <h2>別のサービスを使用してログインしてください。</h2>
    @Html.Action("ExternalLoginsList", new { ReturnUrl = ViewBag.ReturnUrl })
</section>

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

Manage.cshtml

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

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

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

<p><strong>@User.Identity.Name</strong> としてログインしています。</p>

@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")
}

PasswordReset.cshtml

@model WithWebMatrixAuthentication.Models.ResetPasswordModel

@{
    ViewBag.Title = "パスワードの再設定";
}

<h2>パスワードの再設定</h2>

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>パスワード・再設定

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

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

        <div class="editor-field">
            @Html.HiddenFor(model => model.ResetToken)
        </div>

        <p>
            <input type="submit" value="再設定" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

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

Register.cshtml

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

<hgroup class="title">
    <h1>@ViewBag.Title</h1>
    <h2>新しいアカウントを作成します。</h2>
</hgroup>

<p>入力されたメールアドレス宛にメールアドレス確認のために、確認コードと確認のアクセス先 URL をお送りします。</p>

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary()

    <fieldset>
        <legend>登録フォーム</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.Email)
                @Html.TextBoxFor(m => m.Email)
                @Html.ValidationMessageFor(m => m.Email)
            </li>
            <li>
                @Html.LabelFor(m => m.Password)
                @Html.PasswordFor(m => m.Password)
                @Html.ValidationMessageFor(m => m.Password)
            </li>
            <li>
                @Html.LabelFor(m => m.ConfirmPassword)
                @Html.PasswordFor(m => m.ConfirmPassword)
                @Html.ValidationMessageFor(m => m.ConfirmPassword)
            </li>
        </ol>
        <input type="submit" value="登録" />
    </fieldset>
}

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

Thanks.cshtml

@{
    ViewBag.Title = "ご登録ありがとうございます";
}

<h2>しかし、まだ完了していません。</h2>
<p>
   間もなくアカウントを有効にする方法が記載された電子メールが届きます。
</p>

以上で、 WebMatrix で提供されているユーザー認証関係がひととおり動きます。
最初のほうで書いたとおり、このままだとユーザー名としてメールアドレスが表示される状態で、見た目が良くないのと、ブログ的な書き込んだユーザー名が表示されるようなシステムではユーザー名がメールアドレスなのはどうなの?というのがあるので、次回はユーザー名をユーザーが指定できるように変更します。


コメントを残す

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