Merge pull request #2059 from umbraco/temp-U4-8643

U4-8643 Usermanagement - Store password algorithm in Usertable
This commit is contained in:
Stephan
2017-07-28 12:26:41 +02:00
committed by GitHub
21 changed files with 569 additions and 249 deletions

View File

@@ -40,6 +40,13 @@ namespace Umbraco.Core.Models.Rdbms
[Column("userPassword")]
[Length(500)]
public string Password { get; set; }
/// <summary>
/// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations)
/// </summary>
[Column("passwordConfig")]
[Length(500)]
public string PasswordConfig { get; set; }
[Column("userEmail")]
public string Email { get; set; }

View File

@@ -1,7 +1,10 @@
using System.Linq;
using System.Web.Security;
using Newtonsoft.Json;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Security;
namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZero
{
@@ -28,6 +31,19 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe
if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("invitedDate")) == false)
Create.Column("invitedDate").OnTable("umbracoUser").AsDateTime().Nullable();
if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("passwordConfig")) == false)
{
Create.Column("passwordConfig").OnTable("umbracoUser").AsString(500).Nullable();
//Check if we have a known config, we only want to store config for hashing
var membershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider();
if (membershipProvider.PasswordFormat == MembershipPasswordFormat.Hashed)
{
var json = JsonConvert.SerializeObject(new { hashAlgorithm = Membership.HashAlgorithmType });
Execute.Sql("UPDATE umbracoUser SET passwordConfig = '" + json + "'");
}
}
}
public override void Down()

View File

@@ -4,6 +4,8 @@ using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Web.Security;
using Newtonsoft.Json;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Models.Membership;
@@ -15,6 +17,7 @@ using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Relators;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Security;
namespace Umbraco.Core.Persistence.Repositories
{
@@ -305,9 +308,18 @@ ORDER BY colName";
if (entity.SecurityStamp.IsNullOrWhiteSpace())
{
entity.SecurityStamp = Guid.NewGuid().ToString();
}
}
var userDto = UserFactory.BuildDto(entity);
var userDto = UserFactory.BuildDto(entity);
//Check if we have a known config, we only want to store config for hashing
//TODO: This logic will need to be updated when we do http://issues.umbraco.org/issue/U4-10089
var membershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider();
if (membershipProvider.PasswordFormat == MembershipPasswordFormat.Hashed)
{
var json = JsonConvert.SerializeObject(new { hashAlgorithm = Membership.HashAlgorithmType });
userDto.PasswordConfig = json;
}
var id = Convert.ToInt32(Database.Insert(userDto));
entity.Id = id;
@@ -401,6 +413,15 @@ ORDER BY colName";
userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString();
changedCols.Add("securityStampToken");
}
//Check if we have a known config, we only want to store config for hashing
//TODO: This logic will need to be updated when we do http://issues.umbraco.org/issue/U4-10089
var membershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider();
if (membershipProvider.PasswordFormat == MembershipPasswordFormat.Hashed)
{
var json = JsonConvert.SerializeObject(new { hashAlgorithm = Membership.HashAlgorithmType });
userDto.PasswordConfig = json;
}
}
//only update the changed cols

View File

