4

When a user forgets his password, he/she can click on forgot password link which will trigger an email with password reset link.

How to implement this functionality in a Sitecore application? Since Sitecore is using ASP.NET Membership provider, is there any methods to generate password reset token with expiration ?

1 Answer 1

9

Disclosure: answer taken from Securing the password recovery experience in Sitecore 8 by Anders Laub Christofferson

First we do some configuration re-organizing.

We copy paste the old passwordRecovery pipeline and call the new one confirmedPasswordRecovery

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
      <processors>
      <confirmPasswordRecovery argsType="Sitecore.Pipelines.PasswordRecovery.PasswordRecoveryArgs">
        <processor mode="on" type="Sitecore.Pipelines.PasswordRecovery.VerifyUsername, Sitecore.Kernel" />
        <processor mode="on" type="Sitecore.Pipelines.PasswordRecovery.GeneratePassword, Sitecore.Kernel" />
        <processor mode="on" type="Sitecore.Pipelines.PasswordRecovery.PopulateMail, Sitecore.Kernel" />
        <processor mode="on" type="Sitecore.Pipelines.PasswordRecovery.SendPasswordRecoveryMail, Sitecore.Kernel" />
      </confirmPasswordRecovery>
      </processors>
    </sitecore>
</configuration>

We replace the GeneratePassword with a processor called GenerateToken for generating and storing a token on the user profile:

public class GenerateToken : PasswordRecoveryProcessor
{
  public override void Process(PasswordRecoveryArgs args)
  {
    Assert.ArgumentNotNull(args, "args");
    var user = User.FromName(args.Username, true);
    if (user == null)
    {
      args.AbortPipeline();
      return;
    }
    var token = ID.NewID.ToShortID().ToString();
    StoreTokenOnUser(user, token);
    args.CustomData.Add(Constants.ConfirmTokenKey, token);
  }

  private void StoreTokenOnUser(User user, string confirmToken)
  {
    user.Profile.SetCustomProperty(Constants.ConfirmTokenKey, confirmToken);
    user.Profile.Save();
  }
}

To make this work you need to add a field called PasswordToken on the user profile

internal struct Constants
{
  internal const string ConfirmTokenKey = "PasswordToken";
}

And then we replace PopulateMail with PopulateConfirmMail that populates a html email with a confirm link.

public class PopulateConfirmMail : PopulateMail
{
  public override void Process(PasswordRecoveryArgs args)
  {
    Assert.ArgumentNotNull(args, "args");
    var token = args.CustomData[Constants.ConfirmTokenKey] as string;
    if (string.IsNullOrEmpty(token))
      return;
    var confirmLink = GenerateConfirmLink(token, args.Username);
    args.SendFromDisplayName = "Sitecore website";
    args.SendFromEmail = "[email protected]";
    args.Subject = "Confirm password recovery";
    var user = User.FromName(args.Username, false);
    args.HtmlEmailContent = GetHtmlEmailContent(user, confirmLink);
  }

  protected virtual string GenerateConfirmLink(string token, string userName)
  {
    var serverUrl = StringUtil.EnsurePostfix('/', WebUtil.GetServerUrl());
    return serverUrl + "sitecore/api/passwordrecovery/confirm/" + userName.Replace('\\', '|') + "/" + token;
  }

  protected virtual string GetHtmlEmailContent(User user, string confirmLink)
  {
    var sb = new StringBuilder();
    sb.AppendLine("<html><head><title>");
    sb.AppendLine("Sitecore password recovery");
    sb.AppendLine("</title></head><body>");
    sb.AppendLine("<h1>Please confirm</h1>");
    sb.AppendLine("<p>Hi " + user.Profile.FullName + ",<br/></p>");
    sb.AppendLine("<p>Please follow the link below to recover your password</p>");
    sb.AppendLine("<a href=\"" + confirmLink + "\">" + confirmLink + "</a>");
    sb.AppendLine("</body>");
    sb.AppendLine("</html>");
    return sb.ToString();
  }
}

Notice the confirm link that we create, this is for a ApiController with a HttpGet method that expects a username and a token.

public class ConfirmRecoveryController : ApiController
{
  [HttpGet]
  public IHttpActionResult Confirm(string userName, string token)
  {
    userName = userName.Replace('|', '\\');
    var user = Sitecore.Security.Accounts.User.FromName(userName, true);
    if (user == null || !TokenIsValid(user, token))
      return new StatusCodeResult(HttpStatusCode.Unauthorized, this);

    var passwordRecoveryArgs = new PasswordRecoveryArgs(HttpContext.Current)
    {
      Username = userName
    };
    Pipeline.Start("confirmPasswordRecovery", passwordRecoveryArgs);
    if (!passwordRecoveryArgs.Aborted)
      DeleteToken(user);

    return new StatusCodeResult(HttpStatusCode.OK, this);
  }

  private void DeleteToken(User user)
  {
    user.Profile.SetCustomProperty(Constants.ConfirmTokenKey, string.Empty);
    user.Profile.Save();
  }

  private bool TokenIsValid(User user, string token)
  {
    return !string.IsNullOrEmpty(token) && ShortID.IsShortID(token) && TokenExists(user, token);
  }

  private bool TokenExists(User user, string confirmToken)
  {
    var tokenOnProfile = user.Profile.GetCustomProperty(Constants.ConfirmTokenKey);
    return !string.IsNullOrEmpty(tokenOnProfile) && tokenOnProfile.Equals(confirmToken, StringComparison.InvariantCultureIgnoreCase);
  }
}

This controller executes the new (old) confirmPasswordRecovery pipeline, then deletes the token and redirects to the login page.

We need to register the route so we create a processor for the initialize pipeline:

public class RegisterHttpRoutes
{
  public void Process(PipelineArgs args)
  {
    GlobalConfiguration.Configure(Configure);
  }

  protected void Configure(HttpConfiguration configuration)
  {
    var routes = configuration.Routes;
    routes.MapHttpRoute("PasswordRecovery_Confirm", "sitecore/api/passwordrecovery/{action}/{userName}/{token}", new
    {
      controller = "ConfirmRecovery",
      action = "Index"
    });
  }
}

And patch it into the initialize pipeline:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor patch:after="processor[@type='Sitecore.Pipelines.Loader.EnsureAnonymousUsers, Sitecore.Kernel']"
          type="LaubPlusCo.PasswordRecovery.Infrastructure.RegisterHttpRoutes, LaubPlusCo.PasswordRecovery" />
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

And the password recovery mail is the standard one that you can modify in the core database on the item /sitecore/system/Settings/Security/Password recovery/Password Recovery Email

Link to blog post here

Update:

If you want to have token expiration functionality, you need to add to Custom User Profile new field like Created, where you will store token created DateTime value and when user will click link in email, you need to check this value with your condition.

1
  • How could you do this in Sitecore 7.1? Since the PasswordRecovery doesn't exist Commented Sep 28, 2018 at 8:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.