Splits user manager into a base class that can be reused changes base class of IdentityUser to UmbracoIdentityUser

This commit is contained in:
Shannon
2020-12-04 01:38:36 +11:00
parent 8e9dfad381
commit 35af86c3d3
6 changed files with 309 additions and 391 deletions

View File

@@ -11,6 +11,8 @@
<Rule Id="SA1311" Action="None" />
<Rule Id="SA1402" Action="None" />
<Rule Id="SA1413" Action="None" />
<Rule Id="SA1611" Action="None" />
<Rule Id="SA1615" Action="None" />
<Rule Id="SA1629" Action="None" />
</Rules>
</RuleSet>

View File

@@ -17,7 +17,7 @@ namespace Umbraco.Core.Models.Identity
/// to a user. We will continue using this approach since it works fine for what we need which does the change tracking of
/// claims, roles and logins directly on the user model.
/// </remarks>
public abstract class IdentityUser : IRememberBeingDirty
public abstract class UmbracoIdentityUser : IRememberBeingDirty
{
private int _id;
private string _email;
@@ -32,9 +32,9 @@ namespace Umbraco.Core.Models.Identity
private ObservableCollection<IdentityUserRole> _roles;
/// <summary>
/// Initializes a new instance of the <see cref="IdentityUser"/> class.
/// Initializes a new instance of the <see cref="UmbracoIdentityUser"/> class.
/// </summary>
public IdentityUser()
public UmbracoIdentityUser()
{
// must initialize before setting groups
_roles = new ObservableCollection<IdentityUserRole>();

View File

@@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Identity;
@@ -12,7 +8,10 @@ using Umbraco.Core.Models.Membership;
namespace Umbraco.Core.Security
{
public class BackOfficeIdentityUser : IdentityUser
/// <summary>
/// The identity user used for the back office
/// </summary>
public class BackOfficeIdentityUser : UmbracoIdentityUser
{
private string _name;
private string _passwordConfig;
@@ -34,11 +33,7 @@ namespace Umbraco.Core.Security
/// <summary>
/// Used to construct a new instance without an identity
/// </summary>
/// <param name="globalSettings"></param>
/// <param name="username"></param>
/// <param name="email">This is allowed to be null (but would need to be filled in if trying to persist this instance)</param>
/// <param name="culture"></param>
/// <returns></returns>
public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string username, string email, string culture, string name = null)
{
if (string.IsNullOrWhiteSpace(username))
@@ -80,9 +75,6 @@ namespace Umbraco.Core.Security
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeIdentityUser"/> class.
/// </summary>
/// <param name="globalSettings"></param>
/// <param name="userId"></param>
/// <param name="groups"></param>
public BackOfficeIdentityUser(GlobalSettings globalSettings, int userId, IEnumerable<IReadOnlyUserGroup> groups)
: this(globalSettings, groups.ToArray())
{
@@ -184,7 +176,7 @@ namespace Umbraco.Core.Security
/// <summary>
/// Based on the user's lockout end date, this will determine if they are locked out
/// Gets a value indicating whether the user is locked out based on the user's lockout end date
/// </summary>
public bool IsLockedOut
{
@@ -196,7 +188,7 @@ namespace Umbraco.Core.Security
}
/// <summary>
/// This is a 1:1 mapping with IUser.IsApproved
/// Gets or sets a value indicating the IUser IsApproved
/// </summary>
public bool IsApproved { get; set; }

View File

@@ -6,7 +6,6 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
@@ -18,6 +17,8 @@ using Umbraco.Core.Services;
namespace Umbraco.Core.BackOffice
{
// TODO: Make this into a base class that can be re-used
public class BackOfficeUserStore : DisposableObjectSlim,
IUserPasswordStore<BackOfficeIdentityUser>,
IUserEmailStore<BackOfficeIdentityUser>,
@@ -28,11 +29,11 @@ namespace Umbraco.Core.BackOffice
IUserSessionStore<BackOfficeIdentityUser>
// TODO: This would require additional columns/tables and then a lot of extra coding support to make this happen natively within umbraco
//IUserTwoFactorStore<BackOfficeIdentityUser>,
// IUserTwoFactorStore<BackOfficeIdentityUser>,
// TODO: This would require additional columns/tables for now people will need to implement this on their own
//IUserPhoneNumberStore<BackOfficeIdentityUser, int>,
// IUserPhoneNumberStore<BackOfficeIdentityUser, int>,
// TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation
//IQueryableUserStore<BackOfficeIdentityUser, int>
// IQueryableUserStore<BackOfficeIdentityUser, int>
{
private readonly IScopeProvider _scopeProvider;
private readonly IUserService _userService;
@@ -42,15 +43,16 @@ namespace Umbraco.Core.BackOffice
private readonly UmbracoMapper _mapper;
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeUserStore"/> class.
/// </summary>
public BackOfficeUserStore(IScopeProvider scopeProvider, IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IOptions<GlobalSettings> globalSettings, UmbracoMapper mapper)
{
_scopeProvider = scopeProvider;
_userService = userService;
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_entityService = entityService;
_externalLoginService = externalLoginService;
_externalLoginService = externalLoginService ?? throw new ArgumentNullException(nameof(externalLoginService));
_globalSettings = globalSettings.Value;
if (userService == null) throw new ArgumentNullException("userService");
if (externalLoginService == null) throw new ArgumentNullException("externalLoginService");
_mapper = mapper;
_userService = userService;
_externalLoginService = externalLoginService;
@@ -59,10 +61,7 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObjectSlim"/> which handles common required locking logic.
/// </summary>
protected override void DisposeResources()
{
_disposed = true;
}
protected override void DisposeResources() => _disposed = true;
public Task<string> GetUserIdAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
{
@@ -105,9 +104,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Insert a new user
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<IdentityResult> CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -158,9 +154,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Update a user
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<IdentityResult> UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -206,8 +199,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Delete a user
/// </summary>
/// <param name="user"/>
/// <returns/>
public Task<IdentityResult> DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -227,9 +218,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Finds a user
/// </summary>
/// <param name="userId"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public async Task<BackOfficeIdentityUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -244,9 +232,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Find a user by name
/// </summary>
/// <param name="userName"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public async Task<BackOfficeIdentityUser> FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -265,9 +250,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Set the user password hash
/// </summary>
/// <param name="user"/><param name="passwordHash"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -278,6 +260,7 @@ namespace Umbraco.Core.BackOffice
user.PasswordHash = passwordHash;
user.PasswordConfig = null; // Clear this so that it's reset at the repository level
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
return Task.CompletedTask;
}
@@ -285,9 +268,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Get the user password hash
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<string> GetPasswordHashAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -300,9 +280,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Returns true if a user has a password set
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<bool> HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -315,9 +292,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Set the user email
/// </summary>
/// <param name="user"/><param name="email"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task SetEmailAsync(BackOfficeIdentityUser user, string email, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -333,9 +307,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Get the user email
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<string> GetEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -348,9 +319,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Returns true if the user email is confirmed
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<bool> GetEmailConfirmedAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -363,9 +331,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Sets whether the user email is confirmed
/// </summary>
/// <param name="user"/><param name="confirmed"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -377,9 +342,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Returns the user associated with this email
/// </summary>
/// <param name="email"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<BackOfficeIdentityUser> FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -393,22 +355,14 @@ namespace Umbraco.Core.BackOffice
}
public Task<string> GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
{
return GetEmailAsync(user, cancellationToken);
}
=> GetEmailAsync(user, cancellationToken);
public Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
{
return SetEmailAsync(user, normalizedEmail, cancellationToken);
}
=> SetEmailAsync(user, normalizedEmail, cancellationToken);
/// <summary>
/// Adds a user login with the specified provider and key
/// </summary>
/// <param name="user"/>
/// <param name="login"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -427,11 +381,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Removes the user login with the specified combination if it exists
/// </summary>
/// <param name="user"/>
/// <param name="providerKey"></param>
/// <param name="cancellationToken"></param>
/// <param name="loginProvider"></param>
/// <returns/>
public Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -447,9 +396,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Returns the linked accounts for this user
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<IList<UserLoginInfo>> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -498,10 +444,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Adds a user to a role (user group)
/// </summary>
/// <param name="user"/>
/// <param name="normalizedRoleName"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -523,10 +465,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Removes the role (user group) for the user
/// </summary>
/// <param name="user"/>
/// <param name="normalizedRoleName"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -548,9 +486,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Returns the roles (user groups) for this user
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<IList<string>> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -562,10 +497,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Returns true if a user is in the role
/// </summary>
/// <param name="user"/>
/// <param name="normalizedRoleName"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<bool> IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -597,10 +528,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Set the security stamp for the user
/// </summary>
/// <param name="user"/>
/// <param name="stamp"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -614,9 +541,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Get the user security stamp
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<string> GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -644,10 +568,7 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Returns the DateTimeOffset that represents the end of a user's lockout, any time in the past should be considered not locked out.
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
/// <remarks>
/// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status
/// </remarks>
public Task<DateTimeOffset?> GetLockoutEndDateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
@@ -664,9 +585,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Locks a user out until the specified end date (set to a past date, to unlock a user)
/// </summary>
/// <param name="user"/><param name="lockoutEnd"/>
/// <param name="cancellationToken"></param>
/// <returns/>
/// <remarks>
/// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status
/// </remarks>
@@ -683,9 +601,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Used to record when an attempt to access the user has failed
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<int> IncrementAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -699,9 +614,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Used to reset the access failed count, typically after the account is successfully accessed
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -716,9 +628,6 @@ namespace Umbraco.Core.BackOffice
/// Returns the current number of failed access attempts. This number usually will be reset whenever the password is
/// verified or the account is locked out.
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<int> GetAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -730,9 +639,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Returns true
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task<bool> GetLockoutEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -744,9 +650,6 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// Doesn't actually perform any function, users can always be locked out
/// </summary>
/// <param name="user"/><param name="enabled"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public Task SetLockoutEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -904,8 +807,7 @@ namespace Umbraco.Core.BackOffice
public Task<bool> ValidateSessionIdAsync(string userId, string sessionId)
{
Guid guidSessionId;
if (Guid.TryParse(sessionId, out guidSessionId))
if (Guid.TryParse(sessionId, out Guid guidSessionId))
{
return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId));
}

View File

@@ -11,6 +11,7 @@ using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Extensions;
@@ -20,9 +21,10 @@ using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Common.Security
{
public class BackOfficeUserManager : BackOfficeUserManager<BackOfficeIdentityUser>, IBackOfficeUserManager
public class BackOfficeUserManager : UmbracoUserManager<BackOfficeIdentityUser>, IBackOfficeUserManager
{
private readonly IHttpContextAccessor _httpContextAccessor;
public BackOfficeUserManager(
IIpResolver ipResolver,
IUserStore<BackOfficeIdentityUser> store,
@@ -36,135 +38,11 @@ namespace Umbraco.Web.Common.Security
IHttpContextAccessor httpContextAccessor,
ILogger<UserManager<BackOfficeIdentityUser>> logger,
IOptions<UserPasswordConfigurationSettings> passwordConfiguration)
: base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, httpContextAccessor, logger, passwordConfiguration)
: base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration)
{
}
}
public class BackOfficeUserManager<T> : UserManager<T>
where T : BackOfficeIdentityUser
{
private PasswordGenerator _passwordGenerator;
private readonly IHttpContextAccessor _httpContextAccessor;
public BackOfficeUserManager(
IIpResolver ipResolver,
IUserStore<T> store,
IOptions<BackOfficeIdentityOptions> optionsAccessor,
IPasswordHasher<T> passwordHasher,
IEnumerable<IUserValidator<T>> userValidators,
IEnumerable<IPasswordValidator<T>> passwordValidators,
BackOfficeLookupNormalizer keyNormalizer,
BackOfficeIdentityErrorDescriber errors,
IServiceProvider services,
IHttpContextAccessor httpContextAccessor,
ILogger<UserManager<T>> logger,
IOptions<UserPasswordConfigurationSettings> passwordConfiguration)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver));
_httpContextAccessor = httpContextAccessor;
PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration));
}
// We don't support an IUserClaimStore and don't need to (at least currently)
public override bool SupportsUserClaim => false;
// It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository
public override bool SupportsQueryableUsers => false;
/// <summary>
/// Developers will need to override this to support custom 2 factor auth
/// </summary>
public override bool SupportsUserTwoFactor => false;
// We haven't needed to support this yet, though might be necessary for 2FA
public override bool SupportsUserPhoneNumber => false;
/// <summary>
/// Replace the underlying options property with our own strongly typed version
/// </summary>
public new BackOfficeIdentityOptions Options
{
get => (BackOfficeIdentityOptions)base.Options;
set => base.Options = value;
}
/// <summary>
/// Used to validate a user's session
/// </summary>
/// <param name="userId">The user id</param>
/// <param name="sessionId">The sesion id</param>
/// <returns>True if the sesion is valid, else false</returns>
public virtual async Task<bool> ValidateSessionIdAsync(string userId, string sessionId)
{
var userSessionStore = Store as IUserSessionStore<T>;
// if this is not set, for backwards compat (which would be super rare), we'll just approve it
if (userSessionStore == null)
{
return true;
}
return await userSessionStore.ValidateSessionIdAsync(userId, sessionId);
}
/// <summary>
/// This will determine which password hasher to use based on what is defined in config
/// </summary>
/// <param name="passwordConfiguration">The <see cref="IPasswordConfiguration"/></param>
/// <returns>An <see cref="IPasswordHasher{T}"/></returns>
protected virtual IPasswordHasher<T> GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher<T>();
/// <summary>
/// Gets/sets the default back office user password checker
/// </summary>
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; }
public IPasswordConfiguration PasswordConfiguration { get; protected set; }
public IIpResolver IpResolver { get; }
/// <summary>
/// Helper method to generate a password for a user based on the current password validator
/// </summary>
/// <returns>The generated password</returns>
public string GeneratePassword()
{
if (_passwordGenerator == null)
{
_passwordGenerator = new PasswordGenerator(PasswordConfiguration);
}
var password = _passwordGenerator.GeneratePassword();
return password;
}
/// <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(T user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.IsApproved == false)
{
return true;
}
return await base.IsLockedOutAsync(user);
}
/// <summary>
/// Logic used to validate a username and password
/// </summary>
/// <inheritdoc />
/// <remarks>
/// By default this uses the standard ASP.Net Identity approach which is:
@@ -180,7 +58,7 @@ namespace Umbraco.Web.Common.Security
/// We've allowed this check to be overridden with a simple callback so that developers don't actually
/// have to implement/override this class.
/// </remarks>
public override async Task<bool> CheckPasswordAsync(T user, string password)
public override async Task<bool> CheckPasswordAsync(BackOfficeIdentityUser user, string password)
{
if (BackOfficeUserPasswordChecker != null)
{
@@ -198,36 +76,49 @@ namespace Umbraco.Web.Common.Security
}
}
// we cannot proceed if the user passed in does not have an identity
if (user.HasIdentity == false)
{
return false;
}
// use the default behavior
return await base.CheckPasswordAsync(user, password);
}
/// <summary>
/// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event
/// 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="userId">The userId</param>
/// <param name="token">The reset password token</param>
/// <param name="newPassword">The new password to set it to</param>
/// <returns>The <see cref="IdentityResult"/></returns>
/// <param name="user">The user</param>
/// <returns>True if the user is locked out, else false</returns>
/// <remarks>
/// We use this because in the back office the only way an admin can change another user's password without first knowing their password
/// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset
/// 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 async Task<IdentityResult> ChangePasswordWithResetAsync(int userId, string token, string newPassword)
public override async Task<bool> IsLockedOutAsync(BackOfficeIdentityUser user)
{
T user = await FindByIdAsync(userId.ToString());
if (user == null)
{
throw new InvalidOperationException("Could not find user");
throw new ArgumentNullException(nameof(user));
}
IdentityResult result = await base.ResetPasswordAsync(user, token, newPassword);
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);
// Slightly confusing: this will return a Success if we successfully update the AccessFailed count
if (result.Succeeded)
{
RaiseLoginFailedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
}
return result;
}
public override async Task<IdentityResult> ChangePasswordWithResetAsync(int userId, string token, string newPassword)
{
IdentityResult result = await base.ChangePasswordWithResetAsync(userId, token, newPassword);
if (result.Succeeded)
{
RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, userId);
@@ -236,79 +127,19 @@ namespace Umbraco.Web.Common.Security
return result;
}
public override async Task<IdentityResult> ChangePasswordAsync(T user, string currentPassword, string newPassword)
public override async Task<IdentityResult> ChangePasswordAsync(BackOfficeIdentityUser user, string currentPassword, string newPassword)
{
IdentityResult result = await base.ChangePasswordAsync(user, currentPassword, newPassword);
if (result.Succeeded)
{
RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
}
return result;
}
/// <summary>
/// Override to determine how to hash the password
/// </summary>
/// <inheritdoc/>
protected override async Task<IdentityResult> UpdatePasswordHash(T user, string newPassword, bool validatePassword)
{
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
if (validatePassword)
{
IdentityResult validate = await ValidatePasswordAsync(user, newPassword);
if (!validate.Succeeded)
{
return validate;
}
}
var passwordStore = Store as IUserPasswordStore<T>;
if (passwordStore == null)
{
throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>));
}
var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null;
await passwordStore.SetPasswordHashAsync(user, hash, CancellationToken);
await UpdateSecurityStampInternal(user);
return IdentityResult.Success;
}
/// <summary>
/// This is copied from the underlying .NET base class since they decided to not expose it
/// </summary>
private async Task UpdateSecurityStampInternal(T user)
{
if (SupportsUserSecurityStamp == false)
{
return;
}
await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None);
}
/// <summary>
/// This is copied from the underlying .NET base class since they decided to not expose it
/// </summary>
private IUserSecurityStampStore<T> GetSecurityStore()
{
var store = Store as IUserSecurityStampStore<T>;
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 decided to not expose it
/// </summary>
private static string NewSecurityStamp() => Guid.NewGuid().ToString();
/// <inheritdoc/>
public override async Task<IdentityResult> SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd)
public override async Task<IdentityResult> SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd)
{
if (user == null)
{
@@ -320,7 +151,7 @@ namespace Umbraco.Web.Common.Security
// The way we unlock is by setting the lockoutEnd date to the current datetime
if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow)
{
RaiseAccountLockedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
RaiseAccountLockedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
}
else
{
@@ -334,62 +165,12 @@ namespace Umbraco.Web.Common.Security
}
/// <inheritdoc/>
public override async Task<IdentityResult> ResetAccessFailedCountAsync(T user)
public override async Task<IdentityResult> ResetAccessFailedCountAsync(BackOfficeIdentityUser user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var lockoutStore = (IUserLockoutStore<T>)Store;
var accessFailedCount = await GetAccessFailedCountAsync(user);
if (accessFailedCount == 0)
{
return IdentityResult.Success;
}
await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None);
IdentityResult result = await base.ResetAccessFailedCountAsync(user);
// raise the event now that it's reset
RaiseResetAccessFailedCountEvent(_httpContextAccessor.HttpContext?.User, user.Id);
return await UpdateAsync(user);
}
/// <summary>
/// Overrides the Microsoft ASP.NET user management method
/// </summary>
/// <inheritdoc/>
public override async Task<IdentityResult> AccessFailedAsync(T user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var lockoutStore = Store as IUserLockoutStore<T>;
if (lockoutStore == null)
{
throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>));
}
var count = await lockoutStore.IncrementAccessFailedCountAsync(user, CancellationToken.None);
if (count >= Options.Lockout.MaxFailedAccessAttempts)
{
await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), CancellationToken.None);
// NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0
// here we are persisting the value for the back office
}
IdentityResult result = await UpdateAsync(user);
// Slightly confusing: this will return a Success if we successfully update the AccessFailed count
if (result.Succeeded)
{
RaiseLoginFailedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
}
return result;
}

