Merge remote-tracking branch 'origin/dev-v7' into dev-v7.7

# Conflicts:
#	src/Umbraco.Core/Security/BackOfficeSignInManager.cs
#	src/Umbraco.Core/Security/BackOfficeUserManager.cs
#	src/Umbraco.Web/Editors/AuthenticationController.cs
#	src/Umbraco.Web/Security/MembershipHelper.cs
#	src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs
This commit is contained in:
Sebastiaan Janssen
2017-09-15 16:34:51 +02:00
10 changed files with 528 additions and 106 deletions

View File

@@ -0,0 +1,106 @@
using System;
using System.Threading;
using System.Web;
using Umbraco.Core.Security;
namespace Umbraco.Core.Auditing
{
/// <summary>
/// This class is used by events raised from hthe BackofficeUserManager
/// </summary>
public class IdentityAuditEventArgs : EventArgs
{
/// <summary>
/// The action that got triggered from the audit event
/// </summary>
public AuditEvent Action { get; set; }
/// <summary>
/// Current date/time in UTC format
/// </summary>
public DateTime DateTimeUtc { get; private set; }
/// <summary>
/// The source IP address of the user performing the action
/// </summary>
public string IpAddress { get; set; }
/// <summary>
/// The user affected by the event raised
/// </summary>
public int AffectedUser { get; set; }
/// <summary>
/// If a user is perfoming an action on a different user, then this will be set. Otherwise it will be -1
/// </summary>
public int PerformingUser { get; set; }
/// <summary>
/// An optional comment about the action being logged
/// </summary>
public string Comment { get; set; }
/// <summary>
/// This property is always empty except in the LoginFailed event for an unknown user trying to login
/// </summary>
public string Username { get; set; }
/// <summary>
/// Sets the properties on the event being raised, all parameters are optional except for the action being performed
/// </summary>
/// <param name="action">An action based on the AuditEvent enum</param>
/// <param name="ipAddress">The client's IP address. This is usually automatically set but could be overridden if necessary</param>
/// <param name="performingUser">The Id of the user performing the action (if different from the user affected by the action)</param>
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;
}
/// <summary>
/// Returns the current request IP address for logging if there is one
/// </summary>
/// <returns></returns>
protected string GetCurrentRequestIpAddress()
{
var httpContext = HttpContext.Current == null ? (HttpContextBase)null : new HttpContextWrapper(HttpContext.Current);
return httpContext.GetCurrentRequestIpAddress();
}
/// <summary>
/// Returns the current logged in backoffice user's Id logging if there is one
/// </summary>
/// <returns></returns>
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
}
}

View File

@@ -49,7 +49,7 @@ namespace Umbraco.Core.Security
public override async Task<SignInStatus> 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,

View File

@@ -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<IdentityResult> 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;
}
/// <summary>
@@ -484,5 +490,241 @@ namespace Umbraco.Core.Security
}
#endregion
public override Task<IdentityResult> 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<IdentityResult> 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);
}
/// <summary>
/// Clears a lock so that the membership user can be validated.
/// </summary>
/// <param name="memberService">The IMemberService to user for unlocking</param>
/// <param name="username">The membership user to clear the lock status for.</param>
/// <returns>
/// true if the membership user was successfully unlocked; otherwise, false.
/// </returns>
public bool UnlockUser<TEntity>(IMembershipMemberService<TEntity> 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<IdentityResult> 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);
}
}
}

View File

@@ -148,6 +148,7 @@
<Compile Include="Attempt{T}.cs" />
<Compile Include="Auditing\Audit.cs" />
<Compile Include="Auditing\AuditTypes.cs" />
<Compile Include="Auditing\IdentityAuditEventArgs.cs" />
<Compile Include="BindingRedirects.cs" />
<Compile Include="ByteArrayExtensions.cs" />
<Compile Include="CacheHelper.cs" />

View File

@@ -56,12 +56,12 @@ namespace Umbraco.Web.Editors
/// <returns></returns>
[WebApi.UmbracoAuthorize(requireApproval: false)]
public IDictionary<string, object> 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);
}
}
/// <summary>
/// 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<UserDisplay> 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<UserDisplay>(user);
}
}
[WebApi.UmbracoAuthorize]
[ValidateAngularAntiForgeryToken]
public async Task<HttpResponseMessage> 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.
/// </remarks>
[WebApi.UmbracoAuthorize]
[SetAngularAntiForgeryTokens]
[SetAngularAntiForgeryTokens]
[CheckIfUserTicketDataIsStale]
public UserDetail GetCurrentUser()
{
@@ -166,7 +166,7 @@ namespace Umbraco.Web.Editors
return result;
}
/// <summary>
/// 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
/// <remarks>
/// We cannot user GetCurrentUser since that requires they are approved, this is the same as GetCurrentUser but doesn't require them to be approved
/// </remarks>
[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<UserDetail>(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<Dictionary<string, string>> GetCurrentUserLinkedLogins()
public async Task<Dictionary<string, string>> 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<HttpResponseMessage> 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");
}
}
}
/// <summary>
/// Processes a set password request. Validates the request and sets a new password.
/// </summary>
@@ -396,16 +412,16 @@ namespace Umbraco.Web.Editors
{
Logger.Info<AuthenticationController>(
"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<AuthenticationController>("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);
}
/// <summary>
/// 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
/// </summary>
@@ -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)

View File

@@ -89,7 +89,7 @@ namespace Umbraco.Web.Editors
/// </returns>
public async Task<ModelWithNotifications<string>> 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)

View File

@@ -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;
}
/// <summary>
@@ -148,6 +151,20 @@ namespace Umbraco.Web.Editors
if (passwordModel == null) throw new ArgumentNullException("passwordModel");
if (membershipProvider == null) throw new ArgumentNullException("membershipProvider");
BackOfficeUserManager<BackOfficeIdentityUser> 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" }) });
}
}
}
}

View File

@@ -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)

View File

@@ -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
/// </remarks>
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);
}
/// <summary>
/// Returns true if the current membership provider is the Umbraco built-in one.
/// </summary>
@@ -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<MembershipHelper>("The member validated but then no member was returned with the username " + username);
LogHelper.Warn<MembershipHelper>("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<string> ?? 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
/// <returns></returns>
public virtual Attempt<PasswordChangedModel> 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);
}
/// <summary>
/// Updates a membership user with all of it's writable properties
/// </summary>
@@ -775,7 +776,7 @@ namespace Umbraco.Web.Security
return Attempt<MembershipUser>.Fail(member);
}
/// <summary>
/// Returns the currently logged in IMember object - this should never be exposed to the front-end since it's returning a business logic entity!
/// </summary>
@@ -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("-");

View File

@@ -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
/// </returns>
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<BackOfficeIdentityUser> GetBackofficeUserManager()
{
return HttpContext.Current == null
? null
: HttpContext.Current.GetOwinContext().GetBackOfficeUserManager();
}
/// <summary>
@@ -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<UmbracoMembershipProviderBase>(
@@ -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;
}
}
}