I was able to get this working a few months back and forgot to post how I was able to do it.
in Core/Authorization/Users folder I edited User.cs with the following...
public virtual bool LockoutEnabled { get; set; }
public virtual int AccessFailedCount { get; set; }
public virtual DateTime? LockoutEndDateUtc { get; set; }
Then in UserStore the following ...
public class UserStore : AbpUserStore<Tenant, Role, User>, IUserLockoutStore<User, long>
{
private readonly IRepository<User, long> _userRepository;
public UserStore(
IRepository<User, long> userRepository,
IRepository<UserLogin, long> userLoginRepository,
IRepository<UserRole, long> userRoleRepository,
IRepository<Role> roleRepository,
IRepository<UserPermissionSetting, long> userPermissionSettingRepository,
IUnitOfWorkManager unitOfWorkManager
)
: base(
userRepository,
userLoginRepository,
userRoleRepository,
roleRepository,
userPermissionSettingRepository,
unitOfWorkManager
)
{
_userRepository = userRepository;
}
/// <summary>
/// Get user lock out end date
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task<DateTimeOffset> GetLockoutEndDateAsync(User user)
{
return
Task.FromResult(user.LockoutEndDateUtc.HasValue
? new DateTimeOffset(DateTime.SpecifyKind(user.LockoutEndDateUtc.Value, DateTimeKind.Utc))
: new DateTimeOffset());
}
/// <summary>
/// Set user lockout end date
/// </summary>
/// <param name="user"></param>
/// <param name="lockoutEnd"></param>
/// <returns></returns>
public Task SetLockoutEndDateAsync(User user, DateTimeOffset lockoutEnd)
{
user.LockoutEndDateUtc = lockoutEnd.UtcDateTime;
_userRepository.Update(user);
return Task.FromResult(0);
}
/// <summary>
/// Increment failed access count
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public async Task<int> IncrementAccessFailedCountAsync(User user)
{
user.AccessFailedCount++;
_userRepository.Update(user);
return await Task.FromResult(user.AccessFailedCount);
}
/// <summary>
/// Reset failed access count
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task ResetAccessFailedCountAsync(User user)
{
user.AccessFailedCount = 0;
_userRepository.Update(user);
return Task.FromResult(0);
}
/// <summary>
/// Get failed access count
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task<int> GetAccessFailedCountAsync(User user)
{
return Task.FromResult(user.AccessFailedCount);
}
/// <summary>
/// Get if lockout is enabled for the user
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task<bool> GetLockoutEnabledAsync(User user)
{
return Task.FromResult(user.LockoutEnabled);
}
/// <summary>
/// Set lockout enabled for user
/// </summary>
/// <param name="user"></param>
/// <param name="enabled"></param>
/// <returns></returns>
public Task SetLockoutEnabledAsync(User user, bool enabled)
{
user.LockoutEnabled = enabled;
_userRepository.Update(user);
return Task.FromResult(0);
}
}
In the Application/Authorization/Users folder, I added this to the UserAppService.cs in the CreateUserAsync method under the line user.ShouldChangePasswordOnNextLogin = input.User.ShouldChangePasswordOnNextLogin; ...
//Always enable user Lockout module
user.LockoutEnabled = true;
Then in the Web/Controllers/AccountController.cs file, I made a bunch of changes. In the constructor, I added...
_userManager.UserLockoutEnabledByDefault = Convert.ToBoolean(ConfigurationManager.AppSettings["UserLockoutEnabledByDefault"]);
_userManager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(double.Parse(ConfigurationManager.AppSettings["DefaultAccountLockoutTimeSpan"]));
_userManager.MaxFailedAccessAttemptsBeforeLockout = Convert.ToInt32(ConfigurationManager.AppSettings["MaxFailedAccessAttemptsBeforeLockout"]);
Then in the Login method here is my entire Login method. There may be some better ways to do this but this worked for my particular need.
[HttpPost]
[Honeypot]
[UnitOfWork]
[ValidateAntiForgeryToken]
[DisableAuditing]
public virtual async Task<JsonResult> Login(LoginViewModel loginModel, string returnUrl = "", string returnUrlHash = "")
{
CheckModelState();
var user = await _userManager.FindByNameAsync(loginModel.UsernameOrEmailAddress);
if (user != null)
{
string errorMessage = null;
var validCredentials = await _userManager.FindAsync(loginModel.UsernameOrEmailAddress, loginModel.Password);
if (await _userManager.IsLockedOutAsync(user.Id))
{
errorMessage = string.Format("Your account has been locked out for {0} minutes due to multiple failed login attempts.", ConfigurationManager.AppSettings["DefaultAccountLockoutTimeSpan"]);
throw new UserFriendlyException(errorMessage);
}
else if (await _userManager.GetLockoutEnabledAsync(user.Id) && validCredentials == null)
{
await _userManager.AccessFailedAsync(user.Id);
if (await _userManager.IsLockedOutAsync(user.Id))
{
errorMessage = string.Format("Your account has been locked out for {0} minutes due to multiple failed login attempts.", ConfigurationManager.AppSettings["DefaultAccountLockoutTimeSpan"]);
return Json(new MvcAjaxResponse { Success = false, Error = new ErrorInfo(errorMessage) });
}
else
{
var accessFailedCount = await _userManager.GetAccessFailedCountAsync(user.Id);
var attemptsLeft =
Convert.ToInt32(
ConfigurationManager.AppSettings["MaxFailedAccessAttemptsBeforeLockout"]) -
accessFailedCount;
errorMessage = string.Format("Invalid credentials. You have {0} more attempt(s) before your account gets locked out.", attemptsLeft);
return Json(new MvcAjaxResponse { Success = false, Error = new ErrorInfo(errorMessage) });
}
}
else if (validCredentials == null)
{
ModelState.AddModelError("", "Invalid credentials. Please try again.");
errorMessage = string.Format("Invalid credentials.Please try again.");
throw new UserFriendlyException(errorMessage);
}
else
{
_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant);
var loginResult = await GetLoginResultAsync(loginModel.UsernameOrEmailAddress, loginModel.Password, loginModel.TenancyName);
if (loginResult.User.ShouldChangePasswordOnNextLogin)
{
loginResult.User.SetNewPasswordResetCode();
return Json(new MvcAjaxResponse
{
TargetUrl = Url.Action(
"ResetPassword",
new ResetPasswordViewModel
{
UserId = SimpleStringCipher.Encrypt(loginResult.User.Id.ToString()),
ResetCode = loginResult.User.PasswordResetCode
})
});
}
await SignInAsync(loginResult.User, loginResult.Identity, loginModel.RememberMe);
await _userManager.ResetAccessFailedCountAsync(user.Id);
if (string.IsNullOrWhiteSpace(returnUrl))
{
returnUrl = Url.Action("Index", "Application");
}
if (!string.IsNullOrWhiteSpace(returnUrlHash))
{
returnUrl = returnUrl + returnUrlHash;
}
return Json(new MvcAjaxResponse { TargetUrl = returnUrl});
}
}
return Json(new MvcAjaxResponse { TargetUrl = returnUrl });
}
Then in the Web.config file I added these...
<add key="UserLockoutEnabledByDefault" value="true" />
<add key="DefaultAccountLockoutTimeSpan" value="15" />
<add key="MaxFailedAccessAttemptsBeforeLockout" value="3" />
Finally in the Web/Views/Account folder in the Login.js file I added to the loginForm.submit function the below directly after the data: $loginForm.serialize(), line ...
error: function(message) {
abp.message.error(message.error);
}
Good call, that worked. Thanks
I have Visual Studio 2015 update 3.
Thats a good idea. I do like the expiring idea for a future release but for now the permissions based way should work good.
Thank you
Thank you
Thank you, I haven't heard of Table-Per-Hierarchy approach. Do you know of any good articles that explain it?
Okay, so I am guessing in the AppNotifier I would just query the users table in the notification method and then check if each user is subscribed to this notification and then send them the email?
I was looking at that, which did seem like a great way to do it. I just ended up doing it a little differently. In the Login method instead of throwing an exception I just returned a MVCAjaxResponse with Success = false. That way I was able to return an error pretty easily and then catch it in the Ajax call back and display the error message that way.
I use it in the AccountController in the Login method. I am creating a user lockout functionality on 3 failed login attempts.
I actually think I figured something out reading the ASPNET Boilerplate documentation on Unit of Work. I didn't realize that if an exception is thrown that it rolls back the database transaction.
In the Login method when a user failed to provide the correct password I returned a UserFriendlyException with the message of how many login attempts they have left. I haven't tested this theory yet but I am guessing if I change how I am returning the error message it will work. I just need to figure out a better way to return the error.
I am first going to try the [UnitOfWork] attribute but the way the documentation sounded I am not going to be able to do that in the UserStore. I just liked how easy it was to return an error message to the popup error modal using a UserFriendlyException.
Does anyone have any thoughts on this? I am completely stuck. I also tried using the UpdateAsync method that is in the AbpUserStore. That also didn't work. It just doesn't ever update the database. Here is an example that isn't working in the UserStore.
/// <summary>
/// Increment failed access count
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task<int> IncrementAccessFailedCountAsync(User user)
{
user.AccessFailedCount++;
//Example of an update not working.
UpdateAsync(user);
//Also tried this
//_userRepository.UpdateAsync(user);
return Task.FromResult(user.AccessFailedCount);
}