diff --git a/linting/codeanalysis.ruleset b/linting/codeanalysis.ruleset
index 4fde2bef8d..57c9fb7d60 100644
--- a/linting/codeanalysis.ruleset
+++ b/linting/codeanalysis.ruleset
@@ -11,6 +11,8 @@
+
+
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/UmbracoIdentityUser.cs
similarity index 98%
rename from src/Umbraco.Core/Models/Identity/IdentityUser.cs
rename to src/Umbraco.Core/Models/Identity/UmbracoIdentityUser.cs
index 516bd60c49..ffa549ab47 100644
--- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs
+++ b/src/Umbraco.Core/Models/Identity/UmbracoIdentityUser.cs
@@ -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.
///
- 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 _roles;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- public IdentityUser()
+ public UmbracoIdentityUser()
{
// must initialize before setting groups
_roles = new ObservableCollection();
diff --git a/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs
index 051a68a362..4de1ae4d0f 100644
--- a/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs
+++ b/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs
@@ -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
+ ///
+ /// The identity user used for the back office
+ ///
+ public class BackOfficeIdentityUser : UmbracoIdentityUser
{
private string _name;
private string _passwordConfig;
@@ -34,11 +33,7 @@ namespace Umbraco.Core.Security
///
/// Used to construct a new instance without an identity
///
- ///
- ///
/// This is allowed to be null (but would need to be filled in if trying to persist this instance)
- ///
- ///
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
///
/// Initializes a new instance of the class.
///
- ///
- ///
- ///
public BackOfficeIdentityUser(GlobalSettings globalSettings, int userId, IEnumerable groups)
: this(globalSettings, groups.ToArray())
{
@@ -184,7 +176,7 @@ namespace Umbraco.Core.Security
///
- /// 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
///
public bool IsLockedOut
{
@@ -196,7 +188,7 @@ namespace Umbraco.Core.Security
}
///
- /// This is a 1:1 mapping with IUser.IsApproved
+ /// Gets or sets a value indicating the IUser IsApproved
///
public bool IsApproved { get; set; }
diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs
index 234d6eae66..e74690b76f 100644
--- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs
+++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs
@@ -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,
IUserEmailStore,
@@ -28,11 +29,11 @@ namespace Umbraco.Core.BackOffice
IUserSessionStore
// TODO: This would require additional columns/tables and then a lot of extra coding support to make this happen natively within umbraco
- //IUserTwoFactorStore,
+ // IUserTwoFactorStore,
// TODO: This would require additional columns/tables for now people will need to implement this on their own
- //IUserPhoneNumberStore,
+ // IUserPhoneNumberStore,
// TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation
- //IQueryableUserStore
+ // IQueryableUserStore
{
private readonly IScopeProvider _scopeProvider;
private readonly IUserService _userService;
@@ -42,15 +43,16 @@ namespace Umbraco.Core.BackOffice
private readonly UmbracoMapper _mapper;
private bool _disposed = false;
+ ///
+ /// Initializes a new instance of the class.
+ ///
public BackOfficeUserStore(IScopeProvider scopeProvider, IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IOptions 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
///
/// Handles the disposal of resources. Derived from abstract class which handles common required locking logic.
///
- protected override void DisposeResources()
- {
- _disposed = true;
- }
+ protected override void DisposeResources() => _disposed = true;
public Task GetUserIdAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
{
@@ -105,9 +104,6 @@ namespace Umbraco.Core.BackOffice
///
/// Insert a new user
///
- ///
- ///
- ///
public Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -158,9 +154,6 @@ namespace Umbraco.Core.BackOffice
///
/// Update a user
///
- ///
- ///
- ///
public Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -206,8 +199,6 @@ namespace Umbraco.Core.BackOffice
///
/// Delete a user
///
- ///
- ///
public Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -227,9 +218,6 @@ namespace Umbraco.Core.BackOffice
///
/// Finds a user
///
- ///
- ///
- ///
public async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -244,9 +232,6 @@ namespace Umbraco.Core.BackOffice
///
/// Find a user by name
///
- ///
- ///
- ///
public async Task FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -265,9 +250,6 @@ namespace Umbraco.Core.BackOffice
///
/// Set the user password hash
///
- ///
- ///
- ///
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
///
/// Get the user password hash
///
- ///
- ///
- ///
public Task GetPasswordHashAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -300,9 +280,6 @@ namespace Umbraco.Core.BackOffice
///
/// Returns true if a user has a password set
///
- ///
- ///
- ///
public Task HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -315,9 +292,6 @@ namespace Umbraco.Core.BackOffice
///
/// Set the user email
///
- ///
- ///
- ///
public Task SetEmailAsync(BackOfficeIdentityUser user, string email, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -333,9 +307,6 @@ namespace Umbraco.Core.BackOffice
///
/// Get the user email
///
- ///
- ///
- ///
public Task GetEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -348,9 +319,6 @@ namespace Umbraco.Core.BackOffice
///
/// Returns true if the user email is confirmed
///
- ///
- ///
- ///
public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -363,9 +331,6 @@ namespace Umbraco.Core.BackOffice
///
/// Sets whether the user email is confirmed
///
- ///
- ///
- ///
public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -377,9 +342,6 @@ namespace Umbraco.Core.BackOffice
///
/// Returns the user associated with this email
///
- ///
- ///
- ///
public Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -393,22 +355,14 @@ namespace Umbraco.Core.BackOffice
}
public Task 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);
///
/// Adds a user login with the specified provider and key
///
- ///
- ///
- ///
- ///
public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -427,11 +381,6 @@ namespace Umbraco.Core.BackOffice
///
/// Removes the user login with the specified combination if it exists
///
- ///
- ///
- ///
- ///
- ///
public Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -447,9 +396,6 @@ namespace Umbraco.Core.BackOffice
///
/// Returns the linked accounts for this user
///
- ///
- ///
- ///
public Task> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -498,10 +444,6 @@ namespace Umbraco.Core.BackOffice
///
/// Adds a user to a role (user group)
///
- ///
- ///
- ///
- ///
public Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -523,10 +465,6 @@ namespace Umbraco.Core.BackOffice
///
/// Removes the role (user group) for the user
///
- ///
- ///
- ///
- ///
public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -548,9 +486,6 @@ namespace Umbraco.Core.BackOffice
///
/// Returns the roles (user groups) for this user
///
- ///
- ///
- ///
public Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -562,10 +497,6 @@ namespace Umbraco.Core.BackOffice
///
/// Returns true if a user is in the role
///
- ///
- ///
- ///
- ///
public Task IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -597,10 +528,6 @@ namespace Umbraco.Core.BackOffice
///
/// Set the security stamp for the user
///
- ///
- ///
- ///
- ///
public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -614,9 +541,6 @@ namespace Umbraco.Core.BackOffice
///
/// Get the user security stamp
///
- ///
- ///
- ///
public Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -644,10 +568,7 @@ namespace Umbraco.Core.BackOffice
///
/// Returns the DateTimeOffset that represents the end of a user's lockout, any time in the past should be considered not locked out.
///
- ///
- ///
///
- ///
/// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status
///
public Task GetLockoutEndDateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
@@ -664,9 +585,6 @@ namespace Umbraco.Core.BackOffice
///
/// Locks a user out until the specified end date (set to a past date, to unlock a user)
///
- ///
- ///
- ///
///
/// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status
///
@@ -683,9 +601,6 @@ namespace Umbraco.Core.BackOffice
///
/// Used to record when an attempt to access the user has failed
///
- ///
- ///
- ///
public Task IncrementAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -699,9 +614,6 @@ namespace Umbraco.Core.BackOffice
///
/// Used to reset the access failed count, typically after the account is successfully accessed
///
- ///
- ///
- ///
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.
///
- ///
- ///
- ///
public Task GetAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -730,9 +639,6 @@ namespace Umbraco.Core.BackOffice
///
/// Returns true
///
- ///
- ///
- ///
public Task GetLockoutEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -744,9 +650,6 @@ namespace Umbraco.Core.BackOffice
///
/// Doesn't actually perform any function, users can always be locked out
///
- ///
- ///
- ///
public Task SetLockoutEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
@@ -904,8 +807,7 @@ namespace Umbraco.Core.BackOffice
public Task 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));
}
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs
similarity index 56%
rename from src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs
rename to src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs
index cc9b9410a6..9f77cdb7d4 100644
--- a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs
+++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs
@@ -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, IBackOfficeUserManager
+ public class BackOfficeUserManager : UmbracoUserManager, IBackOfficeUserManager
{
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
public BackOfficeUserManager(
IIpResolver ipResolver,
IUserStore store,
@@ -36,135 +38,11 @@ namespace Umbraco.Web.Common.Security
IHttpContextAccessor httpContextAccessor,
ILogger> logger,
IOptions 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 : UserManager
- where T : BackOfficeIdentityUser
- {
- private PasswordGenerator _passwordGenerator;
- private readonly IHttpContextAccessor _httpContextAccessor;
-
- public BackOfficeUserManager(
- IIpResolver ipResolver,
- IUserStore store,
- IOptions optionsAccessor,
- IPasswordHasher passwordHasher,
- IEnumerable> userValidators,
- IEnumerable> passwordValidators,
- BackOfficeLookupNormalizer keyNormalizer,
- BackOfficeIdentityErrorDescriber errors,
- IServiceProvider services,
- IHttpContextAccessor httpContextAccessor,
- ILogger> logger,
- IOptions 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;
-
- ///
- /// Developers will need to override this to support custom 2 factor auth
- ///
- public override bool SupportsUserTwoFactor => false;
-
- // We haven't needed to support this yet, though might be necessary for 2FA
- public override bool SupportsUserPhoneNumber => false;
-
- ///
- /// Replace the underlying options property with our own strongly typed version
- ///
- public new BackOfficeIdentityOptions Options
- {
- get => (BackOfficeIdentityOptions)base.Options;
- set => base.Options = value;
- }
-
- ///
- /// Used to validate a user's session
- ///
- /// The user id
- /// The sesion id
- /// True if the sesion is valid, else false
- public virtual async Task ValidateSessionIdAsync(string userId, string sessionId)
- {
- var userSessionStore = Store as IUserSessionStore;
-
- // 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);
- }
-
- ///
- /// This will determine which password hasher to use based on what is defined in config
- ///
- /// The
- /// An
- protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher();
-
- ///
- /// Gets/sets the default back office user password checker
- ///
- public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; }
- public IPasswordConfiguration PasswordConfiguration { get; protected set; }
- public IIpResolver IpResolver { get; }
-
- ///
- /// Helper method to generate a password for a user based on the current password validator
- ///
- /// The generated password
- public string GeneratePassword()
- {
- if (_passwordGenerator == null)
- {
- _passwordGenerator = new PasswordGenerator(PasswordConfiguration);
- }
-
- var password = _passwordGenerator.GeneratePassword();
- return password;
- }
-
- ///
- /// 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
- ///
- /// The user
- /// True if the user is locked out, else false
- ///
- /// 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
- ///
- public override async Task IsLockedOutAsync(T user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- if (user.IsApproved == false)
- {
- return true;
- }
-
- return await base.IsLockedOutAsync(user);
- }
-
- ///
- /// Logic used to validate a username and password
- ///
///
///
/// 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.
///
- public override async Task CheckPasswordAsync(T user, string password)
+ public override async Task 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);
}
///
- /// 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
///
- /// The userId
- /// The reset password token
- /// The new password to set it to
- /// The
+ /// The user
+ /// True if the user is locked out, else false
///
- /// 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
///
- public async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword)
+ public override async Task 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 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 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 ChangePasswordAsync(T user, string currentPassword, string newPassword)
+ public override async Task 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;
}
- ///
- /// Override to determine how to hash the password
- ///
///
- protected override async Task 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;
- 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;
- }
-
- ///
- /// This is copied from the underlying .NET base class since they decided to not expose it
- ///
- private async Task UpdateSecurityStampInternal(T user)
- {
- if (SupportsUserSecurityStamp == false)
- {
- return;
- }
-
- await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None);
- }
-
- ///
- /// This is copied from the underlying .NET base class since they decided to not expose it
- ///
- private IUserSecurityStampStore GetSecurityStore()
- {
- var store = Store as IUserSecurityStampStore;
- if (store == null)
- {
- throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
- }
-
- return store;
- }
-
- ///
- /// This is copied from the underlying .NET base class since they decided to not expose it
- ///
- private static string NewSecurityStamp() => Guid.NewGuid().ToString();
-
- ///
- public override async Task SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd)
+ public override async Task 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
}
///
- public override async Task ResetAccessFailedCountAsync(T user)
+ public override async Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user)
{
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- var lockoutStore = (IUserLockoutStore)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);
- }
-
- ///
- /// Overrides the Microsoft ASP.NET user management method
- ///
- ///
- public override async Task AccessFailedAsync(T user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- var lockoutStore = Store as IUserLockoutStore;
- 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;
}
diff --git a/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs b/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs
new file mode 100644
index 0000000000..a555cca4be
--- /dev/null
+++ b/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs
@@ -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
+{
+
+ ///
+ /// Abstract class for Umbraco User Managers for back office users or front-end members
+ ///
+ /// The type of user
+ public abstract class UmbracoUserManager : UserManager
+ where T : UmbracoIdentityUser
+ {
+ private PasswordGenerator _passwordGenerator;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UmbracoUserManager(
+ IIpResolver ipResolver,
+ IUserStore store,
+ IOptions optionsAccessor,
+ IPasswordHasher passwordHasher,
+ IEnumerable> userValidators,
+ IEnumerable> passwordValidators,
+ BackOfficeLookupNormalizer keyNormalizer,
+ BackOfficeIdentityErrorDescriber errors,
+ IServiceProvider services,
+ ILogger> logger,
+ IOptions 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;
+
+ ///
+ /// Developers will need to override this to support custom 2 factor auth
+ ///
+ public override bool SupportsUserTwoFactor => false;
+
+ // We haven't needed to support this yet, though might be necessary for 2FA
+ public override bool SupportsUserPhoneNumber => false;
+
+ ///
+ /// Used to validate a user's session
+ ///
+ /// The user id
+ /// The sesion id
+ /// True if the sesion is valid, else false
+ public virtual async Task ValidateSessionIdAsync(string userId, string sessionId)
+ {
+ var userSessionStore = Store as IUserSessionStore;
+
+ // 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);
+ }
+
+ ///
+ /// This will determine which password hasher to use based on what is defined in config
+ ///
+ /// The
+ /// An
+ protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher();
+
+ ///
+ /// Gets or sets the default back office user password checker
+ ///
+ public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; }
+
+ public IPasswordConfiguration PasswordConfiguration { get; protected set; }
+
+ public IIpResolver IpResolver { get; }
+
+ ///
+ /// Helper method to generate a password for a user based on the current password validator
+ ///
+ /// The generated password
+ public string GeneratePassword()
+ {
+ if (_passwordGenerator == null)
+ {
+ _passwordGenerator = new PasswordGenerator(PasswordConfiguration);
+ }
+
+ var password = _passwordGenerator.GeneratePassword();
+ return password;
+ }
+
+ ///
+ public override async Task 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);
+ }
+
+ ///
+ /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event
+ ///
+ /// The userId
+ /// The reset password token
+ /// The new password to set it to
+ /// The
+ ///
+ /// 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
+ ///
+ public virtual async Task 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;
+ }
+
+ ///
+ /// This is copied from the underlying .NET base class since they decided to not expose it
+ ///
+ private IUserSecurityStampStore GetSecurityStore()
+ {
+ var store = Store as IUserSecurityStampStore;
+ if (store == null)
+ {
+ throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
+ }
+
+ return store;
+ }
+
+ ///
+ /// This is copied from the underlying .NET base class since they decided to not expose it
+ ///
+ private static string NewSecurityStamp() => Guid.NewGuid().ToString();
+
+ ///
+ public override async Task 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;
+ }
+
+ ///
+ public override async Task ResetAccessFailedCountAsync(T user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ var lockoutStore = (IUserLockoutStore)Store;
+ var accessFailedCount = await GetAccessFailedCountAsync(user);
+
+ if (accessFailedCount == 0)
+ {
+ return IdentityResult.Success;
+ }
+
+ await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None);
+
+ return await UpdateAsync(user);
+ }
+
+ ///
+ /// Overrides the Microsoft ASP.NET user management method
+ ///
+ ///
+ public override async Task AccessFailedAsync(T user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ var lockoutStore = Store as IUserLockoutStore;
+ 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;
+ }
+
+ }
+}