Page 1 of 1

Professional Level Login Design [Pt.2] Adding Blocking and Purging Rate Topic: ***** 1 Votes

#1 joeyadms  Icon User is offline

  • D.I.C Head
  • member icon

Reputation: 41
  • View blog
  • Posts: 178
  • Joined: 04-May 08

Posted 15 June 2008 - 08:36 AM

Creating a Secure, Professional Login Module [PT. 2]

IMPORTANT
I will not include source from the last part, as it will make things too long and hard to understand. However, you should add what we do in this tutorial onto the code from the last.

I will attatch source of what it should look like alltogether after both tutorials.

Prequel
In the first part we created our base class for authentication, and our system for encrypting and hashing stored sensitive data. If you have not read that tutorial, you should do so now. If you have, continue on.

Intro
In this part, we will create our blocking lists. We will start with
table creation, then work straight into setting up methods for
checking if a user or ip is blocked, and also to add strikes to the
user if a login has failed.


Table Setup
Ok, So we are assuming you have a database setup, and the functions in my scripts will be using the common mysql_ driver. If you
do not want to use this, or have a DB abstraction layer (like you should) then go ahead and change the queries and functions to match what you need.

We will create 2 tables, blocked_ips, blocked_users for
storing info. They will be almost identical, and will have an auto timestamp
column. Here are the create scripts
CREATE TABLE blocked_ips(
ip VARCHAR(50) NOT NULL PRIMARY KEY,
strikes INT NOT NULL,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE blocked_users(
user VARCHAR(100) NOT NULL PRIMARY KEY,
strikes INT NOT NULL,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);



Tables go like this, IP/USER holds identifier data, strikes is how many failed login attempts made, and ts is the MYSQL timestamp that is created by default on insertion and updated when the record is updated.

Now I have no idea how your users are identified. Wether by username, userID, email address, whatever. The user column in the blocked_users will hold the unique identifier such as a username of the user that is trying
to login.

Class Variables - Configuration
Our module will use several variables for configuration. Let's set them up now.
class userAuth {

// Attempt Configuration
private $max_attempts = 5;
private $attempts_before_captcha = 2;

// Blocked Time Configuration
private $blocked_hours = 1;

// Captcha Holder
private $use_captcha;

}



Alright Here is what they mean.

- max_attempts = How many strikes before user/ip is banned
- attempts_before_captcha = after how many strikes should captcha be implemented
- blocked_hours = how many hours should user/ip be banned for . Note: .5 = 30 minutes
- use_captcha = this will be set to 1 if strikes have reached number above


Good!

Creating Block Time
We need to calculate time, in seconds, for our timestamps to compare to
see if user has been banned has reached past the number of hours set in config.

Easy, add this method
class userAuth {

// Attempt Configuration
private $max_attempts = 5;
private $attempts_before_captcha = 2;

// Blocked Time Configuration
private $blocked_hours = 1;

// Captcha Holder
private $use_captcha;


	public function createBlockTime(){
		return (60 * 60 * $this->blocked_hours);
	}
}



Ok that was easy, It simply returns time, in seconds, from the number of hours in config.

THE HARD PART- function is_blocked()
Ok this is the hard part, well not ready, but the code is big.

First lets create our function
function is_blocked($ip,$user){

}


From the looks of it, we will take our ip address, and user from a tried authentication, and return wether or not they should be blocked our not.

First We need to create our query to select rows that match the ip and user.

The Query
Our query will be selecting from both tables at once. By doing this we are decreasing overhead
by only making one query to the DB, this will increase performance.
$query = "SELECT `strikes`,UNIX_TIMESTAMP(ts) AS `ts`,'ip' AS `source`
		  FROM blocked_ips WHERE `ip`='$ip'
		  UNION SELECT `strikes`,UNIX_TIMESTAMP(ts) as `ts`,`user` AS `source`
		  FROM blocked_users WHERE `user`='$user'";



Ok lets walk through this.

This is a Union Select query. We are selecting rows from both the blocked_ips, and blocked_users tables.

We want this query to return 4 columns, strikes/ts/source
-strikes = number of failed logins
-ts = Timestamp of last update
-source = Which table did it come from/ ip or user

So we select strikes, and use the mysql function UNIX_TIMESTAMP() to get the unix timestamp from our ts column, and select 'ip' or 'user' as source depending on which table, WHERE ip= our supplied ip or user= supplied
user.

Simple when we dissect it.

Do we have any rows?
We will use mysql_num_rows to see if we have any matches, if we do, then there are some failed attempts
if now, then our ip and user are not blocked.
	public function is_blocked($ip,$user){
			$query = "SELECT `strikes`,UNIX_TIMESTAMP(ts) AS `ts`,'ip' AS `source`
					  FROM blocked_ips WHERE `ip`='$ip'
					  UNION SELECT `strikes`,UNIX_TIMESTAMP(ts) as `ts`,`user` AS `source`
					  FROM blocked_users WHERE `user`='$user'";

			$results = mysql_query($query);
			
			if(mysql_num_rows($results)) {
				$blockTime = $this->createBlockTime();
				while($row = mysql_fetch_assoc($results)){
				
				
				}
			} else {
				$return = 0;
			}
			return $return;
	}



So if no rows are returned, we return 0, meaning user/ip is not blocked.

We also went ahead and grabbed our block time created by our createBlockTime() function from earlier.

So now that we checked if there were no results returned, what if there was?

We need to loop through the results and check if $row['source'] = ip or user, and check their strikes against
our configuration.
	public function is_blocked($ip,$user){
			$query = "SELECT `strikes`,UNIX_TIMESTAMP(ts) AS `ts`,'ip' AS `source`
					  FROM blocked_ips WHERE `ip`='$ip'
					  UNION SELECT `strikes`,UNIX_TIMESTAMP(ts) as `ts`,`user` AS `source`
					  FROM blocked_users WHERE `user`='$user'";
					  
			$results = $this->query($query);
			
			if(mysql_num_rows($results)) {
				$blockTime = $this->createBlockTime();
				while($row = mysql_fetch_assoc($results)){
				
					if($row['source'] == 'ip'){
					
						$diff = time() - $row['ts'];
						if($row['strikes'] >= $this->max_attempts){
							// Check our timestamps
						} else if($row['strikes'] >= $this->attempts_before_captcha){
							$this->use_captcha = 1;
							$return = 0;
						} else {
							$return = 0;
						}
						
					} else if($row['source'] == 'user'){
					
						$diff = time() - $row['ts'];				
						if($row['strikes'] >= $this->max_attempts){
							// Check our timestamps
						} else if($row['strikes'] >= $this->attempts_before_captcha){
							$this->use_captcha = 1;
							$return = 0;
						} else {
							$return = 0;
						}
					}
				}
			} else {
				$return = 0;
			}
			return $return;
	}



Ok, a lot more code, but lets examine. We enumerate $row['source'] and parse results from each table differently.

We create our different time by subtracting our timestamp from the database from what time it is now. That will give us how much time
has passed since the last failed attempt.

Now if the number of strikes is greater or equal to the max in config, we put a placeholder where we need to check our timestamps to see if enough time has passed since being banned.

If strikes are not more than max in config, but are more or equal to the number to require captcha, we set our 'use_captcha' property to 1, and set our return to 0 , since the ip/user is not banned.

How long have you been banned?
Now we need to see enough time has passed between last failed login, if it has been more than enough time, then subtracting the difference time from our blockTime will be negative, and we delete the record from database and return 0. If the result is positive, that means not enough time has passed, and we immediately return 1 because
the user is blocked.
	public function is_blocked($ip,$user){
			$query = "SELECT `strikes`,UNIX_TIMESTAMP(ts) AS `ts`,'ip' AS `source`
					  FROM blocked_ips WHERE `ip`='$ip'
					  UNION SELECT `strikes`,UNIX_TIMESTAMP(ts) as `ts`,`user` AS `source`
					  FROM blocked_users WHERE `user`='$user'";
			$results = $this->query($query);
			if(mysql_num_rows($results)) {
				$blockTime = $this->createBlockTime();
				while($row = mysql_fetch_assoc($results)){
					if($row['source'] == 'ip'){
						$this->ip_strikes = $row['strikes'];
						$diff = time() - $row['ts'];
						if($row['strikes'] >= $this->max_attempts){
							if(($blockTime - $diff) < 0){
								$delete = "DELETE FROM blocked_ips WHERE `ip`='$ip'";
								$this->query($delete);
								$return = 0;
							} else {
								return 1;
							}
						} else if($row['strikes'] >= $this->attempts_before_captcha){
							$this->use_captcha = 1;
							$return = 0;
						} else {
							$return = 0;
						}
					} else if($row['source'] == 'user'){
						$diff = time() - $row['ts'];				
						if($row['strikes'] >= $this->max_attempts){
							if(($blockTime - $diff) < 0){
								$delete = "DELETE FROM blocked_users WHERE `user`='$user'";
								$this->query($delete);
								$return = 0;
							} else {
								return 1;
							}
						} else if($row['strikes'] >= $this->attempts_before_captcha){
							$this->use_captcha = 1;
							$return = 0;
						} else {
							$return = 0;
						}
					}
				}
			} else {
				$return = 0;
			}
			return $return;
	}




AWESOME. Ok now we are half way done. We now need to create a function to add strikes to the user/ip if
they fail a login.

First, our skeleton
function failed_login($user,$ip) {

}



Now our query, We will simply increase the strike by one, again we will use 1 query.
function failed_login($user,$ip) {
	$query = "UPDATE blocked_ips i, blocked_users u SET
			  i.strikes = i.strikes + 1, u.strikes = u.strikes + 1
			  WHERE i.ip = '$ip' && u.user = '$user'";
	mysql_query($query);
}




Our Class, from this tutorial, should look like this now.
class userAuth {

// Attempt Configuration
private $max_attempts = 5;
private $attempts_before_captcha = 2;

// Blocked Time Configuration
private $blocked_hours = 1;

// Captcha Holder
private $use_captcha;


	function failed_login($user,$ip) {
		$query = "UPDATE blocked_ips i, blocked_users u SET
				  i.strikes = i.strikes + 1, u.strikes = u.strikes + 1
				  WHERE i.ip = '$ip' && u.user = '$user'";
		mysql_query($query);
	}

	public function is_blocked($ip,$user){
			$query = "SELECT `strikes`,UNIX_TIMESTAMP(ts) AS `ts`,'ip' AS `source`
					  FROM blocked_ips WHERE `ip`='$ip'
					  UNION SELECT `strikes`,UNIX_TIMESTAMP(ts) as `ts`,`user` AS `source`
					  FROM blocked_users WHERE `user`='$user'";
			$results = $this->query($query);
			if(mysql_num_rows($results)) {
				$blockTime = $this->createBlockTime();
				while($row = mysql_fetch_assoc($results)){
					if($row['source'] == 'ip'){
						$diff = time() - $row['ts'];
						if($row['strikes'] >= $this->max_attempts){
							if(($blockTime - $diff) < 0){
								$delete = "DELETE FROM blocked_ips WHERE `ip`='$ip'";
								$this->query($delete);
								$return = 0;
							} else {
								return 1;
							}
						} else if($row['strikes'] >= $this->attempts_before_captcha){
							$this->use_captcha = 1;
							$return = 0;
						} else {
							$return = 0;
						}
					} else if($row['source'] == 'user'){
						$diff = time() - $row['ts'];				
						if($row['strikes'] >= $this->max_attempts){
							if(($blockTime - $diff) < 0){
								$delete = "DELETE FROM blocked_users WHERE `user`='$user'";
								$this->query($delete);
								$return = 0;
							} else {
								return 1;
							}
						} else if($row['strikes'] >= $this->attempts_before_captcha){
							$this->use_captcha = 1;
							$return = 0;
						} else {
							$return = 0;
						}
					}
				}
			} else {
				$return = 0;
			}
			return $return;
	}
	
	public function createBlockTime(){
		return (60 * 60 * $this->blocked_hours);
	}
}



NOW, lets create a function that purges old records. We will use a Multiple Delete Statement
public function purge(){
	$blockTime = $this->createBlockTime();
	$query = "DELETE blocked_ips.*, blocked_users.*
		  FROM blocked_ips, blocked_users
		  WHERE ('$blockTime' - UNIX_TIMESTAMP(blocked_ips.ts) < 0)
		  AND ('$blockTime' - UNIX_TIMESTAMP(blocked_users.ts) < 0)";
	mysql_query($query);
}



Very Simple, very easy to understand. We grab our blocktime and delete records from each table that are older than the required ban time in our configuration.

Now our last step, lets add a conditional that randomly calls purge, so we don't purge requests all the time.

I will use '6' because I find it a little more random, but basically, if there is no remainder left over from rand() and 6, then it will purge.
public function is_blocked($ip,$user){
	(rand() % 6) == 0 ? $this->purge():null;



As you see we just add that right on top of our 'is_blocked' method.



Conclusion
In this tutorial, we built our blocking functions to not only add strikes for failed logins, but to check to see if a user/ip is banned, and to expel records that are older than our ban date.

In the next part, we will implement our CAPTCHA and authentication mechanism, so stay syndicated!

Attached File(s)



Is This A Good Question/Topic? 2
  • +

Replies To: Professional Level Login Design [Pt.2]

#2 didgy58  Icon User is offline

  • D.I.C Head

Reputation: 3
  • View blog
  • Posts: 246
  • Joined: 23-October 07

Posted 15 October 2008 - 01:08 AM

superb tutorials joeyadams, cant wait for the follow on
Was This Post Helpful? 0
  • +
  • -

#3 Spatlabor  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 2
  • Joined: 07-November 08

Posted 07 November 2008 - 08:37 PM

Excellent. Can't wait for the last part :)

Hopefully you will add the last part..
Was This Post Helpful? 0
  • +
  • -

#4 codemaster5150  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 7
  • Joined: 13-April 09

Posted 29 August 2010 - 12:38 AM

I really likes this tutorial, is there any way that I could read the first part. :online2long:
Was This Post Helpful? 0
  • +
  • -

#5 liquidsnake3d  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 1
  • Joined: 07-April 11

Posted 07 April 2011 - 01:43 PM

Very good tutorial. I'm new at this and I found it very educational.just wanted to tell you that you are appreciated! can wait for the next step
Was This Post Helpful? 0
  • +
  • -

#6 nacholibre  Icon User is offline

  • D.I.C Head

Reputation: 1
  • View blog
  • Posts: 76
  • Joined: 19-September 09

Posted 11 June 2011 - 11:39 AM

very nice tutorial. i learned alot.
I was expecting to see the 3rd part but i dont see it. is it out?
it would be very appreciated!
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1