diff --git a/src/Umbraco.Core/Auditing/IdentityAuditEventArgs.cs b/src/Umbraco.Core/Auditing/IdentityAuditEventArgs.cs new file mode 100644 index 0000000000..e4fed75e33 --- /dev/null +++ b/src/Umbraco.Core/Auditing/IdentityAuditEventArgs.cs @@ -0,0 +1,106 @@ +using System; +using System.Threading; +using System.Web; +using Umbraco.Core.Security; + +namespace Umbraco.Core.Auditing +{ + /// + /// This class is used by events raised from hthe BackofficeUserManager + /// + public class IdentityAuditEventArgs : EventArgs + { + /// + /// The action that got triggered from the audit event + /// + public AuditEvent Action { get; set; } + + /// + /// Current date/time in UTC format + /// + public DateTime DateTimeUtc { get; private set; } + + /// + /// The source IP address of the user performing the action + /// + public string IpAddress { get; set; } + + /// + /// The user affected by the event raised + /// + public int AffectedUser { get; set; } + + /// + /// If a user is perfoming an action on a different user, then this will be set. Otherwise it will be -1 + /// + public int PerformingUser { get; set; } + + /// + /// An optional comment about the action being logged + /// + public string Comment { get; set; } + + /// + /// This property is always empty except in the LoginFailed event for an unknown user trying to login + /// + public string Username { get; set; } + + /// + /// Sets the properties on the event being raised, all parameters are optional except for the action being performed + /// + /// An action based on the AuditEvent enum + /// The client's IP address. This is usually automatically set but could be overridden if necessary + /// The Id of the user performing the action (if different from the user affected by the action) + public IdentityAuditEventArgs(AuditEvent action, string ipAddress = "", int performingUser = -1) + { + DateTimeUtc = DateTime.UtcNow; + Action = action; + + IpAddress = string.IsNullOrWhiteSpace(ipAddress) + ? GetCurrentRequestIpAddress() + : ipAddress; + + PerformingUser = performingUser == -1 + ? GetCurrentRequestBackofficeUserId() + : performingUser; + } + + /// + /// Returns the current request IP address for logging if there is one + /// + /// + protected string GetCurrentRequestIpAddress() + { + var httpContext = HttpContext.Current == null ? (HttpContextBase)null : new HttpContextWrapper(HttpContext.Current); + return httpContext.GetCurrentRequestIpAddress(); + } + + /// + /// Returns the current logged in backoffice user's Id logging if there is one + /// + /// + protected int GetCurrentRequestBackofficeUserId() + { + var userId = -1; + var backOfficeIdentity = Thread.CurrentPrincipal.GetUmbracoIdentity(); + if (backOfficeIdentity != null) + int.TryParse(backOfficeIdentity.Id.ToString(), out userId); + return userId; + } + } + + public enum AuditEvent + { + AccountLocked, + AccountUnlocked, + ForgotPasswordRequested, + ForgotPasswordChangedSuccess, + LoginFailed, + LoginRequiresVerification, + LoginSucces, + LogoutSuccess, + PasswordChanged, + PasswordReset, + ResetAccessFailedCount + } +} diff --git a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs index 3d56ffc102..b0290082ec 100644 --- a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs @@ -49,7 +49,7 @@ namespace Umbraco.Core.Security 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: @@ -122,15 +122,34 @@ namespace Umbraco.Core.Security 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; } @@ -183,7 +202,7 @@ namespace Umbraco.Core.Security AllowRefresh = true, IssuedUtc = nowUtc, ExpiresUtc = nowUtc.AddMinutes(GlobalSettings.TimeOutInMinutes) - }, userIdentity, rememberBrowserIdentity); + }, userIdentity, rememberBrowserIdentity); } else { @@ -198,7 +217,9 @@ namespace Umbraco.Core.Security //track the last login date user.LastLoginDateUtc = DateTime.UtcNow; - user.AccessFailedCount = 0; + if (user.AccessFailedCount > 0) + //we have successfully logged in, reset the AccessFailedCount + user.AccessFailedCount = 0; await UserManager.UpdateAsync(user); _logger.WriteCore(TraceEventType.Information, 0, diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index febb613fd8..caaebf8f42 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Configuration.Provider; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -7,9 +8,11 @@ using System.Web.Security; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security.DataProtection; +using Umbraco.Core.Auditing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; namespace Umbraco.Core.Security @@ -43,7 +46,7 @@ namespace Umbraco.Core.Security IContentSection contentSectionConfig) : base(store) { - if (options == null) throw new ArgumentNullException("options"); ; + if (options == null) throw new ArgumentNullException("options"); InitUserManager(this, membershipProvider, contentSectionConfig, options); } @@ -403,7 +406,10 @@ namespace Umbraco.Core.Security public override Task ChangePasswordAsync(int userId, string currentPassword, string newPassword) { - return base.ChangePasswordAsync(userId, currentPassword, newPassword); + var result = base.ChangePasswordAsync(userId, currentPassword, newPassword); + if (result.Result.Succeeded) + RaisePasswordChangedEvent(userId); + return result; } /// @@ -484,5 +490,241 @@ namespace Umbraco.Core.Security } #endregion + + public override Task SetLockoutEndDateAsync(int userId, DateTimeOffset lockoutEnd) + { + var result = base.SetLockoutEndDateAsync(userId, lockoutEnd); + + // The way we unlock is by setting the lockoutEnd date to the current datetime + if (result.Result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) + RaiseAccountLockedEvent(userId); + else + RaiseAccountUnlockedEvent(userId); + + return result; + } + + public override async Task ResetAccessFailedCountAsync(int userId) + { + var user = ApplicationContext.Current.Services.UserService.GetUserById(userId); + + if (user == null) + { + throw new ProviderException(string.Format("No user with the id {0} found", userId)); + } + + if (user.FailedPasswordAttempts > 0) + { + user.FailedPasswordAttempts = 0; + ApplicationContext.Current.Services.UserService.Save(user); + RaiseResetAccessFailedCountEvent(userId); + } + + return await Task.FromResult(IdentityResult.Success); + } + + /// + /// Clears a lock so that the membership user can be validated. + /// + /// The IMemberService to user for unlocking + /// The membership user to clear the lock status for. + /// + /// true if the membership user was successfully unlocked; otherwise, false. + /// + public bool UnlockUser(IMembershipMemberService memberService, string username) where TEntity : class, IMembershipUser + { + var member = memberService.GetByUsername(username); + + if (member == null) + { + throw new ProviderException(string.Format("No member with the username '{0}' found", username)); + } + + // Non need to update + if (member.IsLockedOut == false) return true; + + member.IsLockedOut = false; + member.FailedPasswordAttempts = 0; + + memberService.Save(member); + + RaiseAccountUnlockedEvent(member.Id); + + return true; + } + + public override Task AccessFailedAsync(int userId) + { + var result = base.AccessFailedAsync(userId); + + //Slightly confusing: this will return a Success if we successfully update the AccessFailed count + if (result.Result.Succeeded) + RaiseLoginFailedEvent(userId); + + return result; + } + + internal void RaiseAccountLockedEvent(int userId) + { + OnAccountLocked(new IdentityAuditEventArgs(AuditEvent.AccountLocked) + { + AffectedUser = userId + }); + } + + internal void RaiseAccountUnlockedEvent(int userId) + { + OnAccountUnlocked(new IdentityAuditEventArgs(AuditEvent.AccountUnlocked) + { + AffectedUser = userId + }); + } + + internal void RaiseForgotPasswordRequestedEvent(int userId) + { + OnForgotPasswordRequested(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordRequested) + { + AffectedUser = userId + }); + } + + internal void RaiseForgotPasswordChangedSuccessEvent(int userId) + { + OnForgotPasswordChangedSuccess(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordChangedSuccess) + { + AffectedUser = userId + }); + } + + public void RaiseLoginFailedEvent(int userId) + { + OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed) + { + AffectedUser = userId + }); + } + + public void RaiseInvalidLoginAttemptEvent(string username) + { + OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed) + { + Username = username, + Comment = string.Format("Attempted login for username '{0}' failed", username) + }); + } + + internal void RaiseLoginRequiresVerificationEvent(int userId) + { + OnLoginRequiresVerification(new IdentityAuditEventArgs(AuditEvent.LoginRequiresVerification) + { + AffectedUser = userId + }); + } + + internal void RaiseLoginSuccessEvent(int userId) + { + OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces) + { + AffectedUser = userId + }); + } + + internal void RaiseLogoutSuccessEvent(int userId) + { + OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess) + { + AffectedUser = userId + }); + } + + internal void RaisePasswordChangedEvent(int userId) + { + OnPasswordChanged(new IdentityAuditEventArgs(AuditEvent.PasswordChanged) + { + AffectedUser = userId + }); + } + + internal void RaisePasswordResetEvent(int userId) + { + OnPasswordReset(new IdentityAuditEventArgs(AuditEvent.PasswordReset) + { + AffectedUser = userId + }); + } + internal void RaiseResetAccessFailedCountEvent(int userId) + { + OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount) + { + 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.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 94d6116d85..c4c1d58fdc 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -148,6 +148,7 @@ + diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 411d5106e7..ac9e8d0d52 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -56,12 +56,12 @@ namespace Umbraco.Web.Editors /// [WebApi.UmbracoAuthorize(requireApproval: false)] public IDictionary GetMembershipProviderConfig() - { - //TODO: Check if the current PasswordValidator is an IMembershipProviderPasswordValidator, if + { + //TODO: Check if the current PasswordValidator is an IMembershipProviderPasswordValidator, if //it's not than we should return some generic defaults var provider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider(); return provider.GetConfiguration(Services.UserService); - } + } /// /// Checks if a valid token is specified for an invited user and if so logs the user in and returns the user object @@ -76,13 +76,13 @@ namespace Umbraco.Web.Editors public async Task PostVerifyInvite([FromUri]int id, [FromUri]string token) { if (string.IsNullOrWhiteSpace(token)) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); var decoded = token.FromUrlBase64(); if (decoded.IsNullOrWhiteSpace()) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var identityUser = await UserManager.FindByIdAsync(id); + var identityUser = await UserManager.FindByIdAsync(id); if (identityUser == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); @@ -95,15 +95,15 @@ namespace Umbraco.Web.Editors Request.TryGetOwinContext().Result.Authentication.SignOut( Core.Constants.Security.BackOfficeAuthenticationType, - Core.Constants.Security.BackOfficeExternalAuthenticationType); + Core.Constants.Security.BackOfficeExternalAuthenticationType); await SignInManager.SignInAsync(identityUser, false, false); - var user = ApplicationContext.Services.UserService.GetUserById(id); + var user = ApplicationContext.Services.UserService.GetUserById(id); return Mapper.Map(user); - } - + } + [WebApi.UmbracoAuthorize] [ValidateAngularAntiForgeryToken] public async Task PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel) @@ -136,7 +136,7 @@ namespace Umbraco.Web.Editors if (attempt == ValidateRequestAttempt.Success) { return true; - } + } return false; } @@ -151,7 +151,7 @@ namespace Umbraco.Web.Editors /// cookies which means that the auth cookie could be valid but the csrf cookies are no longer there, in that case we need to re-set the csrf cookies. /// [WebApi.UmbracoAuthorize] - [SetAngularAntiForgeryTokens] + [SetAngularAntiForgeryTokens] [CheckIfUserTicketDataIsStale] public UserDetail GetCurrentUser() { @@ -166,7 +166,7 @@ namespace Umbraco.Web.Editors return result; } - + /// /// When a user is invited they are not approved but we need to resolve the partially logged on (non approved) /// user. @@ -175,7 +175,7 @@ namespace Umbraco.Web.Editors /// /// We cannot user GetCurrentUser since that requires they are approved, this is the same as GetCurrentUser but doesn't require them to be approved /// - [WebApi.UmbracoAuthorize(requireApproval:false)] + [WebApi.UmbracoAuthorize(requireApproval: false)] [SetAngularAntiForgeryTokens] public UserDetail GetCurrentInvitedUser() { @@ -185,7 +185,7 @@ namespace Umbraco.Web.Editors { //if they are approved, than they are no longer invited and we can return an error throw new HttpResponseException(Request.CreateUserNoAccessResponse()); - } + } var result = Mapper.Map(user); var httpContextAttempt = TryGetHttpContext(); @@ -197,11 +197,11 @@ namespace Umbraco.Web.Editors return result; } - + //TODO: This should be on the CurrentUserController? [WebApi.UmbracoAuthorize] [ValidateAngularAntiForgeryToken] - public async Task> GetCurrentUserLinkedLogins() + public async Task> GetCurrentUserLinkedLogins() { var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId()); return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); @@ -215,18 +215,22 @@ namespace Umbraco.Web.Editors public async Task PostLogin(LoginModel loginModel) { var http = EnsureHttpContext(); - - //Sign the user in with username/password, this also gives a chance for developers to + + //Sign the user in with username/password, this also gives a chance for developers to //custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker var result = await SignInManager.PasswordSignInAsync( - loginModel.Username, loginModel.Password, isPersistent: true, shouldLockout: true); - + loginModel.Username, loginModel.Password, isPersistent: true, shouldLockout: true); + switch (result) { case SignInStatus.Success: - + //get the user var user = Services.UserService.GetByUsername(loginModel.Username); + + if (UserManager != null) + UserManager.RaiseLoginSuccessEvent(user.Id); + return SetPrincipalAndReturnUserDetail(user); case SignInStatus.RequiresVerification: @@ -235,10 +239,10 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException( Request.CreateErrorResponse( - HttpStatusCode.BadRequest, + HttpStatusCode.BadRequest, "UserManager does not implement " + typeof(IUmbracoBackOfficeTwoFactorOptions))); - } - + } + var twofactorView = twofactorOptions.GetTwoFactorView( TryGetOwinContext().Result, UmbracoContext, @@ -252,24 +256,27 @@ namespace Umbraco.Web.Editors 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 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 }); + if (UserManager != null) + 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 + //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 msg indicating - // that the user doesn't have access to perform this function, we just want to return a normal invalid msg. + // authorized and we don't want to return a 403 because angular will show a warning msg indicating + // that the user doesn't have access to perform this function, we just want to return a normal invalid msg. throw new HttpResponseException(HttpStatusCode.BadRequest); } } @@ -297,16 +304,19 @@ namespace Umbraco.Web.Editors var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id); var callbackUrl = ConstructCallbackUrl(identityUser.Id, code); - var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", - //Ensure the culture of the found user is used for the email! + var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", + //Ensure the culture of the found user is used for the email! UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService), - new[] {identityUser.UserName, callbackUrl}); + new[] { identityUser.UserName, callbackUrl }); await UserManager.SendEmailAsync(identityUser.Id, - Services.TextService.Localize("login/resetPasswordEmailCopySubject", - //Ensure the culture of the found user is used for the email! + Services.TextService.Localize("login/resetPasswordEmailCopySubject", + //Ensure the culture of the found user is used for the email! UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService)), message); + + if (UserManager != null) + UserManager.RaiseForgotPasswordRequestedEvent(user.Id); } } @@ -366,21 +376,27 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent: true, rememberBrowser: false); + var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent: true, rememberBrowser: false); + + var user = Services.UserService.GetByUsername(userName); switch (result) { case SignInStatus.Success: - //get the user - var user = Services.UserService.GetByUsername(userName); + if (UserManager != null) + UserManager.RaiseLoginSuccessEvent(user.Id); + return SetPrincipalAndReturnUserDetail(user); case SignInStatus.LockedOut: - return Request.CreateValidationErrorResponse("User is locked out"); + if (UserManager != null) + UserManager.RaiseAccountLockedEvent(user.Id); + + return Request.CreateValidationErrorResponse("User is locked out"); case SignInStatus.Failure: default: return Request.CreateValidationErrorResponse("Invalid code"); } - } - + } + /// /// Processes a set password request. Validates the request and sets a new password. /// @@ -396,16 +412,16 @@ namespace Umbraco.Web.Editors { Logger.Info( "User {0} is currently locked out, unlocking and resetting AccessFailedCount", - () => model.UserId); + () => model.UserId); //var user = await UserManager.FindByIdAsync(model.UserId); - var unlockResult = await UserManager.SetLockoutEndDateAsync(model.UserId, DateTimeOffset.Now); - if(unlockResult.Succeeded == false) + var unlockResult = await UserManager.SetLockoutEndDateAsync(model.UserId, DateTimeOffset.Now); + if (unlockResult.Succeeded == false) { Logger.Warn("Could not unlock for user {0} - error {1}", - () => model.UserId, () => unlockResult.Errors.First()); - } - + () => model.UserId, () => unlockResult.Errors.First()); + } + var resetAccessFailedCountResult = await UserManager.ResetAccessFailedCountAsync(model.UserId); if (resetAccessFailedCountResult.Succeeded == false) { @@ -414,6 +430,8 @@ namespace Umbraco.Web.Editors } } + if (UserManager != null) + UserManager.RaiseForgotPasswordChangedSuccessEvent(model.UserId); return Request.CreateResponse(HttpStatusCode.OK); } return Request.CreateValidationErrorResponse( @@ -437,10 +455,16 @@ namespace Umbraco.Web.Editors () => User.Identity == null ? "UNKNOWN" : User.Identity.Name, () => TryGetOwinContext().Result.Request.RemoteIpAddress); + if (UserManager != null) + { + var userId = -1; + int.TryParse(User.Identity.GetUserId(), out userId); + UserManager.RaiseLogoutSuccessEvent(userId); + } + return Request.CreateResponse(HttpStatusCode.OK); } - /// /// This is used when the user is auth'd successfully and we need to return an OK with user details along with setting the current Principal in the request /// @@ -468,7 +492,7 @@ namespace Umbraco.Web.Editors // Get an mvc helper to get the url var http = EnsureHttpContext(); var urlHelper = new UrlHelper(http.Request.RequestContext); - var action = urlHelper.Action("ValidatePasswordResetCode", "BackOffice", + var action = urlHelper.Action("ValidatePasswordResetCode", "BackOffice", new { area = GlobalSettings.UmbracoMvcArea, @@ -480,19 +504,19 @@ namespace Umbraco.Web.Editors var applicationUri = new Uri(ApplicationContext.UmbracoApplicationUrl); var callbackUri = new Uri(applicationUri, action); return callbackUri.ToString(); - } - - + } + + private HttpContextBase EnsureHttpContext() { var attempt = this.TryGetHttpContext(); if (attempt.Success == false) throw new InvalidOperationException("This method requires that an HttpContext be active"); return attempt.Result; - } - - - + } + + + private void AddModelErrors(IdentityResult result, string prefix = "") { foreach (var error in result.Errors) diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index f1edc5394b..f75576e1ee 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -89,7 +89,7 @@ namespace Umbraco.Web.Editors /// public async Task> PostChangePassword(ChangingPasswordModel data) { - var passwordChanger = new PasswordChanger(Logger, Services.UserService); + var passwordChanger = new PasswordChanger(Logger, Services.UserService, UmbracoContext.HttpContext); var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, Security.CurrentUser, data, UserManager); if (passwordChangeResult.Success) diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index 88e92c0ad2..7ddf361d23 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using System.Web; using System.Web.Http.ModelBinding; using System.Web.Security; using Microsoft.AspNet.Identity; @@ -22,11 +23,13 @@ namespace Umbraco.Web.Editors { private readonly ILogger _logger; private readonly IUserService _userService; + private readonly HttpContextBase _httpContext; - public PasswordChanger(ILogger logger, IUserService userService) + public PasswordChanger(ILogger logger, IUserService userService, HttpContextBase httpContext) { _logger = logger; _userService = userService; + _httpContext = httpContext; } /// @@ -148,6 +151,20 @@ namespace Umbraco.Web.Editors if (passwordModel == null) throw new ArgumentNullException("passwordModel"); if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); + BackOfficeUserManager backofficeUserManager = null; + var userId = -1; + + if (membershipProvider.IsUmbracoUsersProvider()) + { + backofficeUserManager = _httpContext.GetOwinContext().GetBackOfficeUserManager(); + if (backofficeUserManager != null) + { + var profile = _userService.GetProfileByUserName(username); + if (profile != null) + int.TryParse(profile.Id.ToString(), out userId); + } + } + //Are we resetting the password?? if (passwordModel.Reset.HasValue && passwordModel.Reset.Value) { @@ -167,6 +184,9 @@ namespace Umbraco.Web.Editors username, membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null); + if (membershipProvider.IsUmbracoUsersProvider() && backofficeUserManager != null && userId >= 0) + backofficeUserManager.RaisePasswordResetEvent(userId); + //return the generated pword return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass }); } @@ -217,6 +237,10 @@ namespace Umbraco.Web.Editors try { var result = membershipProvider.ChangePassword(username, passwordModel.OldPassword, passwordModel.NewPassword); + + if (result && backofficeUserManager != null && userId >= 0) + backofficeUserManager.RaisePasswordChangedEvent(userId); + return result == false ? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "oldPassword" }) }) : Attempt.Succeed(new PasswordChangedModel()); @@ -266,6 +290,5 @@ namespace Umbraco.Web.Editors return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex2.Message + " (see log for full details)", new[] { "value" }) }); } } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 45fce0e33f..665d86c5e2 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -557,7 +557,7 @@ namespace Umbraco.Web.Editors if (userSave.ChangePassword != null) { - var passwordChanger = new PasswordChanger(Logger, Services.UserService); + var passwordChanger = new PasswordChanger(Logger, Services.UserService, UmbracoContext.HttpContext); var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, found, userSave.ChangePassword, UserManager); if (passwordChangeResult.Success) diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index 54b4d06ec5..eeb9bad52b 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -39,7 +39,7 @@ namespace Umbraco.Web.Security [EditorBrowsable(EditorBrowsableState.Never)] public MembershipHelper(ApplicationContext applicationContext, HttpContextBase httpContext) : this(applicationContext, httpContext, MPE.GetMembersMembershipProvider(), Roles.Enabled ? Roles.Provider : new MembersRoleProvider(applicationContext.Services.MemberService)) - { + { } [Obsolete("Use the constructor specifying an UmbracoContext")] @@ -54,11 +54,11 @@ namespace Umbraco.Web.Security _httpContext = httpContext; _membershipProvider = membershipProvider; _roleProvider = roleProvider; - } + } public MembershipHelper(UmbracoContext umbracoContext) - : this(umbracoContext, MPE.GetMembersMembershipProvider(), Roles.Enabled ? Roles.Provider: new MembersRoleProvider(umbracoContext.Application.Services.MemberService)) - { + : this(umbracoContext, MPE.GetMembersMembershipProvider(), Roles.Enabled ? Roles.Provider : new MembersRoleProvider(umbracoContext.Application.Services.MemberService)) + { } public MembershipHelper(UmbracoContext umbracoContext, MembershipProvider membershipProvider, RoleProvider roleProvider) @@ -117,11 +117,11 @@ namespace Umbraco.Web.Security /// private bool HasAccess(string path, RoleProvider roleProvider) { - return _umbracoContext.PublishedContentRequest == null - ? _applicationContext.Services.PublicAccessService.HasAccess(path, CurrentUserName, roleProvider.GetRolesForUser) + return _umbracoContext.PublishedContentRequest == null + ? _applicationContext.Services.PublicAccessService.HasAccess(path, CurrentUserName, roleProvider.GetRolesForUser) : _applicationContext.Services.PublicAccessService.HasAccess(path, CurrentUserName, _umbracoContext.PublishedContentRequest.GetRolesForLogin); } - + /// /// Returns true if the current membership provider is the Umbraco built-in one. /// @@ -253,7 +253,7 @@ namespace Umbraco.Web.Security { //Set member online provider.GetUser(model.Username, true); - + //Log them in FormsAuthentication.SetAuthCookie(membershipUser.UserName, model.CreatePersistentLoginCookie); } @@ -280,7 +280,7 @@ namespace Umbraco.Web.Security if (member == null) { //this should not happen - LogHelper.Warn("The member validated but then no member was returned with the username " + username); + LogHelper.Warn("The member validated but then no member was returned with the username " + username); return false; } //Log them in @@ -389,7 +389,7 @@ namespace Umbraco.Web.Security var result = GetCurrentMember(); return result == null ? -1 : result.Id; } - + #endregion #region Model Creation methods for member data editing on the front-end @@ -408,7 +408,7 @@ namespace Umbraco.Web.Security var provider = _membershipProvider; if (provider.IsUmbracoMembershipProvider()) - { + { var membershipUser = provider.GetCurrentUserOnline(); var member = GetCurrentPersistedMember(); //this shouldn't happen but will if the member is deleted in the back office while the member is trying @@ -496,7 +496,7 @@ namespace Umbraco.Web.Security if (propValue != null && propValue.Value != null) { value = propValue.Value.ToString(); - } + } } var viewProperty = new UmbracoProperty @@ -636,7 +636,7 @@ namespace Umbraco.Web.Security // Allow by default var allowAction = true; - + if (IsLoggedIn() == false) { // If not logged on, not allowed @@ -671,7 +671,7 @@ namespace Umbraco.Web.Security var member = provider.GetCurrentUser(); username = member.UserName; } - + // If groups defined, check member is of one of those groups var allowGroupsList = allowGroups as IList ?? allowGroups.ToList(); if (allowAction && allowGroupsList.Any(allowGroup => allowGroup != string.Empty)) @@ -681,7 +681,7 @@ namespace Umbraco.Web.Security allowAction = allowGroupsList.Select(s => s.ToLowerInvariant()).Intersect(groups.Select(myGroup => myGroup.ToLowerInvariant())).Any(); } - + } return allowAction; @@ -701,6 +701,7 @@ namespace Umbraco.Web.Security { throw new InvalidOperationException("Could not find provider with name " + membershipProviderName); } + return ChangePassword(username, passwordModel, provider); } @@ -713,10 +714,10 @@ namespace Umbraco.Web.Security /// public virtual Attempt ChangePassword(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider) { - var passwordChanger = new PasswordChanger(_applicationContext.ProfilingLogger.Logger, _applicationContext.Services.UserService); - return passwordChanger.ChangePasswordWithMembershipProvider(username, passwordModel, membershipProvider); + var passwordChanger = new PasswordChanger(_applicationContext.ProfilingLogger.Logger, _applicationContext.Services.UserService, UmbracoContext.Current.HttpContext); + return passwordChanger.ChangePasswordWithMembershipProvider(username, passwordModel, membershipProvider); } - + /// /// Updates a membership user with all of it's writable properties /// @@ -775,7 +776,7 @@ namespace Umbraco.Web.Security return Attempt.Fail(member); } - + /// /// Returns the currently logged in IMember object - this should never be exposed to the front-end since it's returning a business logic entity! /// @@ -799,7 +800,7 @@ namespace Umbraco.Web.Security private string GetCacheKey(string key, params object[] additional) { - var sb = new StringBuilder(string.Format("{0}-{1}", typeof (MembershipHelper).Name, key)); + var sb = new StringBuilder(string.Format("{0}-{1}", typeof(MembershipHelper).Name, key)); foreach (var s in additional) { sb.Append("-"); diff --git a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs index a3618010b6..e0f7bf9a7f 100644 --- a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Specialized; using System.Configuration.Provider; using System.Linq; using System.Text; +using System.Web; using System.Web.Configuration; using System.Web.Security; using Umbraco.Core; @@ -12,6 +13,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Security; using Umbraco.Core.Services; +using Umbraco.Core.Models.Identity; namespace Umbraco.Web.Security.Providers { @@ -449,22 +451,15 @@ namespace Umbraco.Web.Security.Providers /// public override bool UnlockUser(string username) { - var member = MemberService.GetByUsername(username); + var userManager = GetBackofficeUserManager(); + return userManager != null && userManager.UnlockUser(MemberService, username); + } - if (member == null) - { - throw new ProviderException(string.Format("No member with the username '{0}' found", username)); - } - - // Non need to update - if (member.IsLockedOut == false) return true; - - member.IsLockedOut = false; - member.FailedPasswordAttempts = 0; - - MemberService.Save(member); - - return true; + internal BackOfficeUserManager GetBackofficeUserManager() + { + return HttpContext.Current == null + ? null + : HttpContext.Current.GetOwinContext().GetBackOfficeUserManager(); } /// @@ -547,6 +542,7 @@ namespace Umbraco.Web.Security.Providers } var authenticated = CheckPassword(password, member.RawPasswordValue); + var backofficeUserManager = GetBackofficeUserManager(); if (authenticated == false) { @@ -566,6 +562,9 @@ namespace Umbraco.Web.Security.Providers "Login attempt failed for username {0} from IP address {1}, the user is now locked out, max invalid password attempts exceeded", username, GetCurrentRequestIpAddress())); + + if(backofficeUserManager != null) + backofficeUserManager.RaiseAccountLockedEvent(member.Id); } else { @@ -578,7 +577,14 @@ namespace Umbraco.Web.Security.Providers } else { - member.FailedPasswordAttempts = 0; + if (member.FailedPasswordAttempts > 0) + { + //we have successfully logged in, reset the AccessFailedCount + member.FailedPasswordAttempts = 0; + if (backofficeUserManager != null) + backofficeUserManager.RaiseResetAccessFailedCountEvent(member.Id); + } + member.LastLoginDate = DateTime.Now; LogHelper.Info( @@ -586,6 +592,9 @@ namespace Umbraco.Web.Security.Providers "Login attempt succeeded for username {0} from IP address {1}", username, GetCurrentRequestIpAddress())); + + if (backofficeUserManager != null) + backofficeUserManager.RaiseLoginSuccessEvent(member.Id); } //don't raise events for this! It just sets the member dates, if we do raise events this will @@ -600,10 +609,5 @@ namespace Umbraco.Web.Security.Providers return authenticated; } - - - - - } } \ No newline at end of file