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
This commit is contained in:
Zoltán Lehóczky
2016-07-28 21:59:13 +02:00
committed by GitHub
parent c016a03b40
commit d32847bee1
29 changed files with 509 additions and 165 deletions

View File

@@ -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<string, LocalizedString> 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;
}
}
}
}

View File

@@ -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";
}
}

View File

@@ -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,
IMembershipService membershipService,
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;
}
@@ -83,9 +90,18 @@ 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);
}
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);
_userEventHandler.LoggedIn(user);
@@ -103,23 +119,18 @@ 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<RegistrationSettingsPart>();
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);
}
@@ -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<RegistrationSettingsPart>();
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<UserPart>().EmailStatus == UserStatus.Pending ) {
if (user.As<UserPart>().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<UserPart>(), nonce => Url.MakeAbsolute(Url.Action("ChallengeEmail", "Account", new {Area = "Orchard.Users", nonce = nonce}), siteUrl));
_userService.SendChallengeEmail(user.As<UserPart>(), nonce => Url.MakeAbsolute(Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", nonce = nonce }), siteUrl));
_userEventHandler.SentChallengeEmail(user);
return RedirectToAction("ChallengeEmailSent", new { ReturnUrl = returnUrl });
@@ -175,8 +186,8 @@ namespace Orchard.Users.Controllers {
[AlwaysAccessible]
public ActionResult RequestLostPassword() {
// ensure users can request lost password
var registrationSettings = _orchardServices.WorkContext.CurrentSite.As<RegistrationSettingsPart>();
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<RegistrationSettingsPart>();
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();
}
@@ -212,7 +223,8 @@ namespace Orchard.Users.Controllers {
[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 ( validated != null ) {
_membershipService.SetPassword(validated, newPassword);
_userEventHandler.ChangedPassword(validated);
if (PasswordChangeIsSuccess(currentPassword, newPassword, _orchardServices.WorkContext.CurrentUser.UserName)) {
return RedirectToAction("ChangePasswordSuccess");
}
else {
return ChangePassword();
}
}
[AlwaysAccessible]
public ActionResult ChangeExpiredPassword(string username) {
var membershipSettings = _membershipService.GetSettings();
var lastPasswordChangeUtc = _membershipService.GetUser(username).As<UserPart>().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 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<string, LocalizedString> 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.

View File

@@ -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<string, LocalizedString> validationErrors;
if (!_userService.PasswordMeetsPolicies(createModel.Password, out validationErrors)) {
ModelState.AddModelErrors(validationErrors);
}
var user = Services.ContentManager.New<IUser>("User");
if (ModelState.IsValid) {
user = _membershipService.CreateUser(new CreateUserParams(

View File

@@ -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<UserPart> {
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<string, LocalizedString> validationErrors;
if (!_userService.PasswordMeetsPolicies(editModel.Password, out validationErrors)) {
updater.AddModelErrors(validationErrors);
}
}
}
return Editor(part, shapeHelper);

View File

@@ -0,0 +1,7 @@
namespace Orchard.Security {
public static class MembershipSettingsExtensions {
public static int GetMinimumPasswordLength(this IMembershipSettings membershipSettings) {
return membershipSettings.EnableCustomPasswordPolicy ? membershipSettings.MinimumPasswordLength : 7;
}
}
}

View File

@@ -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<string, LocalizedString> validationErrors) {
foreach (var error in validationErrors) {
modelStateDictionary.AddModelError(error.Key, error.Value);
}
}
}
}

View File

@@ -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<string, LocalizedString> validationErrors) {
foreach (var error in validationErrors) {
updateModel.AddModelError(error.Key, error.Value);
}
}
}
}

View File

@@ -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<DateTime>("CreatedUtc")
.Column<DateTime>("LastLoginUtc")
.Column<DateTime>("LastLogoutUtc")
.Column<DateTime>("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<DateTime>("LastPasswordChangeUtc");
});
return 5;
}
}
}

