Account lockout - C-Sharp

Account lockout - C-Sharp

Need

Prevention of unauthorized account lockouts

Context

  • Usage of C# 7.0 for modern language features and enhancements
  • Usage of Microsoft.AspNetCore.Mvc for building web applications using the ASP.NET Core MVC framework
  • Usage of Microsoft.AspNetCore.Identity for managing user authentication and authorization in ASP.NET Core applications

Description

Non compliant code

        public class AccountController : Controller
{
    private readonly UserManager _userManager;

    public AccountController(UserManager userManager)
    {
        _userManager = userManager;
    }

    [HttpPost]
    public async Task Login(LoginViewModel model)
    {
        if (ModelState.IsValid)
        {
            var user = await _userManager.FindByNameAsync(model.Email);
            if (user != null)
            {
                var result = await _userManager.CheckPasswordAsync(user, model.Password);
                if (result)
                {
                    // User authenticated successfully, handle accordingly
                }
                else
                {
                    // User authentication failed, handle accordingly
                }
            }
        }

        return View(model);
    }
}
        
        

In the above code, when a user tries to log in, the application checks if the user exists and then checks if the password is correct. If the password is not correct, the application simply returns the view without taking any further actions.

This is a vulnerability because an attacker could use this to cause a denial of service (DoS) attack by continuously sending incorrect passwords for a user. Since the application does not limit the number of failed login attempts, the attacker could effectively lock out the user from their account by making the system believe that the user has forgotten their password. This is known as an Account Lockout vulnerability.

The vulnerability exists in the Login method of the AccountController class. When the password check fails ( var result = await _userManager.CheckPasswordAsync(user, model.Password);), the application does not increment a counter or set a lockout end date for the user. As a result, an attacker could continuously send login requests with incorrect passwords without being blocked by the application.

Steps

  • Implement account lockout policy
  • Track failed login attempts
  • Increment failed login attempts counter
  • Lock user account after a certain number of failed attempts
  • Reset failed login attempts counter after successful login

Compliant code

        public class AccountController : Controller
{
    private readonly UserManager _userManager;

    public AccountController(UserManager userManager)
    {
        _userManager = userManager;
    }

    [HttpPost]
    public async Task Login(LoginViewModel model)
    {
        if (ModelState.IsValid)
        {
            var user = await _userManager.FindByNameAsync(model.Email);
            if (user != null)
            {
                var result = await _userManager.CheckPasswordAsync(user, model.Password);
                if (result.Succeeded)
                {
                    // User authenticated successfully, handle accordingly
                    // Reset the account lockout count
                    await _userManager.ResetAccessFailedCountAsync(user);
                }
                else
                {
                    // User authentication failed, handle accordingly
                    // Increment the account lockout count
                    await _userManager.AccessFailedAsync(user);
                    if (await _userManager.IsLockedOutAsync(user))
                    {
                        // The user is locked out, handle accordingly
                    }
                }
            }
        }

        return View(model);
    }
}
        
        

The updated code now includes an account lockout policy. This policy is implemented by tracking failed login attempts and locking the user account after a certain number of failed attempts.

When a user attempts to login, the CheckPasswordAsync method is called to verify the user's credentials. If the credentials are valid, the ResetAccessFailedCountAsync method is called to reset the count of failed login attempts for the user.

If the credentials are not valid, the AccessFailedAsync method is called to increment the count of failed login attempts for the user. After this, the IsLockedOutAsync method is called to check if the user is locked out. If the user is locked out, appropriate action is taken.

This approach ensures that users are locked out after a certain number of consecutive failed login attempts, thereby mitigating the risk of account lockout attacks.

References