diff --git a/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs b/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs index 84fb0e6bb8..7efa803c52 100644 --- a/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs +++ b/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs @@ -24,17 +24,17 @@ namespace Umbraco.Web.Compose public void Initialize() { - //BackOfficeUserManager.AccountLocked += ; - //BackOfficeUserManager.AccountUnlocked += ; - BackOfficeUserManager.ForgotPasswordRequested += OnForgotPasswordRequest; - BackOfficeUserManager.ForgotPasswordChangedSuccess += OnForgotPasswordChange; - BackOfficeUserManager.LoginFailed += OnLoginFailed; - //BackOfficeUserManager.LoginRequiresVerification += ; - BackOfficeUserManager.LoginSuccess += OnLoginSuccess; - BackOfficeUserManager.LogoutSuccess += OnLogoutSuccess; - BackOfficeUserManager.PasswordChanged += OnPasswordChanged; - BackOfficeUserManager.PasswordReset += OnPasswordReset; - //BackOfficeUserManager.ResetAccessFailedCount += ; + //BackOfficeUserManager2.AccountLocked += ; + //BackOfficeUserManager2.AccountUnlocked += ; + BackOfficeUserManager2.ForgotPasswordRequested += OnForgotPasswordRequest; + BackOfficeUserManager2.ForgotPasswordChangedSuccess += OnForgotPasswordChange; + BackOfficeUserManager2.LoginFailed += OnLoginFailed; + //BackOfficeUserManager2.LoginRequiresVerification += ; + BackOfficeUserManager2.LoginSuccess += OnLoginSuccess; + BackOfficeUserManager2.LogoutSuccess += OnLogoutSuccess; + BackOfficeUserManager2.PasswordChanged += OnPasswordChanged; + BackOfficeUserManager2.PasswordReset += OnPasswordReset; + //BackOfficeUserManager2.ResetAccessFailedCount += ; } public void Terminate() diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index dbfb6f81a4..99be05c861 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -8,8 +8,7 @@ using System.Threading.Tasks; using System.Web; using System.Web.Http; using System.Web.Mvc; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; +using Microsoft.AspNetCore.Identity; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models; @@ -41,8 +40,8 @@ namespace Umbraco.Web.Editors [IsBackOffice] public class AuthenticationController : UmbracoApiController { - private BackOfficeUserManager _userManager; - private BackOfficeSignInManager _signInManager; + private BackOfficeUserManager2 _userManager; + private BackOfficeSignInManager2 _signInManager; private readonly IUserPasswordConfiguration _passwordConfiguration; private readonly IRuntimeState _runtimeState; private readonly IUmbracoSettingsSection _umbracoSettingsSection; @@ -70,11 +69,11 @@ namespace Umbraco.Web.Editors _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); } - protected BackOfficeUserManager UserManager => _userManager - ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); + protected BackOfficeUserManager2 UserManager => _userManager + ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager2()); - protected BackOfficeSignInManager SignInManager => _signInManager - ?? (_signInManager = TryGetOwinContext().Result.GetBackOfficeSignInManager()); + protected BackOfficeSignInManager2 SignInManager => _signInManager + ?? (_signInManager = TryGetOwinContext().Result.GetBackOfficeSignInManager2()); /// /// Returns the configuration for the backoffice user membership provider - used to configure the change password dialog @@ -105,11 +104,11 @@ namespace Umbraco.Web.Editors if (decoded.IsNullOrWhiteSpace()) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var identityUser = await UserManager.FindByIdAsync(id); + var identityUser = await UserManager.FindByIdAsync(id.ToString()); if (identityUser == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var result = await UserManager.ConfirmEmailAsync(id, decoded); + var result = await UserManager.ConfirmEmailAsync(identityUser, decoded); if (result.Succeeded == false) { @@ -131,13 +130,15 @@ namespace Umbraco.Web.Editors [ValidateAngularAntiForgeryToken] public async Task PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel) { + var user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); + var result = await UserManager.RemoveLoginAsync( - User.Identity.GetUserId(), - new UserLoginInfo(unlinkLoginModel.LoginProvider, unlinkLoginModel.ProviderKey)); + user, + unlinkLoginModel.LoginProvider, + unlinkLoginModel.ProviderKey); if (result.Succeeded) { - var user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); await SignInManager.SignInAsync(user, isPersistent: true, rememberBrowser: false); return Request.CreateResponse(HttpStatusCode.OK); } @@ -225,7 +226,7 @@ namespace Umbraco.Web.Editors [ValidateAngularAntiForgeryToken] public async Task> GetCurrentUserLinkedLogins() { - var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId().ResultOr(0)); + var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId().ResultOr(0).ToString()); return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); } @@ -244,61 +245,58 @@ namespace Umbraco.Web.Editors var result = await SignInManager.PasswordSignInAsync( loginModel.Username, loginModel.Password, isPersistent: true, shouldLockout: true); - switch (result) + if (result.Succeeded) { - case SignInStatus.Success: + // get the user + var user = Services.UserService.GetByUsername(loginModel.Username); + UserManager.RaiseLoginSuccessEvent(user.Id); - // get the user - var user = Services.UserService.GetByUsername(loginModel.Username); - UserManager.RaiseLoginSuccessEvent(user.Id); - - return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User); - case SignInStatus.RequiresVerification: - - var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions; - if (twofactorOptions == null) - { - throw new HttpResponseException( - Request.CreateErrorResponse( - HttpStatusCode.BadRequest, - "UserManager does not implement " + typeof(IUmbracoBackOfficeTwoFactorOptions))); - } - - var twofactorView = twofactorOptions.GetTwoFactorView( - owinContext, - UmbracoContext, - loginModel.Username); - - if (twofactorView.IsNullOrWhiteSpace()) - { - throw new HttpResponseException( - Request.CreateErrorResponse( - HttpStatusCode.BadRequest, - typeof(IUmbracoBackOfficeTwoFactorOptions) + ".GetTwoFactorView returned an empty string")); - } - - var attemptedUser = Services.UserService.GetByUsername(loginModel.Username); - - // create a with information to display a custom two factor send code view - var verifyResponse = Request.CreateResponse(HttpStatusCode.PaymentRequired, new - { - twoFactorView = twofactorView, - userId = attemptedUser.Id - }); - - UserManager.RaiseLoginRequiresVerificationEvent(attemptedUser.Id); - - return verifyResponse; - - case SignInStatus.LockedOut: - case SignInStatus.Failure: - default: - // return BadRequest (400), we don't want to return a 401 because that get's intercepted - // by our angular helper because it thinks that we need to re-perform the request once we are - // authorized and we don't want to return a 403 because angular will show a warning message indicating - // that the user doesn't have access to perform this function, we just want to return a normal invalid message. - throw new HttpResponseException(HttpStatusCode.BadRequest); + return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User); } + + if (result.RequiresTwoFactor) + { + var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions; + if (twofactorOptions == null) + { + throw new HttpResponseException( + Request.CreateErrorResponse( + HttpStatusCode.BadRequest, + "UserManager does not implement " + typeof(IUmbracoBackOfficeTwoFactorOptions))); + } + + var twofactorView = twofactorOptions.GetTwoFactorView( + owinContext, + UmbracoContext, + loginModel.Username); + + if (twofactorView.IsNullOrWhiteSpace()) + { + throw new HttpResponseException( + Request.CreateErrorResponse( + HttpStatusCode.BadRequest, + typeof(IUmbracoBackOfficeTwoFactorOptions) + ".GetTwoFactorView returned an empty string")); + } + + var attemptedUser = Services.UserService.GetByUsername(loginModel.Username); + + // create a with information to display a custom two factor send code view + var verifyResponse = Request.CreateResponse(HttpStatusCode.PaymentRequired, new + { + twoFactorView = twofactorView, + userId = attemptedUser.Id + }); + + UserManager.RaiseLoginRequiresVerificationEvent(attemptedUser.Id); + + return verifyResponse; + } + + // return BadRequest (400), we don't want to return a 401 because that get's intercepted + // by our angular helper because it thinks that we need to re-perform the request once we are + // authorized and we don't want to return a 403 because angular will show a warning message indicating + // that the user doesn't have access to perform this function, we just want to return a normal invalid message. + throw new HttpResponseException(HttpStatusCode.BadRequest); } /// @@ -315,13 +313,13 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException(HttpStatusCode.BadRequest); } - var identityUser = await SignInManager.UserManager.FindByEmailAsync(model.Email); + var identityUser = await UserManager.FindByEmailAsync(model.Email); if (identityUser != null) { var user = Services.UserService.GetByEmail(model.Email); if (user != null) { - var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id); + var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser); var callbackUrl = ConstructCallbackUrl(identityUser.Id, code); var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", @@ -329,11 +327,12 @@ namespace Umbraco.Web.Editors UmbracoUserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings), new[] { identityUser.UserName, callbackUrl }); - await UserManager.SendEmailAsync(identityUser.Id, + // TODO: SB: SendEmailAsync + /*await UserManager.SendEmailAsync(identityUser.Id, Services.TextService.Localize("login/resetPasswordEmailCopySubject", // Ensure the culture of the found user is used for the email! UmbracoUserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings)), - message); + message);*/ UserManager.RaiseForgotPasswordRequestedEvent(user.Id); } @@ -355,7 +354,10 @@ namespace Umbraco.Web.Editors Logger.Warn("Get2FAProviders :: No verified user found, returning 404"); throw new HttpResponseException(HttpStatusCode.NotFound); } - var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(userId); + + var user = await UserManager.FindByIdAsync(userId.ToString()); + var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(user); + return userFactors; } @@ -399,18 +401,19 @@ namespace Umbraco.Web.Editors var owinContext = TryGetOwinContext().Result; var user = Services.UserService.GetByUsername(userName); - switch (result) + if (result.Succeeded) { - case SignInStatus.Success: - UserManager.RaiseLoginSuccessEvent(user.Id); - return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User); - case SignInStatus.LockedOut: - UserManager.RaiseAccountLockedEvent(user.Id); - return Request.CreateValidationErrorResponse("User is locked out"); - case SignInStatus.Failure: - default: - return Request.CreateValidationErrorResponse("Invalid code"); + UserManager.RaiseLoginSuccessEvent(user.Id); + return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User); } + + if (result.IsLockedOut) + { + UserManager.RaiseAccountLockedEvent(user.Id); + return Request.CreateValidationErrorResponse("User is locked out"); + } + + return Request.CreateValidationErrorResponse("Invalid code"); } /// @@ -420,22 +423,24 @@ namespace Umbraco.Web.Editors [SetAngularAntiForgeryTokens] public async Task PostSetPassword(SetPasswordModel model) { - var result = await UserManager.ResetPasswordAsync(model.UserId, model.ResetCode, model.Password); + var identityUser = await UserManager.FindByIdAsync(model.UserId.ToString()); + + var result = await UserManager.ResetPasswordAsync(identityUser, model.ResetCode, model.Password); if (result.Succeeded) { - var lockedOut = await UserManager.IsLockedOutAsync(model.UserId); + var lockedOut = await UserManager.IsLockedOutAsync(identityUser); if (lockedOut) { Logger.Info("User {UserId} is currently locked out, unlocking and resetting AccessFailedCount", model.UserId); //// var user = await UserManager.FindByIdAsync(model.UserId); - var unlockResult = await UserManager.SetLockoutEndDateAsync(model.UserId, DateTimeOffset.Now); + var unlockResult = await UserManager.SetLockoutEndDateAsync(identityUser, DateTimeOffset.Now); if (unlockResult.Succeeded == false) { Logger.Warn("Could not unlock for user {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First()); } - var resetAccessFailedCountResult = await UserManager.ResetAccessFailedCountAsync(model.UserId); + var resetAccessFailedCountResult = await UserManager.ResetAccessFailedCountAsync(identityUser); if (resetAccessFailedCountResult.Succeeded == false) { Logger.Warn("Could not reset access failed count {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First()); @@ -446,13 +451,13 @@ namespace Umbraco.Web.Editors // if user was only invited, then they have not been approved // but a successful forgot password flow (e.g. if their token had expired and they did a forgot password instead of request new invite) // means we have verified their email - if (!UserManager.IsEmailConfirmed(model.UserId)) + + // TODO: SB: ConfirmEmailAsync + if (!await UserManager.IsEmailConfirmedAsync(identityUser)) { - await UserManager.ConfirmEmailAsync(model.UserId, model.ResetCode); + await UserManager.ConfirmEmailAsync(identityUser, model.ResetCode); } - // if the user is invited, enable their account on forgot password - var identityUser = await UserManager.FindByIdAsync(model.UserId); // invited is not approved, never logged in, invited date present /* if (LastLoginDate == default && IsApproved == false && InvitedDate != null) @@ -474,7 +479,7 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } return Request.CreateValidationErrorResponse( - result.Errors.Any() ? result.Errors.First() : "Set password failed"); + result.Errors.Any() ? result.Errors.First().Description : "Set password failed"); } @@ -561,9 +566,8 @@ namespace Umbraco.Web.Editors { foreach (var error in result.Errors) { - ModelState.AddModelError(prefix, error); + ModelState.AddModelError(prefix, error.Description); } } - } } diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 64cd0d1a0d..85cb9e91c8 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -7,8 +7,6 @@ using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using System.Web.UI; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security; using Newtonsoft.Json; using Umbraco.Core; @@ -30,6 +28,7 @@ using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Hosting; using Umbraco.Core.IO; using Umbraco.Web.Trees; +using UserLoginInfo = Microsoft.AspNetCore.Identity.UserLoginInfo; namespace Umbraco.Web.Editors { @@ -44,8 +43,8 @@ namespace Umbraco.Web.Editors private readonly IManifestParser _manifestParser; private readonly UmbracoFeatures _features; private readonly IRuntimeState _runtimeState; - private BackOfficeUserManager _userManager; - private BackOfficeSignInManager _signInManager; + private BackOfficeUserManager2 _userManager; + private BackOfficeSignInManager2 _signInManager; private readonly IUmbracoVersion _umbracoVersion; private readonly IGridConfig _gridConfig; private readonly IUmbracoSettingsSection _umbracoSettingsSection; @@ -85,9 +84,9 @@ namespace Umbraco.Web.Editors _httpContextAccessor = httpContextAccessor; } - protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = OwinContext.GetBackOfficeSignInManager()); + protected BackOfficeSignInManager2 SignInManager => _signInManager ?? (_signInManager = OwinContext.GetBackOfficeSignInManager2()); - protected BackOfficeUserManager UserManager => _userManager ?? (_userManager = OwinContext.GetBackOfficeUserManager()); + protected BackOfficeUserManager2 UserManager => _userManager ?? (_userManager = OwinContext.GetBackOfficeUserManager2()); protected IAuthenticationManager AuthenticationManager => OwinContext.Authentication; @@ -139,21 +138,15 @@ namespace Umbraco.Web.Editors } var id = parts[0]; - int intId; - if (int.TryParse(id, out intId) == false) - { - Logger.Warn("VerifyUser endpoint reached with invalid token: {Invite}", invite); - return RedirectToAction("Default"); - } - var identityUser = await UserManager.FindByIdAsync(intId); + var identityUser = await UserManager.FindByIdAsync(id); if (identityUser == null) { Logger.Warn("VerifyUser endpoint reached with non existing user: {UserId}", id); return RedirectToAction("Default"); } - var result = await UserManager.ConfirmEmailAsync(intId, decoded); + var result = await UserManager.ConfirmEmailAsync(identityUser, decoded); if (result.Succeeded == false) { @@ -329,10 +322,10 @@ namespace Umbraco.Web.Editors [HttpGet] public async Task ValidatePasswordResetCode([Bind(Prefix = "u")]int userId, [Bind(Prefix = "r")]string resetCode) { - var user = UserManager.FindById(userId); + var user = await UserManager.FindByIdAsync(userId.ToString()); if (user != null) { - var result = await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", resetCode, UserManager, user); + var result = await UserManager.VerifyUserTokenAsync(user, "ResetPassword", "ResetPassword", resetCode); // TODO: SB: password reset token provider if (result) { //Add a flag and redirect for it to be displayed @@ -360,7 +353,10 @@ namespace Umbraco.Web.Editors return RedirectToLocal(Url.Action("Default", "BackOffice")); } - var result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), loginInfo.Login); + var user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); + + var result = await UserManager.AddLoginAsync(user, + new UserLoginInfo(loginInfo.Login.LoginProvider, loginInfo.Login.ProviderKey, loginInfo.Login.LoginProvider)); if (result.Succeeded) { return RedirectToLocal(Url.Action("Default", "BackOffice")); @@ -403,7 +399,7 @@ namespace Umbraco.Web.Editors return await ExternalSignInAsync(loginInfo, externalSignInResponse); } - private async Task ExternalSignInAsync(ExternalLoginInfo loginInfo, Func response) + private async Task ExternalSignInAsync(ExternalLoginInfo2 loginInfo, Func response) { if (loginInfo == null) throw new ArgumentNullException("loginInfo"); if (response == null) throw new ArgumentNullException("response"); @@ -424,7 +420,7 @@ namespace Umbraco.Web.Editors } // Sign in the user with this external login provider if the user already has a login - var user = await UserManager.FindAsync(loginInfo.Login); + var user = await UserManager.FindByLoginAsync(loginInfo.Login.LoginProvider, loginInfo.Login.ProviderKey); if (user != null) { // TODO: It might be worth keeping some of the claims associated with the ExternalLoginInfo, in which case we @@ -466,7 +462,7 @@ namespace Umbraco.Web.Editors return response(); } - private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, ExternalSignInAutoLinkOptions autoLinkOptions) + private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo2 loginInfo, ExternalSignInAutoLinkOptions autoLinkOptions) { if (autoLinkOptions == null) return false; @@ -515,21 +511,22 @@ namespace Umbraco.Web.Editors if (userCreationResult.Succeeded == false) { - ViewData.SetExternalSignInError(userCreationResult.Errors); + ViewData.SetExternalSignInError(userCreationResult.Errors.Select(x => x.Description).ToList()); } else { - var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); + var linkResult = await UserManager.AddLoginAsync(autoLinkUser, + new UserLoginInfo(loginInfo.Login.LoginProvider, loginInfo.Login.ProviderKey, loginInfo.Login.LoginProvider)); if (linkResult.Succeeded == false) { - ViewData.SetExternalSignInError(linkResult.Errors); + ViewData.SetExternalSignInError(linkResult.Errors.Select(x => x.Description).ToList()); //If this fails, we should really delete the user since it will be in an inconsistent state! var deleteResult = await UserManager.DeleteAsync(autoLinkUser); if (deleteResult.Succeeded == false) { //DOH! ... this isn't good, combine all errors to be shown - ViewData.SetExternalSignInError(linkResult.Errors.Concat(deleteResult.Errors)); + ViewData.SetExternalSignInError(linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList()); } } else diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index 36f39940cb..35ec09eaca 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -158,7 +158,8 @@ namespace Umbraco.Web.Editors [OverrideAuthorization] public async Task PostSetInvitedUserPassword([FromBody]string newPassword) { - var result = await UserManager.AddPasswordAsync(Security.GetUserId().ResultOr(0), newPassword); + var user = await UserManager.FindByIdAsync(Security.GetUserId().ResultOr(0).ToString()); + var result = await UserManager.AddPasswordAsync(user, newPassword); if (result.Succeeded == false) { diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index 179ef8fb45..a6f47efd73 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -34,7 +34,7 @@ namespace Umbraco.Web.Editors IUser currentUser, IUser savingUser, ChangingPasswordModel passwordModel, - BackOfficeUserManager userMgr) + BackOfficeUserManager2 userMgr) { if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel)); if (userMgr == null) throw new ArgumentNullException(nameof(userMgr)); @@ -44,6 +44,13 @@ namespace Umbraco.Web.Editors return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) }); } + var backOfficeIdentityUser = await userMgr.FindByIdAsync(savingUser.Id.ToString()); + if (backOfficeIdentityUser == null) + { + //this really shouldn't ever happen... but just in case + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password could not be verified", new[] { "oldPassword" }) }); + } + //Are we just changing another user's password? if (passwordModel.OldPassword.IsNullOrWhiteSpace()) { @@ -60,7 +67,7 @@ namespace Umbraco.Web.Editors } //ok, we should be able to reset it - var resetToken = await userMgr.GeneratePasswordResetTokenAsync(savingUser.Id); + var resetToken = await userMgr.GeneratePasswordResetTokenAsync(backOfficeIdentityUser); var resetResult = await userMgr.ChangePasswordWithResetAsync(savingUser.Id, resetToken, passwordModel.NewPassword); @@ -74,15 +81,6 @@ namespace Umbraco.Web.Editors return Attempt.Succeed(new PasswordChangedModel()); } - //we're changing our own password... - - //get the user - var backOfficeIdentityUser = await userMgr.FindByIdAsync(savingUser.Id); - if (backOfficeIdentityUser == null) - { - //this really shouldn't ever happen... but just in case - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password could not be verified", new[] { "oldPassword" }) }); - } //is the old password correct? var validateResult = await userMgr.CheckPasswordAsync(backOfficeIdentityUser, passwordModel.OldPassword); if (validateResult == false) @@ -91,7 +89,7 @@ namespace Umbraco.Web.Editors return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Incorrect password", new[] { "oldPassword" }) }); } //can we change to the new password? - var changeResult = await userMgr.ChangePasswordAsync(savingUser.Id, passwordModel.OldPassword, passwordModel.NewPassword); + var changeResult = await userMgr.ChangePasswordAsync(backOfficeIdentityUser, passwordModel.OldPassword, passwordModel.NewPassword); if (changeResult.Succeeded == false) { //no, fail with error messages for "password" diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 3390677a08..d4daf8ae28 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using System.Web; using System.Web.Http; using System.Web.Mvc; -using Microsoft.AspNet.Identity; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; @@ -20,7 +19,6 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence; -using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Web.Editors.Filters; @@ -321,23 +319,18 @@ namespace Umbraco.Web.Editors Request.CreateNotificationValidationErrorResponse(string.Join(", ", created.Errors))); } - //we need to generate a password, however we can only do that if the user manager has a password validator that - //we can read values from - var passwordValidator = UserManager.PasswordValidator as PasswordValidator; - var resetPassword = string.Empty; - if (passwordValidator != null) - { - var password = UserManager.GeneratePassword(); + string resetPassword; + var password = UserManager.GeneratePassword(); - var result = await UserManager.AddPasswordAsync(identityUser.Id, password); - if (result.Succeeded == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse(string.Join(", ", created.Errors))); - } - resetPassword = password; + var result = await UserManager.AddPasswordAsync(identityUser, password); + if (result.Succeeded == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse(string.Join(", ", created.Errors))); } + resetPassword = password; + //now re-look the user back up which will now exist var user = Services.UserService.GetByEmail(userSave.Email); @@ -472,7 +465,8 @@ namespace Umbraco.Web.Editors private async Task SendUserInviteEmailAsync(UserBasic userDisplay, string from, string fromEmail, IUser to, string message) { - var token = await UserManager.GenerateEmailConfirmationTokenAsync((int)userDisplay.Id); + var user = await UserManager.FindByIdAsync(((int) userDisplay.Id).ToString()); + var token = await UserManager.GenerateEmailConfirmationTokenAsync(user); var inviteToken = string.Format("{0}{1}{2}", (int)userDisplay.Id, @@ -501,7 +495,8 @@ namespace Umbraco.Web.Editors UmbracoUserExtensions.GetUserCulture(to.Language, Services.TextService, GlobalSettings), new[] { userDisplay.Name, from, message, inviteUri.ToString(), fromEmail }); - await UserManager.EmailService.SendAsync( + // TODO: SB: EmailService.SendAsync + /*await UserManager.EmailService.SendAsync( //send the special UmbracoEmailMessage which configures it's own sender //to allow for events to handle sending the message if no smtp is configured new UmbracoEmailMessage(new EmailSender(GlobalSettings, true)) @@ -509,7 +504,7 @@ namespace Umbraco.Web.Editors Body = emailBody, Destination = userDisplay.Email, Subject = emailSubject - }); + });*/ } @@ -705,22 +700,26 @@ namespace Umbraco.Web.Editors if (userIds.Length <= 0) return Request.CreateResponse(HttpStatusCode.OK); + var user = await UserManager.FindByIdAsync(userIds[0].ToString()); + if (userIds.Length == 1) { - var unlockResult = await UserManager.SetLockoutEndDateAsync(userIds[0], DateTimeOffset.Now); + + + var unlockResult = await UserManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now); if (unlockResult.Succeeded == false) { return Request.CreateValidationErrorResponse( string.Format("Could not unlock for user {0} - error {1}", userIds[0], unlockResult.Errors.First())); } - var user = await UserManager.FindByIdAsync(userIds[0]); + return Request.CreateNotificationSuccessResponse( Services.TextService.Localize("speechBubbles/unlockUserSuccess", new[] { user.Name })); } foreach (var u in userIds) { - var unlockResult = await UserManager.SetLockoutEndDateAsync(u, DateTimeOffset.Now); + var unlockResult = await UserManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now); if (unlockResult.Succeeded == false) { return Request.CreateValidationErrorResponse( diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index e47a8f4b5f..6cc77a369f 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -56,15 +56,15 @@ namespace Umbraco.Web.Install.InstallSteps throw new InvalidOperationException("Could not find the super user!"); } - var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager(); - var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId); + var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager2(); + var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId.ToString()); if (membershipUser == null) { throw new InvalidOperationException($"No user found in membership provider with id of {Constants.Security.SuperUserId}."); } //To change the password here we actually need to reset it since we don't have an old one to use to change - var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id); + var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser); var resetResult = await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); if (!resetResult.Succeeded) { diff --git a/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs b/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs index 0baac36032..65d260b9c5 100644 --- a/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs +++ b/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs @@ -1,32 +1,7 @@ -using Microsoft.AspNet.Identity; -using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Identity; namespace Umbraco.Web.Models.Identity { - internal class UserLoginInfoWrapper : IUserLoginInfo - { - private readonly UserLoginInfo _info; - - public static IUserLoginInfo Wrap(UserLoginInfo info) => new UserLoginInfoWrapper(info); - - private UserLoginInfoWrapper(UserLoginInfo info) - { - _info = info; - } - - public string LoginProvider - { - get => _info.LoginProvider; - set => _info.LoginProvider = value; - } - - public string ProviderKey - { - get => _info.ProviderKey; - set => _info.ProviderKey = value; - } - } - internal class UserLoginInfoWrapper2 : IUserLoginInfo { private readonly Microsoft.AspNetCore.Identity.UserLoginInfo _info; diff --git a/src/Umbraco.Web/OwinExtensions.cs b/src/Umbraco.Web/OwinExtensions.cs index 25edda2647..66e7e5353e 100644 --- a/src/Umbraco.Web/OwinExtensions.cs +++ b/src/Umbraco.Web/OwinExtensions.cs @@ -57,10 +57,10 @@ namespace Umbraco.Web /// /// /// - public static BackOfficeSignInManager GetBackOfficeSignInManager(this IOwinContext owinContext) + public static BackOfficeSignInManager2 GetBackOfficeSignInManager(this IOwinContext owinContext) { - return owinContext.Get() - ?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeSignInManager)} from the {typeof(IOwinContext)}."); + return owinContext.Get() + ?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeSignInManager2)} from the {typeof(IOwinContext)}."); } /// @@ -83,13 +83,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 BackOfficeUserManager2 GetBackOfficeUserManager(this IOwinContext owinContext) { - var marker = owinContext.Get(BackOfficeUserManager.OwinMarkerKey) + var marker = owinContext.Get(BackOfficeUserManager2.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 (BackOfficeUserManager2)} from the {typeof (IOwinContext)}."); } /// @@ -107,7 +107,7 @@ namespace Umbraco.Web ?? throw new NullReferenceException($"No {typeof (IBackOfficeUserManagerMarker2)}, 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 (BackOfficeUserManager2)} from the {typeof (IOwinContext)}."); } // TODO: SB: OWIN DI diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index 041b56f1c8..e7f2d0272a 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -1,8 +1,8 @@ using System; using System.Threading; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; -using Microsoft.Owin; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Microsoft.Owin.Extensions; using Microsoft.Owin.Logging; using Microsoft.Owin.Security; @@ -16,7 +16,6 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Mapping; -using Umbraco.Core.Models.Identity; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Net; @@ -34,16 +33,10 @@ namespace Umbraco.Web.Security /// /// Configure Default Identity User Manager for Umbraco /// - /// - /// - /// - /// - /// public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, ServiceContext services, - UmbracoMapper mapper, - IContentSection contentSettings, IGlobalSettings globalSettings, + UmbracoMapper mapper, // TODO: This could probably be optional? IPasswordConfiguration passwordConfiguration, IIpResolver ipResolver) @@ -51,39 +44,37 @@ 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( - options, + app.CreatePerOwinContext( + (options, owinContext) => BackOfficeUserManager2.Create( services.UserService, services.EntityService, services.ExternalLoginService, - mapper, - contentSettings, globalSettings, + mapper, passwordConfiguration, - ipResolver)); + ipResolver, + new OptionsWrapper(new IdentityOptions()), + new UserAwarePasswordHasher2(new PasswordSecurity(passwordConfiguration)), + new[] {new UserValidator(),}, + new[] {new PasswordValidator()}, + new UpperInvariantLookupNormalizer(), + new IdentityErrorDescriber(), + null, + new NullLogger>())); - app.SetBackOfficeUserManagerType(); + app.SetBackOfficeUserManagerType(); //Create a sign in manager per request - app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger())); + app.CreatePerOwinContext((options, context) => BackOfficeSignInManager2.Create(context, globalSettings, app.CreateLogger())); } /// /// Configure a custom UserStore with the Identity User Manager for Umbraco /// - /// - /// - /// - /// - /// - /// - /// public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, IRuntimeState runtimeState, - IContentSection contentSettings, IGlobalSettings globalSettings, - BackOfficeUserStore customUserStore, + BackOfficeUserStore2 customUserStore, // TODO: This could probably be optional? IPasswordConfiguration passwordConfiguration, IIpResolver ipResolver) @@ -92,22 +83,28 @@ 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( - options, - customUserStore, - contentSettings, + app.CreatePerOwinContext( + (options, owinContext) => BackOfficeUserManager2.Create( passwordConfiguration, ipResolver, - globalSettings)); + customUserStore, + new OptionsWrapper(new IdentityOptions()), + new UserAwarePasswordHasher2(new PasswordSecurity(passwordConfiguration)), + new[] { new Microsoft.AspNetCore.Identity.UserValidator(), }, + new[] { new PasswordValidator() }, + new UpperInvariantLookupNormalizer(), + new IdentityErrorDescriber(), + null, + new NullLogger>())); - app.SetBackOfficeUserManagerType(); + app.SetBackOfficeUserManagerType(); //Create a sign in manager per request - app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName))); + app.CreatePerOwinContext((options, context) => BackOfficeSignInManager2.Create(context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager2).FullName))); } - /// + // TODO: SB: ConfigureUserManagerForUmbracoBackOffice using IdentityFactoryOptions + /*/// /// Configure a custom BackOfficeUserManager for Umbraco /// /// @@ -118,7 +115,7 @@ namespace Umbraco.Web.Security IRuntimeState runtimeState, IGlobalSettings globalSettings, Func, IOwinContext, TManager> userManager) - where TManager : BackOfficeUserManager + where TManager : BackOfficeUserManager2 where TUser : BackOfficeIdentityUser { if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); @@ -130,9 +127,9 @@ namespace Umbraco.Web.Security app.SetBackOfficeUserManagerType(); //Create a sign in manager per request - app.CreatePerOwinContext( - (options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName))); - } + app.CreatePerOwinContext( + (options, context) => BackOfficeSignInManager2.Create(context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager2).FullName))); + }*/ /// /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline @@ -190,14 +187,15 @@ namespace Umbraco.Web.Security authOptions.Provider = new BackOfficeCookieAuthenticationProvider(userService, runtimeState, globalSettings, ioHelper, umbracoSettingsSection) { + // TODO: SB: SecurityStampValidator // Enables the application to validate the security stamp when the user // logs in. This is a security feature which is used when you // change a password or add an external login to your account. - OnValidateIdentity = SecurityStampValidator - .OnValidateIdentity( + /*OnValidateIdentity = SecurityStampValidator + .OnValidateIdentity( TimeSpan.FromMinutes(30), (manager, user) => manager.GenerateUserIdentityAsync(user), - identity => identity.GetUserId()), + identity => identity.GetUserId()),*/ }; @@ -268,7 +266,7 @@ namespace Umbraco.Web.Security /// differently in the owin context /// private static void SetBackOfficeUserManagerType(this IAppBuilder app) - where TManager : BackOfficeUserManager + where TManager : BackOfficeUserManager2 where TUser : BackOfficeIdentityUser { if (_markerSet) throw new InvalidOperationException("The back office user manager marker has already been set, only one back office user manager can be configured"); @@ -278,7 +276,7 @@ namespace Umbraco.Web.Security // a generic strongly typed instance app.Use((context, func) => { - context.Set(BackOfficeUserManager.OwinMarkerKey, new BackOfficeUserManagerMarker()); + context.Set(BackOfficeUserManager2.OwinMarkerKey, new BackOfficeUserManagerMarker2()); return func(); }); } diff --git a/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs b/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs index 5da9c77d6b..d648d9ce78 100644 --- a/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs +++ b/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs @@ -1,15 +1,14 @@ using System; using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; +using Microsoft.AspNetCore.Identity; using Microsoft.Owin.Security; namespace Umbraco.Web.Security { public static class AuthenticationManagerExtensions { - private static ExternalLoginInfo GetExternalLoginInfo(AuthenticateResult result) + private static ExternalLoginInfo2 GetExternalLoginInfo(AuthenticateResult result) { if (result == null || result.Identity == null) { @@ -26,11 +25,12 @@ namespace Umbraco.Web.Security { name = name.Replace(" ", ""); } - var email = result.Identity.FindFirstValue(ClaimTypes.Email); - return new ExternalLoginInfo + + var email = result.Identity.FindFirst(ClaimTypes.Email)?.Value; + return new ExternalLoginInfo2 { ExternalIdentity = result.Identity, - Login = new UserLoginInfo(idClaim.Issuer, idClaim.Value), + Login = new UserLoginInfo(idClaim.Issuer, idClaim.Value, idClaim.Issuer), DefaultUserName = name, Email = email }; @@ -47,7 +47,7 @@ namespace Umbraco.Web.Security /// dictionary /// /// - public static async Task GetExternalLoginInfoAsync(this IAuthenticationManager manager, + public static async Task GetExternalLoginInfoAsync(this IAuthenticationManager manager, string authenticationType, string xsrfKey, string expectedValue) { @@ -74,7 +74,7 @@ namespace Umbraco.Web.Security /// /// /// - public static async Task GetExternalLoginInfoAsync(this IAuthenticationManager manager, string authenticationType) + public static async Task GetExternalLoginInfoAsync(this IAuthenticationManager manager, string authenticationType) { if (manager == null) { @@ -83,4 +83,19 @@ namespace Umbraco.Web.Security return GetExternalLoginInfo(await manager.AuthenticateAsync(authenticationType)); } } + + public class ExternalLoginInfo2 + { + /// Associated login data + public UserLoginInfo Login { get; set; } + + /// Suggested user name for a user + public string DefaultUserName { get; set; } + + /// Email claim from the external identity + public string Email { get; set; } + + /// The external identity + public ClaimsIdentity ExternalIdentity { get; set; } + } } diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs deleted file mode 100644 index fe5b061d15..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System; -using System.Diagnostics; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; -using Microsoft.Owin; -using Microsoft.Owin.Logging; -using Microsoft.Owin.Security; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models.Identity; -using Umbraco.Core.Security; -using Umbraco.Web.Models.Identity; -using Constants = Umbraco.Core.Constants; - -namespace Umbraco.Web.Security -{ - // TODO: In v8 we need to change this to use an int? nullable TKey instead, see notes against overridden TwoFactorSignInAsync - public class BackOfficeSignInManager : SignInManager - { - private readonly ILogger _logger; - private readonly IOwinRequest _request; - private readonly IGlobalSettings _globalSettings; - - public BackOfficeSignInManager(UserManager userManager, IAuthenticationManager authenticationManager, ILogger logger, IGlobalSettings globalSettings, IOwinRequest request) - : base(userManager, authenticationManager) - { - if (logger == null) throw new ArgumentNullException("logger"); - if (request == null) throw new ArgumentNullException("request"); - _logger = logger; - _request = request; - _globalSettings = globalSettings; - AuthenticationType = Constants.Security.BackOfficeAuthenticationType; - } - - public override Task CreateUserIdentityAsync(BackOfficeIdentityUser user) - { - return ((BackOfficeUserManager)UserManager).GenerateUserIdentityAsync(user); - } - - public static BackOfficeSignInManager Create(IdentityFactoryOptions options, IOwinContext context, IGlobalSettings globalSettings, ILogger logger) - { - return new BackOfficeSignInManager( - context.GetBackOfficeUserManager(), - context.Authentication, - logger, - globalSettings, - context.Request); - } - - /// - /// Sign in the user in using the user name and password - /// - /// - /// - public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout) - { - var result = await PasswordSignInAsyncImpl(userName, password, isPersistent, shouldLockout); - - switch (result) - { - case SignInStatus.Success: - _logger.WriteCore(TraceEventType.Information, 0, - $"User: {userName} logged in from IP address {_request.RemoteIpAddress}", null, null); - break; - case SignInStatus.LockedOut: - _logger.WriteCore(TraceEventType.Information, 0, - $"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, the user is locked", null, null); - break; - case SignInStatus.RequiresVerification: - _logger.WriteCore(TraceEventType.Information, 0, - $"Login attempt requires verification for username {userName} from IP address {_request.RemoteIpAddress}", null, null); - break; - case SignInStatus.Failure: - _logger.WriteCore(TraceEventType.Information, 0, - $"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}", null, null); - break; - default: - throw new ArgumentOutOfRangeException(); - } - - return result; - } - - /// - /// Borrowed from Microsoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type - /// - /// - /// - /// - /// - /// - private async Task PasswordSignInAsyncImpl(string userName, string password, bool isPersistent, bool shouldLockout) - { - if (UserManager == null) - { - return SignInStatus.Failure; - } - - var user = await UserManager.FindByNameAsync(userName); - - //if the user is null, create an empty one which can be used for auto-linking - if (user == null) - user = BackOfficeIdentityUser.CreateNew(_globalSettings, userName, null, _globalSettings.DefaultUILanguage); - - //check the password for the user, this will allow a developer to auto-link - //an account if they have specified an IBackOfficeUserPasswordChecker - if (await UserManager.CheckPasswordAsync(user, password)) - { - //the underlying call to this will query the user by Id which IS cached! - if (await UserManager.IsLockedOutAsync(user.Id)) - { - return SignInStatus.LockedOut; - } - - // We need to verify that the user belongs to one or more groups that define content and media start nodes. - // To do so we have to create the user claims identity and validate the calculated start nodes. - var userIdentity = await CreateUserIdentityAsync(user); - if (userIdentity is UmbracoBackOfficeIdentity backOfficeIdentity) - { - if (backOfficeIdentity.StartContentNodes.Length == 0 || backOfficeIdentity.StartMediaNodes.Length == 0) - { - _logger.WriteCore(TraceEventType.Information, 0, - $"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, no content and/or media start nodes could be found for any of the user's groups", null, null); - return SignInStatus.Failure; - } - } - - await UserManager.ResetAccessFailedCountAsync(user.Id); - return await SignInOrTwoFactor(user, isPersistent); - } - - var requestContext = _request.Context; - - if (user.HasIdentity && shouldLockout) - { - // If lockout is requested, increment access failed count which might lock out the user - await UserManager.AccessFailedAsync(user.Id); - if (await UserManager.IsLockedOutAsync(user.Id)) - { - //at this point we've just locked the user out after too many failed login attempts - - if (requestContext != null) - { - var backofficeUserManager = requestContext.GetBackOfficeUserManager(); - if (backofficeUserManager != null) - backofficeUserManager.RaiseAccountLockedEvent(user.Id); - } - - return SignInStatus.LockedOut; - } - } - - if (requestContext != null) - { - var backofficeUserManager = requestContext.GetBackOfficeUserManager(); - if (backofficeUserManager != null) - backofficeUserManager.RaiseInvalidLoginAttemptEvent(userName); - } - - return SignInStatus.Failure; - } - - /// - /// Borrowed from Microsoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type - /// - /// - /// - /// - private async Task SignInOrTwoFactor(BackOfficeIdentityUser user, bool isPersistent) - { - var id = Convert.ToString(user.Id); - if (await UserManager.GetTwoFactorEnabledAsync(user.Id) - && (await UserManager.GetValidTwoFactorProvidersAsync(user.Id)).Count > 0) - { - var identity = new ClaimsIdentity(Constants.Security.BackOfficeTwoFactorAuthenticationType); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id)); - identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, user.UserName)); - AuthenticationManager.SignIn(identity); - return SignInStatus.RequiresVerification; - } - await SignInAsync(user, isPersistent, false); - return SignInStatus.Success; - } - - /// - /// Creates a user identity and then signs the identity using the AuthenticationManager - /// - /// - /// - /// - /// - public override async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, bool rememberBrowser) - { - var userIdentity = await CreateUserIdentityAsync(user); - - // Clear any partial cookies from external or two factor partial sign ins - AuthenticationManager.SignOut( - Constants.Security.BackOfficeExternalAuthenticationType, - Constants.Security.BackOfficeTwoFactorAuthenticationType); - - var nowUtc = DateTime.Now.ToUniversalTime(); - - if (rememberBrowser) - { - var rememberBrowserIdentity = AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(ConvertIdToString(user.Id)); - AuthenticationManager.SignIn(new AuthenticationProperties() - { - IsPersistent = isPersistent, - AllowRefresh = true, - IssuedUtc = nowUtc, - ExpiresUtc = nowUtc.AddMinutes(_globalSettings.TimeOutInMinutes) - }, userIdentity, rememberBrowserIdentity); - } - else - { - AuthenticationManager.SignIn(new AuthenticationProperties() - { - IsPersistent = isPersistent, - AllowRefresh = true, - IssuedUtc = nowUtc, - ExpiresUtc = nowUtc.AddMinutes(_globalSettings.TimeOutInMinutes) - }, userIdentity); - } - - //track the last login date - user.LastLoginDateUtc = DateTime.UtcNow; - if (user.AccessFailedCount > 0) - //we have successfully logged in, reset the AccessFailedCount - user.AccessFailedCount = 0; - await UserManager.UpdateAsync(user); - - //set the current request's principal to the identity just signed in! - _request.User = new ClaimsPrincipal(userIdentity); - - _logger.WriteCore(TraceEventType.Information, 0, - string.Format( - "Login attempt succeeded for username {0} from IP address {1}", - user.UserName, - _request.RemoteIpAddress), null, null); - } - - /// - /// Get the user id that has been verified already or int.MinValue if the user has not been verified yet - /// - /// - /// - /// Replaces the underlying call which is not flexible and doesn't support a custom cookie - /// - public new async Task GetVerifiedUserIdAsync() - { - var result = await AuthenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); - if (result != null && result.Identity != null && string.IsNullOrEmpty(result.Identity.GetUserId()) == false) - { - return ConvertIdFromString(result.Identity.GetUserId()); - } - return int.MinValue; - } - - /// - /// Get the username that has been verified already or null. - /// - /// - public async Task GetVerifiedUserNameAsync() - { - var result = await AuthenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); - if (result != null && result.Identity != null && string.IsNullOrEmpty(result.Identity.GetUserName()) == false) - { - return result.Identity.GetUserName(); - } - return null; - } - - /// - /// Two factor verification step - /// - /// - /// - /// - /// - /// - /// - /// This is implemented because we cannot override GetVerifiedUserIdAsync and instead we have to shadow it - /// so due to this and because we are using an INT as the TKey and not an object, it can never be null. Adding to that - /// the default(int) value returned by the base class is always a valid user (i.e. the admin) so we just have to duplicate - /// all of this code to check for int.MinValue - /// - public override async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberBrowser) - { - var userId = await GetVerifiedUserIdAsync(); - if (userId == int.MinValue) - { - return SignInStatus.Failure; - } - var user = await UserManager.FindByIdAsync(userId); - if (user == null) - { - return SignInStatus.Failure; - } - if (await UserManager.IsLockedOutAsync(user.Id)) - { - return SignInStatus.LockedOut; - } - if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, provider, code)) - { - // When token is verified correctly, clear the access failed count used for lockout - await UserManager.ResetAccessFailedCountAsync(user.Id); - await SignInAsync(user, isPersistent, rememberBrowser); - return SignInStatus.Success; - } - // If the token is incorrect, record the failure which also may cause the user to be locked out - await UserManager.AccessFailedAsync(user.Id); - return SignInStatus.Failure; - } - - /// Send a two factor code to a user - /// - /// - /// - /// This is implemented because we cannot override GetVerifiedUserIdAsync and instead we have to shadow it - /// so due to this and because we are using an INT as the TKey and not an object, it can never be null. Adding to that - /// the default(int) value returned by the base class is always a valid user (i.e. the admin) so we just have to duplicate - /// all of this code to check for int.MinVale instead. - /// - public override async Task SendTwoFactorCodeAsync(string provider) - { - var userId = await GetVerifiedUserIdAsync(); - if (userId == int.MinValue) - return false; - - var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider); - var identityResult = await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token); - return identityResult.Succeeded; - } - } -} diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager2.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager2.cs index ac33eb208d..37284cc04c 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager2.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager2.cs @@ -18,7 +18,7 @@ namespace Umbraco.Web.Security /// Custom sign in manager due to SignInManager not being .NET Standard. /// Can be removed once the web project moves to .NET Core. /// - public class BackOfficeSignInManager2 + public class BackOfficeSignInManager2 : IDisposable { private readonly BackOfficeUserManager2 _userManager; private readonly IAuthenticationManager _authenticationManager; @@ -350,5 +350,10 @@ namespace Umbraco.Web.Security { return id == null ? default(int) : (int) Convert.ChangeType(id, typeof(int), CultureInfo.InvariantCulture); } + + public void Dispose() + { + _userManager?.Dispose(); + } } } diff --git a/src/Umbraco.Web/Security/BackOfficeUserManager.cs b/src/Umbraco.Web/Security/BackOfficeUserManager.cs deleted file mode 100644 index 6dda5a8b44..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeUserManager.cs +++ /dev/null @@ -1,641 +0,0 @@ -using System; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; -using Microsoft.Owin.Security.DataProtection; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Mapping; -using Umbraco.Core.Models.Identity; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Net; -using Umbraco.Web.Models.Identity; -using IPasswordHasher = Microsoft.AspNet.Identity.IPasswordHasher; - -namespace Umbraco.Web.Security -{ - /// - /// Default back office user manager - /// - public class BackOfficeUserManager : BackOfficeUserManager - { - public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker"; - - public BackOfficeUserManager( - IUserStore store, - IdentityFactoryOptions options, - IContentSection contentSectionConfig, - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IGlobalSettings globalSettings) - : base(store, passwordConfiguration, ipResolver) - { - if (options == null) throw new ArgumentNullException("options"); - InitUserManager(this, passwordConfiguration, options.DataProtectionProvider, contentSectionConfig, globalSettings); - } - - #region Static Create methods - - /// - /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static BackOfficeUserManager Create( - IdentityFactoryOptions options, - IUserService userService, - IEntityService entityService, - IExternalLoginService externalLoginService, - UmbracoMapper mapper, - IContentSection contentSectionConfig, - IGlobalSettings globalSettings, - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver) - { - if (options == null) throw new ArgumentNullException("options"); - if (userService == null) throw new ArgumentNullException("userService"); - if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); - - var store = new BackOfficeUserStore(userService, entityService, externalLoginService, globalSettings, mapper); - var manager = new BackOfficeUserManager(store, options, contentSectionConfig, passwordConfiguration, ipResolver, globalSettings); - return manager; - } - - /// - /// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance - /// - /// - /// - /// - /// - /// - public static BackOfficeUserManager Create( - IdentityFactoryOptions options, - BackOfficeUserStore customUserStore, - IContentSection contentSectionConfig, - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IGlobalSettings globalSettings) - { - var manager = new BackOfficeUserManager(customUserStore, options, contentSectionConfig, passwordConfiguration, ipResolver, globalSettings); - return manager; - } - #endregion - - - } - - /// - /// Generic Back office user manager - /// - public class BackOfficeUserManager : UserManager - where T : BackOfficeIdentityUser - { - private PasswordGenerator _passwordGenerator; - - public BackOfficeUserManager(IUserStore store, - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver) - : base(store) - { - PasswordConfiguration = passwordConfiguration; - IpResolver = ipResolver; - } - - #region What we support do not currently - - // TODO: We could support this - but a user claims will mostly just be what is in the auth cookie - public override bool SupportsUserClaim - { - get { return false; } - } - - // TODO: Support this - public override bool SupportsQueryableUsers - { - get { return false; } - } - - /// - /// Developers will need to override this to support custom 2 factor auth - /// - public override bool SupportsUserTwoFactor - { - get { return false; } - } - - // TODO: Support this - public override bool SupportsUserPhoneNumber - { - get { return false; } - } - #endregion - - public virtual async Task GenerateUserIdentityAsync(T user) - { - // NOTE the authenticationType must match the umbraco one - // defined in CookieAuthenticationOptions.AuthenticationType - var userIdentity = await CreateIdentityAsync(user, Core.Constants.Security.BackOfficeAuthenticationType); - return userIdentity; - } - - /// - /// Initializes the user manager with the correct options - /// - /// - /// - /// - /// - /// - protected void InitUserManager( - BackOfficeUserManager manager, - IPasswordConfiguration passwordConfig, - IDataProtectionProvider dataProtectionProvider, - IContentSection contentSectionConfig, - IGlobalSettings globalSettings) - { - // Configure validation logic for usernames - manager.UserValidator = new BackOfficeUserValidator(manager) - { - AllowOnlyAlphanumericUserNames = false, - RequireUniqueEmail = true - }; - - // Configure validation logic for passwords - manager.PasswordValidator = new ConfiguredPasswordValidator(passwordConfig); - - //use a custom hasher based on our membership provider - manager.PasswordHasher = GetDefaultPasswordHasher(passwordConfig); - - if (dataProtectionProvider != null) - { - manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")) - { - TokenLifespan = TimeSpan.FromDays(3) - }; - } - - manager.UserLockoutEnabledByDefault = true; - manager.MaxFailedAccessAttemptsBeforeLockout = passwordConfig.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. - manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(30); - - //custom identity factory for creating the identity object for which we auth against in the back office - manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); - - manager.EmailService = new EmailService( - contentSectionConfig.NotificationEmailAddress, - new EmailSender(globalSettings)); - - //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to support it - - //// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user - //// You can write your own provider and plug in here. - //manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider - //{ - // MessageFormat = "Your security code is: {0}" - //}); - //manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider - //{ - // Subject = "Security Code", - // BodyFormat = "Your security code is: {0}" - //}); - - //manager.SmsService = new SmsService(); - } - - /// - /// Used to validate a user's session - /// - /// - /// - /// - public virtual async Task ValidateSessionIdAsync(int 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(int userId) - { - var user = await FindByIdAsync(userId); - if (user == null) - throw new InvalidOperationException("No user found by id " + userId); - if (user.IsApproved == false) - return true; - - return await base.IsLockedOutAsync(userId); - } - - #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); - } - - public override Task ResetPasswordAsync(int userId, string token, string newPassword) - { - var result = base.ResetPasswordAsync(userId, token, newPassword); - return result; - } - - /// - /// 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 Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) - { - var result = base.ResetPasswordAsync(userId, token, newPassword); - if (result.Result.Succeeded) - RaisePasswordChangedEvent(userId); - return result; - } - - public override Task ChangePasswordAsync(int userId, string currentPassword, string newPassword) - { - var result = base.ChangePasswordAsync(userId, currentPassword, newPassword); - if (result.Result.Succeeded) - RaisePasswordChangedEvent(userId); - return result; - } - - /// - /// Override to determine how to hash the password - /// - /// - /// - /// - /// - protected override async Task VerifyPasswordAsync(IUserPasswordStore store, T user, string password) - { - var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher; - if (userAwarePasswordHasher == null) - return await base.VerifyPasswordAsync(store, user, password); - - var hash = await store.GetPasswordHashAsync(user); - return userAwarePasswordHasher.VerifyHashedPassword(user, hash, password) != PasswordVerificationResult.Failed; - } - - /// - /// 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 UpdatePassword(IUserPasswordStore passwordStore, T user, string newPassword) - { - user.LastPasswordChangeDateUtc = DateTime.UtcNow; - var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher; - if (userAwarePasswordHasher == null) - return await base.UpdatePassword(passwordStore, user, newPassword); - - var result = await PasswordValidator.ValidateAsync(newPassword); - if (result.Succeeded == false) - return result; - - await passwordStore.SetPasswordHashAsync(user, userAwarePasswordHasher.HashPassword(user, newPassword)); - 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(BackOfficeIdentityUser user) - { - if (SupportsUserSecurityStamp == false) - return; - await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp()); - } - - /// - /// 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(int userId, DateTimeOffset lockoutEnd) - { - var result = await base.SetLockoutEndDateAsync(userId, lockoutEnd); - - // The way we unlock is by setting the lockoutEnd date to the current datetime - if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) - { - RaiseAccountLockedEvent(userId); - } - else - { - RaiseAccountUnlockedEvent(userId); - //Resets the login attempt fails back to 0 when unlock is clicked - await ResetAccessFailedCountAsync(userId); - - } - - return result; - } - - public override async Task ResetAccessFailedCountAsync(int userId) - { - var lockoutStore = (IUserLockoutStore)Store; - var user = await FindByIdAsync(userId); - if (user == null) - throw new InvalidOperationException("No user found by user id " + userId); - - var accessFailedCount = await GetAccessFailedCountAsync(user.Id); - - if (accessFailedCount == 0) - return IdentityResult.Success; - - await lockoutStore.ResetAccessFailedCountAsync(user); - //raise the event now that it's reset - RaiseResetAccessFailedCountEvent(userId); - 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(int userId) - { - var lockoutStore = (IUserLockoutStore)Store; - var user = await FindByIdAsync(userId); - if (user == null) - throw new InvalidOperationException("No user found by user id " + userId); - - var count = await lockoutStore.IncrementAccessFailedCountAsync(user); - - if (count >= MaxFailedAccessAttemptsBeforeLockout) - { - await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(DefaultAccountLockoutTimeSpan)); - //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(userId); - - 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/BackOfficeUserManager2.cs b/src/Umbraco.Web/Security/BackOfficeUserManager2.cs index b22f9523c0..0adc92b77e 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserManager2.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserManager2.cs @@ -42,14 +42,6 @@ namespace Umbraco.Web.Security /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager /// - /// - /// - /// - /// - /// - /// - /// - /// public static BackOfficeUserManager2 Create( IUserService userService, IEntityService entityService, @@ -85,11 +77,10 @@ namespace Umbraco.Web.Security /// /// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance /// - /// public static BackOfficeUserManager2 Create( IPasswordConfiguration passwordConfiguration, IIpResolver ipResolver, - IUserStore store, + IUserStore customUserStore, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, @@ -102,7 +93,7 @@ namespace Umbraco.Web.Security return new BackOfficeUserManager2( passwordConfiguration, ipResolver, - store, + customUserStore, optionsAccessor, passwordHasher, userValidators, @@ -207,9 +198,9 @@ namespace Umbraco.Web.Security /// /// /// - public virtual async Task ValidateSessionIdAsync(int userId, string sessionId) + public virtual async Task ValidateSessionIdAsync(string userId, string sessionId) { - var userSessionStore = Store as IUserSessionStore2; + var userSessionStore = Store as IUserSessionStore2; //if this is not set, for backwards compat (which would be super rare), we'll just approve it if (userSessionStore == null) return true; diff --git a/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs b/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs index 03477db730..d9f430c84f 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs @@ -1,8 +1,5 @@ using System; -using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; -using Umbraco.Core.Models.Identity; -using Umbraco.Core.Security; using Umbraco.Web.Models.Identity; namespace Umbraco.Web.Security @@ -14,14 +11,14 @@ namespace Umbraco.Web.Security /// /// /// - internal class BackOfficeUserManagerMarker : IBackOfficeUserManagerMarker - where TManager : BackOfficeUserManager + internal class BackOfficeUserManagerMarker2 : IBackOfficeUserManagerMarker2 + where TManager : BackOfficeUserManager2 where TUser : BackOfficeIdentityUser { - public BackOfficeUserManager GetManager(IOwinContext owin) + public BackOfficeUserManager2 GetManager(IOwinContext owin) { - var mgr = owin.Get() as BackOfficeUserManager; - if (mgr == null) throw new InvalidOperationException("Could not cast the registered back office user of type " + typeof(TManager) + " to " + typeof(BackOfficeUserManager)); + var mgr = owin.Get() as BackOfficeUserManager2; + if (mgr == null) throw new InvalidOperationException("Could not cast the registered back office user of type " + typeof(TManager) + " to " + typeof(BackOfficeUserManager2)); return mgr; } } diff --git a/src/Umbraco.Web/Security/BackOfficeUserStore.cs b/src/Umbraco.Web/Security/BackOfficeUserStore.cs deleted file mode 100644 index 78eeba5eff..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeUserStore.cs +++ /dev/null @@ -1,776 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNet.Identity; -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; -using Umbraco.Web.Models.Identity; -using IUser = Umbraco.Core.Models.Membership.IUser; -using Task = System.Threading.Tasks.Task; - -namespace Umbraco.Core.Security -{ - public class BackOfficeUserStore : DisposableObjectSlim, - IUserStore, - 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; - } - - /// - /// Insert a new user - /// - /// - /// - public Task CreateAsync(BackOfficeIdentityUser user) - { - 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(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(0); - } - - /// - /// Update a user - /// - /// - /// - public async Task UpdateAsync(BackOfficeIdentityUser user) - { - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - var asInt = user.Id.TryConvertTo(); - if (asInt == false) - { - throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); - } - - var found = _userService.GetUserById(asInt.Result); - 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)); - } - } - } - - /// - /// Delete a user - /// - /// - /// - public Task DeleteAsync(BackOfficeIdentityUser user) - { - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - var asInt = user.Id.TryConvertTo(); - if (asInt == false) - { - throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); - } - - var found = _userService.GetUserById(asInt.Result); - if (found != null) - { - _userService.Delete(found); - } - _externalLoginService.DeleteUserLogins(asInt.Result); - - return Task.FromResult(0); - } - - /// - /// Finds a user - /// - /// - /// - public async Task FindByIdAsync(int userId) - { - ThrowIfDisposed(); - var user = _userService.GetUserById(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) - { - 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) - { - 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.FromResult(0); - } - - /// - /// Get the user password hash - /// - /// - /// - public Task GetPasswordHashAsync(BackOfficeIdentityUser user) - { - 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) - { - 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) - { - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException("email"); - - user.Email = email; - - return Task.FromResult(0); - } - - /// - /// Get the user email - /// - /// - /// - public Task GetEmailAsync(BackOfficeIdentityUser user) - { - 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) - { - 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) - { - ThrowIfDisposed(); - user.EmailConfirmed = confirmed; - return Task.FromResult(0); - } - - /// - /// Returns the user associated with this email - /// - /// - /// - public Task FindByEmailAsync(string email) - { - ThrowIfDisposed(); - var user = _userService.GetByEmail(email); - var result = user == null - ? null - : _mapper.Map(user); - - return Task.FromResult(AssignLoginsCallback(result)); - } - - /// - /// Adds a user login with the specified provider and key - /// - /// - /// - public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) - { - 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.FromResult(0); - } - - /// - /// Removes the user login with the specified combination if it exists - /// - /// - /// - public Task RemoveLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) - { - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (login == null) throw new ArgumentNullException(nameof(login)); - - var provider = login.LoginProvider; - var key = login.ProviderKey; - var userLogin = user.Logins.SingleOrDefault((l => l.LoginProvider == provider && l.ProviderKey == key)); - if (userLogin != null) - user.Logins.Remove(userLogin); - - return Task.FromResult(0); - } - - /// - /// Returns the linked accounts for this user - /// - /// - /// - public Task> GetLoginsAsync(BackOfficeIdentityUser user) - { - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult((IList) - user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey)).ToList()); - } - - /// - /// Returns the user associated with this login - /// - /// - public Task FindAsync(UserLoginInfo login) - { - ThrowIfDisposed(); - if (login == null) throw new ArgumentNullException(nameof(login)); - - //get all logins associated with the login id - var result = _externalLoginService.Find(UserLoginInfoWrapper.Wrap(login)).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 roleName) - { - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (roleName == null) throw new ArgumentNullException(nameof(roleName)); - if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(roleName)); - - var userRole = user.Roles.SingleOrDefault(r => r.RoleId == roleName); - - if (userRole == null) - { - user.AddRole(roleName); - } - - return Task.FromResult(0); - } - - /// - /// Removes the role (user group) for the user - /// - /// - /// - public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName) - { - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (roleName == null) throw new ArgumentNullException(nameof(roleName)); - if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(roleName)); - - var userRole = user.Roles.SingleOrDefault(r => r.RoleId == roleName); - - if (userRole != null) - { - user.Roles.Remove(userRole); - } - - return Task.FromResult(0); - } - - /// - /// Returns the roles (user groups) for this user - /// - /// - /// - public Task> GetRolesAsync(BackOfficeIdentityUser user) - { - 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 roleName) - { - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(roleName)); - } - - /// - /// Set the security stamp for the user - /// - /// - /// - public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp) - { - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.SecurityStamp = stamp; - return Task.FromResult(0); - } - - /// - /// Get the user security stamp - /// - /// - /// - public Task GetSecurityStampAsync(BackOfficeIdentityUser user) - { - 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) - { - user.TwoFactorEnabled = false; - return Task.FromResult(0); - } - - /// - /// Returns whether two factor authentication is enabled for the user - /// - /// - /// - public virtual Task GetTwoFactorEnabledAsync(BackOfficeIdentityUser user) - { - 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) - { - 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) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - user.LockoutEndDateUtc = lockoutEnd.UtcDateTime; - return Task.FromResult(0); - } - - /// - /// Used to record when an attempt to access the user has failed - /// - /// - /// - public Task IncrementAccessFailedCountAsync(BackOfficeIdentityUser user) - { - 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) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - user.AccessFailedCount = 0; - return Task.FromResult(0); - } - - /// - /// 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) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult(user.AccessFailedCount); - } - - /// - /// Returns true - /// - /// - /// - public Task GetLockoutEnabledAsync(BackOfficeIdentityUser user) - { - 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) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - user.LockoutEnabled = enabled; - return Task.FromResult(0); - } - #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(int userId, string sessionId) - { - Guid guidSessionId; - if (Guid.TryParse(sessionId, out guidSessionId)) - { - return Task.FromResult(_userService.ValidateLoginSession(userId, guidSessionId)); - } - return Task.FromResult(false); - } - } -} diff --git a/src/Umbraco.Web/Security/BackOfficeUserStore2.cs b/src/Umbraco.Web/Security/BackOfficeUserStore2.cs index 65de3191de..d5ed266f4f 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserStore2.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserStore2.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Security IUserSecurityStampStore, IUserLockoutStore, IUserTwoFactorStore, - IUserSessionStore2 + IUserSessionStore2 // TODO: This would require additional columns/tables for now people will need to implement this on their own //IUserPhoneNumberStore, @@ -119,9 +119,9 @@ namespace Umbraco.Web.Security // 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 Microsoft.AspNet.Identity.PasswordHasher(); + var aspHasher = new PasswordHasher(); var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix + - aspHasher.HashPassword(Guid.NewGuid().ToString("N")); + aspHasher.HashPassword(user, Guid.NewGuid().ToString("N")); var userEntity = new User(_globalSettings, user.Name, user.Email, user.UserName, emptyPasswordValue) { @@ -902,12 +902,13 @@ namespace Umbraco.Web.Security throw new ObjectDisposedException(GetType().Name); } - public Task ValidateSessionIdAsync(int userId, string sessionId) + public Task ValidateSessionIdAsync(string userId, string sessionId) { Guid guidSessionId; if (Guid.TryParse(sessionId, out guidSessionId)) { - return Task.FromResult(_userService.ValidateLoginSession(userId, guidSessionId)); + // TODO: SB: Normalize string to int conversion + return Task.FromResult(_userService.ValidateLoginSession(int.Parse(sessionId), guidSessionId)); } return Task.FromResult(false); } diff --git a/src/Umbraco.Web/Security/BackOfficeUserValidator.cs b/src/Umbraco.Web/Security/BackOfficeUserValidator.cs deleted file mode 100644 index 0f6b9aa1d4..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeUserValidator.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNet.Identity; -using Umbraco.Web.Models.Identity; - -namespace Umbraco.Core.Security -{ - /// - /// Custom validator to not validate a user's username or email if they haven't changed - /// - /// - internal class BackOfficeUserValidator : UserValidator - where T : BackOfficeIdentityUser - { - public BackOfficeUserValidator(UserManager manager) : base(manager) - { - } - - public override async Task ValidateAsync(T item) - { - //Don't validate if the user's email or username hasn't changed otherwise it's just wasting SQL queries. - if (item.IsPropertyDirty("Email") || item.IsPropertyDirty("UserName")) - { - return await base.ValidateAsync(item); - } - return IdentityResult.Success; - } - } -} diff --git a/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs index 283f6b6b99..021461916f 100644 --- a/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs @@ -1,8 +1,6 @@ using System; -using Microsoft.AspNet.Identity.Owin; using Umbraco.Core; using Umbraco.Web.Composing; -using Umbraco.Core.Models.Identity; using Umbraco.Web.Models.Identity; namespace Umbraco.Web.Security @@ -33,22 +31,22 @@ namespace Umbraco.Web.Security /// /// A callback executed during account auto-linking and before the user is persisted /// - public Action OnAutoLinking { get; set; } + public Action OnAutoLinking { get; set; } /// /// A callback executed during every time a user authenticates using an external login. /// returns a boolean indicating if sign in should continue or not. /// - public Func OnExternalLogin { get; set; } + public Func OnExternalLogin { get; set; } - /// + /// B /// The default User group aliases to use for auto-linking users /// /// /// /// - public string[] GetDefaultUserGroups(IUmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + public string[] GetDefaultUserGroups(IUmbracoContext umbracoContext, ExternalLoginInfo2 loginInfo) { return _defaultUserGroups; } @@ -61,7 +59,7 @@ namespace Umbraco.Web.Security /// /// For public auth providers this should always be false!!! /// - public bool ShouldAutoLinkExternalAccount(IUmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + public bool ShouldAutoLinkExternalAccount(IUmbracoContext umbracoContext, ExternalLoginInfo2 loginInfo) { return _autoLinkExternalAccount; } @@ -71,7 +69,7 @@ namespace Umbraco.Web.Security /// /// The default Culture to use for auto-linking users /// - public string GetDefaultCulture(IUmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + public string GetDefaultCulture(IUmbracoContext umbracoContext, ExternalLoginInfo2 loginInfo) { return _defaultCulture; } diff --git a/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs b/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs index 92fea0bf40..93b51eec21 100644 --- a/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs +++ b/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.Security /// internal interface IBackOfficeUserManagerMarker { - BackOfficeUserManager GetManager(IOwinContext owin); + BackOfficeUserManager2 GetManager(IOwinContext owin); } /// /// This interface is only here due to the fact that IOwinContext Get / Set only work in generics, if they worked diff --git a/src/Umbraco.Web/Security/IUserAwarePasswordHasher.cs b/src/Umbraco.Web/Security/IUserAwarePasswordHasher.cs deleted file mode 100644 index 4af6d7accd..0000000000 --- a/src/Umbraco.Web/Security/IUserAwarePasswordHasher.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using Microsoft.AspNet.Identity; - -namespace Umbraco.Core.Security -{ - /// - /// A password hasher that is User aware so that it can process the hashing based on the user's settings - /// - /// - /// - public interface IUserAwarePasswordHasher : Microsoft.AspNet.Identity.IPasswordHasher - where TUser : class, IUser - where TKey : IEquatable - { - string HashPassword(TUser user, string password); - PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword); - } -} diff --git a/src/Umbraco.Web/Security/IUserSessionStore.cs b/src/Umbraco.Web/Security/IUserSessionStore.cs index 524017d4ad..d410504ef7 100644 --- a/src/Umbraco.Web/Security/IUserSessionStore.cs +++ b/src/Umbraco.Web/Security/IUserSessionStore.cs @@ -1,6 +1,5 @@ -using System; using System.Threading.Tasks; -using Microsoft.AspNet.Identity; +using Microsoft.AspNetCore.Identity; namespace Umbraco.Core.Security { @@ -8,16 +7,9 @@ namespace Umbraco.Core.Security /// An IUserStore interface part to implement if the store supports validating user session Ids /// /// - /// - public interface IUserSessionStore : IUserStore, IDisposable - where TUser : class, IUser - { - Task ValidateSessionIdAsync(int userId, string sessionId); - } - - public interface IUserSessionStore2 : Microsoft.AspNetCore.Identity.IUserStore, IDisposable + public interface IUserSessionStore2 : IUserStore where TUser : class { - Task ValidateSessionIdAsync(int userId, string sessionId); + Task ValidateSessionIdAsync(string userId, string sessionId); // TODO: SB: Should take a user??? } } diff --git a/src/Umbraco.Web/Security/SessionIdValidator.cs b/src/Umbraco.Web/Security/SessionIdValidator.cs index 8f6782aac2..2e2afa8da5 100644 --- a/src/Umbraco.Web/Security/SessionIdValidator.cs +++ b/src/Umbraco.Web/Security/SessionIdValidator.cs @@ -2,15 +2,12 @@ using System; using System.Security.Claims; using System.Threading.Tasks; using System.Web; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Microsoft.Owin.Infrastructure; using Microsoft.Owin.Security.Cookies; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; -using Umbraco.Core.Security; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Security @@ -88,16 +85,16 @@ namespace Umbraco.Web.Security if (validate == false) return true; - var manager = owinCtx.GetUserManager(); + var manager = owinCtx.Get(); if (manager == null) return false; - var userId = currentIdentity.GetUserId(); + var userId = currentIdentity.GetUserId(); var user = await manager.FindByIdAsync(userId); if (user == null) return false; - var sessionId = currentIdentity.FindFirstValue(Constants.Security.SessionIdClaimType); + var sessionId = currentIdentity.FindFirst(Constants.Security.SessionIdClaimType)?.Value; if (await manager.ValidateSessionIdAsync(userId, sessionId) == false) return false; diff --git a/src/Umbraco.Web/Security/UserAwarePasswordHasher.cs b/src/Umbraco.Web/Security/UserAwarePasswordHasher.cs deleted file mode 100644 index bbfc4905bf..0000000000 --- a/src/Umbraco.Web/Security/UserAwarePasswordHasher.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using Microsoft.AspNet.Identity; -using Umbraco.Web.Models.Identity; - -namespace Umbraco.Core.Security -{ - /// - /// The default password hasher that is User aware so that it can process the hashing based on the user's settings - /// - public class UserAwarePasswordHasher : IUserAwarePasswordHasher - { - private readonly PasswordSecurity _passwordSecurity; - - public UserAwarePasswordHasher(PasswordSecurity passwordSecurity) - { - _passwordSecurity = passwordSecurity; - } - - public string HashPassword(string password) - { - return _passwordSecurity.HashPasswordForStorage(password); - } - - public string HashPassword(BackOfficeIdentityUser user, string password) - { - // TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089 - //NOTE: For now this just falls back to the hashing we are currently using - - return HashPassword(password); - } - - public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) - { - return _passwordSecurity.VerifyPassword(providedPassword, hashedPassword) - ? PasswordVerificationResult.Success - : PasswordVerificationResult.Failed; - } - - public PasswordVerificationResult VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword) - { - // TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089 - //NOTE: For now this just falls back to the hashing we are currently using - - return VerifyHashedPassword(hashedPassword, providedPassword); - } - } -} diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 6f16eb3c52..08a2626cb3 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -1,11 +1,9 @@ using System; -using System.Linq; using System.Security; using System.Web; using Umbraco.Core; using Umbraco.Core.Services; using Umbraco.Core.Models.Membership; -using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Umbraco.Core.Configuration; using Umbraco.Core.IO; @@ -54,17 +52,17 @@ namespace Umbraco.Web.Security } } - private BackOfficeSignInManager _signInManager; - private BackOfficeSignInManager SignInManager + private BackOfficeSignInManager2 _signInManager; + private BackOfficeSignInManager2 SignInManager { get { if (_signInManager == null) { - var mgr = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().Get(); + var mgr = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().Get(); if (mgr == null) { - throw new NullReferenceException("Could not resolve an instance of " + typeof(BackOfficeSignInManager) + " from the " + typeof(IOwinContext)); + throw new NullReferenceException("Could not resolve an instance of " + typeof(BackOfficeSignInManager2) + " from the " + typeof(IOwinContext)); } _signInManager = mgr; } @@ -72,9 +70,9 @@ namespace Umbraco.Web.Security } } - private BackOfficeUserManager _userManager; - protected BackOfficeUserManager UserManager - => _userManager ?? (_userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager()); + private BackOfficeUserManager2 _userManager; + protected BackOfficeUserManager2 UserManager + => _userManager ?? (_userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager2()); [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")] public double PerformLogin(int userId) @@ -84,7 +82,7 @@ namespace Umbraco.Web.Security //ensure it's done for owin too owinCtx.Authentication.SignOut(Constants.Security.BackOfficeExternalAuthenticationType); - var user = UserManager.FindByIdAsync(userId).Result; + var user = UserManager.FindByIdAsync(userId.ToString()).Result; SignInManager.SignInAsync(user, isPersistent: true, rememberBrowser: false).Wait(); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index aed9a34b82..d124a96209 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -200,13 +200,11 @@ - + - - @@ -214,7 +212,6 @@ - @@ -230,7 +227,6 @@ - @@ -247,8 +243,6 @@ - - diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index a30f77b217..9fd8ab7baa 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -88,9 +88,8 @@ namespace Umbraco.Web // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) app.ConfigureUserManagerForUmbracoBackOffice( Services, - Mapper, - UmbracoSettings.Content, GlobalSettings, + Mapper, UserPasswordConfig, IpResolver); }