View File

@@ -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); }
}
}
}

View File

@@ -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<UserPartRecord>, 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); }
}
}
}

View File

@@ -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; }
}
}

View File

@@ -99,15 +99,19 @@
<Compile Include="Activities\UserActivity.cs" />
<Compile Include="Activities\UserIsApprovedActivity.cs" />
<Compile Include="Commands\UserCommands.cs" />
<Compile Include="Constants\UserPasswordValidationResults.cs" />
<Compile Include="Controllers\AccountController.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Drivers\UserPartDriver.cs" />
<Compile Include="Drivers\UserPartPasswordDriver.cs" />
<Compile Include="Events\LoginUserEventHandler.cs" />
<Compile Include="Extensions\MembershipSettingsExtensions.cs" />
<Compile Include="Extensions\UpdateModelExtensions.cs" />
<Compile Include="Forms\SignInUserForm.cs" />
<Compile Include="Forms\VerifyUserUnicityForm.cs" />
<Compile Include="Forms\CreateUserForm.cs" />
<Compile Include="Handlers\WorkflowUserEventHandler.cs" />
<Compile Include="Extensions\ModelStateDistionaryExtensions.cs" />
<Compile Include="Migrations.cs" />
<Compile Include="Events\UserContext.cs" />
<Compile Include="Handlers\RegistrationSettingsPartHandler.cs" />
@@ -211,7 +215,9 @@
<Content Include="Views\LogOn.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Web.config" />
<Content Include="Web.config">
<SubType>Designer</SubType>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="Styles\Web.config">
@@ -236,6 +242,9 @@
<ItemGroup>
<Content Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Account\ChangeExpiredPassword.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@@ -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<string, LocalizedString> validationErrors);
}
}

View File

@@ -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,10 +56,8 @@ 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<RegistrationSettingsPart>();
}
public IUser CreateUser(CreateUserParams createUserParams) {
@@ -157,6 +155,10 @@ namespace Orchard.Users.Services {
return user;
}
public bool PasswordIsExpired(IUser user, int days){
return user.As<UserPart>().LastPasswordChangeUtc.Value.AddDays(days) < _clock.UtcNow;
}
public void SetPassword(IUser user, string password) {
if (!user.Is<UserPart>())
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) {

View File

@@ -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<string, LocalizedString> validationErrors) {
validationErrors = new Dictionary<string, LocalizedString>();
var settings = _siteService.GetSiteSettings().As<RegistrationSettingsPart>();
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;
}
}
}

View File

@@ -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)]

View File

@@ -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)]

View File

@@ -0,0 +1,31 @@
@model dynamic
<h1>@Html.TitleForPage(T("Change Expired Password"))</h1>
<p>@T("Your password has expired. Use the form below to change your password.")</p>
<p>@T.Plural("The password can't be empty.", "Passwords are required to be a minimum of {0} characters in length.", (int)Model.PasswordLength)</p>
@Html.ValidationSummary(T("Password change was unsuccessful. Please correct the errors and try again.").Text)
@using (Html.BeginFormAntiForgeryPost()) {
<fieldset>
<legend>@T("Account Information")</legend>
<div>
@T("Username: {0}", Model.Username)
</div>
<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")
@Html.ValidationMessage("newPassword")
</div>
<div>
<label for="confirmPassword">@T("Confirm New Password:")</label>
@Html.Password("confirmPassword")
@Html.ValidationMessage("confirmPassword")
</div>
<div>
<button class="primaryAction" type="submit">@T("Change Password")</button>
</div>
</fieldset>
}

View File

