MediaWizards Grimoire

Mar 23, 2023 - Huw Reddick

Implementing a Forgot password for members part 1

This question tends to come up a lot on the forum so I have decided to write an article explaing how to accomplish this in Umbraco 9+.

The flow is fairly standard:

  1. User submits their email
  2. If the user exists, we generate a token for them using IMemberManager GeneratePasswordResetTokenAsync(member)
  3. The token is sent to the member via email
  4. The member clicks the link, opening a form with the token sent above.
  5. Member submits the form with new password
  6. We reset the password using IMemberManager ResetPasswordAsync OR ChangePasswordWithResetAsync

We will start by creating a controller to handle all the posts etc.

MemberSurfaceController.cs

using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.Website.Controllers;

namespace Controllers
{
    public class MemberSurfaceController : SurfaceController
    {
        private readonly IMemberService _memberService;
        private readonly IMemberManager _memberManager;
        private readonly IMemberMailService _mailService;

        public MemberSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider,
            IMemberService memberService, IMemberManager memberManager,IMemberMailService mailService) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
        {
            _memberService = memberService;
            _memberManager = memberManager;
            _mailService = mailService;
        }

        [HttpPost]
        public async Task<IActionResult> ForgotPasswordAsync(FormCollection form)
        {
            TempData["ResetSent"] = false;
            if (!ModelState.IsValid)
            {
                return CurrentUmbracoPage();
            }
            //if the form has a NewPassword field, then process the password reset
            if(form.Keys.Contains("NewPassword"))
            {
                var member = _memberService.GetById(Convert.ToInt32(form["userid"]));
                var token = form["token"];
                var newPassword = form["NewPassword"];
                #region validate the form
                var validPassword = await _memberManager.ValidatePasswordAsync(newPassword);
                if (!validPassword.Succeeded)
                {
                    ModelState.AddModelError("NoPass", "Password is not valid");
                }
                if (string.IsNullOrWhiteSpace(newPassword))
                {
                    ModelState.AddModelError("NoPass", "You must enter a password");
                }
                if (newPassword != form["ConfirmPassword"])
                {
                    ModelState.AddModelError("NoMatch", "passwords do not match");
                }
                if (form["token"][0].Replace(" ","+") != token)
                {
                    ModelState.AddModelError("TokenInv", "Reset token is invalid");
                }
                if (!ModelState.IsValid)
                {
                    TempData["Message"] = "Validation Error";
                    return CurrentUmbracoPage();
                }
                #endregion

                #region reset the Umbraco password
                var identityUser = _memberManager.FindByIdAsync(form["userid"]).Result;
                
                var result =  _memberManager.ResetPasswordAsync(identityUser, token, newPassword).Result;
                if (!result.Succeeded)
                {
                    TempData["Message"] = "Reset password error Error";
                    return CurrentUmbracoPage();  
                }

                #endregion
                //everything ok so redirect to the login page.
                return Redirect("~/login");
            }else{
                var member = _memberService.GetByEmail(form["EmailAddress"]);
                if (member != null)
                {
                    var memberIdentity = await _memberManager.FindByIdAsync(member.Id.ToString());
                    // we found a user with that email so generate a token ....
                    var token = await _memberManager.GeneratePasswordResetTokenAsync(memberIdentity);
                    var encodedToken = !string.IsNullOrEmpty(token) ? HttpUtility.UrlEncode(token) : string.Empty;

                    // send email ....
                    await _mailService.SendResetPasswordAsync(member.Email,encodedToken);

                    TempData["ResetSent"] = true;
                }
                else
                {
                    ModelState.AddModelError("ForgotPasswordForm", "Member not found");
                }                
            }


            return CurrentUmbracoPage();
        }

    }
}

 

We also need a form for the member to enter their email address, the form is dual purpose, if there are values for id and token in the querystring it displays the change password fields, otherwise it displays the email field.

Forgot password form

