diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs index 6d2ad081c3..99d0265d06 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -56,7 +56,7 @@ namespace Umbraco.Core.BackOffice // TODO: Support this public override bool SupportsUserPhoneNumber => false; #endregion - + /// /// Used to validate a user's session /// @@ -86,7 +86,7 @@ namespace Umbraco.Core.BackOffice /// Gets/sets the default back office user password checker /// public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } - public IPasswordConfiguration PasswordConfiguration { get; } + public IPasswordConfiguration PasswordConfiguration { get; protected set; } public IIpResolver IpResolver { get; } /// @@ -332,57 +332,57 @@ namespace Umbraco.Core.BackOffice return result; } - internal void RaiseAccountLockedEvent(int userId) + public void RaiseAccountLockedEvent(int userId) { OnAccountLocked(new IdentityAuditEventArgs(AuditEvent.AccountLocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseAccountUnlockedEvent(int userId) + public void RaiseAccountUnlockedEvent(int userId) { OnAccountUnlocked(new IdentityAuditEventArgs(AuditEvent.AccountUnlocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseForgotPasswordRequestedEvent(int userId) + public void RaiseForgotPasswordRequestedEvent(int userId) { OnForgotPasswordRequested(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordRequested, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseForgotPasswordChangedSuccessEvent(int userId) + public void RaiseForgotPasswordChangedSuccessEvent(int userId) { OnForgotPasswordChangedSuccess(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordChangedSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseLoginFailedEvent(int userId) + public void RaiseLoginFailedEvent(int userId) { OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseInvalidLoginAttemptEvent(string username) + public void RaiseInvalidLoginAttemptEvent(string username) { OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), username, string.Format("Attempted login for username '{0}' failed", username))); } - internal void RaiseLoginRequiresVerificationEvent(int userId) + public void RaiseLoginRequiresVerificationEvent(int userId) { OnLoginRequiresVerification(new IdentityAuditEventArgs(AuditEvent.LoginRequiresVerification, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseLoginSuccessEvent(int userId) + public void RaiseLoginSuccessEvent(int userId) { OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseLogoutSuccessEvent(int userId) + public void RaiseLogoutSuccessEvent(int userId) { OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaisePasswordChangedEvent(int userId) + public void RaisePasswordChangedEvent(int userId) { OnPasswordChanged(new IdentityAuditEventArgs(AuditEvent.PasswordChanged, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseResetAccessFailedCountEvent(int userId) + public void RaiseResetAccessFailedCountEvent(int userId) { OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } diff --git a/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs b/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs index 99a2b323dd..cf3d54a4ce 100644 --- a/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs +++ b/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs @@ -10,7 +10,7 @@ using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; using Umbraco.Net; -using BackOfficeUserManager = Umbraco.Web.Security.BackOfficeUserManager; +using Umbraco.Web.Security; namespace Umbraco.Tests.Security { @@ -32,7 +32,7 @@ namespace Umbraco.Tests.Security mockPasswordConfiguration.Setup(x => x.HashAlgorithmType) .Returns("HMACSHA256"); - var userManager = BackOfficeUserManager.Create( + var userManager = BackOfficeOwinUserManager.Create( mockPasswordConfiguration.Object, mockIpResolver.Object, mockUserStore.Object, diff --git a/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs b/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs index 4adfe15ad7..b80e526037 100644 --- a/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs +++ b/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs @@ -23,7 +23,7 @@ namespace Umbraco.Tests.Security public class UmbracoSecurityStampValidatorTests { private Mock _mockOwinContext; - private Mock> _mockUserManager; + private Mock _mockUserManager; private Mock _mockSignInManager; private AuthenticationTicket _testAuthTicket; @@ -35,7 +35,7 @@ namespace Umbraco.Tests.Security public void OnValidateIdentity_When_GetUserIdCallback_Is_Null_Expect_ArgumentNullException() { Assert.Throws(() => UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MaxValue, null, null)); } @@ -43,7 +43,7 @@ namespace Umbraco.Tests.Security public async Task OnValidateIdentity_When_Validation_Interval_Not_Met_Expect_No_Op() { var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MaxValue, null, identity => throw new Exception()); _testAuthTicket.Properties.IssuedUtc = DateTimeOffset.UtcNow; @@ -62,11 +62,11 @@ namespace Umbraco.Tests.Security public void OnValidateIdentity_When_Time_To_Validate_But_No_UserManager_Expect_InvalidOperationException() { var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => throw new Exception()); - _mockOwinContext.Setup(x => x.Get>(It.IsAny())) - .Returns((Umbraco.Web.Security.BackOfficeUserManager) null); + _mockOwinContext.Setup(x => x.Get(It.IsAny())) + .Returns((BackOfficeOwinUserManager) null); var context = new CookieValidateIdentityContext( _mockOwinContext.Object, @@ -80,7 +80,7 @@ namespace Umbraco.Tests.Security public void OnValidateIdentity_When_Time_To_Validate_But_No_SignInManager_Expect_InvalidOperationException() { var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => throw new Exception()); _mockOwinContext.Setup(x => x.Get(It.IsAny())) @@ -100,7 +100,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)) @@ -123,7 +123,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -146,7 +146,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -170,7 +170,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -194,7 +194,7 @@ namespace Umbraco.Tests.Security var expectedIdentity = new ClaimsIdentity(new List {new Claim("sub", "bob")}); var regenFuncCalled = false; - Func, BackOfficeIdentityUser, Task> regenFunc = + Func> regenFunc = (signInManager, userManager, user) => { regenFuncCalled = true; @@ -202,7 +202,7 @@ namespace Umbraco.Tests.Security }; var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, regenFunc, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -250,7 +250,7 @@ namespace Umbraco.Tests.Security new AuthenticationProperties()); _testOptions = new CookieAuthenticationOptions { AuthenticationType = _testAuthType }; - _mockUserManager = new Mock>( + _mockUserManager = new Mock( new Mock().Object, new Mock().Object, new Mock>().Object, @@ -267,7 +267,7 @@ namespace Umbraco.Tests.Security new Mock().Object); _mockOwinContext = new Mock(); - _mockOwinContext.Setup(x => x.Get>(It.IsAny())) + _mockOwinContext.Setup(x => x.Get(It.IsAny())) .Returns(_mockUserManager.Object); _mockOwinContext.Setup(x => x.Get(It.IsAny())) .Returns(_mockSignInManager.Object); diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index 80deb5a800..6905c749e6 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -469,7 +469,7 @@ namespace Umbraco.Tests.Web.Controllers mockUserManager.Verify(); } - private UsersController CreateSut(IMock> mockUserManager = null) + private UsersController CreateSut(IMock mockUserManager = null) { var mockLocalizedTextService = new Mock(); mockLocalizedTextService.Setup(x => x.Localize(It.IsAny(), It.IsAny(), It.IsAny>())) @@ -509,9 +509,9 @@ namespace Umbraco.Tests.Web.Controllers return usersController; } - private static Mock> CreateMockUserManager() + private static Mock CreateMockUserManager() { - return new Mock>( + return new Mock( new Mock().Object, new Mock().Object, new Mock>().Object, diff --git a/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs b/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs index 84fb0e6bb8..dcb5fac32d 100644 --- a/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs +++ b/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs @@ -26,14 +26,14 @@ namespace Umbraco.Web.Compose { //BackOfficeUserManager.AccountLocked += ; //BackOfficeUserManager.AccountUnlocked += ; - BackOfficeUserManager.ForgotPasswordRequested += OnForgotPasswordRequest; - BackOfficeUserManager.ForgotPasswordChangedSuccess += OnForgotPasswordChange; - BackOfficeUserManager.LoginFailed += OnLoginFailed; + BackOfficeOwinUserManager.ForgotPasswordRequested += OnForgotPasswordRequest; + BackOfficeOwinUserManager.ForgotPasswordChangedSuccess += OnForgotPasswordChange; + BackOfficeOwinUserManager.LoginFailed += OnLoginFailed; //BackOfficeUserManager.LoginRequiresVerification += ; - BackOfficeUserManager.LoginSuccess += OnLoginSuccess; - BackOfficeUserManager.LogoutSuccess += OnLogoutSuccess; - BackOfficeUserManager.PasswordChanged += OnPasswordChanged; - BackOfficeUserManager.PasswordReset += OnPasswordReset; + BackOfficeOwinUserManager.LoginSuccess += OnLoginSuccess; + BackOfficeOwinUserManager.LogoutSuccess += OnLogoutSuccess; + BackOfficeOwinUserManager.PasswordChanged += OnPasswordChanged; + BackOfficeOwinUserManager.PasswordReset += OnPasswordReset; //BackOfficeUserManager.ResetAccessFailedCount += ; } diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index b38bbbb19d..d65bb043c5 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -40,7 +40,7 @@ namespace Umbraco.Web.Editors [IsBackOffice] public class AuthenticationController : UmbracoApiController { - private Security.BackOfficeUserManager _userManager; + private BackOfficeOwinUserManager _userManager; private BackOfficeSignInManager _signInManager; private readonly IUserPasswordConfiguration _passwordConfiguration; private readonly IHostingEnvironment _hostingEnvironment; @@ -71,8 +71,8 @@ namespace Umbraco.Web.Editors _requestAccessor = requestAccessor ?? throw new ArgumentNullException(nameof(securitySettings)); } - protected Security.BackOfficeUserManager UserManager => _userManager - ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); + protected BackOfficeOwinUserManager UserManager => _userManager + ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = TryGetOwinContext().Result.GetBackOfficeSignInManager()); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 782dea61ac..db7505aa3c 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -45,7 +45,7 @@ namespace Umbraco.Web.Editors { private readonly UmbracoFeatures _features; private readonly IRuntimeState _runtimeState; - private BackOfficeUserManager _userManager; + private BackOfficeOwinUserManager _userManager; private BackOfficeSignInManager _signInManager; private readonly IUmbracoVersion _umbracoVersion; private readonly IGridConfig _gridConfig; @@ -92,7 +92,7 @@ namespace Umbraco.Web.Editors protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = OwinContext.GetBackOfficeSignInManager()); - protected BackOfficeUserManager UserManager => _userManager ?? (_userManager = OwinContext.GetBackOfficeUserManager()); + protected BackOfficeOwinUserManager UserManager => _userManager ?? (_userManager = OwinContext.GetBackOfficeUserManager()); protected IAuthenticationManager AuthenticationManager => OwinContext.Authentication; diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index 10a5248045..256da61478 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Web.Models; @@ -32,7 +31,7 @@ namespace Umbraco.Web.Editors IUser currentUser, IUser savingUser, ChangingPasswordModel passwordModel, - Security.BackOfficeUserManager userMgr) + BackOfficeOwinUserManager userMgr) { if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel)); if (userMgr == null) throw new ArgumentNullException(nameof(userMgr)); diff --git a/src/Umbraco.Web/OwinExtensions.cs b/src/Umbraco.Web/OwinExtensions.cs index 67e9375ab5..4ea4040ec6 100644 --- a/src/Umbraco.Web/OwinExtensions.cs +++ b/src/Umbraco.Web/OwinExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.Owin; using Microsoft.Owin.Security; using Umbraco.Core; using Umbraco.Web.Security; -using BackOfficeIdentityUser = Umbraco.Core.BackOffice.BackOfficeIdentityUser; namespace Umbraco.Web { @@ -72,13 +71,13 @@ namespace Umbraco.Web /// This is required because to extract the user manager we need to user a custom service since owin only deals in generics and /// developers could register their own user manager types /// - public static BackOfficeUserManager GetBackOfficeUserManager(this IOwinContext owinContext) + public static BackOfficeOwinUserManager GetBackOfficeUserManager(this IOwinContext owinContext) { - var marker = owinContext.Get(BackOfficeUserManager.OwinMarkerKey) + var marker = owinContext.Get(BackOfficeOwinUserManager.OwinMarkerKey) ?? throw new NullReferenceException($"No {typeof (IBackOfficeUserManagerMarker)}, i.e. no Umbraco back-office, has been registered with Owin."); return marker.GetManager(owinContext) - ?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeUserManager)} from the {typeof (IOwinContext)}."); + ?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeOwinUserManager)} from the {typeof (IOwinContext)}."); } /// diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index 6254dabeb9..81c5408e98 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -43,8 +43,8 @@ namespace Umbraco.Web.Security if (services == null) throw new ArgumentNullException(nameof(services)); //Configure Umbraco user manager to be created per request - app.CreatePerOwinContext( - (options, owinContext) => BackOfficeUserManager.Create( + app.CreatePerOwinContext( + (options, owinContext) => BackOfficeOwinUserManager.Create( services.UserService, services.EntityService, services.ExternalLoginService, @@ -56,7 +56,7 @@ namespace Umbraco.Web.Security app.GetDataProtectionProvider(), new NullLogger>())); - app.SetBackOfficeUserManagerType(); + app.SetBackOfficeUserManagerType(); //Create a sign in manager per request app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger())); @@ -77,8 +77,8 @@ namespace Umbraco.Web.Security if (customUserStore == null) throw new ArgumentNullException(nameof(customUserStore)); //Configure Umbraco user manager to be created per request - app.CreatePerOwinContext( - (options, owinContext) => BackOfficeUserManager.Create( + app.CreatePerOwinContext( + (options, owinContext) => BackOfficeOwinUserManager.Create( passwordConfiguration, ipResolver, customUserStore, @@ -86,7 +86,7 @@ namespace Umbraco.Web.Security app.GetDataProtectionProvider(), new NullLogger>())); - app.SetBackOfficeUserManagerType(); + app.SetBackOfficeUserManagerType(); //Create a sign in manager per request app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName))); @@ -153,7 +153,7 @@ namespace Umbraco.Web.Security // logs in. This is a security feature which is used when you // change a password or add an external login to your account. OnValidateIdentity = UmbracoSecurityStampValidator - .OnValidateIdentity( + .OnValidateIdentity( TimeSpan.FromMinutes(30), (signInManager, manager, user) => signInManager.CreateUserIdentityAsync(user), identity => identity.GetUserId()), @@ -240,7 +240,7 @@ namespace Umbraco.Web.Security // a generic strongly typed instance app.Use((context, func) => { - context.Set(BackOfficeUserManager.OwinMarkerKey, new BackOfficeUserManagerMarker()); + context.Set(BackOfficeOwinUserManager.OwinMarkerKey, new BackOfficeUserManagerMarker()); return func(); }); } diff --git a/src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs b/src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs new file mode 100644 index 0000000000..3a5ba8f706 --- /dev/null +++ b/src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Owin.Security.DataProtection; +using Umbraco.Core; +using Umbraco.Core.BackOffice; +using Umbraco.Core.Configuration; +using Umbraco.Core.Mapping; +using Umbraco.Core.Services; +using Umbraco.Net; + +namespace Umbraco.Web.Security +{ + public class BackOfficeOwinUserManager : BackOfficeUserManager + { + public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker"; + + public BackOfficeOwinUserManager( + IPasswordConfiguration passwordConfiguration, + IIpResolver ipResolver, + IUserStore store, + IOptions optionsAccessor, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IDataProtectionProvider dataProtectionProvider, + ILogger> logger) + : base(ipResolver, store, optionsAccessor, null, userValidators, passwordValidators, keyNormalizer, errors, null, logger) + { + PasswordConfiguration = passwordConfiguration; + InitUserManager(this, dataProtectionProvider); + } + + #region Static Create methods + + /// + /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager + /// + public static BackOfficeOwinUserManager Create( + IUserService userService, + IEntityService entityService, + IExternalLoginService externalLoginService, + IGlobalSettings globalSettings, + UmbracoMapper mapper, + IPasswordConfiguration passwordConfiguration, + IIpResolver ipResolver, + IdentityErrorDescriber errors, + IDataProtectionProvider dataProtectionProvider, + ILogger> logger) + { + var store = new BackOfficeUserStore(userService, entityService, externalLoginService, globalSettings, mapper); + + return Create( + passwordConfiguration, + ipResolver, + store, + errors, + dataProtectionProvider, + logger); + } + + /// + /// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance + /// + public static BackOfficeOwinUserManager Create( + IPasswordConfiguration passwordConfiguration, + IIpResolver ipResolver, + IUserStore customUserStore, + IdentityErrorDescriber errors, + IDataProtectionProvider dataProtectionProvider, + ILogger> logger) + { + var options = new IdentityOptions(); + + // Configure validation logic for usernames + var userValidators = new List> { new BackOfficeUserValidator() }; + options.User.RequireUniqueEmail = true; + + // Configure validation logic for passwords + var passwordValidators = new List> { new PasswordValidator() }; + options.Password.RequiredLength = passwordConfiguration.RequiredLength; + options.Password.RequireNonAlphanumeric = passwordConfiguration.RequireNonLetterOrDigit; + options.Password.RequireDigit = passwordConfiguration.RequireDigit; + options.Password.RequireLowercase = passwordConfiguration.RequireLowercase; + options.Password.RequireUppercase = passwordConfiguration.RequireUppercase; + + // Ensure Umbraco security stamp claim type is used + options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier; + options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name; + options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role; + options.ClaimsIdentity.SecurityStampClaimType = Constants.Web.SecurityStampClaimType; + + options.Lockout.AllowedForNewUsers = true; + options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.MaxFailedAccessAttemptsBeforeLockout; + //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked + // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are + // locked out or not. + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); + + return new BackOfficeOwinUserManager( + passwordConfiguration, + ipResolver, + customUserStore, + new OptionsWrapper(options), + userValidators, + passwordValidators, + new NopLookupNormalizer(), + errors, + dataProtectionProvider, + logger); + } + + #endregion + + protected void InitUserManager(BackOfficeOwinUserManager manager, IDataProtectionProvider dataProtectionProvider) + { + // use a custom hasher based on our membership provider + PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration); + + // set OWIN data protection token provider as default + if (dataProtectionProvider != null) + { + manager.RegisterTokenProvider( + TokenOptions.DefaultProvider, + new OwinDataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")) + { + TokenLifespan = TimeSpan.FromDays(3) + }); + } + + // register ASP.NET Core Identity token providers + manager.RegisterTokenProvider(TokenOptions.DefaultEmailProvider, new EmailTokenProvider()); + manager.RegisterTokenProvider(TokenOptions.DefaultPhoneProvider, new PhoneNumberTokenProvider()); + manager.RegisterTokenProvider(TokenOptions.DefaultAuthenticatorProvider, new AuthenticatorTokenProvider()); + } + } +} diff --git a/src/Umbraco.Web/Security/BackOfficeUserManager.cs b/src/Umbraco.Web/Security/BackOfficeUserManager.cs deleted file mode 100644 index ed2df536e7..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeUserManager.cs +++ /dev/null @@ -1,582 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Owin.Security.DataProtection; -using Umbraco.Core; -using Umbraco.Core.BackOffice; -using Umbraco.Core.Configuration; -using Umbraco.Core.Mapping; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Net; - -namespace Umbraco.Web.Security -{ - public class BackOfficeUserManager : BackOfficeUserManager - { - public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker"; - - public BackOfficeUserManager( - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IUserStore store, - IOptions optionsAccessor, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IDataProtectionProvider dataProtectionProvider, - ILogger> logger) - : base(passwordConfiguration, ipResolver, store, optionsAccessor, userValidators, passwordValidators, keyNormalizer, errors, null, logger) - { - InitUserManager(this, dataProtectionProvider); - } - - #region Static Create methods - - /// - /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager - /// - public static BackOfficeUserManager Create( - IUserService userService, - IEntityService entityService, - IExternalLoginService externalLoginService, - IGlobalSettings globalSettings, - UmbracoMapper mapper, - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IdentityErrorDescriber errors, - IDataProtectionProvider dataProtectionProvider, - ILogger> logger) - { - var store = new BackOfficeUserStore(userService, entityService, externalLoginService, globalSettings, mapper); - - return Create( - passwordConfiguration, - ipResolver, - store, - errors, - dataProtectionProvider, - logger); - } - - /// - /// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance - /// - public static BackOfficeUserManager Create( - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IUserStore customUserStore, - IdentityErrorDescriber errors, - IDataProtectionProvider dataProtectionProvider, - ILogger> logger) - { - var options = new IdentityOptions(); - - // Configure validation logic for usernames - var userValidators = new List> { new BackOfficeUserValidator() }; - options.User.RequireUniqueEmail = true; - - // Configure validation logic for passwords - var passwordValidators = new List> { new PasswordValidator() }; - options.Password.RequiredLength = passwordConfiguration.RequiredLength; - options.Password.RequireNonAlphanumeric = passwordConfiguration.RequireNonLetterOrDigit; - options.Password.RequireDigit = passwordConfiguration.RequireDigit; - options.Password.RequireLowercase = passwordConfiguration.RequireLowercase; - options.Password.RequireUppercase = passwordConfiguration.RequireUppercase; - - // Ensure Umbraco security stamp claim type is used - options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier; - options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name; - options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role; - options.ClaimsIdentity.SecurityStampClaimType = Constants.Web.SecurityStampClaimType; - - options.Lockout.AllowedForNewUsers = true; - options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.MaxFailedAccessAttemptsBeforeLockout; - //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked - // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are - // locked out or not. - options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); - - return new BackOfficeUserManager( - passwordConfiguration, - ipResolver, - customUserStore, - new OptionsWrapper(options), - userValidators, - passwordValidators, - new NopLookupNormalizer(), - errors, - dataProtectionProvider, - logger); - } - - #endregion - } - - public class BackOfficeUserManager : UserManager - where T : BackOfficeIdentityUser - { - private PasswordGenerator _passwordGenerator; - - public BackOfficeUserManager( - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IUserStore store, - IOptions optionsAccessor, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IServiceProvider services, - ILogger> logger) - : base(store, optionsAccessor, null, userValidators, passwordValidators, keyNormalizer, errors, services, logger) - { - PasswordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration)); - IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); - } - - #region What we do not currently support - // TODO: We could support this - but a user claims will mostly just be what is in the auth cookie - public override bool SupportsUserClaim => false; - - // TODO: Support this - public override bool SupportsQueryableUsers => false; - - /// - /// Developers will need to override this to support custom 2 factor auth - /// - public override bool SupportsUserTwoFactor => false; - - // TODO: Support this - public override bool SupportsUserPhoneNumber => false; - #endregion - - /// - /// Initializes the user manager with the correct options - /// - protected void InitUserManager( - BackOfficeUserManager manager, - IDataProtectionProvider dataProtectionProvider) - { - // use a custom hasher based on our membership provider - PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration); - - // set OWIN data protection token provider as default - if (dataProtectionProvider != null) - { - manager.RegisterTokenProvider( - TokenOptions.DefaultProvider, - new OwinDataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")) - { - TokenLifespan = TimeSpan.FromDays(3) - }); - } - - // register ASP.NET Core Identity token providers - manager.RegisterTokenProvider(TokenOptions.DefaultEmailProvider, new EmailTokenProvider()); - manager.RegisterTokenProvider(TokenOptions.DefaultPhoneProvider, new PhoneNumberTokenProvider()); - manager.RegisterTokenProvider(TokenOptions.DefaultAuthenticatorProvider, new AuthenticatorTokenProvider()); - } - - /// - /// Used to validate a user's session - /// - /// - /// - /// - 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 - /// - /// - protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) - { - //we can use the user aware password hasher (which will be the default and preferred way) - return new UserAwarePasswordHasher(new PasswordSecurity(passwordConfiguration)); - } - - /// - /// Gets/sets the default back office user password checker - /// - public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } - public IPasswordConfiguration PasswordConfiguration { get; } - public IIpResolver IpResolver { get; } - - /// - /// Helper method to generate a password for a user based on the current password validator - /// - /// - 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 - /// - /// - /// - /// - /// 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); - } - - #region Overrides for password logic - - /// - /// Logic used to validate a username and password - /// - /// - /// - /// - /// - /// By default this uses the standard ASP.Net Identity approach which is: - /// * Get password store - /// * Call VerifyPasswordAsync with the password store + user + password - /// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password - /// - /// In some cases people want simple custom control over the username/password check, for simplicity - /// sake, developers would like the users to simply validate against an LDAP directory but the user - /// data remains stored inside of Umbraco. - /// See: http://issues.umbraco.org/issue/U4-7032 for the use cases. - /// - /// 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) - { - if (BackOfficeUserPasswordChecker != null) - { - var result = await BackOfficeUserPasswordChecker.CheckPasswordAsync(user, password); - - if (user.HasIdentity == false) - { - return false; - } - - //if the result indicates to not fallback to the default, then return true if the credentials are valid - if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker) - { - return result == BackOfficeUserPasswordCheckerResult.ValidCredentials; - } - } - - //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 - /// - /// - /// - /// - /// - /// - /// 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 async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) - { - var user = await base.FindByIdAsync(userId.ToString()); - if (user == null) throw new InvalidOperationException("Could not find user"); - - var result = await base.ResetPasswordAsync(user, token, newPassword); - if (result.Succeeded) RaisePasswordChangedEvent(userId); - return result; - } - - public override async Task ChangePasswordAsync(T user, string currentPassword, string newPassword) - { - var result = await base.ChangePasswordAsync(user, currentPassword, newPassword); - if (result.Succeeded) RaisePasswordChangedEvent(user.Id); - return result; - } - - /// - /// Override to determine how to hash the password - /// - /// - /// - /// - /// - /// - /// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used) - /// - protected override async Task UpdatePasswordHash(T user, string newPassword, bool validatePassword) - { - user.LastPasswordChangeDateUtc = DateTime.UtcNow; - - if (validatePassword) - { - var 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() - { - return Guid.NewGuid().ToString(); - } - - #endregion - - public override async Task SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - var 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) - { - RaiseAccountLockedEvent(user.Id); - } - else - { - RaiseAccountUnlockedEvent(user.Id); - //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); - //raise the event now that it's reset - RaiseResetAccessFailedCountEvent(user.Id); - return await UpdateAsync(user); - } - - /// - /// Overrides the Microsoft ASP.NET user management method - /// - /// - /// - /// returns a Async Task - /// - /// - /// Doesn't set fail attempts back to 0 - /// - 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 - } - - var result = await UpdateAsync(user); - - //Slightly confusing: this will return a Success if we successfully update the AccessFailed count - if (result.Succeeded) RaiseLoginFailedEvent(user.Id); - - return result; - } - - internal void RaiseAccountLockedEvent(int userId) - { - OnAccountLocked(new IdentityAuditEventArgs(AuditEvent.AccountLocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseAccountUnlockedEvent(int userId) - { - OnAccountUnlocked(new IdentityAuditEventArgs(AuditEvent.AccountUnlocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseForgotPasswordRequestedEvent(int userId) - { - OnForgotPasswordRequested(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordRequested, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseForgotPasswordChangedSuccessEvent(int userId) - { - OnForgotPasswordChangedSuccess(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordChangedSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseLoginFailedEvent(int userId) - { - OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseInvalidLoginAttemptEvent(string username) - { - OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), username, string.Format("Attempted login for username '{0}' failed", username))); - } - - internal void RaiseLoginRequiresVerificationEvent(int userId) - { - OnLoginRequiresVerification(new IdentityAuditEventArgs(AuditEvent.LoginRequiresVerification, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseLoginSuccessEvent(int userId) - { - OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseLogoutSuccessEvent(int userId) - { - OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaisePasswordChangedEvent(int userId) - { - OnPasswordChanged(new IdentityAuditEventArgs(AuditEvent.PasswordChanged, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseResetAccessFailedCountEvent(int userId) - { - OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - public static event EventHandler AccountLocked; - public static event EventHandler AccountUnlocked; - public static event EventHandler ForgotPasswordRequested; - public static event EventHandler ForgotPasswordChangedSuccess; - public static event EventHandler LoginFailed; - public static event EventHandler LoginRequiresVerification; - public static event EventHandler LoginSuccess; - public static event EventHandler LogoutSuccess; - public static event EventHandler PasswordChanged; - public static event EventHandler PasswordReset; - public static event EventHandler ResetAccessFailedCount; - - protected virtual void OnAccountLocked(IdentityAuditEventArgs e) - { - if (AccountLocked != null) AccountLocked(this, e); - } - - protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e) - { - if (AccountUnlocked != null) AccountUnlocked(this, e); - } - - protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e) - { - if (ForgotPasswordRequested != null) ForgotPasswordRequested(this, e); - } - - protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e) - { - if (ForgotPasswordChangedSuccess != null) ForgotPasswordChangedSuccess(this, e); - } - - protected virtual void OnLoginFailed(IdentityAuditEventArgs e) - { - if (LoginFailed != null) LoginFailed(this, e); - } - - protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e) - { - if (LoginRequiresVerification != null) LoginRequiresVerification(this, e); - } - - protected virtual void OnLoginSuccess(IdentityAuditEventArgs e) - { - if (LoginSuccess != null) LoginSuccess(this, e); - } - - protected virtual void OnLogoutSuccess(IdentityAuditEventArgs e) - { - if (LogoutSuccess != null) LogoutSuccess(this, e); - } - - protected virtual void OnPasswordChanged(IdentityAuditEventArgs e) - { - if (PasswordChanged != null) PasswordChanged(this, e); - } - - protected virtual void OnPasswordReset(IdentityAuditEventArgs e) - { - if (PasswordReset != null) PasswordReset(this, e); - } - - protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e) - { - if (ResetAccessFailedCount != null) ResetAccessFailedCount(this, e); - } - } -} diff --git a/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs b/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs index d9d3f0e6f6..dd657b48bf 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs @@ -15,9 +15,9 @@ namespace Umbraco.Web.Security where TManager : BackOfficeUserManager where TUser : BackOfficeIdentityUser { - public BackOfficeUserManager GetManager(IOwinContext owin) + public BackOfficeOwinUserManager GetManager(IOwinContext owin) { - var mgr = owin.Get() as BackOfficeUserManager; + var mgr = owin.Get() as BackOfficeOwinUserManager; if (mgr == null) throw new InvalidOperationException("Could not cast the registered back office user of type " + typeof(TManager) + " to " + typeof(BackOfficeUserManager)); return mgr; } diff --git a/src/Umbraco.Web/Security/BackOfficeUserStore.cs b/src/Umbraco.Web/Security/BackOfficeUserStore.cs deleted file mode 100644 index d9d1900951..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeUserStore.cs +++ /dev/null @@ -1,917 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Umbraco.Core; -using Umbraco.Core.BackOffice; -using Umbraco.Core.Configuration; -using Umbraco.Core.Mapping; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Identity; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Services; - -namespace Umbraco.Web.Security -{ - public class BackOfficeUserStore : DisposableObjectSlim, - IUserPasswordStore, - IUserEmailStore, - IUserLoginStore, - IUserRoleStore, - IUserSecurityStampStore, - IUserLockoutStore, - IUserTwoFactorStore, - IUserSessionStore - - // TODO: This would require additional columns/tables for now people will need to implement this on their own - //IUserPhoneNumberStore, - // TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation - //IQueryableUserStore - { - private readonly IUserService _userService; - private readonly IEntityService _entityService; - private readonly IExternalLoginService _externalLoginService; - private readonly IGlobalSettings _globalSettings; - private readonly UmbracoMapper _mapper; - private bool _disposed = false; - - public BackOfficeUserStore(IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IGlobalSettings globalSettings, UmbracoMapper mapper) - { - _userService = userService; - _entityService = entityService; - _externalLoginService = externalLoginService; - _globalSettings = globalSettings; - if (userService == null) throw new ArgumentNullException("userService"); - if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); - _mapper = mapper; - - _userService = userService; - _externalLoginService = externalLoginService; - } - - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// - protected override void DisposeResources() - { - _disposed = true; - } - - public Task GetUserIdAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.Id.ToString()); - } - - public Task GetUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.UserName); - } - - public Task SetUserNameAsync(BackOfficeIdentityUser user, string userName, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.UserName = userName; - return Task.CompletedTask; - } - - public Task GetNormalizedUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - return GetUserNameAsync(user, cancellationToken); - } - - public Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string normalizedName, CancellationToken cancellationToken) - { - return SetUserNameAsync(user, normalizedName, cancellationToken); - } - - /// - /// Insert a new user - /// - /// - /// - /// - public Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - //the password must be 'something' it could be empty if authenticating - // with an external provider so we'll just generate one and prefix it, the - // prefix will help us determine if the password hasn't actually been specified yet. - //this will hash the guid with a salt so should be nicely random - var aspHasher = new PasswordHasher(); - var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix + - aspHasher.HashPassword(user, Guid.NewGuid().ToString("N")); - - var userEntity = new User(_globalSettings, user.Name, user.Email, user.UserName, emptyPasswordValue) - { - Language = user.Culture ?? _globalSettings.DefaultUILanguage, - StartContentIds = user.StartContentIds ?? new int[] { }, - StartMediaIds = user.StartMediaIds ?? new int[] { }, - IsLockedOut = user.IsLockedOut, - }; - - UpdateMemberProperties(userEntity, user); - - // TODO: We should deal with Roles --> User Groups here which we currently are not doing - - _userService.Save(userEntity); - - if (!userEntity.HasIdentity) throw new DataException("Could not create the user, check logs for details"); - - //re-assign id - user.Id = userEntity.Id; - - return Task.FromResult(IdentityResult.Success); - } - - /// - /// Update a user - /// - /// - /// - /// - public async Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - var found = _userService.GetUserById(user.Id); - if (found != null) - { - // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - var isLoginsPropertyDirty = user.IsPropertyDirty("Logins"); - - if (UpdateMemberProperties(found, user)) - { - _userService.Save(found); - } - - if (isLoginsPropertyDirty) - { - var logins = await GetLoginsAsync(user); - _externalLoginService.SaveUserLogins(found.Id, logins.Select(UserLoginInfoWrapper.Wrap)); - } - } - - return IdentityResult.Success; - } - - /// - /// Delete a user - /// - /// - /// - public Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - var found = _userService.GetUserById(user.Id); - if (found != null) - { - _userService.Delete(found); - } - _externalLoginService.DeleteUserLogins(user.Id); - - return Task.FromResult(IdentityResult.Success); - } - - /// - /// Finds a user - /// - /// - /// - /// - public async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - var user = _userService.GetUserById(UserIdToInt(userId)); - if (user == null) return null; - - return await Task.FromResult(AssignLoginsCallback(_mapper.Map(user))); - } - - /// - /// Find a user by name - /// - /// - /// - /// - public async Task FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - var user = _userService.GetByUsername(userName); - if (user == null) - { - return null; - } - - var result = AssignLoginsCallback(_mapper.Map(user)); - - return await Task.FromResult(result); - } - - /// - /// Set the user password hash - /// - /// - /// - /// - public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash)); - if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash)); - - user.PasswordHash = passwordHash; - - return Task.CompletedTask; - } - - /// - /// Get the user password hash - /// - /// - /// - /// - public Task GetPasswordHashAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.PasswordHash); - } - - /// - /// Returns true if a user has a password set - /// - /// - /// - /// - public Task HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false); - } - - /// - /// Set the user email - /// - /// - /// - /// - public Task SetEmailAsync(BackOfficeIdentityUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(email)); - - user.Email = email; - - return Task.CompletedTask; - } - - /// - /// Get the user email - /// - /// - /// - /// - public Task GetEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.Email); - } - - /// - /// Returns true if the user email is confirmed - /// - /// - /// - /// - public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.EmailConfirmed); - } - - /// - /// Sets whether the user email is confirmed - /// - /// - /// - /// - public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - user.EmailConfirmed = confirmed; - return Task.CompletedTask; - } - - /// - /// Returns the user associated with this email - /// - /// - /// - /// - public Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - var user = _userService.GetByEmail(email); - var result = user == null - ? null - : _mapper.Map(user); - - return Task.FromResult(AssignLoginsCallback(result)); - } - - public Task GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - return GetEmailAsync(user, cancellationToken); - } - - public Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) - { - return 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(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (login == null) throw new ArgumentNullException(nameof(login)); - - var logins = user.Logins; - var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id); - var userLogin = instance; - logins.Add(userLogin); - - return Task.CompletedTask; - } - - /// - /// 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(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - var userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); - if (userLogin != null) user.Logins.Remove(userLogin); - - return Task.CompletedTask; - } - - /// - /// Returns the linked accounts for this user - /// - /// - /// - /// - public Task> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult((IList) - user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList()); - } - - /// - /// Returns the user associated with this login - /// - /// - public Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - //get all logins associated with the login id - var result = _externalLoginService.Find(UserLoginInfoWrapper.Wrap(new UserLoginInfo(loginProvider, providerKey, loginProvider))).ToArray(); - if (result.Any()) - { - //return the first user that matches the result - BackOfficeIdentityUser output = null; - foreach (var l in result) - { - var user = _userService.GetUserById(l.UserId); - if (user != null) - { - output = _mapper.Map(user); - break; - } - } - - return Task.FromResult(AssignLoginsCallback(output)); - } - - return Task.FromResult(null); - } - - - /// - /// Adds a user to a role (user group) - /// - /// - /// - /// - /// - public Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName)); - if (string.IsNullOrWhiteSpace(normalizedRoleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName)); - - var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); - - if (userRole == null) - { - user.AddRole(normalizedRoleName); - } - - return Task.CompletedTask; - } - - /// - /// Removes the role (user group) for the user - /// - /// - /// - /// - /// - public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (user == null) throw new ArgumentNullException(nameof(user)); - if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName)); - if (string.IsNullOrWhiteSpace(normalizedRoleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName)); - - var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); - - if (userRole != null) - { - user.Roles.Remove(userRole); - } - - return Task.CompletedTask; - } - - /// - /// Returns the roles (user groups) for this user - /// - /// - /// - /// - public Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); - } - - /// - /// Returns true if a user is in the role - /// - /// - /// - /// - /// - public Task IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName)); - } - - /// - /// Lists all users of a given role. - /// - /// - /// Identity Role names are equal to Umbraco UserGroup alias. - /// - public Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName)); - - var userGroup = _userService.GetUserGroupByAlias(normalizedRoleName); - - var users = _userService.GetAllInGroup(userGroup.Id); - IList backOfficeIdentityUsers = users.Select(x => _mapper.Map(x)).ToList(); - - return Task.FromResult(backOfficeIdentityUsers); - } - - /// - /// Set the security stamp for the user - /// - /// - /// - /// - /// - public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.SecurityStamp = stamp; - return Task.CompletedTask; - } - - /// - /// Get the user security stamp - /// - /// - /// - /// - public Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - //the stamp cannot be null, so if it is currently null then we'll just return a hash of the password - return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace() - ? user.PasswordHash.GenerateHash() - : user.SecurityStamp); - } - - private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user) - { - if (user != null) - { - user.SetLoginsCallback(new Lazy>(() => - _externalLoginService.GetAll(user.Id))); - } - return user; - } - - /// - /// Sets whether two factor authentication is enabled for the user - /// - /// - /// - /// - /// - public virtual Task SetTwoFactorEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - user.TwoFactorEnabled = false; - return Task.CompletedTask; - } - - /// - /// Returns whether two factor authentication is enabled for the user - /// - /// - /// - public virtual Task GetTwoFactorEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - return Task.FromResult(false); - } - - #region IUserLockoutStore - - /// - /// 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)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return user.LockoutEndDateUtc.HasValue - ? Task.FromResult(DateTimeOffset.MaxValue) - : Task.FromResult(DateTimeOffset.MinValue); - } - - /// - /// 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 - /// - public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.LockoutEndDateUtc = lockoutEnd.Value.UtcDateTime; - return Task.CompletedTask; - } - - /// - /// Used to record when an attempt to access the user has failed - /// - /// - /// - /// - public Task IncrementAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.AccessFailedCount++; - return Task.FromResult(user.AccessFailedCount); - } - - /// - /// 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(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.AccessFailedCount = 0; - return Task.CompletedTask; - } - - /// - /// 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(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult(user.AccessFailedCount); - } - - /// - /// Returns true - /// - /// - /// - /// - public Task GetLockoutEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult(user.LockoutEnabled); - } - - /// - /// 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(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.LockoutEnabled = enabled; - return Task.CompletedTask; - } - #endregion - - private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityUser) - { - var anythingChanged = false; - - //don't assign anything if nothing has changed as this will trigger the track changes of the model - - if (identityUser.IsPropertyDirty("LastLoginDateUtc") - || (user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) - || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) - { - anythingChanged = true; - //if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime - var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); - user.LastLoginDate = dt; - } - if (identityUser.IsPropertyDirty("LastPasswordChangeDateUtc") - || (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) - || identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value) - { - anythingChanged = true; - user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime(); - } - if (identityUser.IsPropertyDirty("EmailConfirmed") - || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) - || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed)) - { - anythingChanged = true; - user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null; - } - if (identityUser.IsPropertyDirty("Name") - && user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.Name = identityUser.Name; - } - if (identityUser.IsPropertyDirty("Email") - && user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.Email = identityUser.Email; - } - if (identityUser.IsPropertyDirty("AccessFailedCount") - && user.FailedPasswordAttempts != identityUser.AccessFailedCount) - { - anythingChanged = true; - user.FailedPasswordAttempts = identityUser.AccessFailedCount; - } - if (user.IsLockedOut != identityUser.IsLockedOut) - { - anythingChanged = true; - user.IsLockedOut = identityUser.IsLockedOut; - - if (user.IsLockedOut) - { - //need to set the last lockout date - user.LastLockoutDate = DateTime.Now; - } - - } - if (identityUser.IsPropertyDirty("UserName") - && user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.Username = identityUser.UserName; - } - if (identityUser.IsPropertyDirty("PasswordHash") - && user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.RawPasswordValue = identityUser.PasswordHash; - } - - if (identityUser.IsPropertyDirty("Culture") - && user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.Language = identityUser.Culture; - } - if (identityUser.IsPropertyDirty("StartMediaIds") - && user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false) - { - anythingChanged = true; - user.StartMediaIds = identityUser.StartMediaIds; - } - if (identityUser.IsPropertyDirty("StartContentIds") - && user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false) - { - anythingChanged = true; - user.StartContentIds = identityUser.StartContentIds; - } - if (user.SecurityStamp != identityUser.SecurityStamp) - { - anythingChanged = true; - user.SecurityStamp = identityUser.SecurityStamp; - } - - // TODO: Fix this for Groups too - if (identityUser.IsPropertyDirty("Roles") || identityUser.IsPropertyDirty("Groups")) - { - var userGroupAliases = user.Groups.Select(x => x.Alias).ToArray(); - - var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray(); - var identityUserGroups = identityUser.Groups.Select(x => x.Alias).ToArray(); - - var combinedAliases = identityUserRoles.Union(identityUserGroups).ToArray(); - - if (userGroupAliases.ContainsAll(combinedAliases) == false - || combinedAliases.ContainsAll(userGroupAliases) == false) - { - anythingChanged = true; - - //clear out the current groups (need to ToArray since we are modifying the iterator) - user.ClearGroups(); - - //go lookup all these groups - var groups = _userService.GetUserGroupsByAlias(combinedAliases).Select(x => x.ToReadOnlyGroup()).ToArray(); - - //use all of the ones assigned and add them - foreach (var group in groups) - { - user.AddGroup(group); - } - - //re-assign - identityUser.Groups = groups; - } - } - - //we should re-set the calculated start nodes - identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService); - identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService); - - //reset all changes - identityUser.ResetDirtyProperties(false); - - return anythingChanged; - } - - private void ThrowIfDisposed() - { - if (_disposed) throw new ObjectDisposedException(GetType().Name); - } - - public Task ValidateSessionIdAsync(string userId, string sessionId) - { - Guid guidSessionId; - if (Guid.TryParse(sessionId, out guidSessionId)) - { - return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId)); - } - - return Task.FromResult(false); - } - - private static int UserIdToInt(string userId) - { - var attempt = userId.TryConvertTo(); - if (attempt.Success) return attempt.Result; - - throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception); - } - } -} diff --git a/src/Umbraco.Web/Security/BackOfficeUserValidator.cs b/src/Umbraco.Web/Security/BackOfficeUserValidator.cs deleted file mode 100644 index 951bdee4ed..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeUserValidator.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Umbraco.Core.BackOffice; - -namespace Umbraco.Web.Security -{ - public class BackOfficeUserValidator : UserValidator - where T : BackOfficeIdentityUser - { - public override async Task ValidateAsync(UserManager manager, T user) - { - // Don't validate if the user's email or username hasn't changed otherwise it's just wasting SQL queries. - if (user.IsPropertyDirty("Email") || user.IsPropertyDirty("UserName")) - { - return await base.ValidateAsync(manager, user); - } - return IdentityResult.Success; - } - } -} diff --git a/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs b/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs index 2b1f9927f2..16c0666c9c 100644 --- a/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs +++ b/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs @@ -1,5 +1,4 @@ using Microsoft.Owin; -using Umbraco.Core.BackOffice; namespace Umbraco.Web.Security { @@ -10,6 +9,6 @@ namespace Umbraco.Web.Security /// internal interface IBackOfficeUserManagerMarker { - BackOfficeUserManager GetManager(IOwinContext owin); + BackOfficeOwinUserManager GetManager(IOwinContext owin); } } diff --git a/src/Umbraco.Web/Security/SessionIdValidator.cs b/src/Umbraco.Web/Security/SessionIdValidator.cs index 9edae8da10..090b6c6dac 100644 --- a/src/Umbraco.Web/Security/SessionIdValidator.cs +++ b/src/Umbraco.Web/Security/SessionIdValidator.cs @@ -86,7 +86,7 @@ namespace Umbraco.Web.Security if (validate == false) return true; - var manager = owinCtx.Get(); + var manager = owinCtx.Get(); if (manager == null) return false; diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 3c17bd370a..6d3de9be1d 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -70,8 +70,8 @@ namespace Umbraco.Web.Security } } - private BackOfficeUserManager _userManager; - protected BackOfficeUserManager UserManager + private BackOfficeOwinUserManager _userManager; + protected BackOfficeOwinUserManager UserManager => _userManager ?? (_userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager()); [Obsolete("This needs to be removed, ASP.NET Identity should always be used for this operation, this is currently only used in the installer which needs to be updated")] diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 9817431f76..3bc0b68906 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -195,12 +195,10 @@ - + - - diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index b171356e23..f0c79b24f2 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -3,7 +3,6 @@ using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Web.WebApi.Filters; -using Umbraco.Core.BackOffice; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Web.Security; @@ -30,7 +29,7 @@ namespace Umbraco.Web.WebApi [EnableDetailedErrors] public abstract class UmbracoAuthorizedApiController : UmbracoApiController { - private Security.BackOfficeUserManager _userManager; + private BackOfficeOwinUserManager _userManager; protected UmbracoAuthorizedApiController() { @@ -44,7 +43,7 @@ namespace Umbraco.Web.WebApi /// /// Gets the user manager. /// - protected Security.BackOfficeUserManager UserManager + protected BackOfficeOwinUserManager UserManager => _userManager ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); } }