@@ -141,7 +141,9 @@ namespace Umbraco.Core.Security
/// Initializes the user manager with the correct options
/// </summary>
/// <param name="manager"></param>
/// <param name="membershipProvider"></param>
/// <param name="membershipProvider">
/// The <see cref="MembershipProviderBase"/> for the users called UsersMembershipProvider
/// </param>
/// <param name="dataProtectionProvider"></param>
/// <returns></returns>
protected void InitUserManager(
@@ -157,11 +159,10 @@ namespace Umbraco.Core.Security
};
// Configure validation logic for passwords
var provider = MembershipProviderExtensions.GetUsersMembershipProvider();
manager.PasswordValidator = new MembershipProviderPasswordValidator(provider);
manager.PasswordValidator = new MembershipProviderPasswordValidator(membershipProvider);
//use a custom hasher based on our membership provider
manager.PasswordHasher = new MembershipPasswordHasher(membershipProvider);
manager.PasswordHasher = GetDefaultPasswordHasher(membershipProvider);
if (dataProtectionProvider != null)
{
@@ -196,6 +197,76 @@ namespace Umbraco.Core.Security
//manager.SmsService = new SmsService();
}
/// <summary>
/// This will determine which password hasher to use based on what is defined in config
/// </summary>
/// <returns></returns>
protected virtual IPasswordHasher GetDefaultPasswordHasher(MembershipProviderBase provider)
{
//if the current user membership provider is unkown (this would be rare), then return the default password hasher
if (provider.IsUmbracoUsersProvider() == false)
return new PasswordHasher();
//if the configured provider has legacy features enabled, then return the membership provider password hasher
if (provider.AllowManuallyChangingPassword || provider.DefaultUseLegacyEncoding)
return new MembershipProviderPasswordHasher(provider);
//we can use the user aware password hasher (which will be the default and preferred way)
return new UserAwareMembershipProviderPasswordHasher(provider);
}
/// <summary>
/// Gets/sets the default back office user password checker
/// </summary>
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; }
/// <summary>
/// Helper method to generate a password for a user based on the current password validator
/// </summary>
/// <returns></returns>
public string GeneratePassword()
{
var passwordValidator = PasswordValidator as PasswordValidator;
if (passwordValidator == null)
{
var membershipPasswordHasher = PasswordHasher as IMembershipProviderPasswordHasher;
//get the real password validator, this should not be null but in some very rare cases it could be, in which case
//we need to create a default password validator to use since we have no idea what it actually is or what it's rules are
//this is an Edge Case!
passwordValidator = PasswordValidator as PasswordValidator
?? (membershipPasswordHasher != null
? new MembershipProviderPasswordValidator(membershipPasswordHasher.MembershipProvider)
: new PasswordValidator());
}
var password = Membership.GeneratePassword(
passwordValidator.RequiredLength,
passwordValidator.RequireNonLetterOrDigit ? 2 : 0);
var random = new Random();
var passwordChars = password.ToCharArray();
if (passwordValidator.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x)))
password += Convert.ToChar(random.Next(48, 58)); // 0-9
if (passwordValidator.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x)))
password += Convert.ToChar(random.Next(97, 123)); // a-z
if (passwordValidator.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x)))
password += Convert.ToChar(random.Next(65, 91)); // A-Z
if (passwordValidator.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x)))
password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./
return password;
}
#region Overrides for password logic
/// <summary>
/// Logic used to validate a username and password
@@ -243,44 +314,88 @@ namespace Umbraco.Core.Security
return await base.CheckPasswordAsync(user, password);
}
/// <summary>
/// Gets/sets the default back office user password checker
/// </summary>
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; }
public override Task<IdentityResult> ChangePasswordAsync(int userId, string currentPassword, string newPassword)
{
return base.ChangePasswordAsync(userId, currentPassword, newPassword);
}
/// <summary>
/// Helper method to generate a password for a user based on the current password validator
/// Override to determine how to hash the password
/// </summary>
/// <param name="store"></param>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
protected override async Task<bool> VerifyPasswordAsync(IUserPasswordStore<T, int> store, T user, string password)
{
var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher<BackOfficeIdentityUser, int>;
if (userAwarePasswordHasher == null)
return await base.VerifyPasswordAsync(store, user, password);
var hash = await store.GetPasswordHashAsync(user);
return userAwarePasswordHasher.VerifyHashedPassword(user, hash, password) != PasswordVerificationResult.Failed;
}
/// <summary>
/// Override to determine how to hash the password
/// </summary>
/// <param name="passwordStore"></param>
/// <param name="user"></param>
/// <param name="newPassword"></param>
/// <returns></returns>
/// <remarks>
/// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used)
/// </remarks>
protected override async Task<IdentityResult> UpdatePassword(IUserPasswordStore<T, int> passwordStore, T user, string newPassword)
{
var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher<BackOfficeIdentityUser, int>;
if (userAwarePasswordHasher == null)
return await base.UpdatePassword(passwordStore, user, newPassword);
var result = await PasswordValidator.ValidateAsync(newPassword);
if (result.Succeeded == false)
return result;
await passwordStore.SetPasswordHashAsync(user, userAwarePasswordHasher.HashPassword(user, newPassword));
await UpdateSecurityStampInternal(user);
return IdentityResult.Success;
}
/// <summary>
/// This is copied from the underlying .NET base class since they decied to not expose it
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
private async Task UpdateSecurityStampInternal(BackOfficeIdentityUser user)
{
if (SupportsUserSecurityStamp == false)
return;
await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp());
}
/// <summary>
/// This is copied from the underlying .NET base class since they decied to not expose it
/// </summary>
/// <returns></returns>
public string GeneratePassword()
private IUserSecurityStampStore<BackOfficeIdentityUser, int> GetSecurityStore()
{
var passwordValidator = PasswordValidator as PasswordValidator;
if (passwordValidator != null)
{
var password = Membership.GeneratePassword(
passwordValidator.RequiredLength,
passwordValidator.RequireNonLetterOrDigit ? 2 : 0);
var random = new Random();
var passwordChars = password.ToCharArray();
if (passwordValidator.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x)))
password += Convert.ToChar(random.Next(48, 58)); // 0-9
if (passwordValidator.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x)))
password += Convert.ToChar(random.Next(97, 123)); // a-z
if (passwordValidator.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x)))
password += Convert.ToChar(random.Next(65, 91)); // A-Z
if (passwordValidator.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x)))
password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./
return password;
}
throw new NotSupportedException("Cannot generate a password since the type of the password validator (" + PasswordValidator.GetType() + ") is not known");
var store = Store as IUserSecurityStampStore<BackOfficeIdentityUser, int>;
if (store == null)
throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
return store;
}
/// <summary>
/// This is copied from the underlying .NET base class since they decied to not expose it
/// </summary>
/// <returns></returns>
private static string NewSecurityStamp()
{
return Guid.NewGuid().ToString();
}
#endregion
}
}

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNet.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// A password hasher that is based on the rules configured for a membership provider
/// </summary>
public interface IMembershipProviderPasswordHasher : IPasswordHasher
{
MembershipProviderBase MembershipProvider { get; }
}
}