@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.ForgotPassword>
@using Controllers
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;

@{

    Layout = "_LayoutBody.cshtml";
    bool change = false;

    var user = Context.Request.Query["id"];
    var token = Context.Request.Query["token"];

    if (!string.IsNullOrWhiteSpace(token))
    {
        change = true;
        TempData["Message"] = "Reset password";
    }

}

<section>
    <div class="container">
        <h3>@title</h3>
        <div class="row">
            <div class="col-10 offset-1">
                @using (Html.BeginUmbracoForm<MemberSurfaceController>("ForgotPassword"))
                {
                        <!-- If the change flag is true render the change password form -->
                        if (change)
                        {
                            <fieldset>
                                <div class="form-group ">
                                    <label class="control-label col-sm-5">New password</label>
                                    <div class="col-sm-7">
                                        <div class="input-group" id="show_hide_password">
                                            <input type="password" id="NewPassword" required minlength="10" name="NewPassword" placeholder="new password" class="form-control ltr" />
                                            <div class="input-group-addon">
                                                <a href=""><i class="fa fa-eye-slash" aria-hidden="true"></i></a>
                                            </div>
                                        </div>

                                    </div>
                                </div>
                                <div class="form-group">
                                    <label class="control-label col-sm-5">Confirm password</label>
                                    <div class="col-sm-7">
                                        <input type="password" id="ConfirmPassword" required minlength="8" name="ConfirmPassword" placeholder="confirm password" class="form-control ltr" />
                                    </div>
                                </div>
                                <div class="form-group">
                                    <input type="hidden" id="userid" name="userid" value="@user" />
                                    <input type="hidden" id="token" name="token" value="@token" />
                                    <input type="submit" value="Reset password" class="btn btn-danger" />
                                </div>

                            </fieldset>
                        }
                        else
                        {
                            @Html.Raw(TempData["Message"])
                        }
                        <div class="form-group">
                            @Html.ValidationSummary()
                        </div>
                    }
                    else
                    {

                        <fieldset class="form-group">
                            <div class="form-group">
                                <input required type="email" id="email" data-toggle="tooltip" name="email" placeholder="login email" class="form-control ltr" />
                            </div>
                            <div class="form-group">
                                @Html.ValidationSummary()
                            </div>
                            <div class="form-group">
                                <input type="submit" value="Send" class="btn btn-danger" />
                            </div>

                        </fieldset>
                    }

            </div>
        </div>
    </div>
</section>

 

When a member enters an email address and submits the form, the controller method checks that a member exists with that email address and if there is it generates a password reset token and sends an email to that address which contains a link for the member to reset their password.

If the member has received the email and opens the link they will be presented with the change password form

Change password form

This form posts back to the same controller method and changes the members password.

In part 2 I will discuss implementing the IMemberMailService used above

In this blog post I explain how to implement an email validation flow for Member registration.

In part 2 of my Implementing a Forgot password for members I explain how to implement the IMemberMailService to send the reset password email.

How to implement a ForgotPassword process for Umbraco members in Umbraco 9+

Custom views give you complete control over how a Block is rendered in the backoffice and this enables you to give a better representation of the content. In this article I will explain how I created a custom Block view based on the fullcalendar.io javascript library to display events in the backoffice.

These are my experiences of creating an Umbraco package for the MediaWiz Forums, using package targets, razor class libraries, static web assets and template views.

Many thanks go to Kevin Jump and Luuk Peters without whose help I would probably have given up.

Custom error handling might make your site look more on-brand and minimize the impact of errors on user experience - for example, a custom 404 with some helpful links (or a search function) could bring some value to the site.

As some of you are aware, adding custom style formats to Umbraco 9+ is somewhat cumbersome as rather than accepting nicely formatted json it is expecting a string which requires the double quotes to be escaped and therfore makes it difficult to maintain.

How to use Umbraco Blocklist to build custom forms in the backoffice..