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