Base solution for your next web application
Open Closed

Lockout after 5 login attempts #1101


User avatar
0
joe704la created

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)
  • User Avatar
    0
    joe704la created

    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.

  • User Avatar
    0
    hikalkan created
    Support Team

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

  • User Avatar
    0
    joe704la created

    Awesome to hear. I am going to try and implement it myself as I am going through making other security enhancements.

  • User Avatar
    0
    joe704la created

    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);
                        }
    
  • User Avatar
    0
    ismcagdas created
    Support Team

    Thank you very much for your solution and detailed explanation :)