Block repeated login attempts by IP address

Repeated login attempts are a common way to attack password-protected applications. PHPRunner and ASPRunner.NET include a built-in option to lock a user account after three unsuccessful login attempts. If that is all you need, enable Lock user account after three unsuccessful logins in the audit and locking settings.

This article demonstrates a different approach: temporarily blocking login attempts from the same IP address. After three unsuccessful attempts from the same IP address, login access is blocked for 30 minutes.

Note: IP-based blocking is not the same as locking a user account. Multiple users may share the same public IP address, and applications running behind a proxy or load balancer may require additional configuration to determine the actual client IP address.

1. Create the LoginAttempts table

Create a table named LoginAttempts to store the number and time of unsuccessful login attempts for each IP address.

Microsoft SQL Server

CREATE TABLE LoginAttempts
(
	IP VARCHAR(45) NOT NULL PRIMARY KEY,
	Attempts INT NOT NULL DEFAULT 0,
	LastLogin DATETIME NULL
);

MySQL

CREATE TABLE LoginAttempts
(
	IP VARCHAR(45) NOT NULL PRIMARY KEY,
	Attempts INT NOT NULL DEFAULT 0,
	LastLogin DATETIME NULL
);

The IP field is 45 characters long so it can store both IPv4 and IPv6 addresses.

2. Configure application security

Open your PHPRunner or ASPRunner.NET project and configure the normal login security settings for your application.

3. Add the login events

Open the Events screen and add the following three global Login page events:

  • Before Login
  • After Successful Login
  • After Unsuccessful Login

Before Login

Before validating the username and password, check whether this IP address has reached the limit. If three unsuccessful attempts occurred within the last 30 minutes, stop the login process.

PHPRunner

$ip = $_SERVER["REMOTE_ADDR"];

$keys = array();
$keys["IP"] = $ip;

$rs = DB::Select("LoginAttempts", $keys);
$data = $rs->fetchAssoc();

if ($data && intval($data["Attempts"]) >= 3)
{
	$lastAttempt = strtotime($data["LastLogin"]);

	if ($lastAttempt && time() - $lastAttempt < 30 * 60)
	{
		$message = "Access denied for 30 minutes";
		return false;
	}

	$values = array();
	$values["Attempts"] = 0;

	DB::Update("LoginAttempts", $values, $keys);
}

return true;

ASPRunner.NET

string ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];

dynamic keys = XVar.Array();
keys["IP"] = ip;

dynamic rs = DB.Select("LoginAttempts", keys);
dynamic data = rs.fetchAssoc();

if (data && Convert.ToInt32(data["Attempts"]) >= 3)
{
	DateTime lastAttempt = Convert.ToDateTime(data["LastLogin"]);

	if ((DateTime.Now - lastAttempt).TotalMinutes < 30)
	{
		message = "Access denied for 30 minutes";
		return false;
	}

	dynamic values = XVar.Array();
	values["Attempts"] = 0;

	DB.Update("LoginAttempts", values, keys);
}

return true;

After Successful Login

After a successful login, reset the failed-attempt counter for the current IP address.

PHPRunner

$ip = $_SERVER["REMOTE_ADDR"];

$keys = array();
$keys["IP"] = $ip;

$values = array();
$values["Attempts"] = 0;

DB::Update("LoginAttempts", $values, $keys);

ASPRunner.NET

string ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];

dynamic keys = XVar.Array();
keys["IP"] = ip;

dynamic values = XVar.Array();
values["Attempts"] = 0;

DB.Update("LoginAttempts", values, keys);

After Unsuccessful Login

After an unsuccessful login, increase the attempt counter and save the time of the attempt.

PHPRunner

$ip = $_SERVER["REMOTE_ADDR"];

$keys = array();
$keys["IP"] = $ip;

$rs = DB::Select("LoginAttempts", $keys);
$data = $rs->fetchAssoc();

if ($data)
{
	$values = array();
	$values["Attempts"] = intval($data["Attempts"]) + 1;
	$values["LastLogin"] = date("Y-m-d H:i:s");

	DB::Update("LoginAttempts", $values, $keys);
}
else
{
	$values = array();
	$values["IP"] = $ip;
	$values["Attempts"] = 1;
	$values["LastLogin"] = date("Y-m-d H:i:s");

	DB::Insert("LoginAttempts", $values);
}

ASPRunner.NET

string ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];

dynamic keys = XVar.Array();
keys["IP"] = ip;

dynamic rs = DB.Select("LoginAttempts", keys);
dynamic data = rs.fetchAssoc();

if (data)
{
	dynamic values = XVar.Array();
	values["Attempts"] = Convert.ToInt32(data["Attempts"]) + 1;
	values["LastLogin"] = DateTime.Now;

	DB.Update("LoginAttempts", values, keys);
}
else
{
	dynamic values = XVar.Array();
	values["IP"] = ip;
	values["Attempts"] = 1;
	values["LastLogin"] = DateTime.Now;

	DB.Insert("LoginAttempts", values);
}

4. Build and test the application

Build the project and open the Login page. After three unsuccessful login attempts from the same IP address, additional login attempts from that address are blocked for 30 minutes.

After the 30-minute period expires, the failed-attempt counter is reset and the user can try again. A successful login also resets the counter.

Account lockout or IP-based blocking?

For most applications, the built-in account lockout in PHPRunner and ASPRunner.NET is the simpler option. It locks the user account rather than the IP address and requires no custom event code.

IP-based blocking can be useful as an additional measure when you specifically want to slow repeated login attempts coming from the same source.

Applies to

  • PHPRunner
  • ASPRunner.NET

Back to top