* remove the temp login screen * set login build back to esm * convert razor entrypoint to show new login screen * enable loading a user defined stylesheet that can be overridden through RCL mechanics * remove unused file * for now, remove the call to the old `localizedtext` endpoint until a replacement has been built * add fallback font * remove login to the old backoffice * change models for twoFactorView * Send view that have to be used for 2fa. * get 2fa providers from the login call directly * Return 2fa providers * map enabledTwoFactorProviderNames to the view * use correct endpoints for 2fa * Send link * change key to id in querystring * improve localization * merge authUrl * Added flow query parameter * remove unused getter * remove debug info * fix fallback value * fallback value * Added invite url to email * Clean up * Added password configuration to the verify responses, so the client knows, and have confirmed the user is allwed to see it * allow reset password * Allow anonymous on invite create password * open api * check for invite * fix fallback text * validate invite token * try to extract the problem details object * add error logging * fix invite user parameters * Use correct id for performing user * Allow password reset on yourself without the old password, if you are currently invited * hardcode the authorize endpoint url for now * fix handlers and disable icons for now * import icons from backoffice client * add backoffice path to icons * fix handler for 2fa custom view * update image temporarily * remove old icon registry * convert login components to UmbLitElement * convert `UmbAuthContext` into a real context with a token * cleanup dependencies * optimise vite * remove lit * optimise external login component loader * use generated resources for reset password * use generated resources for all methods * import and register the main bundle * register localization * change localization keys * update all localization keys to new format * replace tokens * copy code * added danish translations * convert to lowercase * all languages should have same weight * added german translations * add missing variable * missing text * added dutch translations * added swedish translations * added norwegian translations * add temporary fix so the login app can be built * make sure BuildLogin is run only after BuildBellissima has been run to ensure the dependencies are present on disk * run the real login build in pipelines * set vite language to en-us * optimise msw warnings * wait a bit before rendering the form so we know everything has been loaded * Add external login endpoint + move models around * Allow FORM submissions to the external login endpoint * rename `IdentityProvider` back to `Provider` to avoid a breaking change from V13 * type in url for login-external manually (for now) since route attributes are no longer a thing * move GET back to POST for external forms * load in public manifests on boot of the login screen * Clean up * handle the case where an external login provider has disabled local login and show a message instead of the login form * remove external login providers from the server login screen * add more translations * use the friendly greeting for the error layout * show login form * add mock handler for public manifest endpoint * remove the external login layout * fix test * Added generic English localization as a fallback language. --------- Co-authored-by: Bjarke Berg <mail@bergmania.dk> Co-authored-by: kjac <kja@umbraco.dk> Co-authored-by: leekelleher <leekelleher@gmail.com>
125 lines
5.4 KiB
C#
125 lines
5.4 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Changes the password for an identity user
|
|
/// </summary>
|
|
internal class PasswordChanger<TUser> : IPasswordChanger<TUser> where TUser : UmbracoIdentityUser
|
|
{
|
|
private readonly ILogger<PasswordChanger<TUser>> _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PasswordChanger{TUser}"/> class.
|
|
/// Password changing functionality
|
|
/// </summary>
|
|
/// <param name="logger">Logger for this class</param>
|
|
public PasswordChanger(ILogger<PasswordChanger<TUser>> logger) => _logger = logger;
|
|
|
|
public Task<Attempt<PasswordChangedModel?>> ChangePasswordWithIdentityAsync(ChangingPasswordModel passwordModel, IUmbracoUserManager<TUser> userMgr) => ChangePasswordWithIdentityAsync(passwordModel, userMgr, null);
|
|
|
|
/// <summary>
|
|
/// Changes the password for a user based on the many different rules and config options
|
|
/// </summary>
|
|
/// <param name="changingPasswordModel">The changing password model.</param>
|
|
/// <param name="userMgr">The identity manager to use to update the password.</param>
|
|
/// <param name="currentUser">The user performing the operation.</param>
|
|
/// Create an adapter to pass through everything - adapting the member into a user for this functionality
|
|
/// <returns>The outcome of the password changed model</returns>
|
|
public async Task<Attempt<PasswordChangedModel?>> ChangePasswordWithIdentityAsync(
|
|
ChangingPasswordModel changingPasswordModel,
|
|
IUmbracoUserManager<TUser> 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());
|
|
}
|
|
}
|