Search code examples
asp.net-mvcasp.net-mvc-5asp.net-identity

Identity 2 - Confirm Email and then allow user to set password


I am writing an app (MVC5, Identity 2.0 and Entity Framework 6) that has a Users section that only Administrators can access. The only place to add users is in this section, unless you're registering a new Organisation (which is a parent of Users).

I have everything up and running but want to make the adding of users flow better. At present, the admin creates the user with a hard coded password and an e-mail is sent to them asking them to confirm their account. They click on this and the account is confirmed and then they have to login. Obviously, hard coding passwords is not suitable for a production app!

I would really like it for the user to be added, a random password generated, the confirm account e-mail to be sent and then once the user clicks on it, their account is confirmed and then they are re-directed to a page where they can reset their own password.

So, code ninjas, is this possible? If so, any advice would be appreciated!


Solution

  • Yes it is. You can remove the hard-coded password put in by the admin and replace your call to create user with var result = await UserManager.CreateAsync(user); Provide no password at this point. Make sure to dispatch a mail on creation to user to confirm email. Here's an example.

    In the confirmEmail action view, you can create a password set form and post to back to confirmEmail. Sample below:

    The HTTP Get ConfirmEmail

        [AllowAnonymous]
        public async Task<ActionResult> ConfirmEmail(string userId, string code)
        {
            if (userId == null || code == null)
            {
                return View("Error");
            }
            var confirmed = await UserManager.IsEmailConfirmedAsync(userId);
            if(confirmed)
            {
                return await RedirectToDashboard(userId);
            }
    
            var result = await UserManager.ConfirmEmailAsync(userId, code);
    
            if (result.Succeeded)
            {
                ViewBag.userId = userId;
                ViewBag.code = code;
            }
            return View(result.Succeeded ? "ConfirmEmail" : "Error");
        }
    

    The HTTP POST to ConfirmEmail from your set password form:

        [HttpPost]
        [ValidateAntiForgeryToken]
        [AllowAnonymous]
        public async Task<ActionResult> ConfirmEmail(SetPasswordViewModel model, string userId, string code)
        {
            if (userId == null || code == null)
            {
                return View("Error");
            }
            if (!ModelState.IsValid)
            {
                return View(model);
            }
            var result = await UserManager.AddPasswordAsync(userId, model.NewPassword);
            if (result.Succeeded)
            {
                var user = await UserManager.FindByIdAsync(userId);
                if (user != null)
                {
                    await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
                }
                return await RedirectToDashboard(userId);
            }
    
            ViewBag.userId = userId;
            ViewBag.code = code;
    
            AddErrors(result);
            return View(model);
        }
    

    Sample form to be put into the ConfirmEmailView

        @using (Html.BeginForm("ConfirmEmail", "Account", new { userId = ViewBag.userId, code = ViewBag.code }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
                    {
                        @Html.AntiForgeryToken()
    
                        @Html.ValidationSummary("", new { @class = "color_orange" })
                          @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control", placeholder = "New Password" })
                          @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control", placeholder = "Confirm Password" })
                          <input type="submit" value="Set password" class="btn" />
    
    
                    }
    

    Remember to add a model to your confirmEmail view @model [ProjectName].Models.SetPasswordViewModel

    And create the SetPasswordViewModel:

        public class SetPasswordViewModel
    {
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "New password")]
        public string NewPassword { get; set; }
    
        [DataType(DataType.Password)]
        [Display(Name = "Confirm new password")]
        [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }