diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs index 6e11e20c8f..8a7186da77 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs @@ -14,7 +14,7 @@ using Umbraco.Net; namespace Umbraco.Core.BackOffice { - public class BackOfficeUserManager : BackOfficeUserManager + public class BackOfficeUserManager : BackOfficeUserManager, IBackOfficeUserManager { public BackOfficeUserManager( IIpResolver ipResolver, diff --git a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs new file mode 100644 index 0000000000..e4cff3e042 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + + +namespace Umbraco.Core.BackOffice +{ + public interface IBackOfficeUserManager : IBackOfficeUserManager + { + + } + public interface IBackOfficeUserManager: IDisposable + where TUser : BackOfficeIdentityUser + { + + /// + /// Finds and returns a user, if any, who has the specified . + /// + /// The user ID to search for. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + Task FindByIdAsync(string userId); + + /// + /// Generates a password reset token for the specified , using + /// the configured password reset token provider. + /// + /// The user to generate a password reset token for. + /// The that represents the asynchronous operation, + /// containing a password reset token for the specified . + Task GeneratePasswordResetTokenAsync(TUser user); + + /// + /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event + /// + /// + /// + /// + /// + /// + /// 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 + /// + Task ChangePasswordWithResetAsync(int userId, string token, string newPassword); + + /// + /// Validates that an email confirmation token matches the specified . + /// + /// The user to validate the token against. + /// The email confirmation token to validate. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task ConfirmEmailAsync(TUser user, string token); + + /// + /// Gets the user, if any, associated with the normalized value of the specified email address. + /// Note: Its recommended that identityOptions.User.RequireUniqueEmail be set to true when using this method, otherwise + /// the store may throw if there are users with duplicate emails. + /// + /// The email address to return the user for. + /// + /// The task object containing the results of the asynchronous lookup operation, the user, if any, associated with a normalized value of the specified email address. + /// + Task FindByEmailAsync(string email); + + /// + /// Resets the 's password to the specified after + /// validating the given password reset . + /// + /// The user whose password should be reset. + /// The password reset token to verify. + /// The new password to set if reset token verification succeeds. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task ResetPasswordAsync(TUser user, string token, string newPassword); + + /// + /// 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 + /// + /// + /// + /// + /// 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 + /// + Task IsLockedOutAsync(TUser user); + + /// + /// Locks out a user until the specified end date has passed. Setting a end date in the past immediately unlocks a user. + /// + /// The user whose lockout date should be set. + /// The after which the 's lockout should end. + /// The that represents the asynchronous operation, containing the of the operation. + Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd); + + /// + /// Gets a flag indicating whether the email address for the specified has been verified, true if the email address is verified otherwise + /// false. + /// + /// The user whose email confirmation status should be returned. + /// + /// The task object containing the results of the asynchronous operation, a flag indicating whether the email address for the specified + /// has been confirmed or not. + /// + Task IsEmailConfirmedAsync(TUser user); + + /// + /// Updates the specified in the backing store. + /// + /// The user to update. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task UpdateAsync(TUser user); + + /// + /// Returns a flag indicating whether the specified is valid for + /// the given and . + /// + /// The user to validate the token against. + /// The token provider used to generate the token. + /// The purpose the token should be generated for. + /// The token to validate + /// + /// The that represents the asynchronous operation, returning true if the + /// is valid, otherwise false. + /// + Task VerifyUserTokenAsync(TUser user, string tokenProvider, string purpose, + string token); + + /// + /// Adds the to the specified only if the user + /// does not already have a password. + /// + /// The user whose password should be set. + /// The password to set. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task AddPasswordAsync(TUser user, string password); + + + /// + /// Returns a flag indicating whether the given is valid for the + /// specified . + /// + /// The user whose password should be validated. + /// The password to validate + /// The that represents the asynchronous operation, containing true if + /// the specified matches the one store for the , + /// otherwise false. + Task CheckPasswordAsync(TUser user, string password); + + /// + /// Changes a user's password after confirming the specified is correct, + /// as an asynchronous operation. + /// + /// The user whose password should be set. + /// The current password to validate before changing. + /// The new password to set for the specified . + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task ChangePasswordAsync(TUser user, string currentPassword, + string newPassword); + + /// + /// Used to validate a user's session + /// + /// + /// + /// + Task ValidateSessionIdAsync(string userId, string sessionId); + + /// + /// Creates the specified in the backing store with no password, + /// as an asynchronous operation. + /// + /// The user to create. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task CreateAsync(TUser user); + + /// + /// Helper method to generate a password for a user based on the current password validator + /// + /// + string GeneratePassword(); + + + /// + /// Generates an email confirmation token for the specified user. + /// + /// The user to generate an email confirmation token for. + /// + /// The that represents the asynchronous operation, an email confirmation token. + /// + Task GenerateEmailConfirmationTokenAsync(TUser user); + + /// + /// Finds and returns a user, if any, who has the specified user name. + /// + /// The user name to search for. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + Task FindByNameAsync(string userName); + + /// + /// Increments the access failed count for the user as an asynchronous operation. + /// If the failed access account is greater than or equal to the configured maximum number of attempts, + /// the user will be locked out for the configured lockout time span. + /// + /// The user whose failed access count to increment. + /// The that represents the asynchronous operation, containing the of the operation. + Task AccessFailedAsync(TUser user); + + /// + /// Returns a flag indicating whether the specified has two factor authentication enabled or not, + /// as an asynchronous operation. + /// + /// The user whose two factor authentication enabled status should be retrieved. + /// + /// The that represents the asynchronous operation, true if the specified + /// has two factor authentication enabled, otherwise false. + /// + Task GetTwoFactorEnabledAsync(TUser user); + + /// + /// Gets a list of valid two factor token providers for the specified , + /// as an asynchronous operation. + /// + /// The user the whose two factor authentication providers will be returned. + /// + /// The that represents result of the asynchronous operation, a list of two + /// factor authentication providers for the specified user. + /// + Task> GetValidTwoFactorProvidersAsync(TUser user); + + /// + /// Verifies the specified two factor authentication against the . + /// + /// The user the token is supposed to be for. + /// The provider which will verify the token. + /// The token to verify. + /// + /// The that represents result of the asynchronous operation, true if the token is valid, + /// otherwise false. + /// + Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token); + + Task ResetAccessFailedCountAsync(TUser user); + + void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId); + void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId); + void RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId); + + void RaiseLoginSuccessEvent(TUser currentUser, int userId); + } +} diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index f2e9556a48..96e4a9ae34 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -34,7 +34,7 @@ namespace Umbraco.Web.Install.InstallSteps private readonly SecuritySettings _securitySettings; private readonly ConnectionStrings _connectionStrings; private readonly ICookieManager _cookieManager; - private readonly BackOfficeUserManager _userManager; + private readonly IBackOfficeUserManager _userManager; public NewInstallStep( IUserService userService, @@ -43,7 +43,7 @@ namespace Umbraco.Web.Install.InstallSteps IOptions securitySettings, IOptions connectionStrings, ICookieManager cookieManager, - BackOfficeUserManager userManager) + IBackOfficeUserManager userManager) { _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index 51fce283f8..58be305b91 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -32,7 +32,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Extensions [Test] public void AddUmbracoBackOfficeIdentity_ExpectBackOfficeUserManagerResolvable() { - var userManager = Services.GetService(); + var userManager = Services.GetService(); Assert.NotNull(userManager); } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs index 4c038a58e6..97a90be908 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs @@ -15,14 +15,14 @@ namespace Umbraco.Tests.Web.Controllers { [Test,AutoMoqData] public void PostUnlockUsers_When_User_Lockout_Update_Fails_Expect_Failure_Response( - [Frozen] IUserStore userStore, + [Frozen] IBackOfficeUserManager backOfficeUserManager, UsersController sut, BackOfficeIdentityUser user, int[] userIds, string expectedMessage) { - Mock.Get(userStore) - .Setup(x => x.FindByIdAsync(It.IsAny(), It.IsAny())) + Mock.Get(backOfficeUserManager) + .Setup(x => x.FindByIdAsync(It.IsAny())) .ReturnsAsync(user); Assert.ThrowsAsync(() => sut.PostUnlockUsers(userIds)); diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 185269b6f1..51ed87a7e7 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -41,7 +41,7 @@ namespace Umbraco.Web.BackOffice.Controllers public class AuthenticationController : UmbracoApiControllerBase { private readonly IBackofficeSecurityAccessor _backofficeSecurityAccessor; - private readonly BackOfficeUserManager _userManager; + private readonly IBackOfficeUserManager _userManager; private readonly BackOfficeSignInManager _signInManager; private readonly IUserService _userService; private readonly ILocalizedTextService _textService; @@ -60,7 +60,7 @@ namespace Umbraco.Web.BackOffice.Controllers public AuthenticationController( IBackofficeSecurityAccessor backofficeSecurityAccessor, - BackOfficeUserManager backOfficeUserManager, + IBackOfficeUserManager backOfficeUserManager, BackOfficeSignInManager signInManager, IUserService userService, ILocalizedTextService textService, diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 6db4efab40..efcf7d9e12 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -36,11 +36,10 @@ namespace Umbraco.Web.BackOffice.Controllers [PluginController(Constants.Web.Mvc.BackOfficeArea)] public class BackOfficeController : Controller { - private readonly BackOfficeUserManager _userManager; + private readonly IBackOfficeUserManager _userManager; private readonly IRuntimeMinifier _runtimeMinifier; private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly ILocalizedTextService _textService; private readonly IGridConfig _gridConfig; private readonly BackOfficeServerVariables _backOfficeServerVariables; @@ -50,11 +49,10 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly ILogger _logger; public BackOfficeController( - BackOfficeUserManager userManager, + IBackOfficeUserManager userManager, IRuntimeMinifier runtimeMinifier, IOptions globalSettings, IHostingEnvironment hostingEnvironment, - IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IGridConfig gridConfig, BackOfficeServerVariables backOfficeServerVariables, @@ -67,7 +65,6 @@ namespace Umbraco.Web.BackOffice.Controllers _runtimeMinifier = runtimeMinifier; _globalSettings = globalSettings.Value; _hostingEnvironment = hostingEnvironment; - _umbracoContextAccessor = umbracoContextAccessor; _textService = textService; _gridConfig = gridConfig ?? throw new ArgumentNullException(nameof(gridConfig)); _backOfficeServerVariables = backOfficeServerVariables; diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index 3aab1853dc..abd9e6ab1d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -43,7 +43,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IBackofficeSecurityAccessor _backofficeSecurityAccessor; private readonly IUserService _userService; private readonly UmbracoMapper _umbracoMapper; - private readonly BackOfficeUserManager _backOfficeUserManager; + private readonly IBackOfficeUserManager _backOfficeUserManager; private readonly ILogger _logger; private readonly ILocalizedTextService _localizedTextService; private readonly AppCaches _appCaches; @@ -57,7 +57,7 @@ namespace Umbraco.Web.BackOffice.Controllers IBackofficeSecurityAccessor backofficeSecurityAccessor, IUserService userService, UmbracoMapper umbracoMapper, - BackOfficeUserManager backOfficeUserManager, + IBackOfficeUserManager backOfficeUserManager, ILogger logger, ILocalizedTextService localizedTextService, AppCaches appCaches, diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index cfa1e980bd..3e7216e497 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -72,7 +72,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IMediaService _mediaService; private readonly IContentService _contentService; private readonly GlobalSettings _globalSettings; - private readonly BackOfficeUserManager _backOfficeUserManager; + private readonly IBackOfficeUserManager _backOfficeUserManager; private readonly ILogger _logger; private readonly LinkGenerator _linkGenerator; @@ -95,7 +95,7 @@ namespace Umbraco.Web.BackOffice.Controllers IMediaService mediaService, IContentService contentService, IOptions globalSettings, - BackOfficeUserManager backOfficeUserManager, + IBackOfficeUserManager backOfficeUserManager, ILogger logger, LinkGenerator linkGenerator) { diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index d9eb0987d0..4e00525c2a 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; @@ -57,7 +58,7 @@ namespace Umbraco.Extensions services.BuildUmbracoBackOfficeIdentity() .AddDefaultTokenProviders() .AddUserStore() - .AddUserManager() + .AddUserManager() .AddSignInManager() .AddClaimsPrincipalFactory>(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs new file mode 100644 index 0000000000..eab7142665 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core.BackOffice; + +namespace Umbraco.Extensions +{ + public static class IdentityBuilderExtensions + { + /// + /// Adds a for the . + /// + /// The type of the user manager to add. + /// + /// The current instance. + public static IdentityBuilder AddUserManager(this IdentityBuilder identityBuilder) where TUserManager : UserManager, TInterface + { + identityBuilder.AddUserManager(); + identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); + return identityBuilder; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs index e91a517496..b5974c870a 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs @@ -37,9 +37,9 @@ namespace Umbraco.Web.BackOffice.Security private readonly ISystemClock _systemClock; private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; - private readonly BackOfficeUserManager _userManager; + private readonly IBackOfficeUserManager _userManager; - public BackOfficeSessionIdValidator(ISystemClock systemClock, IOptions globalSettings, IHostingEnvironment hostingEnvironment, BackOfficeUserManager userManager) + public BackOfficeSessionIdValidator(ISystemClock systemClock, IOptions globalSettings, IHostingEnvironment hostingEnvironment, IBackOfficeUserManager userManager) { _systemClock = systemClock; _globalSettings = globalSettings.Value; diff --git a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs index 2ee74aad01..76ecaa32e3 100644 --- a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.BackOffice.Security IUser currentUser, IUser savingUser, ChangingPasswordModel passwordModel, - BackOfficeUserManager userMgr) + IBackOfficeUserManager userMgr) { if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel)); if (userMgr == null) throw new ArgumentNullException(nameof(userMgr)); diff --git a/src/Umbraco.Web.Common/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeSignInManager.cs index 07f7470243..6d4b2ddd4f 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeSignInManager.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web.Common.Security public class BackOfficeSignInManager : SignInManager { - private readonly BackOfficeUserManager _userManager; + private readonly IBackOfficeUserManager _userManager; public BackOfficeSignInManager( BackOfficeUserManager userManager, diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs index 0dc86f7b1b..8c14d2544f 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web.Security /// public class BackOfficeSignInManager : IDisposable { - private readonly BackOfficeUserManager _userManager; + private readonly IBackOfficeUserManager _userManager; private readonly IUserClaimsPrincipalFactory _claimsPrincipalFactory; private readonly IAuthenticationManager _authenticationManager; private readonly ILogger _logger; @@ -28,7 +28,7 @@ namespace Umbraco.Web.Security private readonly IOwinRequest _request; public BackOfficeSignInManager( - BackOfficeUserManager userManager, + IBackOfficeUserManager userManager, IUserClaimsPrincipalFactory claimsPrincipalFactory, IAuthenticationManager authenticationManager, ILogger logger,