diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index 0bdc479706..10a05d042e 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -15,6 +15,40 @@ namespace Umbraco.Core.Security /// public abstract class MembershipProviderBase : MembershipProvider { + /// + /// Providers can override this setting, default is 7 + /// + protected virtual int DefaultMinPasswordLength + { + get { return 7; } + } + + /// + /// Providers can override this setting, default is 1 + /// + protected virtual int DefaultMinNonAlphanumericChars + { + get { return 1; } + } + + /// + /// Providers can override this setting, default is false to use better security + /// + protected virtual bool DefaultUseLegacyEncoding + { + get { return false; } + } + + /// + /// Providers can override this setting, by default this is false which means that the provider will + /// authenticate the username + password when ChangePassword is called. This property exists purely for + /// backwards compatibility. + /// + internal virtual bool AllowManuallyChangingPassword + { + get { return false; } + } + private string _applicationName; private bool _enablePasswordReset; private bool _enablePasswordRetrieval; @@ -177,8 +211,8 @@ namespace Umbraco.Core.Security _requiresUniqueEmail = config.GetValue("requiresUniqueEmail", false); _maxInvalidPasswordAttempts = GetIntValue(config, "maxInvalidPasswordAttempts", 5, false, 0); _passwordAttemptWindow = GetIntValue(config, "passwordAttemptWindow", 10, false, 0); - _minRequiredPasswordLength = GetIntValue(config, "minRequiredPasswordLength", 7, true, 0x80); - _minRequiredNonAlphanumericCharacters = GetIntValue(config, "minRequiredNonalphanumericCharacters", 1, true, 0x80); + _minRequiredPasswordLength = GetIntValue(config, "minRequiredPasswordLength", DefaultMinPasswordLength, true, 0x80); + _minRequiredNonAlphanumericCharacters = GetIntValue(config, "minRequiredNonalphanumericCharacters", DefaultMinNonAlphanumericChars, true, 0x80); _passwordStrengthRegularExpression = config["passwordStrengthRegularExpression"]; _applicationName = config["applicationName"]; @@ -186,7 +220,7 @@ namespace Umbraco.Core.Security _applicationName = GetDefaultAppName(); //by default we will continue using the legacy encoding. - _useLegacyEncoding = config.GetValue("useLegacyEncoding", true); + _useLegacyEncoding = config.GetValue("useLegacyEncoding", DefaultUseLegacyEncoding); // make sure password format is clear by default. string str = config["passwordFormat"] ?? "Clear"; @@ -238,6 +272,26 @@ namespace Umbraco.Core.Security Strength } + /// + /// Checks to ensure the AllowManuallyChangingPassword rule is adhered to + /// + /// + /// + /// + /// + public sealed override bool ChangePassword(string username, string oldPassword, string newPassword) + { + if (oldPassword.IsNullOrWhiteSpace() && AllowManuallyChangingPassword == false) + { + //If the old password is empty and AllowManuallyChangingPassword is false, than this provider cannot just arbitrarily change the password + throw new NotSupportedException("This provider does not support manually changing the password"); + } + + return PerformChangePassword(username, oldPassword, newPassword); + } + + protected abstract bool PerformChangePassword(string username, string oldPassword, string newPassword); + protected internal static Attempt IsPasswordValid(string password, int minRequiredNonAlphanumericChars, string strengthRegex, int minLength) { if (minRequiredNonAlphanumericChars > 0) diff --git a/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx b/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx index 5e835e807d..8dc2c51d0f 100644 --- a/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx +++ b/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx @@ -77,7 +77,7 @@
- +
diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index c4a55cd99f..b44a80c832 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -232,78 +232,95 @@ namespace Umbraco.Web.Security return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, error: " + ex.Message + " (see log for full details)", new[] { "resetPassword" }) }); } } - if (passwordModel.NewPassword.IsNullOrWhiteSpace() == false) + + //we're not resetting it so we need to try to change it. + + if (passwordModel.NewPassword.IsNullOrWhiteSpace()) { - //we're not resetting it so we need to try to change it. + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) }); + } - if (passwordModel.OldPassword.IsNullOrWhiteSpace() && membershipProvider.EnablePasswordRetrieval == false) + //This is an edge case and is only necessary for backwards compatibility: + var umbracoBaseProvider = membershipProvider as MembershipProviderBase; + if (umbracoBaseProvider != null && umbracoBaseProvider.AllowManuallyChangingPassword) + { + //this provider allows manually changing the password without the old password, so we can just do it + try { - //if password retrieval is not enabled but there is no old password we cannot continue - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "value" }) }); + var result = umbracoBaseProvider.ChangePassword(username, "", passwordModel.NewPassword); + return result == false + ? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "value" }) }) + : Attempt.Succeed(new PasswordChangedModel()); } - if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false) + catch (Exception ex) { - //if an old password is suplied try to change it - - try - { - var result = membershipProvider.ChangePassword(username, passwordModel.OldPassword, passwordModel.NewPassword); - if (result == false) - { - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "value" }) }); - } - } - catch (Exception ex) - { - LogHelper.WarnWithException("Could not change member password", ex); - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) }); - } - } - else if (membershipProvider.EnablePasswordRetrieval == false) - { - //we cannot continue if we cannot get the current password - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "value" }) }); - } - else if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace()) - { - //if the question answer is required but there isn't one, we cannot continue - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the password answer", new[] { "value" }) }); - } - else - { - //lets try to get the old one so we can change it - - try - { - var oldPassword = membershipProvider.GetPassword( - username, - membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null); - - try - { - var result = membershipProvider.ChangePassword(username, oldPassword, passwordModel.NewPassword); - if (result == false) - { - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password", new[] { "value" }) }); - } - } - catch (Exception ex1) - { - LogHelper.WarnWithException("Could not change member password", ex1); - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex1.Message + " (see log for full details)", new[] { "value" }) }); - } - - } - catch (Exception ex2) - { - LogHelper.WarnWithException("Could not retrieve member password", ex2); - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex2.Message + " (see log for full details)", new[] { "value" }) }); - } + LogHelper.WarnWithException("Could not change member password", ex); + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) }); } } - //woot! - return Attempt.Succeed(new PasswordChangedModel()); + //The provider does not support manually chaning the password but no old password supplied - need to return an error + if (passwordModel.OldPassword.IsNullOrWhiteSpace() && membershipProvider.EnablePasswordRetrieval == false) + { + //if password retrieval is not enabled but there is no old password we cannot continue + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "value" }) }); + } + + if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false) + { + //if an old password is suplied try to change it + + try + { + var result = membershipProvider.ChangePassword(username, passwordModel.OldPassword, passwordModel.NewPassword); + return result == false + ? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "value" }) }) + : Attempt.Succeed(new PasswordChangedModel()); + } + catch (Exception ex) + { + LogHelper.WarnWithException("Could not change member password", ex); + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) }); + } + } + + if (membershipProvider.EnablePasswordRetrieval == false) + { + //we cannot continue if we cannot get the current password + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "value" }) }); + } + if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace()) + { + //if the question answer is required but there isn't one, we cannot continue + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the password answer", new[] { "value" }) }); + } + + //lets try to get the old one so we can change it + try + { + var oldPassword = membershipProvider.GetPassword( + username, + membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null); + + try + { + var result = membershipProvider.ChangePassword(username, oldPassword, passwordModel.NewPassword); + return result == false + ? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password", new[] { "value" }) }) + : Attempt.Succeed(new PasswordChangedModel()); + } + catch (Exception ex1) + { + LogHelper.WarnWithException("Could not change member password", ex1); + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex1.Message + " (see log for full details)", new[] { "value" }) }); + } + + } + catch (Exception ex2) + { + LogHelper.WarnWithException("Could not retrieve member password", ex2); + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex2.Message + " (see log for full details)", new[] { "value" }) }); + } } /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/passwordChanger.ascx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/passwordChanger.ascx.cs index 1a87a1ef20..482d36d77f 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/passwordChanger.ascx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/passwordChanger.ascx.cs @@ -10,6 +10,7 @@ using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; using Umbraco.Core; +using Umbraco.Core.Security; using Umbraco.Web.Models; namespace umbraco.controls @@ -23,6 +24,23 @@ namespace umbraco.controls get { return Membership.Providers[MembershipProviderName]; } } + /// + /// Determines whether to show the old password field or not + /// + protected bool ShowOldPassword + { + get + { + var umbProvider = Provider as MembershipProviderBase; + if (umbProvider != null && umbProvider.AllowManuallyChangingPassword) + { + return false; + } + + return Provider.EnablePasswordRetrieval == false; + } + } + public bool IsChangingPassword { get diff --git a/src/umbraco.providers/UsersMembershipProvider.cs b/src/umbraco.providers/UsersMembershipProvider.cs index 889cff8fab..ddce6a7c2e 100644 --- a/src/umbraco.providers/UsersMembershipProvider.cs +++ b/src/umbraco.providers/UsersMembershipProvider.cs @@ -18,6 +18,30 @@ namespace umbraco.providers /// public class UsersMembershipProvider : MembershipProviderBase { + /// + /// Override to maintain backwards compatibility with 0 required non-alphanumeric chars + /// + protected override int DefaultMinNonAlphanumericChars + { + get { return 0; } + } + + /// + /// Override to maintain backwards compatibility with only 4 required length + /// + protected override int DefaultMinPasswordLength + { + get { return 4; } + } + + /// + /// Override to maintain backwards compatibility + /// + protected override bool DefaultUseLegacyEncoding + { + get { return true; } + } + public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { if (config == null) throw new ArgumentNullException("config"); @@ -41,8 +65,10 @@ namespace umbraco.providers /// During installation the application will not be configured, if this is the case and the 'default' password /// is stored in the database then we will validate the user - this will allow for an admin password reset if required /// - public override bool ChangePassword(string username, string oldPassword, string newPassword) + protected override bool PerformChangePassword(string username, string oldPassword, string newPassword) { + + if (ApplicationContext.Current.IsConfigured == false && oldPassword == "default" || ValidateUser(username, oldPassword)) { diff --git a/src/umbraco.providers/members/MembersMembershipProvider.cs b/src/umbraco.providers/members/MembersMembershipProvider.cs index 49139f5e74..a9f3c85ea3 100644 --- a/src/umbraco.providers/members/MembersMembershipProvider.cs +++ b/src/umbraco.providers/members/MembersMembershipProvider.cs @@ -66,6 +66,38 @@ namespace umbraco.providers.members } #endregion + + /// + /// Override to maintain backwards compatibility with 0 required non-alphanumeric chars + /// + protected override int DefaultMinNonAlphanumericChars + { + get { return 0; } + } + + /// + /// Override to maintain backwards compatibility with only 4 required length + /// + protected override int DefaultMinPasswordLength + { + get { return 4; } + } + + /// + /// Override to maintain backwards compatibility + /// + protected override bool DefaultUseLegacyEncoding + { + get { return true; } + } + + /// + /// For backwards compatibility, this provider supports this option + /// + internal override bool AllowManuallyChangingPassword + { + get { return true; } + } #region Initialization Method /// @@ -148,16 +180,19 @@ namespace umbraco.providers.members /// Processes a request to update the password for a membership user. /// /// The user to update the password for. - /// The current password for the specified user. + /// This property is ignore for this provider /// The new password for the specified user. /// /// true if the password was updated successfully; otherwise, false. /// - public override bool ChangePassword(string username, string oldPassword, string newPassword) + protected override bool PerformChangePassword(string username, string oldPassword, string newPassword) { - + //NOTE: due to backwards compatibilty reasons, this provider doesn't care about the old password and + // allows simply setting the password manually so we don't really care about the old password. + // This is allowed based on the overridden AllowManuallyChangingPassword option. + // in order to support updating passwords from the umbraco core, we can't validate the old password - var m = Member.GetMemberFromLoginNameAndPassword(username, oldPassword); + var m = Member.GetMemberFromLoginName(username); if (m == null) return false; var args = new ValidatePasswordEventArgs(username, newPassword, false);