View File

@@ -0,0 +1,18 @@
using System;
using Microsoft.AspNet.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// A password hasher that is User aware so that it can process the hashing based on the user's settings
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TUser"></typeparam>
public interface IUserAwarePasswordHasher<in TUser, TKey>
where TUser : class, IUser<TKey>
where TKey : IEquatable<TKey>
{
string HashPassword(TUser user, string password);
PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword);
}
}

View File

@@ -1,34 +0,0 @@
using System;
using Microsoft.AspNet.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// A custom password hasher that conforms to the current password hashing done in Umbraco
/// </summary>
internal class MembershipPasswordHasher : IPasswordHasher
{
private readonly MembershipProviderBase _provider;
public MembershipPasswordHasher(MembershipProviderBase provider)
{
_provider = provider;
}
public string HashPassword(string password)
{
return _provider.HashPasswordForStorage(password);
}
public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
{
if (string.IsNullOrWhiteSpace(hashedPassword)) throw new ArgumentException("Value cannot be null or whitespace.", "hashedPassword");
return _provider.VerifyPassword(providedPassword, hashedPassword)
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
}
}
}

View File

@@ -33,19 +33,19 @@ namespace Umbraco.Core.Security
}
/// <summary>
/// Providers can override this setting, default is 7
/// Providers can override this setting, default is 10
/// </summary>
public virtual int DefaultMinPasswordLength
{
get { return 7; }
get { return 10; }
}
/// <summary>
/// Providers can override this setting, default is 1
/// Providers can override this setting, default is 0
/// </summary>
public virtual int DefaultMinNonAlphanumericChars
{
get { return 1; }
get { return 0; }
}
/// <summary>
@@ -225,7 +225,7 @@ namespace Umbraco.Core.Security
base.Initialize(name, config);
_enablePasswordRetrieval = config.GetValue("enablePasswordRetrieval", false);
_enablePasswordReset = config.GetValue("enablePasswordReset", false);
_enablePasswordReset = config.GetValue("enablePasswordReset", true);
_requiresQuestionAndAnswer = config.GetValue("requiresQuestionAndAnswer", false);
_requiresUniqueEmail = config.GetValue("requiresUniqueEmail", true);
_maxInvalidPasswordAttempts = GetIntValue(config, "maxInvalidPasswordAttempts", 5, false, 0);