View File

@@ -0,0 +1,241 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;
using Umbraco.Net;
namespace Umbraco.Web.Common.Security
{
/// <summary>
/// Abstract class for Umbraco User Managers for back office users or front-end members
/// </summary>
/// <typeparam name="T">The type of user</typeparam>
public abstract class UmbracoUserManager<T> : UserManager<T>
where T : UmbracoIdentityUser
{
private PasswordGenerator _passwordGenerator;
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoUserManager{T}"/> class.
/// </summary>
public UmbracoUserManager(
IIpResolver ipResolver,
IUserStore<T> store,
IOptions<BackOfficeIdentityOptions> optionsAccessor,
IPasswordHasher<T> passwordHasher,
IEnumerable<IUserValidator<T>> userValidators,
IEnumerable<IPasswordValidator<T>> passwordValidators,
BackOfficeLookupNormalizer keyNormalizer,
BackOfficeIdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<T>> logger,
IOptions<UserPasswordConfigurationSettings> passwordConfiguration)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver));
PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration));
}
// We don't support an IUserClaimStore and don't need to (at least currently)
public override bool SupportsUserClaim => false;
// It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository
public override bool SupportsQueryableUsers => false;
/// <summary>
/// Developers will need to override this to support custom 2 factor auth
/// </summary>
public override bool SupportsUserTwoFactor => false;
// We haven't needed to support this yet, though might be necessary for 2FA
public override bool SupportsUserPhoneNumber => false;
/// <summary>
/// Used to validate a user's session
/// </summary>
/// <param name="userId">The user id</param>
/// <param name="sessionId">The sesion id</param>
/// <returns>True if the sesion is valid, else false</returns>
public virtual async Task<bool> ValidateSessionIdAsync(string userId, string sessionId)
{
var userSessionStore = Store as IUserSessionStore<T>;
// if this is not set, for backwards compat (which would be super rare), we'll just approve it
if (userSessionStore == null)
{
return true;
}
return await userSessionStore.ValidateSessionIdAsync(userId, sessionId);
}
/// <summary>
/// This will determine which password hasher to use based on what is defined in config
/// </summary>
/// <param name="passwordConfiguration">The <see cref="IPasswordConfiguration"/></param>
/// <returns>An <see cref="IPasswordHasher{T}"/></returns>
protected virtual IPasswordHasher<T> GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher<T>();
/// <summary>
/// Gets or sets the default back office user password checker
/// </summary>
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; }
public IPasswordConfiguration PasswordConfiguration { get; protected set; }
public IIpResolver IpResolver { get; }
/// <summary>
/// Helper method to generate a password for a user based on the current password validator
/// </summary>
/// <returns>The generated password</returns>
public string GeneratePassword()
{
if (_passwordGenerator == null)
{
_passwordGenerator = new PasswordGenerator(PasswordConfiguration);
}
var password = _passwordGenerator.GeneratePassword();
return password;
}
/// <inheritdoc />
public override async Task<bool> CheckPasswordAsync(T user, string password)
{
// we cannot proceed if the user passed in does not have an identity
if (user.HasIdentity == false)
{
return false;
}
// use the default behavior
return await base.CheckPasswordAsync(user, password);
}
/// <summary>
/// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event
/// </summary>
/// <param name="userId">The userId</param>
/// <param name="token">The reset password token</param>
/// <param name="newPassword">The new password to set it to</param>
/// <returns>The <see cref="IdentityResult"/></returns>
/// <remarks>
/// We use this because in the back office the only way an admin can change another user's password without first knowing their password
/// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset
/// </remarks>
public virtual async Task<IdentityResult> ChangePasswordWithResetAsync(int userId, string token, string newPassword)
{
T user = await FindByIdAsync(userId.ToString());
if (user == null)
{
throw new InvalidOperationException("Could not find user");
}
IdentityResult result = await base.ResetPasswordAsync(user, token, newPassword);
return result;
}
/// <summary>
/// This is copied from the underlying .NET base class since they decided to not expose it
/// </summary>
private IUserSecurityStampStore<T> GetSecurityStore()
{
var store = Store as IUserSecurityStampStore<T>;
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 decided to not expose it
/// </summary>
private static string NewSecurityStamp() => Guid.NewGuid().ToString();
/// <inheritdoc/>
public override async Task<IdentityResult> SetLockoutEndDateAsync(T 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)
{
// Resets the login attempt fails back to 0 when unlock is clicked
await ResetAccessFailedCountAsync(user);
}
return result;
}
/// <inheritdoc/>
public override async Task<IdentityResult> ResetAccessFailedCountAsync(T user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var lockoutStore = (IUserLockoutStore<T>)Store;
var accessFailedCount = await GetAccessFailedCountAsync(user);
if (accessFailedCount == 0)
{
return IdentityResult.Success;
}
await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None);
return await UpdateAsync(user);
}
/// <summary>
/// Overrides the Microsoft ASP.NET user management method
/// </summary>
/// <inheritdoc/>
public override async Task<IdentityResult> AccessFailedAsync(T user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var lockoutStore = Store as IUserLockoutStore<T>;
if (lockoutStore == null)
{
throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>));
}
var count = await lockoutStore.IncrementAccessFailedCountAsync(user, CancellationToken.None);
if (count >= Options.Lockout.MaxFailedAccessAttempts)
{
await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), CancellationToken.None);
// NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0
// here we are persisting the value for the back office
}
IdentityResult result = await UpdateAsync(user);
return result;
}
}
}