I would like to create some kind of user lockout feature after 5 or so unsuccessful login attempts. Do you have any suggestions on how to do this?
5 Answer(s)
-
0
For example, I would like to do something like this. <a class="postlink" href="http://www.jlum.ws/post/2014/5/27/user-lockouts-in-aspnet-identity-2-with-aspnet-mvc-5">http://www.jlum.ws/post/2014/5/27/user- ... pnet-mvc-5</a>
Since you use Asp.net Identity it should be possible. But I am confused on one part. In that article the three lines they add to the IdentityConfig.cs file I am wondering where something like that would go in Asp.net Zero project.
-
0
Hi,
For our solution, you can set these properties in the UserManager's constructor. But I suppose you also need to implement IUserLockoutStore<User, long> for UserStore to make it working.
Actually, we see that this feature is pretty useful. We may consider to add this into module-zero by default. I created an issue for that: <a class="postlink" href="https://github.com/aspnetboilerplate/module-zero/issues/192">https://github.com/aspnetboilerplate/mo ... issues/192</a>
You can either implement it yourself or wait us to make it (but we can not provide a deadline yet). As better, you can contribute it :)
-
0
Awesome to hear. I am going to try and implement it myself as I am going through making other security enhancements.
-
0
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); }
-
0
Thank you very much for your solution and detailed explanation :)