View File

@@ -0,0 +1,34 @@
using Microsoft.AspNet.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// A password hasher that conforms to the password hashing done with membership providers
/// </summary>
public class MembershipProviderPasswordHasher : IMembershipProviderPasswordHasher
{
/// <summary>
/// Exposes the underlying MembershipProvider
/// </summary>
public MembershipProviderBase MembershipProvider { get; private set; }
public MembershipProviderPasswordHasher(MembershipProviderBase provider)
{
MembershipProvider = provider;
}
public string HashPassword(string password)
{
return MembershipProvider.HashPasswordForStorage(password);
}
public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
{
return MembershipProvider.VerifyPassword(providedPassword, hashedPassword)
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using Microsoft.AspNet.Identity;
using Umbraco.Core.Models.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// The default password hasher that is User aware so that it can process the hashing based on the user's settings
/// </summary>
public class UserAwareMembershipProviderPasswordHasher : MembershipProviderPasswordHasher, IUserAwarePasswordHasher<BackOfficeIdentityUser, int>
{
public UserAwareMembershipProviderPasswordHasher(MembershipProviderBase provider) : base(provider)
{
}
public string HashPassword(BackOfficeIdentityUser user, string password)
{
//TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
//NOTE: For now this just falls back to the hashing we are currently using
return base.HashPassword(password);
}
public PasswordVerificationResult VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword)
{
//TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
//NOTE: For now this just falls back to the hashing we are currently using
return base.VerifyHashedPassword(hashedPassword, providedPassword);
}
}
}

View File

@@ -663,10 +663,13 @@
<Compile Include="Security\BackOfficeUserValidator.cs" />
<Compile Include="Security\IBackOfficeUserManagerMarker.cs" />
<Compile Include="Security\IBackOfficeUserPasswordChecker.cs" />
<Compile Include="Security\MembershipPasswordHasher.cs" />
<Compile Include="Security\IMembershipProviderPasswordHasher.cs" />
<Compile Include="Security\IUserAwarePasswordHasher.cs" />
<Compile Include="Security\MembershipProviderPasswordHasher.cs" />
<Compile Include="Security\EmailService.cs" />
<Compile Include="Security\MembershipProviderPasswordValidator.cs" />
<Compile Include="Security\OwinExtensions.cs" />
<Compile Include="Security\UserAwareMembershipProviderPasswordHasher.cs" />
<Compile Include="SemVersionExtensions.cs" />
<Compile Include="Serialization\NoTypeConverterJsonConverter.cs" />
<Compile Include="Serialization\StreamResultExtensions.cs" />

View File

@@ -247,7 +247,7 @@
<providers>
<clear />
<add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="10" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Member" passwordFormat="Hashed" allowManuallyChangingPassword="false" />
<add name="UsersMembershipProvider" type="Umbraco.Web.Security.Providers.UsersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="10" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" passwordFormat="Hashed" allowManuallyChangingPassword="false" />
<add name="UsersMembershipProvider" type="Umbraco.Web.Security.Providers.UsersMembershipProvider, Umbraco" />
</providers>
</membership>
<!-- Role Provider -->

View File

