From d32847bee1db3bbfee21f4114ddbc373fd199b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 28 Jul 2016 21:59:13 +0200 Subject: [PATCH] Implementing configurable password policies (#7051) * Implementing configurable password policies, see #5380. Policies include: - Minimum password length - Password should contain uppercase and/or lowercase characters - Password should contain numbers - Password should contain special characters * Adding missing IMembershipSettings and removing now unneeded MembershipSettings * Removing hard-coded password length limits * Removing unnecessary checks when building the model state dictionary * Simplifying password length policy configuration by removing explicit enable option --- .../Orchard.Users/Commands/UserCommands.cs | 15 +- .../UserPasswordValidationResults.cs | 9 + .../Controllers/AccountController.cs | 196 ++++++++++++------ .../Controllers/AdminController.cs | 15 +- .../Drivers/UserPartPasswordDriver.cs | 17 +- .../MembershipSettingsExtensions.cs | 7 + .../ModelStateDistionaryExtensions.cs | 13 ++ .../Extensions/UpdateModelExtensions.cs | 12 ++ .../Modules/Orchard.Users/Migrations.cs | 16 +- .../Models/RegistrationSettingsPart.cs | 53 ++++- .../Modules/Orchard.Users/Models/UserPart.cs | 11 +- .../Orchard.Users/Models/UserPartRecord.cs | 3 +- .../Orchard.Users/Orchard.Users.csproj | 11 +- .../Orchard.Users/Services/IUserService.cs | 5 + .../Services/MembershipService.cs | 41 ++-- .../Orchard.Users/Services/UserService.cs | 69 ++++-- .../ViewModels/UserCreateViewModel.cs | 1 - .../ViewModels/UserEditPasswordViewModel.cs | 1 - .../Account/ChangeExpiredPassword.cshtml | 31 +++ .../Views/Account/ChangePassword.cshtml | 2 +- .../Views/Account/LostPassword.cshtml | 2 +- .../Parts/Users.RegistrationSettings.cshtml | 75 ++++++- .../Orchard.Users/Views/Register.cshtml | 4 +- .../Modules/Orchard.Users/Web.config | 1 + src/Orchard/Orchard.Framework.csproj | 2 +- src/Orchard/Security/IMembershipService.cs | 4 +- src/Orchard/Security/IMembershipSettings.cs | 23 ++ src/Orchard/Security/MembershipSettings.cs | 29 --- src/Orchard/Security/NullMembershipService.cs | 6 +- 29 files changed, 509 insertions(+), 165 deletions(-) create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Constants/UserPasswordValidationResults.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Extensions/MembershipSettingsExtensions.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Extensions/ModelStateDistionaryExtensions.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Extensions/UpdateModelExtensions.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChangeExpiredPassword.cshtml create mode 100644 src/Orchard/Security/IMembershipSettings.cs delete mode 100644 src/Orchard/Security/MembershipSettings.cs diff --git a/src/Orchard.Web/Modules/Orchard.Users/Commands/UserCommands.cs b/src/Orchard.Web/Modules/Orchard.Users/Commands/UserCommands.cs index ca670dcdc..cde58d95b 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Commands/UserCommands.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Commands/UserCommands.cs @@ -1,6 +1,8 @@ using Orchard.Commands; +using Orchard.Localization; using Orchard.Security; using Orchard.Users.Services; +using System.Collections.Generic; namespace Orchard.Users.Commands { public class UserCommands : DefaultOrchardCommandHandler { @@ -40,8 +42,11 @@ namespace Orchard.Users.Commands { return; } - if (Password == null || Password.Length < MinPasswordLength) { - Context.Output.WriteLine(T("You must specify a password of {0} or more characters.", MinPasswordLength)); + IDictionary validationErrors; + if (!_userService.PasswordMeetsPolicies(Password, out validationErrors)) { + foreach (var error in validationErrors) { + Context.Output.WriteLine(error.Value); + } return; } @@ -53,11 +58,5 @@ namespace Orchard.Users.Commands { Context.Output.WriteLine(T("User created successfully")); } - - int MinPasswordLength { - get { - return _membershipService.GetSettings().MinRequiredPasswordLength; - } - } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Constants/UserPasswordValidationResults.cs b/src/Orchard.Web/Modules/Orchard.Users/Constants/UserPasswordValidationResults.cs new file mode 100644 index 000000000..26427efe4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Constants/UserPasswordValidationResults.cs @@ -0,0 +1,9 @@ +namespace Orchard.Users.Constants { + public static class UserPasswordValidationResults { + public const string PasswordIsTooShort = "PasswordIsTooShort"; + public const string PasswordDoesNotContainNumbers = "PasswordDoesNotContainNumbers"; + public const string PasswordDoesNotContainUppercase = "PasswordDoesNotContainUppercase"; + public const string PasswordDoesNotContainLowercase = "PasswordDoesNotContainLowercase"; + public const string PasswordDoesNotContainSpecialCharacters = "PasswordDoesNotContainSpecialCharacters"; + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs index 1544fcf09..271b4edc2 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs @@ -1,20 +1,22 @@ -using System; -using System.Text.RegularExpressions; -using System.Diagnostics.CodeAnalysis; +using Orchard.ContentManagement; using Orchard.Localization; -using System.Web.Mvc; -using System.Web.Security; using Orchard.Logging; using Orchard.Mvc; using Orchard.Mvc.Extensions; using Orchard.Security; using Orchard.Themes; -using Orchard.Users.Services; -using Orchard.ContentManagement; -using Orchard.Users.Models; using Orchard.UI.Notify; using Orchard.Users.Events; +using Orchard.Users.Models; +using Orchard.Users.Services; using Orchard.Utility.Extensions; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using System.Web.Mvc; +using System.Web.Security; +using Orchard.Services; +using System.Collections.Generic; namespace Orchard.Users.Controllers { [HandleError, Themed] @@ -24,18 +26,23 @@ namespace Orchard.Users.Controllers { private readonly IUserService _userService; private readonly IOrchardServices _orchardServices; private readonly IUserEventHandler _userEventHandler; + private readonly IClock _clock; public AccountController( - IAuthenticationService authenticationService, + IAuthenticationService authenticationService, IMembershipService membershipService, - IUserService userService, + IUserService userService, IOrchardServices orchardServices, - IUserEventHandler userEventHandler) { + IUserEventHandler userEventHandler, + IClock clock) { + _authenticationService = authenticationService; _membershipService = membershipService; _userService = userService; _orchardServices = orchardServices; _userEventHandler = userEventHandler; + _clock = clock; + Logger = NullLogger.Instance; T = NullLocalizer.Instance; } @@ -51,7 +58,7 @@ namespace Orchard.Users.Controllers { if (currentUser == null) { Logger.Information("Access denied to anonymous request on {0}", returnUrl); var shape = _orchardServices.New.LogOn().Title(T("Access Denied").Text); - return new ShapeResult(this, shape); + return new ShapeResult(this, shape); } //TODO: (erikpo) Add a setting for whether or not to log access denieds since these can fill up a database pretty fast from bots on a high traffic site @@ -69,7 +76,7 @@ namespace Orchard.Users.Controllers { return this.RedirectLocal(returnUrl); var shape = _orchardServices.New.LogOn().Title(T("Log On").Text); - return new ShapeResult(this, shape); + return new ShapeResult(this, shape); } [HttpPost] @@ -83,7 +90,16 @@ namespace Orchard.Users.Controllers { var user = ValidateLogOn(userNameOrEmail, password); if (!ModelState.IsValid) { var shape = _orchardServices.New.LogOn().Title(T("Log On").Text); - return new ShapeResult(this, shape); + + return new ShapeResult(this, shape); + } + + var membershipSettings = _membershipService.GetSettings(); + if (user != null && + membershipSettings.EnableCustomPasswordPolicy && + membershipSettings.EnablePasswordExpiration && + _membershipService.PasswordIsExpired(user, membershipSettings.PasswordExpirationTimeInDays)) { + return RedirectToAction("ChangeExpiredPassword", new { username = user.UserName }); } _authenticationService.SignIn(user, rememberMe); @@ -103,24 +119,19 @@ namespace Orchard.Users.Controllers { return this.RedirectLocal(returnUrl); } - int MinPasswordLength { - get { - return _membershipService.GetSettings().MinRequiredPasswordLength; - } - } - [AlwaysAccessible] public ActionResult Register() { // ensure users can register - var registrationSettings = _orchardServices.WorkContext.CurrentSite.As(); - if ( !registrationSettings.UsersCanRegister ) { + var membershipSettings = _membershipService.GetSettings(); + if (!membershipSettings.UsersCanRegister) { return HttpNotFound(); } - ViewData["PasswordLength"] = MinPasswordLength; + ViewData["PasswordLength"] = membershipSettings.GetMinimumPasswordLength(); var shape = _orchardServices.New.Register(); - return new ShapeResult(this, shape); + + return new ShapeResult(this, shape); } [HttpPost] @@ -128,12 +139,12 @@ namespace Orchard.Users.Controllers { [ValidateInput(false)] public ActionResult Register(string userName, string email, string password, string confirmPassword, string returnUrl = null) { // ensure users can register - var registrationSettings = _orchardServices.WorkContext.CurrentSite.As(); - if ( !registrationSettings.UsersCanRegister ) { + var membershipSettings = _membershipService.GetSettings(); + if (!membershipSettings.UsersCanRegister) { return HttpNotFound(); } - ViewData["PasswordLength"] = MinPasswordLength; + ViewData["PasswordLength"] = membershipSettings.GetMinimumPasswordLength(); if (ValidateRegistration(userName, email, password, confirmPassword)) { // Attempt to register the user @@ -141,13 +152,13 @@ namespace Orchard.Users.Controllers { var user = _membershipService.CreateUser(new CreateUserParams(userName, password, email, null, null, false)); if (user != null) { - if ( user.As().EmailStatus == UserStatus.Pending ) { + if (user.As().EmailStatus == UserStatus.Pending) { var siteUrl = _orchardServices.WorkContext.CurrentSite.BaseUrl; - if(String.IsNullOrWhiteSpace(siteUrl)) { + if (String.IsNullOrWhiteSpace(siteUrl)) { siteUrl = HttpContext.Request.ToRootUrlString(); } - _userService.SendChallengeEmail(user.As(), nonce => Url.MakeAbsolute(Url.Action("ChallengeEmail", "Account", new {Area = "Orchard.Users", nonce = nonce}), siteUrl)); + _userService.SendChallengeEmail(user.As(), nonce => Url.MakeAbsolute(Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", nonce = nonce }), siteUrl)); _userEventHandler.SentChallengeEmail(user); return RedirectToAction("ChallengeEmailSent", new { ReturnUrl = returnUrl }); @@ -163,20 +174,20 @@ namespace Orchard.Users.Controllers { return this.RedirectLocal(returnUrl); } - + ModelState.AddModelError("_FORM", T(ErrorCodeToString(/*createStatus*/MembershipCreateStatus.ProviderError))); } // If we got this far, something failed, redisplay form var shape = _orchardServices.New.Register(); - return new ShapeResult(this, shape); + return new ShapeResult(this, shape); } [AlwaysAccessible] public ActionResult RequestLostPassword() { // ensure users can request lost password - var registrationSettings = _orchardServices.WorkContext.CurrentSite.As(); - if ( !registrationSettings.EnableLostPassword ) { + var membershipSettings = _membershipService.GetSettings(); + if (!membershipSettings.EnableLostPassword) { return HttpNotFound(); } @@ -187,12 +198,12 @@ namespace Orchard.Users.Controllers { [AlwaysAccessible] public ActionResult RequestLostPassword(string username) { // ensure users can request lost password - var registrationSettings = _orchardServices.WorkContext.CurrentSite.As(); - if ( !registrationSettings.EnableLostPassword ) { + var membershipSettings = _membershipService.GetSettings(); + if (!membershipSettings.EnableLostPassword) { return HttpNotFound(); } - if(String.IsNullOrWhiteSpace(username)){ + if (String.IsNullOrWhiteSpace(username)) { ModelState.AddModelError("username", T("You must specify a username or e-mail.")); return View(); } @@ -205,14 +216,15 @@ namespace Orchard.Users.Controllers { _userService.SendLostPasswordEmail(username, nonce => Url.MakeAbsolute(Url.Action("LostPassword", "Account", new { Area = "Orchard.Users", nonce = nonce }), siteUrl)); _orchardServices.Notifier.Information(T("Check your e-mail for the confirmation link.")); - + return RedirectToAction("LogOn"); } [Authorize] [AlwaysAccessible] public ActionResult ChangePassword() { - ViewData["PasswordLength"] = MinPasswordLength; + var membershipSettings = _membershipService.GetSettings(); + ViewData["PasswordLength"] = membershipSettings.GetMinimumPasswordLength(); return View(); } @@ -224,27 +236,76 @@ namespace Orchard.Users.Controllers { [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; + var membershipSettings = _membershipService.GetSettings(); + ViewData["PasswordLength"] = membershipSettings.GetMinimumPasswordLength(); ; - if ( !ValidateChangePassword(currentPassword, newPassword, confirmPassword) ) { + if (!ValidateChangePassword(currentPassword, newPassword, confirmPassword)) { return View(); } - try { - var validated = _membershipService.ValidateUser(User.Identity.Name, currentPassword); + if (PasswordChangeIsSuccess(currentPassword, newPassword, _orchardServices.WorkContext.CurrentUser.UserName)) { + return RedirectToAction("ChangePasswordSuccess"); + } + else { + return ChangePassword(); + } + } - if ( validated != null ) { + [AlwaysAccessible] + public ActionResult ChangeExpiredPassword(string username) { + var membershipSettings = _membershipService.GetSettings(); + var lastPasswordChangeUtc = _membershipService.GetUser(username).As().LastPasswordChangeUtc; + + if (lastPasswordChangeUtc.Value.AddDays(membershipSettings.PasswordExpirationTimeInDays) > + _clock.UtcNow) { + return RedirectToAction("LogOn"); + } + + var viewModel = _orchardServices.New.ViewModel( + Username: username, + PasswordLength: membershipSettings.GetMinimumPasswordLength()); + + return View(viewModel); + } + + [HttpPost, AlwaysAccessible, ValidateInput(false)] + public ActionResult ChangeExpiredPassword(string currentPassword, string newPassword, string confirmPassword, string username) { + var membershipSettings = _membershipService.GetSettings(); + var viewModel = _orchardServices.New.ViewModel( + Username: username, + PasswordLength: membershipSettings.GetMinimumPasswordLength()); + + if (!ValidateChangePassword(currentPassword, newPassword, confirmPassword)) { + return View(viewModel); + } + + if (PasswordChangeIsSuccess(currentPassword, newPassword, username)) { + return RedirectToAction("ChangePasswordSuccess"); + } + else { + return View(viewModel); + } + } + + private bool PasswordChangeIsSuccess(string currentPassword, string newPassword, string username) { + try { + var validated = _membershipService.ValidateUser(username, currentPassword); + + if (validated != null) { _membershipService.SetPassword(validated, newPassword); _userEventHandler.ChangedPassword(validated); - return RedirectToAction("ChangePasswordSuccess"); + + return true; } - - 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(); + + return false; + } + catch { + ModelState.AddModelError("_FORM", T("The current password is incorrect or the new password is invalid.")); + + return false; } } @@ -254,7 +315,8 @@ namespace Orchard.Users.Controllers { return RedirectToAction("LogOn"); } - ViewData["PasswordLength"] = MinPasswordLength; + var membershipSettings = _membershipService.GetSettings(); + ViewData["PasswordLength"] = membershipSettings.GetMinimumPasswordLength(); return View(); } @@ -268,11 +330,10 @@ namespace Orchard.Users.Controllers { return Redirect("~/"); } - ViewData["PasswordLength"] = MinPasswordLength; + var membershipSettings = _membershipService.GetSettings(); + ViewData["PasswordLength"] = membershipSettings.GetMinimumPasswordLength(); - if (newPassword == null || newPassword.Length < MinPasswordLength) { - ModelState.AddModelError("newPassword", T("You must specify a new password of {0} or more characters.", MinPasswordLength)); - } + ValidatePassword(newPassword); if (!String.Equals(newPassword, confirmPassword, StringComparison.Ordinal)) { ModelState.AddModelError("_FORM", T("The new password and confirmation password do not match.")); @@ -327,10 +388,13 @@ namespace Orchard.Users.Controllers { 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(currentPassword, newPassword, StringComparison.Ordinal)) { + ModelState.AddModelError("newPassword", T("The new password must be different from the current password.")); } + ValidatePassword(newPassword); + if ( !String.Equals(newPassword, confirmPassword, StringComparison.Ordinal) ) { ModelState.AddModelError("_FORM", T("The new password and confirmation password do not match.")); } @@ -397,15 +461,25 @@ namespace Orchard.Users.Controllers { if (!_userService.VerifyUserUnicity(userName, email)) { ModelState.AddModelError("userExists", T("User with that username and/or email already exists.")); } - if (password == null || password.Length < MinPasswordLength) { - ModelState.AddModelError("password", T("You must specify a password of {0} or more characters.", MinPasswordLength)); - } + + ValidatePassword(password); + if (!String.Equals(password, confirmPassword, StringComparison.Ordinal)) { ModelState.AddModelError("_FORM", T("The new password and confirmation password do not match.")); } return ModelState.IsValid; } + private void ValidatePassword(string password) { + IDictionary validationErrors; + + if (!_userService.PasswordMeetsPolicies(password, out validationErrors)) { + foreach (var error in validationErrors) { + ModelState.AddModelError(error.Key, error.Value); + } + } + } + private static string ErrorCodeToString(MembershipCreateStatus createStatus) { // See http://msdn.microsoft.com/en-us/library/system.web.security.membershipcreatestatus.aspx for // a full list of status codes. diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs index 6a3220838..9078d67bd 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -8,16 +9,15 @@ using Orchard.Core.Settings.Models; using Orchard.DisplayManagement; using Orchard.Localization; using Orchard.Mvc; +using Orchard.Mvc.Extensions; using Orchard.Security; +using Orchard.Settings; +using Orchard.UI.Navigation; using Orchard.UI.Notify; using Orchard.Users.Events; using Orchard.Users.Models; using Orchard.Users.Services; using Orchard.Users.ViewModels; -using Orchard.Mvc.Extensions; -using System; -using Orchard.Settings; -using Orchard.UI.Navigation; using Orchard.Utility.Extensions; namespace Orchard.Users.Controllers { @@ -35,6 +35,7 @@ namespace Orchard.Users.Controllers { IShapeFactory shapeFactory, IUserEventHandler userEventHandlers, ISiteService siteService) { + Services = services; _membershipService = membershipService; _userService = userService; @@ -189,6 +190,12 @@ namespace Orchard.Users.Controllers { AddModelError("ConfirmPassword", T("Password confirmation must match")); } + IDictionary validationErrors; + + if (!_userService.PasswordMeetsPolicies(createModel.Password, out validationErrors)) { + ModelState.AddModelErrors(validationErrors); + } + var user = Services.ContentManager.New("User"); if (ModelState.IsValid) { user = _membershipService.CreateUser(new CreateUserParams( diff --git a/src/Orchard.Web/Modules/Orchard.Users/Drivers/UserPartPasswordDriver.cs b/src/Orchard.Web/Modules/Orchard.Users/Drivers/UserPartPasswordDriver.cs index f752f50c0..76d3a0646 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Drivers/UserPartPasswordDriver.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Drivers/UserPartPasswordDriver.cs @@ -4,18 +4,26 @@ using Orchard.Environment.Extensions; using Orchard.Localization; using Orchard.Security; using Orchard.Users.Models; +using Orchard.Users.Services; using Orchard.Users.ViewModels; +using System.Collections.Generic; +using System.Web.Mvc; namespace Orchard.Users.Drivers{ [OrchardFeature("Orchard.Users.PasswordEditor")] public class UserPartPasswordDriver : ContentPartDriver { private readonly IMembershipService _membershipService; - + private readonly IUserService _userService; + public Localizer T { get; set; } - public UserPartPasswordDriver(IMembershipService membershipService) { + public UserPartPasswordDriver( + MembershipService membershipService, + IUserService userService) { + _membershipService = membershipService; + _userService = userService; T = NullLocalizer.Instance; } @@ -43,6 +51,11 @@ namespace Orchard.Users.Drivers{ _membershipService.SetPassword(actUser, editModel.Password); } } + + IDictionary validationErrors; + if (!_userService.PasswordMeetsPolicies(editModel.Password, out validationErrors)) { + updater.AddModelErrors(validationErrors); + } } } return Editor(part, shapeHelper); diff --git a/src/Orchard.Web/Modules/Orchard.Users/Extensions/MembershipSettingsExtensions.cs b/src/Orchard.Web/Modules/Orchard.Users/Extensions/MembershipSettingsExtensions.cs new file mode 100644 index 000000000..3b95cdaca --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Extensions/MembershipSettingsExtensions.cs @@ -0,0 +1,7 @@ +namespace Orchard.Security { + public static class MembershipSettingsExtensions { + public static int GetMinimumPasswordLength(this IMembershipSettings membershipSettings) { + return membershipSettings.EnableCustomPasswordPolicy ? membershipSettings.MinimumPasswordLength : 7; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Extensions/ModelStateDistionaryExtensions.cs b/src/Orchard.Web/Modules/Orchard.Users/Extensions/ModelStateDistionaryExtensions.cs new file mode 100644 index 000000000..c27954160 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Extensions/ModelStateDistionaryExtensions.cs @@ -0,0 +1,13 @@ +using Orchard.Localization; +using System.Collections.Generic; +using Orchard.Mvc.Extensions; + +namespace System.Web.Mvc { + public static class ModelStateDictionaryExtensions { + public static void AddModelErrors(this ModelStateDictionary modelStateDictionary, IDictionary validationErrors) { + foreach (var error in validationErrors) { + modelStateDictionary.AddModelError(error.Key, error.Value); + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Extensions/UpdateModelExtensions.cs b/src/Orchard.Web/Modules/Orchard.Users/Extensions/UpdateModelExtensions.cs new file mode 100644 index 000000000..9c1c18189 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Extensions/UpdateModelExtensions.cs @@ -0,0 +1,12 @@ +using Orchard.Localization; +using System.Collections.Generic; + +namespace Orchard.ContentManagement { + public static class UpdateModelExtensions { + public static void AddModelErrors(this IUpdateModel updateModel, IDictionary validationErrors) { + foreach (var error in validationErrors) { + updateModel.AddModelError(error.Key, error.Value); + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Migrations.cs b/src/Orchard.Web/Modules/Orchard.Users/Migrations.cs index 7a07c47e6..b6dfc3a32 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Migrations.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Migrations.cs @@ -1,7 +1,7 @@ -using System; -using Orchard.ContentManagement.MetaData; +using Orchard.ContentManagement.MetaData; using Orchard.Core.Contents.Extensions; using Orchard.Data.Migration; +using System; namespace Orchard.Users { public class UsersDataMigration : DataMigrationImpl { @@ -23,11 +23,12 @@ namespace Orchard.Users { .Column("CreatedUtc") .Column("LastLoginUtc") .Column("LastLogoutUtc") + .Column("LastPasswordChangeUtc") ); ContentDefinitionManager.AlterTypeDefinition("User", cfg => cfg.Creatable(false)); - return 4; + return 5; } public int UpdateFrom1() { @@ -54,5 +55,14 @@ namespace Orchard.Users { return 4; } + + public int UpdateFrom4() { + SchemaBuilder.AlterTable("UserPartRecord", + table => { + table.AddColumn("LastPasswordChangeUtc"); + }); + + return 5; + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPart.cs b/src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPart.cs index d59cb63fb..1d5bdb030 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPart.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPart.cs @@ -1,7 +1,10 @@ -using Orchard.ContentManagement; +using Orchard.ContentManagement; +using Orchard.Security; +using System.ComponentModel.DataAnnotations; +using System.Web.Security; namespace Orchard.Users.Models { - public class RegistrationSettingsPart : ContentPart { + public class RegistrationSettingsPart : ContentPart, IMembershipSettings { public bool UsersCanRegister { get { return this.Retrieve(x => x.UsersCanRegister); } set { this.Store(x => x.UsersCanRegister, value); } @@ -42,5 +45,51 @@ namespace Orchard.Users.Models { set { this.Store(x => x.EnableLostPassword, value); } } + public bool EnableCustomPasswordPolicy { + get { return this.Retrieve(x => x.EnableCustomPasswordPolicy); } + set { this.Store(x => x.EnableCustomPasswordPolicy, value); } + } + + [Range(1, int.MaxValue, ErrorMessage = "The minimum password length must be at least 1.")] + public int MinimumPasswordLength { + get { return this.Retrieve(x => x.MinimumPasswordLength, 7); } + set { this.Store(x => x.MinimumPasswordLength, value); } + } + + public bool EnablePasswordUppercaseRequirement { + get { return this.Retrieve(x => x.EnablePasswordUppercaseRequirement); } + set { this.Store(x => x.EnablePasswordUppercaseRequirement, value); } + } + + public bool EnablePasswordLowercaseRequirement { + get { return this.Retrieve(x => x.EnablePasswordLowercaseRequirement); } + set { this.Store(x => x.EnablePasswordLowercaseRequirement, value); } + } + + public bool EnablePasswordNumberRequirement { + get { return this.Retrieve(x => x.EnablePasswordNumberRequirement); } + set { this.Store(x => x.EnablePasswordNumberRequirement, value); } + } + + public bool EnablePasswordSpecialRequirement { + get { return this.Retrieve(x => x.EnablePasswordSpecialRequirement); } + set { this.Store(x => x.EnablePasswordSpecialRequirement, value); } + } + + public bool EnablePasswordExpiration { + get { return this.Retrieve(x => x.EnablePasswordExpiration); } + set { this.Store(x => x.EnablePasswordExpiration, value); } + } + + [Range(1, int.MaxValue, ErrorMessage = "The password expiration time must be a minimum of 1 day.")] + public int PasswordExpirationTimeInDays { + get { return this.Retrieve(x => x.PasswordExpirationTimeInDays, 30); } + set { this.Store(x => x.PasswordExpirationTimeInDays, value); } + } + + public MembershipPasswordFormat PasswordFormat { + get { return this.Retrieve(x => x.PasswordFormat, MembershipPasswordFormat.Hashed); } + set { this.Store(x => x.PasswordFormat, value); } + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPart.cs b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPart.cs index 150c43b9b..7afb5819f 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPart.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPart.cs @@ -1,7 +1,7 @@ -using System; -using System.Web.Security; -using Orchard.ContentManagement; +using Orchard.ContentManagement; using Orchard.Security; +using System; +using System.Web.Security; namespace Orchard.Users.Models { public sealed class UserPart : ContentPart, IUser { @@ -76,5 +76,10 @@ namespace Orchard.Users.Models { get { return Retrieve(x => x.LastLogoutUtc); } set { Store(x => x.LastLogoutUtc, value); } } + + public DateTime? LastPasswordChangeUtc { + get { return Retrieve(x => x.LastPasswordChangeUtc); } + set { Store(x => x.LastPasswordChangeUtc, value); } + } } } diff --git a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs index 9614f9f54..b8aa5ffce 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs @@ -1,6 +1,6 @@ +using Orchard.ContentManagement.Records; using System; using System.Web.Security; -using Orchard.ContentManagement.Records; namespace Orchard.Users.Models { public class UserPartRecord : ContentPartRecord { @@ -19,5 +19,6 @@ namespace Orchard.Users.Models { public virtual DateTime? CreatedUtc { get; set; } public virtual DateTime? LastLoginUtc { get; set; } public virtual DateTime? LastLogoutUtc { get; set; } + public virtual DateTime? LastPasswordChangeUtc { get; set; } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj index 457c465d9..007ecabc4 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj +++ b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj @@ -99,15 +99,19 @@ + + + + @@ -211,7 +215,9 @@ - + + Designer + @@ -236,6 +242,9 @@ + + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/src/Orchard.Web/Modules/Orchard.Users/Services/IUserService.cs b/src/Orchard.Web/Modules/Orchard.Users/Services/IUserService.cs index 274cf4448..5fa87c5b9 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Services/IUserService.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Services/IUserService.cs @@ -1,5 +1,8 @@ +using Orchard.Localization; using Orchard.Security; using System; +using System.Collections.Generic; + namespace Orchard.Users.Services { public interface IUserService : IDependency { bool VerifyUserUnicity(string userName, string email); @@ -13,5 +16,7 @@ namespace Orchard.Users.Services { string CreateNonce(IUser user, TimeSpan delay); bool DecryptNonce(string challengeToken, out string username, out DateTime validateByUtc); + + bool PasswordMeetsPolicies(string password, out IDictionary validationErrors); } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs index 317d0d200..48db0715d 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs @@ -1,21 +1,21 @@ -using System; +using Orchard.ContentManagement; +using Orchard.DisplayManagement; +using Orchard.Environment.Configuration; +using Orchard.Environment.Extensions; +using Orchard.Localization; +using Orchard.Logging; +using Orchard.Messaging.Services; +using Orchard.Security; +using Orchard.Services; +using Orchard.Users.Events; +using Orchard.Users.Models; +using System; +using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; -using System.Web.Security; -using Orchard.DisplayManagement; -using Orchard.Localization; -using Orchard.Logging; -using Orchard.ContentManagement; -using Orchard.Security; -using Orchard.Users.Events; -using Orchard.Users.Models; -using Orchard.Messaging.Services; -using System.Collections.Generic; -using Orchard.Services; using System.Web.Helpers; -using Orchard.Environment.Configuration; -using Orchard.Environment.Extensions; +using System.Web.Security; namespace Orchard.Users.Services { [OrchardSuppressDependency("Orchard.Security.NullMembershipService")] @@ -56,11 +56,9 @@ namespace Orchard.Users.Services { public ILogger Logger { get; set; } public Localizer T { get; set; } - public MembershipSettings GetSettings() { - var settings = new MembershipSettings(); - // accepting defaults - return settings; - } + public IMembershipSettings GetSettings(){ + return _orchardServices.WorkContext.CurrentSite.As(); + } public IUser CreateUser(CreateUserParams createUserParams) { Logger.Information("CreateUser {0} {1}", createUserParams.Username, createUserParams.Email); @@ -157,6 +155,10 @@ namespace Orchard.Users.Services { return user; } + public bool PasswordIsExpired(IUser user, int days){ + return user.As().LastPasswordChangeUtc.Value.AddDays(days) < _clock.UtcNow; + } + public void SetPassword(IUser user, string password) { if (!user.Is()) throw new InvalidCastException(); @@ -176,6 +178,7 @@ namespace Orchard.Users.Services { default: throw new ApplicationException(T("Unexpected password format value").ToString()); } + userPart.LastPasswordChangeUtc = _clock.UtcNow; } private bool ValidatePassword(UserPart userPart, string password) { diff --git a/src/Orchard.Web/Modules/Orchard.Users/Services/UserService.cs b/src/Orchard.Web/Modules/Orchard.Users/Services/UserService.cs index f6257305a..0f1b46c1d 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Services/UserService.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Services/UserService.cs @@ -1,19 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using Orchard.ContentManagement; using Orchard.DisplayManagement; +using Orchard.Environment.Configuration; using Orchard.Localization; using Orchard.Logging; -using Orchard.ContentManagement; -using Orchard.Settings; -using Orchard.Users.Models; -using Orchard.Security; -using System.Xml.Linq; -using Orchard.Services; -using System.Globalization; -using System.Text; using Orchard.Messaging.Services; -using Orchard.Environment.Configuration; +using Orchard.Security; +using Orchard.Services; +using Orchard.Settings; +using Orchard.Users.Constants; +using Orchard.Users.Models; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml.Linq; namespace Orchard.Users.Services { public class UserService : IUserService { @@ -38,8 +40,8 @@ namespace Orchard.Users.Services { IEncryptionService encryptionService, IShapeFactory shapeFactory, IShapeDisplay shapeDisplay, - ISiteService siteService - ) { + ISiteService siteService) { + _contentManager = contentManager; _membershipService = membershipService; _clock = clock; @@ -48,7 +50,9 @@ namespace Orchard.Users.Services { _shapeFactory = shapeFactory; _shapeDisplay = shapeDisplay; _siteService = siteService; + Logger = NullLogger.Instance; + T = NullLocalizer.Instance; } public ILogger Logger { get; set; } @@ -194,5 +198,42 @@ namespace Orchard.Users.Services { return user; } + + public bool PasswordMeetsPolicies(string password, out IDictionary validationErrors) { + validationErrors = new Dictionary(); + var settings = _siteService.GetSiteSettings().As(); + + if (string.IsNullOrEmpty(password)) { + validationErrors.Add(UserPasswordValidationResults.PasswordIsTooShort, + T("The password can't be empty.")); + return false; + } + + if (password.Length < settings.GetMinimumPasswordLength()) { + validationErrors.Add(UserPasswordValidationResults.PasswordIsTooShort, + T("You must specify a password of {0} or more characters.", settings.MinimumPasswordLength)); + } + + if (settings.EnableCustomPasswordPolicy) { + if (settings.EnablePasswordNumberRequirement && !Regex.Match(password, "[0-9]").Success) { + validationErrors.Add(UserPasswordValidationResults.PasswordDoesNotContainNumbers, + T("The password must contain at least one number.")); + } + if (settings.EnablePasswordUppercaseRequirement && !password.Any(c => char.IsUpper(c))) { + validationErrors.Add(UserPasswordValidationResults.PasswordDoesNotContainUppercase, + T("The password must contain at least one uppercase letter.")); + } + if (settings.EnablePasswordLowercaseRequirement && !password.Any(c => char.IsLower(c))) { + validationErrors.Add(UserPasswordValidationResults.PasswordDoesNotContainLowercase, + T("The password must contain at least one lowercase letter.")); + } + if (settings.EnablePasswordSpecialRequirement && !Regex.Match(password, "[^a-zA-Z0-9]").Success) { + validationErrors.Add(UserPasswordValidationResults.PasswordDoesNotContainSpecialCharacters, + T("The password must contain at least one special character.")); + } + } + + return validationErrors.Count == 0; + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/ViewModels/UserCreateViewModel.cs b/src/Orchard.Web/Modules/Orchard.Users/ViewModels/UserCreateViewModel.cs index 8018ba5e5..5b3646e15 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/ViewModels/UserCreateViewModel.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/ViewModels/UserCreateViewModel.cs @@ -11,7 +11,6 @@ namespace Orchard.Users.ViewModels { public string Email { get; set; } [Required, DataType(DataType.Password)] - [StringLength(50, MinimumLength = 7)] public string Password { get; set; } [Required, DataType(DataType.Password)] diff --git a/src/Orchard.Web/Modules/Orchard.Users/ViewModels/UserEditPasswordViewModel.cs b/src/Orchard.Web/Modules/Orchard.Users/ViewModels/UserEditPasswordViewModel.cs index fa6c25314..1a6b034c7 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/ViewModels/UserEditPasswordViewModel.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/ViewModels/UserEditPasswordViewModel.cs @@ -7,7 +7,6 @@ namespace Orchard.Users.ViewModels [OrchardFeature("Orchard.Users.EditPasswordByAdmin")] public class UserEditPasswordViewModel { [DataType(DataType.Password)] - [StringLength(50, MinimumLength = 7)] public string Password { get; set; } [DataType(DataType.Password)] diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChangeExpiredPassword.cshtml b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChangeExpiredPassword.cshtml new file mode 100644 index 000000000..dee4a0aca --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChangeExpiredPassword.cshtml @@ -0,0 +1,31 @@ +@model dynamic +

@Html.TitleForPage(T("Change Expired Password"))

+

@T("Your password has expired. Use the form below to change your password.")

+

@T.Plural("The password can't be empty.", "Passwords are required to be a minimum of {0} characters in length.", (int)Model.PasswordLength)

+@Html.ValidationSummary(T("Password change was unsuccessful. Please correct the errors and try again.").Text) +@using (Html.BeginFormAntiForgeryPost()) { +
+ @T("Account Information") +
+ @T("Username: {0}", Model.Username) +
+
+ + @Html.Password("currentPassword") + @Html.ValidationMessage("currentPassword") +
+
+ + @Html.Password("newPassword") + @Html.ValidationMessage("newPassword") +
+
+ + @Html.Password("confirmPassword") + @Html.ValidationMessage("confirmPassword") +
+
+ +
+
+} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChangePassword.cshtml b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChangePassword.cshtml index 1ee069ddc..92955db80 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChangePassword.cshtml +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChangePassword.cshtml @@ -1,7 +1,7 @@ @model dynamic

@Html.TitleForPage(T("Change Password").ToString())

@T("Use the form below to change your password.")

-

@T("New passwords are required to be a minimum of {0} characters in length.", ViewData["PasswordLength"])

+

@T.Plural("The password can't be empty.", "New passwords are required to be a minimum of {0} characters in length.", (int)ViewData["PasswordLength"])

@Html.ValidationSummary(T("Password change was unsuccessful. Please correct the errors and try again.").ToString()) @using (Html.BeginFormAntiForgeryPost()) {
diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/LostPassword.cshtml b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/LostPassword.cshtml index d710b3515..f48b76de8 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/LostPassword.cshtml +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/LostPassword.cshtml @@ -1,7 +1,7 @@ @model dynamic

@Html.TitleForPage(T("Change Password").ToString())

@T("Use the form below to change your password.")

-

@T("New passwords are required to be a minimum of {0} characters in length.", ViewData["PasswordLength"])

+

@T.Plural("The password can't be empty.", "New passwords are required to be a minimum of {0} characters in length.", (int)ViewData["PasswordLength"])

@Html.ValidationSummary(T("Password change was unsuccessful. Please correct the errors and try again.").ToString()) @using (Html.BeginFormAntiForgeryPost()) {
diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/EditorTemplates/Parts/Users.RegistrationSettings.cshtml b/src/Orchard.Web/Modules/Orchard.Users/Views/EditorTemplates/Parts/Users.RegistrationSettings.cshtml index e31ab0160..7006c7712 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Views/EditorTemplates/Parts/Users.RegistrationSettings.cshtml +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/EditorTemplates/Parts/Users.RegistrationSettings.cshtml @@ -1,5 +1,6 @@ @model Orchard.Users.Models.RegistrationSettingsPart @using Orchard.Messaging.Services; +@using System.Web.Security; @{ var messageManager = WorkContext.Resolve(); @@ -9,13 +10,69 @@
@T("Users")
- @Html.EditorFor(m => m.UsersCanRegister) - + @Html.EditorFor(m => m.UsersCanRegister) +
+
+ @Html.EditorFor(m => m.EnableCustomPasswordPolicy) + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + @Html.TextBoxFor(m => m.MinimumPasswordLength, new { @class = "text medium", @Value = Model.MinimumPasswordLength }) + @Html.ValidationMessage("MinimumPasswordLength", "*") +
+
+ + + +
+
+ + @Html.TextBoxFor(m => m.PasswordExpirationTimeInDays, new { @class = "text medium", @Value = Model.PasswordExpirationTimeInDays }) + @Html.ValidationMessage("PasswordExpirationTimeInDays", "*") +
+
+ + @Html.DropDownListFor(m => m.PasswordFormat, new SelectList(new[] + { + new SelectListItem { Text = T("Clear").Text, Value = MembershipPasswordFormat.Clear.ToString() }, + new SelectListItem { Text = T("Hashed").Text, Value = MembershipPasswordFormat.Hashed.ToString(), Selected = true }, + new SelectListItem { Text = T("Encrypted").Text, Value = MembershipPasswordFormat.Encrypted.ToString() } + }, "Value", "Text")) +
+
+
+
- + @if(!emailEnabled) {
@T("This option is available when an email module is activated.")
@@ -24,31 +81,31 @@
- + @if(!emailEnabled) {
@T("This option is available when an email module is activated.")
}
- + @Html.TextBoxFor(m => m.ValidateEmailRegisteredWebsite, new { @class = "text medium" } ) @Html.ValidationMessage("ValidateEmailRegisteredWebsite", "*") @T("The name of your website as it will appear in the verification e-mail.") - + @Html.TextBoxFor(m => m.ValidateEmailContactEMail, new { @class = "text medium" } ) @Html.ValidationMessage("ValidateEmailContactEMail", "*") @T("The e-mail address displayed in the verification e-mail for a Contact Us link. Leave empty for no link.")
@Html.EditorFor(m => m.UsersAreModerated) - +
- + @if(!emailEnabled) {
@T("This option is available when an email module is activated.")
@@ -56,7 +113,7 @@
- + @Html.TextBoxFor(m => m.NotificationsRecipients, new { @class = "text medium" } ) @Html.ValidationMessage("NotificationsRecipients", "*") @T("The usernames to send the notifications to (e.g., \"admin, user1, ...\").") diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Register.cshtml b/src/Orchard.Web/Modules/Orchard.Users/Views/Register.cshtml index e393e9710..c3a2a9978 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Views/Register.cshtml +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Register.cshtml @@ -1,6 +1,6 @@ 

@Html.TitleForPage(T("Create a New Account").ToString())

@T("Use the form below to create a new account.")

-

@T("Passwords are required to be a minimum of {0} characters in length.", ViewData["PasswordLength"])

+

@T.Plural("The password can't be empty.", "Passwords are required to be a minimum of {0} characters in length.", (int)ViewData["PasswordLength"])

@Html.ValidationSummary(T("Account creation was unsuccessful. Please correct the errors and try again.").ToString()) @using (Html.BeginFormAntiForgeryPost(Url.Action("Register", new { ReturnUrl = Request.QueryString["ReturnUrl"] }))) {
@@ -13,7 +13,7 @@
@Html.TextBox("email") - @Html.ValidationMessage("email") + @Html.ValidationMessage("email")
diff --git a/src/Orchard.Web/Modules/Orchard.Users/Web.config b/src/Orchard.Web/Modules/Orchard.Users/Web.config index a3cb5df90..2dbaaf5fd 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Web.config +++ b/src/Orchard.Web/Modules/Orchard.Users/Web.config @@ -32,6 +32,7 @@ + diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 02187c3eb..7c15c9706 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -209,6 +209,7 @@ + @@ -939,7 +940,6 @@ - diff --git a/src/Orchard/Security/IMembershipService.cs b/src/Orchard/Security/IMembershipService.cs index c1de8042d..6c3301728 100644 --- a/src/Orchard/Security/IMembershipService.cs +++ b/src/Orchard/Security/IMembershipService.cs @@ -1,10 +1,12 @@ namespace Orchard.Security { public interface IMembershipService : IDependency { - MembershipSettings GetSettings(); + IMembershipSettings GetSettings(); IUser CreateUser(CreateUserParams createUserParams); IUser GetUser(string username); IUser ValidateUser(string userNameOrEmail, string password); void SetPassword(IUser user, string password); + + bool PasswordIsExpired(IUser user, int days); } } diff --git a/src/Orchard/Security/IMembershipSettings.cs b/src/Orchard/Security/IMembershipSettings.cs new file mode 100644 index 000000000..bf2748c65 --- /dev/null +++ b/src/Orchard/Security/IMembershipSettings.cs @@ -0,0 +1,23 @@ +using System.Web.Security; + +namespace Orchard.Security { + public interface IMembershipSettings { + bool UsersCanRegister { get; set; } + bool UsersMustValidateEmail { get; set; } + string ValidateEmailRegisteredWebsite { get; set; } + string ValidateEmailContactEMail { get; set; } + bool UsersAreModerated { get; set; } + bool NotifyModeration { get; set; } + string NotificationsRecipients { get; set; } + bool EnableLostPassword { get; set; } + bool EnableCustomPasswordPolicy { get; set; } + int MinimumPasswordLength { get; set; } + bool EnablePasswordUppercaseRequirement { get; set; } + bool EnablePasswordLowercaseRequirement { get; set; } + bool EnablePasswordNumberRequirement { get; set; } + bool EnablePasswordSpecialRequirement { get; set; } + bool EnablePasswordExpiration { get; set; } + int PasswordExpirationTimeInDays { get; set; } + MembershipPasswordFormat PasswordFormat { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard/Security/MembershipSettings.cs b/src/Orchard/Security/MembershipSettings.cs deleted file mode 100644 index feb32f946..000000000 --- a/src/Orchard/Security/MembershipSettings.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Web.Security; - -namespace Orchard.Security { - public class MembershipSettings { - public MembershipSettings() { - EnablePasswordRetrieval = false; - EnablePasswordReset = true; - RequiresQuestionAndAnswer = true; - RequiresUniqueEmail = true; - MaxInvalidPasswordAttempts = 5; - PasswordAttemptWindow = 10; - MinRequiredPasswordLength = 7; - MinRequiredNonAlphanumericCharacters = 1; - PasswordStrengthRegularExpression = ""; - PasswordFormat = MembershipPasswordFormat.Hashed; - } - - public bool EnablePasswordRetrieval { get; set; } - public bool EnablePasswordReset { get; set; } - public bool RequiresQuestionAndAnswer { get; set; } - public int MaxInvalidPasswordAttempts { get; set; } - public int PasswordAttemptWindow { get; set; } - public bool RequiresUniqueEmail { get; set; } - public MembershipPasswordFormat PasswordFormat { get; set; } - public int MinRequiredPasswordLength { get; set; } - public int MinRequiredNonAlphanumericCharacters { get; set; } - public string PasswordStrengthRegularExpression { get; set; } - } -} \ No newline at end of file diff --git a/src/Orchard/Security/NullMembershipService.cs b/src/Orchard/Security/NullMembershipService.cs index bb3b763e8..08640b245 100644 --- a/src/Orchard/Security/NullMembershipService.cs +++ b/src/Orchard/Security/NullMembershipService.cs @@ -11,7 +11,7 @@ namespace Orchard.Security { throw new NotImplementedException(); } - public MembershipSettings GetSettings() { + public IMembershipSettings GetSettings() { throw new NotImplementedException(); } @@ -26,5 +26,9 @@ namespace Orchard.Security { public IUser ValidateUser(string userNameOrEmail, string password) { throw new NotImplementedException(); } + + public bool PasswordIsExpired(IUser user, int weeks) { + throw new NotImplementedException(); + } } }