Adding UI to change password

- new Lost Password link in LogOng view
- sends a reset link by mail

Work Item: 16341

--HG--
branch : dev
This commit is contained in:
Sébastien Ros
2010-11-26 17:02:22 -08:00
parent b2eb5e2f66
commit 4631207f51
12 changed files with 191 additions and 102 deletions

View File

@@ -12,6 +12,7 @@ using Orchard.Users.Services;
using Orchard.Users.ViewModels;
using Orchard.ContentManagement;
using Orchard.Users.Models;
using Orchard.UI.Notify;
namespace Orchard.Users.Controllers {
[HandleError, Themed]
@@ -119,8 +120,8 @@ namespace Orchard.Users.Controllers {
if (user != null) {
if ( user.As<UserPart>().EmailStatus == UserStatus.Pending ) {
string challengeToken = _membershipService.GetEncryptedChallengeToken(user.As<UserPart>());
_membershipService.SendChallengeEmail(user.As<UserPart>(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", token = challengeToken })));
string challengeToken = _userService.GetNonce(user.As<UserPart>());
_userService.SendChallengeEmail(user.As<UserPart>(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", token = challengeToken })));
return RedirectToAction("ChallengeEmailSent");
}
@@ -141,6 +142,36 @@ namespace Orchard.Users.Controllers {
return Register();
}
public ActionResult LostPassword() {
return View();
}
[HttpPost]
public ActionResult LostPassword(string username) {
if(String.IsNullOrWhiteSpace(username)){
_orchardServices.Notifier.Error(T("Invalid username or E-mail"));
return View();
}
_userService.SendLostPasswordEmail(username, nonce => Url.AbsoluteAction(() => Url.Action("ValidateLostPassword", "Account", new { Area = "Orchard.Users", nonce = nonce })));
_orchardServices.Notifier.Information(T("Check your e-mail for the confirmation link."));
return RedirectToAction("LogOn");
}
public ActionResult ValidateLostPassword(string nonce) {
IUser user;
if (null != (user = _userService.ValidateLostPassword(nonce))) {
_authenticationService.SignIn(user, false);
return RedirectToAction("ChangePassword");
}
else {
return new RedirectResult("~/");
}
}
[Authorize]
public ActionResult ChangePassword() {
ViewData["PasswordLength"] = MinPasswordLength;
@@ -150,32 +181,23 @@ namespace Orchard.Users.Controllers {
[Authorize]
[HttpPost]
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
Justification = "Exceptions result in password not being changed.")]
public ActionResult ChangePassword(string currentPassword, string newPassword, string confirmPassword) {
ViewData["PasswordLength"] = MinPasswordLength;
if (!ValidateChangePassword(currentPassword, newPassword, confirmPassword)) {
if (newPassword == null || newPassword.Length < MinPasswordLength) {
ModelState.AddModelError("newPassword", T("You must specify a new password of {0} or more characters.", MinPasswordLength));
}
if (!String.Equals(newPassword, confirmPassword, StringComparison.Ordinal)) {
ModelState.AddModelError("_FORM", T("The new password and confirmation password do not match."));
}
if (!ModelState.IsValid) {
return View();
}
try {
var validated = _membershipService.ValidateUser(User.Identity.Name, currentPassword);
if (validated != null) {
_membershipService.SetPassword(validated, newPassword);
return RedirectToAction("ChangePasswordSuccess");
}
else {
ModelState.AddModelError("_FORM",
T("The current password is incorrect or the new password is invalid."));
return ChangePassword();
}
}
catch {
ModelState.AddModelError("_FORM", T("The current password is incorrect or the new password is invalid."));
return ChangePassword();
}
_membershipService.SetPassword(_orchardServices.WorkContext.CurrentUser, newPassword);
return RedirectToAction("ChangePasswordSuccess");
}
public ActionResult RegistrationPending() {
@@ -199,7 +221,7 @@ namespace Orchard.Users.Controllers {
}
public ActionResult ChallengeEmail(string token) {
var user = _membershipService.ValidateChallengeToken(token);
var user = _userService.ValidateChallenge(token);
if ( user != null ) {
_authenticationService.SignIn(user, false /* createPersistentCookie */);
@@ -217,21 +239,6 @@ namespace Orchard.Users.Controllers {
#region Validation Methods
private bool ValidateChangePassword(string currentPassword, string newPassword, string confirmPassword) {
if (String.IsNullOrEmpty(currentPassword)) {
ModelState.AddModelError("currentPassword", T("You must specify a current password."));
}
if (newPassword == null || newPassword.Length < MinPasswordLength) {
ModelState.AddModelError("newPassword", T("You must specify a new password of {0} or more characters.", MinPasswordLength));
}
if (!String.Equals(newPassword, confirmPassword, StringComparison.Ordinal)) {
ModelState.AddModelError("_FORM", T("The new password and confirmation password do not match."));
}
return ModelState.IsValid;
}
private IUser ValidateLogOn(string userNameOrEmail, string password) {
bool validate = true;

View File

@@ -197,8 +197,8 @@ namespace Orchard.Users.Controllers {
var user = Services.ContentManager.Get(id);
if ( user != null ) {
string challengeToken = _membershipService.GetEncryptedChallengeToken(user.As<UserPart>());
_membershipService.SendChallengeEmail(user.As<UserPart>(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new {Area = "Orchard.Users", token = challengeToken})));
string challengeToken = _userService.GetNonce(user.As<UserPart>());
_userService.SendChallengeEmail(user.As<UserPart>(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new {Area = "Orchard.Users", token = challengeToken})));
}
Services.Notifier.Information(T("Challenge email sent"));

View File

@@ -29,11 +29,16 @@ namespace Orchard.Users.Handlers {
context.MailMessage.Body = T("The following user account needs to be moderated: {0}", recipient.UserName).Text;
}
if ( context.Type == MessageTypes.Validation ) {
if (context.Type == MessageTypes.Validation) {
context.MailMessage.Subject = T("User account validation").Text;
context.MailMessage.Body = T("Dear {0}, please <a href=\"{1}\">click here</a> to validate you email address.", recipient.UserName, context.Properties["ChallengeUrl"]).Text;
}
if (context.Type == MessageTypes.LostPassword) {
context.MailMessage.Subject = T("Lost password").Text;
context.MailMessage.Body = T("Dear {0}, please <a href=\"{1}\">click here</a> to change your password.", recipient.UserName, context.Properties["LostPasswordUrl"]).Text;
}
}
public void Sent(MessageContext context) {

View File

@@ -7,6 +7,7 @@ namespace Orchard.Users.Models {
public static class MessageTypes {
public const string Moderation = "ORCHARD_USERS_MODERATION";
public const string Validation = "ORCHARD_USERS_VALIDATION";
public const string LostPassword = "ORCHARD_USERS_RESETPASSWORD";
}
}

View File

@@ -123,6 +123,9 @@
<Content Include="Views\EditorTemplates\Parts\User.Edit.cshtml" />
<Content Include="Views\EditorTemplates\Parts\User.Create.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Account\LostPassword.cshtml" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@@ -1,6 +1,16 @@
using Orchard.Security;
using System;
namespace Orchard.Users.Services {
public interface IUserService : IDependency {
string VerifyUserUnicity(string userName, string email);
string VerifyUserUnicity(int id, string userName, string email);
void SendChallengeEmail(IUser user, string url);
IUser ValidateChallenge(string challengeToken);
bool SendLostPasswordEmail(string usernameOrEmail, Func<string, string> createUrl);
IUser ValidateLostPassword(string nonce);
string GetNonce(IUser user);
}
}

View File

@@ -14,6 +14,7 @@ using Orchard.Users.Events;
using Orchard.Users.Models;
using Orchard.Messaging.Services;
using System.Collections.Generic;
using Orchard.Services;
namespace Orchard.Users.Services {
[UsedImplicitly]
@@ -22,11 +23,13 @@ namespace Orchard.Users.Services {
private readonly IOrchardServices _orchardServices;
private readonly IMessageManager _messageManager;
private readonly IEnumerable<IUserEventHandler> _userEventHandlers;
private readonly IClock _clock;
public MembershipService(IOrchardServices orchardServices, IMessageManager messageManager, IEnumerable<IUserEventHandler> userEventHandlers) {
public MembershipService(IOrchardServices orchardServices, IMessageManager messageManager, IEnumerable<IUserEventHandler> userEventHandlers, IClock clock) {
_orchardServices = orchardServices;
_messageManager = messageManager;
_userEventHandlers = userEventHandlers;
_clock = clock;
Logger = NullLogger.Instance;
T = NullLocalizer.Instance;
}
@@ -87,54 +90,6 @@ namespace Orchard.Users.Services {
return user;
}
public void SendChallengeEmail(IUser user, string url) {
_messageManager.Send(user.ContentItem.Record, MessageTypes.Validation, "email", new Dictionary<string, string> { { "ChallengeUrl", url } });
}
public IUser ValidateChallengeToken(string challengeToken) {
string username;
DateTime validateByUtc;
if(!DecryptChallengeToken(challengeToken, out username, out validateByUtc)) {
return null;
}
if ( validateByUtc < DateTime.UtcNow )
return null;
var user = GetUser(username);
if ( user == null )
return null;
user.As<UserPart>().EmailStatus = UserStatus.Approved;
return user;
}
public string GetEncryptedChallengeToken(IUser user) {
var challengeToken = new XElement("Token", new XAttribute("username", user.UserName), new XAttribute("validate-by-utc", DateTime.UtcNow.Add(DelayToValidate).ToString(CultureInfo.InvariantCulture))).ToString();
var data = Encoding.UTF8.GetBytes(challengeToken);
return MachineKey.Encode(data, MachineKeyProtection.All);
}
private static bool DecryptChallengeToken(string challengeToken, out string username, out DateTime validateByUtc) {
username = null;
validateByUtc = DateTime.UtcNow;
try {
var data = MachineKey.Decode(challengeToken, MachineKeyProtection.All);
var xml = Encoding.UTF8.GetString(data);
var element = XElement.Parse(xml);
username = element.Attribute("username").Value;
validateByUtc = DateTime.Parse(element.Attribute("validate-by-utc").Value, CultureInfo.InvariantCulture);
return true;
}
catch {
return false;
}
}
public IUser GetUser(string username) {
var lowerName = username == null ? "" : username.ToLower();

View File

@@ -1,17 +1,33 @@
using System;
using System.Linq;
using System.Collections.Generic;
using JetBrains.Annotations;
using Orchard.Logging;
using Orchard.ContentManagement;
using Orchard.Users.Models;
using Orchard.Security;
using System.Xml.Linq;
using Orchard.Services;
using System.Globalization;
using System.Text;
using System.Web.Security;
using Orchard.Messaging.Services;
namespace Orchard.Users.Services {
[UsedImplicitly]
public class UserService : IUserService {
private readonly IContentManager _contentManager;
private static readonly TimeSpan DelayToValidate = new TimeSpan(7, 0, 0, 0); // one week to validate email
public UserService(IContentManager contentManager) {
private readonly IContentManager _contentManager;
private readonly IMembershipService _membershipService;
private readonly IClock _clock;
private readonly IMessageManager _messageManager;
public UserService(IContentManager contentManager, IMembershipService membershipService, IClock clock, IMessageManager messageManager) {
_contentManager = contentManager;
_membershipService = membershipService;
_clock = clock;
_messageManager = messageManager;
Logger = NullLogger.Instance;
}
@@ -46,5 +62,86 @@ namespace Orchard.Users.Services {
}
return null;
}
public string GetNonce(IUser user) {
var challengeToken = new XElement("Token", new XAttribute("username", user.UserName), new XAttribute("validate-by-utc", _clock.UtcNow.Add(DelayToValidate).ToString(CultureInfo.InvariantCulture))).ToString();
var data = Encoding.UTF8.GetBytes(challengeToken);
return MachineKey.Encode(data, MachineKeyProtection.All);
}
private bool DecryptNonce(string challengeToken, out string username, out DateTime validateByUtc) {
username = null;
validateByUtc = _clock.UtcNow;
try {
var data = MachineKey.Decode(challengeToken, MachineKeyProtection.All);
var xml = Encoding.UTF8.GetString(data);
var element = XElement.Parse(xml);
username = element.Attribute("username").Value;
validateByUtc = DateTime.Parse(element.Attribute("validate-by-utc").Value, CultureInfo.InvariantCulture);
return true;
}
catch {
return false;
}
}
public IUser ValidateChallenge(string nonce) {
string username;
DateTime validateByUtc;
if (!DecryptNonce(nonce, out username, out validateByUtc)) {
return null;
}
if (validateByUtc < _clock.UtcNow)
return null;
var user = _membershipService.GetUser(username);
if (user == null)
return null;
user.As<UserPart>().EmailStatus = UserStatus.Approved;
return user;
}
public void SendChallengeEmail(IUser user, string url) {
_messageManager.Send(user.ContentItem.Record, MessageTypes.Validation, "email", new Dictionary<string, string> { { "ChallengeUrl", url } });
}
public bool SendLostPasswordEmail(string usernameOrEmail, Func<string, string> createUrl) {
var lowerName = usernameOrEmail.ToLower();
var user = _contentManager.Query<UserPart, UserPartRecord>().Where(u => u.NormalizedUserName == lowerName || u.Email == lowerName).List().FirstOrDefault();
if (user != null) {
string nonce = GetNonce(user);
string url = createUrl(nonce);
_messageManager.Send(user.ContentItem.Record, MessageTypes.LostPassword, "email", new Dictionary<string, string> { { "LostPasswordUrl", url } });
return true;
}
return false;
}
public IUser ValidateLostPassword(string nonce) {
string username;
DateTime validateByUtc;
if (!DecryptNonce(nonce, out username, out validateByUtc)) {
return null;
}
if (validateByUtc < _clock.UtcNow)
return null;
var user = _membershipService.GetUser(username);
if (user == null)
return null;
return user;
}
}
}

View File

@@ -6,11 +6,6 @@
@using (Html.BeginFormAntiForgeryPost()) {
<fieldset>
<legend>@T("Account Information")</legend>
<div>
<label for="currentPassword">@T("Current password:")</label>
@Html.Password("currentPassword")
@Html.ValidationMessage("currentPassword")
</div>
<div>
<label for="newPassword">@T("New password:")</label>
@Html.Password("newPassword")

View File

@@ -6,10 +6,14 @@
}
<h1 class="page-title">@Html.TitleForPage(Model.Title)</h1>
<p>@T("Please enter your username and password.") @if(userCanRegister) { <text> </text> @Html.ActionLink("Register", "Register") @T(" if you don't have an account.") }</p>
<p>
@T("Please enter your username and password.")
@if(userCanRegister) { @Html.ActionLink(T("Register").Text, "Register") @T(" if you don't have an account.") }
@Html.ActionLink(T("Lost your Password?").Text, "LostPassword")
</p>
@Html.ValidationSummary(T("Login was unsuccessful. Please correct the errors and try again.").ToString())
@using (Html.BeginFormAntiForgeryPost(Url.Action("LogOn", new {ReturnUrl = Request.QueryString["ReturnUrl"]}))) {
@using (Html.BeginFormAntiForgeryPost(Url.Action("LogOn", new { ReturnUrl = Request.QueryString["ReturnUrl"] }))) {
<fieldset class="login-form group">
<legend>@T("Account Information")</legend>
<ol>

View File

@@ -0,0 +1,15 @@
<h1>@Html.TitleForPage(T("Lost Password").ToString())</h1>
<p>@T("Please enter your username or email address. You will receive a link to create a new password via email.")</p>
@using (Html.BeginFormAntiForgeryPost()) {
<fieldset>
<legend>@T("Account Information")</legend>
<div>
<label for="username">@T("Username or E-mail:")</label>
@Html.TextBox("username")
@Html.ValidationMessage("username")
</div>
<div>
<button class="primaryAction" type="submit">@T("Send Request")</button>
</div>
</fieldset>
}

View File

@@ -8,8 +8,5 @@
IUser ValidateUser(string userNameOrEmail, string password);
void SetPassword(IUser user, string password);
IUser ValidateChallengeToken(string challengeToken);
void SendChallengeEmail(IUser user, string url);
string GetEncryptedChallengeToken(IUser user);
}
}