@@ -56,7 +56,9 @@ namespace Umbraco.Web.Editors
/// <returns></returns>
[WebApi.UmbracoAuthorize(requireApproval: false)]
public IDictionary<string, object> GetMembershipProviderConfig()
{
{
//TODO: Check if the current PasswordValidator is an IMembershipProviderPasswordValidator, if
//it's not than we should return some generic defaults
var provider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider();
return provider.GetConfiguration(Services.UserService);
}

View File

@@ -87,9 +87,10 @@ namespace Umbraco.Web.Editors
/// <returns>
/// If the password is being reset it will return the newly reset password, otherwise will return an empty value
/// </returns>
public ModelWithNotifications<string> PostChangePassword(ChangingPasswordModel data)
public async Task<ModelWithNotifications<string>> PostChangePassword(ChangingPasswordModel data)
{
var passwordChangeResult = PasswordChangeControllerHelper.ChangePassword(Security.CurrentUser, data, ModelState, Members);
var passwordChanger = new PasswordChanger(Logger, Services.UserService);
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, data, ModelState, UserManager);
if (passwordChangeResult.Success)
{

View File

@@ -1,39 +0,0 @@
using System;
using System.Linq;
using System.Web.Http.ModelBinding;
using Umbraco.Core;
using Umbraco.Core.Models.Membership;
using Umbraco.Web.Models;
using Umbraco.Web.Security;
namespace Umbraco.Web.Editors
{
internal class PasswordChangeControllerHelper
{
public static Attempt<PasswordChangedModel> ChangePassword(
IUser currentUser,
ChangingPasswordModel data,
ModelStateDictionary modelState,
MembershipHelper membersHelper)
{
var userProvider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider();
if (userProvider.RequiresQuestionAndAnswer)
{
throw new NotSupportedException("Currently the user editor does not support providers that have RequiresQuestionAndAnswer specified");
}
var passwordChangeResult = membersHelper.ChangePassword(currentUser.Username, data, userProvider);
if (passwordChangeResult.Success == false)
{
//it wasn't successful, so add the change error to the model state
var fieldName = passwordChangeResult.Result.ChangeError.MemberNames.FirstOrDefault() ?? "password";
modelState.AddModelError(fieldName,
passwordChangeResult.Result.ChangeError.ErrorMessage);
}
return passwordChangeResult;
}
}
}

View File

@@ -0,0 +1,246 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Http.ModelBinding;
using System.Web.Security;
using Microsoft.AspNet.Identity;
using umbraco.cms.businesslogic.packager;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Web.Models;
using Umbraco.Web.Security;
using IUser = Umbraco.Core.Models.Membership.IUser;
namespace Umbraco.Web.Editors
{
internal class PasswordChanger
{
private readonly ILogger _logger;
private readonly IUserService _userService;
public PasswordChanger(ILogger logger, IUserService userService)
{
_logger = logger;
_userService = userService;
}
public async Task<Attempt<PasswordChangedModel>> ChangePasswordWithIdentityAsync(
IUser currentUser,
ChangingPasswordModel passwordModel,
ModelStateDictionary modelState,
BackOfficeUserManager<BackOfficeIdentityUser> userMgr)
{
if (passwordModel == null) throw new ArgumentNullException("passwordModel");
if (userMgr == null) throw new ArgumentNullException("userMgr");
//check if this identity implementation is powered by an underlying membership provider (it will be in most cases)
var membershipPasswordHasher = userMgr.PasswordHasher as IMembershipProviderPasswordHasher;
//check if this identity implementation is powered by an IUserAwarePasswordHasher (it will be by default in 7.7+ but not for upgrades)
var userAwarePasswordHasher = userMgr.PasswordHasher as IUserAwarePasswordHasher<BackOfficeIdentityUser, int>;
if (membershipPasswordHasher != null && userAwarePasswordHasher == null)
{
//if this isn't using an IUserAwarePasswordHasher, then fallback to the old way
if (membershipPasswordHasher.MembershipProvider.RequiresQuestionAndAnswer)
throw new NotSupportedException("Currently the user editor does not support providers that have RequiresQuestionAndAnswer specified");
return ChangePasswordWithMembershipProvider(currentUser.Username, passwordModel, membershipPasswordHasher.MembershipProvider);
}
//if we are here, then a IUserAwarePasswordHasher is available, however we cannot proceed in that case if for some odd reason
//the user has configured the membership provider to not be hashed. This will actually never occur because the BackOfficeUserManager
//will throw if it's not hashed, but we should make sure to check anyways (i.e. in case we want to unit test!)
if (membershipPasswordHasher != null && membershipPasswordHasher.MembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed)
{
throw new InvalidOperationException("The membership provider cannot have a password format of " + membershipPasswordHasher.MembershipProvider.PasswordFormat + " and be configured with secured hashed passwords");
}
//Are we resetting the password??
if (passwordModel.Reset.HasValue && passwordModel.Reset.Value)
{
//ok, we should be able to reset it
var resetToken = await userMgr.GeneratePasswordResetTokenAsync(currentUser.Id);
var newPass = userMgr.GeneratePassword();
var resetResult = await userMgr.ResetPasswordAsync(currentUser.Id, resetToken, newPass);
if (resetResult.Succeeded == false)
{
var errors = string.Join(". ", resetResult.Errors);
_logger.Warn<PasswordChanger>(string.Format("Could not reset member password {0}", errors));
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, errors: " + errors, new[] { "resetPassword" }) });
}
return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass });
}
//we're not resetting it so we need to try to change it.
if (passwordModel.NewPassword.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) });
}
//we cannot arbitrarily change the password without knowing the old one and no old password was supplied - need to return an error
//TODO: What if the current user is admin? We should allow manually changing then?
if (passwordModel.OldPassword.IsNullOrWhiteSpace())
{
//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[] { "oldPassword" }) });
}
if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false)
{
//if an old password is suplied try to change it
var changeResult = await userMgr.ChangePasswordAsync(currentUser.Id, passwordModel.OldPassword, passwordModel.NewPassword);
if (changeResult.Succeeded == false)
{
var errors = string.Join(". ", changeResult.Errors);
_logger.Warn<PasswordChanger>(string.Format("Could not change member password {0}", errors));
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, errors: " + errors, new[] { "value" }) });
}
return Attempt.Succeed(new PasswordChangedModel());
}
//We shouldn't really get here
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid information supplied", new[] { "value" }) });
}
/// <summary>
/// Changes password for a member/user given the membership provider and the password change model
/// </summary>
/// <param name="username"></param>
/// <param name="passwordModel"></param>
/// <param name="membershipProvider"></param>
/// <returns></returns>
public Attempt<PasswordChangedModel> ChangePasswordWithMembershipProvider(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider)
{
// YES! It is completely insane how many options you have to take into account based on the membership provider. yikes!
if (passwordModel == null) throw new ArgumentNullException("passwordModel");
if (membershipProvider == null) throw new ArgumentNullException("membershipProvider");
//Are we resetting the password??
if (passwordModel.Reset.HasValue && passwordModel.Reset.Value)
{
var canReset = membershipProvider.CanResetPassword(_userService);
if (canReset == false)
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not enabled", new[] { "resetPassword" }) });
}
if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset requires a password answer", new[] { "resetPassword" }) });
}
//ok, we should be able to reset it
try
{
var newPass = membershipProvider.ResetPassword(
username,
membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null);
//return the generated pword
return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass });
}
catch (Exception ex)
{
_logger.WarnWithException<PasswordChanger>("Could not reset member password", ex);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, error: " + ex.Message + " (see log for full details)", new[] { "resetPassword" }) });
}
}
//we're not resetting it so we need to try to change it.
if (passwordModel.NewPassword.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) });
}
//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
{
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());
}
catch (Exception ex)
{
_logger.WarnWithException<PasswordChanger>("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" }) });
}
}
//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[] { "oldPassword" }) });
}
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[] { "oldPassword" }) })
: Attempt.Succeed(new PasswordChangedModel());
}
catch (Exception ex)
{
_logger.WarnWithException<PasswordChanger>("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[] { "oldPassword" }) });
}
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)
{
_logger.WarnWithException<PasswordChanger>("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)
{
_logger.WarnWithException<PasswordChanger>("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" }) });
}
}
}
}

