Files
Umbraco-CMS/src/Umbraco.Infrastructure/Security/PasswordChanger.cs
Jacob Overgaard a95a092c39 V14: login screen (#15932)
* 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>
2024-04-03 15:45:09 +02:00

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