@@ -1,7 +1,7 @@
@model dynamic
<h1>@Html.TitleForPage(T("Change Password").ToString()) </h1>
<p>@T("Use the form below to change your password.")</p>
<p>@T("New passwords are required to be a minimum of {0} characters in length.", ViewData["PasswordLength"]) </p>
<p>@T.Plural("The password can't be empty.", "New passwords are required to be a minimum of {0} characters in length.", (int)ViewData["PasswordLength"])</p>
@Html.ValidationSummary(T("Password change was unsuccessful. Please correct the errors and try again.").ToString())
@using (Html.BeginFormAntiForgeryPost()) {
<fieldset>

View File

@@ -1,7 +1,7 @@
@model dynamic
<h1>@Html.TitleForPage(T("Change Password").ToString()) </h1>
<p>@T("Use the form below to change your password.")</p>
<p>@T("New passwords are required to be a minimum of {0} characters in length.", ViewData["PasswordLength"]) </p>
<p>@T.Plural("The password can't be empty.", "New passwords are required to be a minimum of {0} characters in length.", (int)ViewData["PasswordLength"])</p>
@Html.ValidationSummary(T("Password change was unsuccessful. Please correct the errors and try again.").ToString())
@using (Html.BeginFormAntiForgeryPost()) {
<fieldset>

View File

@@ -1,5 +1,6 @@
@model Orchard.Users.Models.RegistrationSettingsPart
@using Orchard.Messaging.Services;
@using System.Web.Security;
@{
var messageManager = WorkContext.Resolve<IMessageManager>();
@@ -10,12 +11,68 @@
<legend>@T("Users")</legend>
<div>
@Html.EditorFor(m => m.UsersCanRegister)
<label class="forcheckbox" for="@Html.FieldIdFor( m => m.UsersCanRegister)">@T("Users can create new accounts on the site")</label>
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.UsersCanRegister)">@T("Users can create new accounts on the site")</label>
</div>
<div>
@Html.EditorFor(m => m.EnableCustomPasswordPolicy)
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.EnableCustomPasswordPolicy)">@T("Passwords must meet custom requirements")</label>
<div data-controllerid="@Html.FieldIdFor(m => m.EnableCustomPasswordPolicy)" style="margin-left: 30px;">
<div>
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.EnablePasswordLowercaseRequirement)"
name="@Html.FieldNameFor(m => m.EnablePasswordLowercaseRequirement)" @(Model.EnablePasswordLowercaseRequirement && Model.EnableCustomPasswordPolicy ? "checked=\"checked\"" : "") />
<input name="@Html.FieldNameFor(m => m.EnablePasswordLowercaseRequirement)" type="hidden" value="false">
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.EnablePasswordLowercaseRequirement)">@T("Password must contain at least one lower case letter (a-z)")</label>
</div>
<div>
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.EnablePasswordUppercaseRequirement)"
name="@Html.FieldNameFor(m => m.EnablePasswordUppercaseRequirement)" @(Model.EnablePasswordUppercaseRequirement && Model.EnableCustomPasswordPolicy ? "checked=\"checked\"" : "") />
<input name="@Html.FieldNameFor(m => m.EnablePasswordUppercaseRequirement)" type="hidden" value="false">
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.EnablePasswordUppercaseRequirement)">@T("Password must contain at least one upper case letter (A-Z)")</label>
</div>
<div>
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.EnablePasswordNumberRequirement)"
name="@Html.FieldNameFor(m => m.EnablePasswordNumberRequirement)" @(Model.EnablePasswordNumberRequirement && Model.EnableCustomPasswordPolicy ? "checked=\"checked\"" : "") />
<input name="@Html.FieldNameFor(m => m.EnablePasswordNumberRequirement)" type="hidden" value="false">
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.EnablePasswordNumberRequirement)">@T("Password must contain at least one number (0-9)")</label>
</div>
<div>
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.EnablePasswordSpecialRequirement)"
name="@Html.FieldNameFor(m => m.EnablePasswordSpecialRequirement)" @(Model.EnablePasswordSpecialRequirement && Model.EnableCustomPasswordPolicy ? "checked=\"checked\"" : "") />
<input name="@Html.FieldNameFor(m => m.EnablePasswordSpecialRequirement)" type="hidden" value="false">
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.EnablePasswordSpecialRequirement)">@T("Password must contain at least one special character")</label>
</div>
<div>
<label for="@Html.FieldIdFor(m => m.MinimumPasswordLength)">@T("Minimum Password length")</label>
@Html.TextBoxFor(m => m.MinimumPasswordLength, new { @class = "text medium", @Value = Model.MinimumPasswordLength })
@Html.ValidationMessage("MinimumPasswordLength", "*")
</div>
<div>
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.EnablePasswordExpiration)"
name="@Html.FieldNameFor(m => m.EnablePasswordExpiration)" @(Model.EnablePasswordExpiration && Model.EnableCustomPasswordPolicy ? "checked=\"checked\"" : "") />
<input name="@Html.FieldNameFor(m => m.EnablePasswordExpiration)" type="hidden" value="false">
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.EnablePasswordExpiration)">@T("Password expires after a time period (30 days by default)")</label>
</div>
<div data-controllerid="@Html.FieldIdFor(m => m.EnablePasswordExpiration)" style="margin-left: 30px;">
<label for="@Html.FieldIdFor(m => m.PasswordExpirationTimeInDays)">@T("Password expiration period in days")</label>
@Html.TextBoxFor(m => m.PasswordExpirationTimeInDays, new { @class = "text medium", @Value = Model.PasswordExpirationTimeInDays })
@Html.ValidationMessage("PasswordExpirationTimeInDays", "*")
</div>
<div>
<label for="@Html.FieldIdFor(m => m.PasswordFormat)">@T("Password format")</label>
@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"))
</div>
</div>
</div>
<div>
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.EnableLostPassword)" name="@Html.FieldNameFor(m => m.EnableLostPassword)" @(Model.EnableLostPassword ? "checked=\"checked\"" : "") @(emailEnabled ? "" : "disabled=\"disabled\"")/>
<input name="@Html.FieldNameFor(m => m.EnableLostPassword)" type="hidden" value="false">
<label class="forcheckbox" for="@Html.FieldIdFor( m => m.EnableLostPassword)">@T("Display a link to enable users to reset their password")</label>
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.EnableLostPassword)">@T("Display a link to enable users to reset their password")</label>
@if(!emailEnabled) {
<div class="message message-Warning">@T("This option is available when an email module is activated.")</div>
@@ -24,31 +81,31 @@
<div>
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.UsersMustValidateEmail)" name="@Html.FieldNameFor(m => m.UsersMustValidateEmail)" @(Model.UsersMustValidateEmail ? "checked=\"checked\"" : "") @(emailEnabled ? "" : "disabled=\"disabled\"")/>
<input name="@Html.FieldNameFor(m => m.UsersMustValidateEmail)" type="hidden" value="false">
<label class="forcheckbox" for="@Html.FieldIdFor( m => m.UsersMustValidateEmail)">@T("Users must verify their email address")</label>
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.UsersMustValidateEmail)">@T("Users must verify their email address")</label>
@if(!emailEnabled) {
<div class="message message-Warning">@T("This option is available when an email module is activated.")</div>
}
</div>
<div data-controllerid="@Html.FieldIdFor(m => m.UsersMustValidateEmail)">
<label for="@Html.FieldIdFor( m => m.ValidateEmailRegisteredWebsite)">@T("Website public name")</label>
<label for="@Html.FieldIdFor(m => m.ValidateEmailRegisteredWebsite)">@T("Website public name")</label>
@Html.TextBoxFor(m => m.ValidateEmailRegisteredWebsite, new { @class = "text medium" } )
@Html.ValidationMessage("ValidateEmailRegisteredWebsite", "*")
<span class="hint">@T("The name of your website as it will appear in the verification e-mail.")</span>
<label for="@Html.FieldIdFor( m => m.ValidateEmailContactEMail)">@T("Contact Us E-Mail address")</label>
<label for="@Html.FieldIdFor(m => m.ValidateEmailContactEMail)">@T("Contact Us E-Mail address")</label>
@Html.TextBoxFor(m => m.ValidateEmailContactEMail, new { @class = "text medium" } )
@Html.ValidationMessage("ValidateEmailContactEMail", "*")
<span class="hint">@T("The e-mail address displayed in the verification e-mail for a Contact Us link. Leave empty for no link.")</span>
</div>
<div>
@Html.EditorFor(m => m.UsersAreModerated)
<label class="forcheckbox" for="@Html.FieldIdFor( m => m.UsersAreModerated)">@T("Users must be approved before they can log in")</label>
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.UsersAreModerated)">@T("Users must be approved before they can log in")</label>
</div>
<div data-controllerid="@Html.FieldIdFor(m => m.UsersAreModerated)">
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.NotifyModeration)" name="@Html.FieldNameFor(m => m.NotifyModeration)" @(Model.NotifyModeration ? "checked=\"checked\"" : "") @(emailEnabled ? "" : "disabled=\"disabled\"")/>
<input name="@Html.FieldNameFor(m => m.NotifyModeration)" type="hidden" value="false">
<label class="forcheckbox" for="@Html.FieldIdFor( m => m.NotifyModeration)">@T("Send a notification when a user needs moderation")</label>
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.NotifyModeration)">@T("Send a notification when a user needs moderation")</label>
@if(!emailEnabled) {
<div class="message message-Warning">@T("This option is available when an email module is activated.")</div>
@@ -56,7 +113,7 @@
</div>
<div data-controllerid="@Html.FieldIdFor(m => m.NotifyModeration)">
<label for="@Html.FieldIdFor( m => m.NotificationsRecipients)">@T("Moderators")</label>
<label for="@Html.FieldIdFor(m => m.NotificationsRecipients)">@T("Moderators")</label>
@Html.TextBoxFor(m => m.NotificationsRecipients, new { @class = "text medium" } )
@Html.ValidationMessage("NotificationsRecipients", "*")
<span class="hint">@T("The usernames to send the notifications to (e.g., \"admin, user1, ...\").")</span>

View File

@@ -1,6 +1,6 @@
<h1>@Html.TitleForPage(T("Create a New Account").ToString())</h1>
<p>@T("Use the form below to create a new account.")</p>
<p>@T("Passwords are required to be a minimum of {0} characters in length.", ViewData["PasswordLength"])</p>
<p>@T.Plural("The password can't be empty.", "Passwords are required to be a minimum of {0} characters in length.", (int)ViewData["PasswordLength"])</p>
@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"] }))) {
<fieldset>

View File

@@ -32,6 +32,7 @@
<add assembly="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add assembly="Orchard.Framework" />
<add assembly="Orchard.Core" />
<add assembly="System.Web.ApplicationServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</assemblies>
</compilation>
</system.web>

View File

@@ -209,6 +209,7 @@
<Compile Include="Reports\Services\ReportsCoordinator.cs" />
<Compile Include="Reports\Services\ReportsManager.cs" />
<Compile Include="Reports\Services\ReportsPersister.cs" />
<Compile Include="Security\IMembershipSettings.cs" />
<Compile Include="Security\IMembershipValidationService.cs" />
<Compile Include="Localization\Services\ILocalizationStreamParser.cs" />
<Compile Include="Localization\Services\LocalizationStreamParser.cs" />
@@ -939,7 +940,6 @@
<Compile Include="Mvc\Wrappers\HttpResponseBaseWrapper.cs" />
<Compile Include="OrchardException.cs" />
<Compile Include="Security\IAuthorizationServiceEventHandler.cs" />
<Compile Include="Security\MembershipSettings.cs" />
<Compile Include="Security\StandardPermissions.cs" />
<Compile Include="Security\OrchardSecurityException.cs" />
<Compile Include="Tasks\Scheduling\IPublishingTaskManager.cs" />

View File

@@ -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);
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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();
}
}
}