View File

@@ -401,7 +401,7 @@ namespace Umbraco.Web.Editors
/// </summary>
/// <param name="userSave"></param>
/// <returns></returns>
public UserDisplay PostSaveUser(UserSave userSave)
public async Task<UserDisplay> PostSaveUser(UserSave userSave)
{
if (userSave == null) throw new ArgumentNullException("userSave");
@@ -457,7 +457,9 @@ namespace Umbraco.Web.Editors
var resetPasswordValue = string.Empty;
if (userSave.ChangePassword != null)
{
var passwordChangeResult = PasswordChangeControllerHelper.ChangePassword(found, userSave.ChangePassword, ModelState, Members);
var passwordChanger = new PasswordChanger(Logger, Services.UserService);
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(found, userSave.ChangePassword, ModelState, UserManager);
if (passwordChangeResult.Success)
{
//depending on how the provider is configured, the password may be reset so let's store that for later

View File

@@ -32,6 +32,7 @@ namespace Umbraco.Web.Install.InstallSteps
_applicationContext = applicationContext;
}
//TODO: Change all logic in this step to use ASP.NET Identity NOT MembershipProviders
private MembershipProvider CurrentProvider
{
get

View File

@@ -13,6 +13,7 @@ using Umbraco.Core.Security;
using Umbraco.Web.Models;
using Umbraco.Web.PublishedCache;
using Umbraco.Core.Cache;
using Umbraco.Web.Editors;
using Umbraco.Web.Security.Providers;
using MPE = global::Umbraco.Core.Security.MembershipProviderExtensions;
@@ -655,128 +656,8 @@ namespace Umbraco.Web.Security
/// <returns></returns>
public virtual Attempt<PasswordChangedModel> ChangePassword(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider)
{
// YES! It is completely insane how many options you have to take into account based on the membership provider. yikes!
if (passwordModel == null) throw new ArgumentNullException("passwordModel");
if (membershipProvider == null) throw new ArgumentNullException("membershipProvider");
//Are we resetting the password??
if (passwordModel.Reset.HasValue && passwordModel.Reset.Value)
{
var canReset = membershipProvider.CanResetPassword(_applicationContext.Services.UserService);
if (canReset == false)
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not enabled", new[] { "resetPassword" }) });
}
if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset requires a password answer", new[] { "resetPassword" }) });
}
//ok, we should be able to reset it
try
{
var newPass = membershipProvider.ResetPassword(
username,
membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null);
//return the generated pword
return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass });
}
catch (Exception ex)
{
LogHelper.WarnWithException<WebSecurity>("Could not reset member password", ex);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, error: " + ex.Message + " (see log for full details)", new[] { "resetPassword" }) });
}
}
//we're not resetting it so we need to try to change it.
if (passwordModel.NewPassword.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) });
}
//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
{
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());
}
catch (Exception ex)
{
LogHelper.WarnWithException<WebSecurity>("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" }) });
}
}
//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[] { "oldPassword" }) });
}
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[] { "oldPassword" }) })
: Attempt.Succeed(new PasswordChangedModel());
}
catch (Exception ex)
{
LogHelper.WarnWithException<WebSecurity>("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[] { "oldPassword" }) });
}
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<WebSecurity>("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<WebSecurity>("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" }) });
}
var passwordChanger = new PasswordChanger(_applicationContext.ProfilingLogger.Logger, _applicationContext.Services.UserService);
return passwordChanger.ChangePasswordWithMembershipProvider(username, passwordModel, membershipProvider);
}
/// <summary>

