Welcome to Dream.In.Code
Getting Help is Easy!

Join 132,682 Programmers for FREE! Get instant access to thousands of experts, tutorials, code snippets, and more! There are 1,224 people online right now. Registration is fast and FREE... Join Now!




Professional Level Login Design [Pt.2]

 
Reply to this topicStart new topic

> Professional Level Login Design [Pt.2], Adding Blocking and Purging

Rating  5
joeyadms
Group Icon



post 15 Jun, 2008 - 07:36 AM
Post #1


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
CODE

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.
CODE

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
CODE

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
CODE

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.
CODE

$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.
CODE

    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.
CODE

    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.
CODE

    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
CODE

function failed_login($user,$ip) {

}


Now our query, We will simply increase the strike by one, again we will use 1 query.
CODE

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.
CODE

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
CODE

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.
CODE

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)
Attached File  userAuth.php ( 3.64k ) Number of downloads: 61
Go to the top of the page
+Quote Post


Register to Make This Ad Go Away!

didgy58
**



post 15 Oct, 2008 - 12:08 AM
Post #2
superb tutorials joeyadams, cant wait for the follow on
Go to the top of the page
+Quote Post

Spatlabor
*



post 7 Nov, 2008 - 07:37 PM
Post #3
Excellent. Can't wait for the last part smile.gif

Hopefully you will add the last part..
Go to the top of the page
+Quote Post


Reply to this topicStart new topic
1 User(s) are reading this topic (1 Guests and 0 Anonymous Users)
0 Members:

 

Lo-Fi Version Time is now: 11/23/08 06:50AM

Live Help!

Tutorials

Programming

Web Development

Reference Sheets

Code Snippets

Bye Bye Ads

Free DIC T-Shirt

T-Shirt Example

Related Sites

Monthly Drawing

Thumb Drive

Partners

Top Contributors

Top 10 Kudos This Month