Account lockout - Scala

Account lockout - Scala

Need

Prevention of unauthorized account lockouts

Context

  • Usage of Scala for building scalable and high-performance applications
  • Usage of play.api.mvc for handling HTTP requests and responses in Play Framework
  • Usage of global execution context for Scala concurrent programming

Description

Non compliant code

        import play.api.mvc._
import scala.concurrent.ExecutionContext.Implicits.global

class UserController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {

  def login = Action.async { implicit request =>
    val form = request.body.asFormUrlEncoded
    val username = form.get("username")(0)
    val password = form.get("password")(0)
    
    UserService.authenticate(username, password).map {
      case Some(user) => Ok("Logged in!")
      case None => Unauthorized("Invalid credentials")
    }
  }
}
        
        

The above code demonstrates a simple login function in a Scala backend application using the Play framework. The login function is an asynchronous function that expects a username and password from the form data of the request body.

The function then calls the authenticate method from the UserService object, which checks if the provided credentials are valid. If the credentials are valid, the method returns Some(user), otherwise it returns None.

The problem with this code is that it does not implement any account lockout feature. This means that an attacker can try an unlimited number of different combinations of usernames and passwords until they find one that works, a type of attack known as a brute force attack.

Without an account lockout mechanism, the system is vulnerable to such attacks. The account lockout mechanism should lock a user account after a certain number of failed login attempts, effectively blocking any further attempts to login, whether they are legitimate or not. This would protect user accounts from being compromised through brute force attacks.

Steps

  • Implement account lockout mechanism
  • Limit the number of login attempts
  • Implement a delay between login attempts
  • Notify the user about account lockout

Compliant code

        import play.api.mvc._
import scala.concurrent.ExecutionContext.Implicits.global

class UserController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
  private val MAX_ATTEMPTS = 5
  private val LOCKOUT_TIME = 300000 // 5 minutes in milliseconds
  private var failedAttempts = Map[String, (Int, Long)]()

  def login = Action.async { implicit request =>
    val form = request.body.asFormUrlEncoded
    val username = form.get("username")(0)
    val password = form.get("password")(0)

    failedAttempts.get(username) match {
      case Some((attempts, lockoutTime)) if attempts >= MAX_ATTEMPTS && System.currentTimeMillis() - lockoutTime < LOCKOUT_TIME =>
        Future.successful(Forbidden("Account locked. Please try again later."))
      case _ =>
        UserService.authenticate(username, password).map {
          case Some(user) =>
            failedAttempts -= username
            Ok("Logged in!")
          case None =>
            val attempts = failedAttempts.getOrElse(username, (0, System.currentTimeMillis()))._1
            failedAttempts += (username -> (attempts + 1, System.currentTimeMillis()))
            Unauthorized("Invalid credentials")
        }
    }
  }
}
        
        

The updated code introduces an account lockout mechanism to prevent brute force attacks.

A failedAttempts map is used to keep track of the number of failed login attempts for each user and the time of the last failed attempt.

When a login request is received, the code first checks if the user has exceeded the maximum number of allowed attempts ( MAX_ATTEMPTS) and if the lockout time ( LOCKOUT_TIME) has not yet passed. If both conditions are met, the user is locked out and receives a "Account locked. Please try again later." message.

If the user has not been locked out, the code proceeds to authenticate the user. If the authentication is successful, the user's failed attempts are reset. If the authentication fails, the number of failed attempts for the user is incremented and the time of the last failed attempt is updated.

This mechanism effectively limits the number of login attempts and implements a delay between login attempts, thus mitigating the risk of account lockout attacks.

References