View File

@@ -170,10 +170,14 @@ namespace Umbraco.Web.Security
/// <param name="username"></param>
/// <param name="password"></param>
/// <returns></returns>
/// <remarks>
/// This uses ASP.NET Identity to perform the validation
/// </remarks>
public virtual bool ValidateBackOfficeCredentials(string username, string password)
{
var membershipProvider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider();
return membershipProvider != null && membershipProvider.ValidateUser(username, password);
var backofficeuser = Mapper.Map<BackOfficeIdentityUser>(CurrentUser);
backofficeuser.UserName = username;
return UserManager.CheckPasswordAsync(backofficeuser, password).Result;
}
[EditorBrowsable(EditorBrowsableState.Never)]

View File

@@ -324,7 +324,7 @@
<Compile Include="Editors\EditorValidator.cs" />
<Compile Include="Editors\FromJsonPathAttribute.cs" />
<Compile Include="Editors\IsCurrentUserModelFilterAttribute.cs" />
<Compile Include="Editors\PasswordChangeControllerHelper.cs" />
<Compile Include="Editors\PasswordChanger.cs" />
<Compile Include="Editors\TemplateController.cs" />
<Compile Include="Editors\ParameterSwapControllerActionSelector.cs" />
<Compile Include="Editors\CodeFileController.cs" />