using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Security;
///
/// Changes the password for an identity user
///
internal class PasswordChanger : IPasswordChanger where TUser : UmbracoIdentityUser
{
private readonly ILogger> _logger;
///
/// Initializes a new instance of the class.
/// Password changing functionality
///
/// Logger for this class
public PasswordChanger(ILogger> logger) => _logger = logger;
public Task> ChangePasswordWithIdentityAsync(ChangingPasswordModel passwordModel, IUmbracoUserManager userMgr) => ChangePasswordWithIdentityAsync(passwordModel, userMgr, null);
///
/// Changes the password for a user based on the many different rules and config options
///
/// The changing password model.
/// The identity manager to use to update the password.
/// The user performing the operation.
/// Create an adapter to pass through everything - adapting the member into a user for this functionality
/// The outcome of the password changed model
public async Task> ChangePasswordWithIdentityAsync(
ChangingPasswordModel changingPasswordModel,
IUmbracoUserManager userMgr,
IUser? currentUser)
{
if (changingPasswordModel == null)
{
throw new ArgumentNullException(nameof(changingPasswordModel));
}
if (userMgr == null)
{
throw new ArgumentNullException(nameof(userMgr));
}
if (changingPasswordModel.NewPassword.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel
{
Error = new ValidationResult("Cannot set an empty password", new[] { "value" })
});
}
var userId = changingPasswordModel.Id.ToString();
TUser? identityUser = await userMgr.FindByIdAsync(userId);
if (identityUser == null)
{
// this really shouldn't ever happen... but just in case
return Attempt.Fail(new PasswordChangedModel
{
Error = new ValidationResult("Password could not be verified", new[] { "oldPassword" })
});
}
// If old password is not specified we either have to change another user's password, or provide a reset password token
if (changingPasswordModel.OldPassword.IsNullOrWhiteSpace())
{
if (changingPasswordModel.Id == currentUser?.Id && changingPasswordModel.ResetPasswordToken is null && currentUser.UserState != UserState.Invited)
{
return Attempt.Fail(new PasswordChangedModel
{
Error = new ValidationResult("Cannot change the password of current user without the old password or a reset password token", new[] { "value" }),
});
}
// ok, we should be able to reset it
IdentityResult resetResult = changingPasswordModel.ResetPasswordToken is not null
? await userMgr.ResetPasswordAsync(identityUser, changingPasswordModel.ResetPasswordToken.FromUrlBase64()!, changingPasswordModel.NewPassword)
: await userMgr.ChangePasswordWithResetAsync(userId, await userMgr.GeneratePasswordResetTokenAsync(identityUser), changingPasswordModel.NewPassword);
if (resetResult.Succeeded == false)
{
var errors = resetResult.Errors.ToErrorMessage();
_logger.LogWarning("Could not reset user password {PasswordErrors}", errors);
return Attempt.Fail(new PasswordChangedModel
{
Error = new ValidationResult(errors, new[] { "value" })
});
}
return Attempt.Succeed(new PasswordChangedModel());
}
// is the old password correct?
var validateResult = await userMgr.CheckPasswordAsync(identityUser, changingPasswordModel.OldPassword);
if (validateResult == false)
{
// no, fail with an error message for "oldPassword"
return Attempt.Fail(new PasswordChangedModel
{
Error = new ValidationResult("Incorrect password", new[] { "oldPassword" })
});
}
// can we change to the new password?
IdentityResult changeResult = await userMgr.ChangePasswordAsync(identityUser, changingPasswordModel.OldPassword!, changingPasswordModel.NewPassword);
if (changeResult.Succeeded == false)
{
// no, fail with error messages for "password"
var errors = changeResult.Errors.ToErrorMessage();
_logger.LogWarning("Could not change user password {PasswordErrors}", errors);
return Attempt.Fail(new PasswordChangedModel
{
Error = new ValidationResult(errors, new[] { "password" })
});
}
return Attempt.Succeed(new PasswordChangedModel());
}
}