Files
Umbraco-CMS/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs

250 lines
9.6 KiB
C#
Raw Normal View History

2021-02-17 11:50:19 +01:00
using System.Security.Claims;
using System.Security.Principal;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Net;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Security;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.Security;
public class BackOfficeUserManager : UmbracoUserManager<BackOfficeIdentityUser, UserPasswordConfigurationSettings>,
IBackOfficeUserManager
{
private readonly IBackOfficeUserPasswordChecker _backOfficeUserPasswordChecker;
private readonly IEventAggregator _eventAggregator;
private readonly IHttpContextAccessor _httpContextAccessor;
public BackOfficeUserManager(
IIpResolver ipResolver,
IUserStore<BackOfficeIdentityUser> store,
IOptions<BackOfficeIdentityOptions> optionsAccessor,
IPasswordHasher<BackOfficeIdentityUser> passwordHasher,
IEnumerable<IUserValidator<BackOfficeIdentityUser>> userValidators,
IEnumerable<IPasswordValidator<BackOfficeIdentityUser>> passwordValidators,
BackOfficeErrorDescriber errors,
IServiceProvider services,
IHttpContextAccessor httpContextAccessor,
ILogger<UserManager<BackOfficeIdentityUser>> logger,
IOptions<UserPasswordConfigurationSettings> passwordConfiguration,
IEventAggregator eventAggregator,
IBackOfficeUserPasswordChecker backOfficeUserPasswordChecker)
: base(
ipResolver,
store,
optionsAccessor,
passwordHasher,
userValidators,
passwordValidators,
errors,
services,
logger,
passwordConfiguration)
{
_httpContextAccessor = httpContextAccessor;
_eventAggregator = eventAggregator;
_backOfficeUserPasswordChecker = backOfficeUserPasswordChecker;
}
/// <summary>
/// Override to check the user approval value as well as the user lock out date, by default this only checks the user's
/// locked out date
/// </summary>
/// <param name="user">The user</param>
/// <returns>True if the user is locked out, else false</returns>
/// <remarks>
/// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking
/// this for Umbraco we need to check both values
/// </remarks>
public override async Task<bool> IsLockedOutAsync(BackOfficeIdentityUser user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.IsApproved == false)
{
return true;
}
return await base.IsLockedOutAsync(user);
}
public override async Task<IdentityResult> AccessFailedAsync(BackOfficeIdentityUser user)
{
IdentityResult result = await base.AccessFailedAsync(user);
Simplified setup of 2FA for users (#12142) * Added functionality to enable 2FA for users.. * Do not use the obsolete ctor in tests * cleanup * Cleanup * Convert User view from overlay to infinite editor * Add support for having additional editors on top of the user (2fa) which overlay does not support * Add controllerAs syntax in the template * Remove unused dependencies * Adjustments to 2fa login view * organize elements * add translations * add a11y helpers * add autocompletion = one-time-code * change to controllerAs syntax * add callback to cancel 2fa and fix error where submit button was not reset when all other validations were * add a cancel/go back button to the 2fa view * replace header with something less obstrusive * move logout button to the footer in the new editor view * change 'edit profile' to an umb-box and move ng-if for password fields out to reduce amount of checks * Add umb-box to external login provider section * add umb-box to user history section * bug: fix bug where notificationsService would not allow new notifications if removeAll had been called * add styling and a11y to configureTwoFactor view - also ensure that the view reloads when changes happen in the custom user view to enable 2fa - ensure that view updates when disabling 2fa - add extra button to show options (disable) for each 2fa provider * add notification when 2fa is disabled * add data-element to support the intro tour also changed a minor selector in the cypress test * correct usage of umb-box with umb-box-content * do not use the .form class twice to prevent double box-shadow * make tranlastion for 2fa placeholder shorter * ensure that field with 2fa provider is always visible when more than 1 provider * move error state of 2fa field to token field * update translation of multiple 2fa providers * move CTA buttons to right side to follow general UI practices * rename options to disable * add disabled state * add helper folders to gitignore so you can work with plugins and custom code without committing it accidentally * move the disable functionality to its own infinite editor view * use properties from umb-control-group correctly * add 'track by' to repeater * make use of umb-control-group * remove unused functions * clean up translations * add Danish translations * copy translations to english * Only return enabled 2fa providers as expected Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2022-04-19 08:33:03 +02:00
// Slightly confusing: this will return a Success if we successfully update the AccessFailed count
if (result.Succeeded)
{
NotifyLoginFailed(_httpContextAccessor.HttpContext?.User, user.Id);
}
return result;
}
2022-09-19 16:14:16 +02:00
public override async Task<IdentityResult> ChangePasswordWithResetAsync(string userId, string token, string? newPassword)
{
IdentityResult result = await base.ChangePasswordWithResetAsync(userId, token, newPassword);
if (result.Succeeded)
{
NotifyPasswordReset(_httpContextAccessor.HttpContext?.User, userId);
}
return result;
}
2022-09-19 16:14:16 +02:00
public override async Task<IdentityResult> ChangePasswordAsync(BackOfficeIdentityUser user, string? currentPassword, string? newPassword)
{
IdentityResult result = await base.ChangePasswordAsync(user, currentPassword, newPassword);
if (result.Succeeded)
{
NotifyPasswordChanged(_httpContextAccessor.HttpContext?.User, user.Id);
}
return result;
}
public override async Task<IdentityResult> SetLockoutEndDateAsync(
BackOfficeIdentityUser user,
DateTimeOffset? lockoutEnd)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IdentityResult result = await base.SetLockoutEndDateAsync(user, lockoutEnd);
// The way we unlock is by setting the lockoutEnd date to the current datetime
if (result.Succeeded && lockoutEnd > DateTimeOffset.UtcNow)
{
NotifyAccountLocked(_httpContextAccessor.HttpContext?.User, user.Id);
}
else
{
NotifyAccountUnlocked(_httpContextAccessor.HttpContext?.User, user.Id);
// Resets the login attempt fails back to 0 when unlock is clicked
await ResetAccessFailedCountAsync(user);
}
Member password roll forward (#10138) * Getting new netcore PublicAccessChecker in place * Adds full test coverage for PublicAccessChecker * remove PublicAccessComposer * adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller * Implements the required methods on IMemberManager, removes old migrated code * Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops * adds note * adds note * Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling. * Changes name to IUmbracoEndpointBuilder * adds note * Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect * fixing build * Updates user manager to correctly validate password hashing and injects the IBackOfficeUserPasswordChecker * Merges PR * Fixes up build and notes * Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware. * adds note * removes unused filter, fixes build * fixes WebPath and tests * Looks up entities in one query * remove usings * Fix test, remove stylesheet * Set status code before we write to response to avoid error * Ensures that users and members are validated when logging in. Shares more code between users and members. * Fixes RepositoryCacheKeys to ensure the keys are normalized * oops didn't mean to commit this * Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy * oops didn't mean to comit this * bah, far out this keeps getting recommitted. sorry Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:45:35 +10:00
return result;
}
public override async Task<IdentityResult> ResetAccessFailedCountAsync(BackOfficeIdentityUser user)
{
IdentityResult result = await base.ResetAccessFailedCountAsync(user);
// notify now that it's reset
NotifyResetAccessFailedCount(_httpContextAccessor.HttpContext?.User, user.Id);
return result;
}
public void NotifyForgotPasswordRequested(IPrincipal currentUser, string userId) => Notify(
currentUser,
(currentUserId, ip) => new UserForgotPasswordRequestedNotification(ip, userId, currentUserId));
public void NotifyForgotPasswordChanged(IPrincipal currentUser, string userId) => Notify(
currentUser,
(currentUserId, ip) => new UserForgotPasswordChangedNotification(ip, userId, currentUserId));
public SignOutSuccessResult NotifyLogoutSuccess(IPrincipal currentUser, string? userId)
{
UserLogoutSuccessNotification notification = Notify(
currentUser,
(currentUserId, ip) => new UserLogoutSuccessNotification(ip, userId, currentUserId));
return new SignOutSuccessResult { SignOutRedirectUrl = notification.SignOutRedirectUrl };
}
public void NotifyAccountLocked(IPrincipal? currentUser, string? userId) => Notify(
currentUser,
(currentUserId, ip) => new UserLockedNotification(ip, userId, currentUserId));
/// <summary>
/// Override to allow checking the password via the <see cref="IBackOfficeUserPasswordChecker" /> if one is configured
/// </summary>
/// <param name="store"></param>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
protected override async Task<PasswordVerificationResult> VerifyPasswordAsync(
IUserPasswordStore<BackOfficeIdentityUser> store,
BackOfficeIdentityUser user,
string password)
{
if (user.HasIdentity == false)
{
return PasswordVerificationResult.Failed;
2020-10-23 14:18:53 +11:00
}
BackOfficeUserPasswordCheckerResult result =
await _backOfficeUserPasswordChecker.CheckPasswordAsync(user, password);
// if the result indicates to not fallback to the default, then return true if the credentials are valid
if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker)
{
return result == BackOfficeUserPasswordCheckerResult.ValidCredentials
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
}
return await base.VerifyPasswordAsync(store, user, password);
}
private string GetCurrentUserId(IPrincipal? currentUser)
{
ClaimsIdentity? umbIdentity = currentUser?.GetUmbracoIdentity();
var currentUserId = umbIdentity?.GetUserId<string>() ?? Core.Constants.Security.SuperUserIdAsString;
return currentUserId;
}
public void NotifyAccountUnlocked(IPrincipal? currentUser, string userId) => Notify(
currentUser,
(currentUserId, ip) => new UserUnlockedNotification(ip, userId, currentUserId));
public void NotifyLoginFailed(IPrincipal? currentUser, string userId) => Notify(
currentUser,
(currentUserId, ip) => new UserLoginFailedNotification(ip, userId, currentUserId));
2020-10-23 14:18:53 +11:00
public void NotifyLoginRequiresVerification(IPrincipal currentUser, string? userId) => Notify(
currentUser,
(currentUserId, ip) => new UserLoginRequiresVerificationNotification(ip, userId, currentUserId));
2020-10-23 14:18:53 +11:00
public void NotifyLoginSuccess(IPrincipal currentUser, string userId) => Notify(
currentUser,
(currentUserId, ip) => new UserLoginSuccessNotification(ip, userId, currentUserId));
public void NotifyPasswordChanged(IPrincipal? currentUser, string userId) => Notify(
currentUser,
(currentUserId, ip) => new UserPasswordChangedNotification(ip, userId, currentUserId));
public void NotifyPasswordReset(IPrincipal? currentUser, string userId) => Notify(
currentUser,
(currentUserId, ip) => new UserPasswordResetNotification(ip, userId, currentUserId));
public void NotifyResetAccessFailedCount(IPrincipal? currentUser, string userId) => Notify(
currentUser,
(currentUserId, ip) => new UserResetAccessFailedCountNotification(ip, userId, currentUserId));
private T Notify<T>(IPrincipal? currentUser, Func<string, string, T> createNotification)
where T : INotification
{
var currentUserId = GetCurrentUserId(currentUser);
var ip = IpResolver.GetCurrentRequestIpAddress();
T notification = createNotification(currentUserId, ip);
_eventAggregator.Publish(notification);
return notification;
}
}