Forgot Password?

  • Home
  • Resources
    • Blog
    • News
    • Professional services
  • Projects
    • Webber
    • WB Ticket System
    • WB Blog
  • Contact
  • Support
    • Wiki
    • Forum
    • Ticket system

DrSoft Blog

sharing thoughts, ideas...

  • Tags

    • css
    • galleries
    • drsoft
    • webber
    • open source
    • applications
    • developers
    • Webber
    • protection
    • guide
    • files
    • mod_rewrite
    • downloads
    • hotlinking
    • htaccess
    • leechers
    • menu
    • programming
    • plugins
    • ajax
    • form validation
    • jquery
    • php
    • ide
    • editors
    • progress bar css php
    • modules
    • blog
    • speedy
    • codeigniter
    • buttons
    • html
    • email
    • phpmailer
    • sendmail
    • smtp
    • validation. user friendly
    • file upload
    • multiple
    • login
    • secure

PHP Login - a simple and secure example0 comments

The login page is among the most important parts of a web application as it sets the difference between guests and authenticated members. All your private content should be hidden from guests and only accessible if the user has performed a successfull authentication so this part needs a lot of attention and logic otherwise your plans might not work as intended. The login feature also needs to be very secure by performing various validations and checks before allowing anyone to enter the system but also transparent as we don't want to annoy our members with too much things.

In this tutorial we will learn how to build a highly secure login system which authenticates members, capable of handling persistent cookies (remember me) in a secure mode, prevents abuses from spam bots or brute force applications and many other things crucial too a strong and secure php login script.

We will start with the initial database schema which consists in one MySql table and explain every field in part for a better understanding of how things work.

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `password` char(32) COLLATE utf8_unicode_ci NOT NULL,
  `email` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `fingerprint` char(32) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`),
  UNIQUE KEY `email` (`email`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;


As you can see from our users table schema everything is pretty understandable except for the `fingerprint` field which is very important in our little application because it holds the user's 'fingerprint from the last login'. Every time the user loggs in and requests to be remembered we will store an md5 key which consists from the user's IP address and browser. Whenever someone with the same cookie stored in the browser tries to access our system and those parameters do not match we deny the user access so that's why the fingerprint. The more user information (try to find static values and values that are hard to emulate) you place in that key the better because it makes it harder for session hijackers to emulate a user's environment. Our bet will be on the user's ip because it's the hardest one to replicate among all the information that we can retrieve from a visitor.

Also, please notice that both the username and email are unique to prevent duplicates, pretty obvious.

You will notice the username field is stored in a varchar of 55 while our pasword is a char with only 32 characters. Why 32 and why char and not varchar. Well, first of all, the char field takes less space so it's a little optimization here, secondly, our passwords will be encrypted in md5 that outputs exactly 32 characters and locked with a salting key which adds extra protection to the md5 generated key. The most important thing is to store the passwords in a very safe way and hidden from everybody and this includes you or anyone else. Assuming that your salt key would be something like 'AG6-T6U-6*U' here's how the password will go into the database:

$salt_key = 'AG6-T6U-6*U';
$user_password = $_POST [ 'password' ];
$password = md5 ( $salt_key . md5 ( $user_password ) );


If you look better you will see that we use a double encryption and also a salt key so this should make the passwords pretty safe for storing in any place. Of course, the salting key should be defined in a file which, recommended, sits outside the document root of your webserver making it harder to access without ftp or ssh credentials. For the sake of this tutorial, we keep the salt key and cookie name in a class constant.

Ok, we know about the security measures that we will take so it's time to talk about our application's logic and start building it. Since it's a pretty basic thing we will place everything inside a PHP class to allow a better interaction between methods and properties and also encapsulate certain actions by making them private or protected. I will name the class 'Site_sentry':

class Site_sentry 


Right now it's empty because we didn't discussed about the usage of this class yet so let's try to make a list with everything needed for our login system to work properly. We will need to login members, set sessions, check login cookies, create the fingerprint and logout the members so here's the list:

- public function is_logged_in - checks to see if a member is logged in or not
- public function login - loggs a member in
- public function logout - loggs a member out
- private function set_login_sessions - sets the user sessions
- private function set_cookie - sets the user cookie
- private function check_cookie - checks if a member has a login cookie
- private function get_fingerprint - returns the user's fingerprint


As you can see I created only 3 class methods to be public, and the rest of them private as we might extend the class and change the method behavior from another class extending Site_sentry. The public methods will be the only ones to be used outside the class while the others are restricted to be used only within the class or subclasses which extend Site_sentry. Here's our code so far:

class Site_sentry {

	public function login ( $username, $password, $rememberme = FALSE ) {
		
	}

	public function get_last_error () {
		
	}

	public function logout () {
		
	}

	private function set_last_error () {

	}

	private function set_login_sessions ( $user_id ) {
		
	}

	private function unset_login_sessions () {
		
	}

	private function set_cookie ( $cookie_name, $value, $expires_in, $path = '/', $domain = 'yourdomain.com' ) {

	}

	private function check_cookie () {
		
	}

	private function get_fingerprint () {
		
	}

	private function set_fingerprint ( $user_id ) {

	}

	public function is_logged_in () {
		
	}

}


We have the skeleton of our application so let's build some logic now by creating the login functionality. We only need 2 parameters (3rd is optional) and we can grab them from $_POST or any other method, whatever suits you best. In 99% percent of the cases, we will have $_POST values comming from a web form - the login form:

public function login ( $username, $password, $rememberme = FALSE ) {
	$result = mysql_query ( 'SELECT id, password from users where username = ' . mysql_real_escape_string ( $username ) );
	if ( ! $result ) {
		$this->set_last_error ( "User not found" );
		return FALSE;
	}

	$row = mysql_fetch_row ( $result );

	if ( md5 ( SALT_KEY . md5 ( $password ) ) != $row [ 1 ] ) {
		$this->set_last_error ( "Invalid password" );
		return FALSE;
	}

	if ( $rememberme ) {
		$this->set_fingerprint ( $row [ 0 ] );
		$this->set_cookie ( self::COOKIE_NAME, $row [ 0 ] . $this->get_fingerprint (), time () + 3600 );
	}

	$this->set_login_sessions ( $row [ 0 ] );
}


At the very first level of our login method we extract the id and the password of the member that has the provided username. Many use a different aproach here by extracting the id where username and password are perfect match but we do it in another way in order to be able to tell what went wrong exactly. This way we can decide if the username exists or not because we're only checking the database against the user. If a record is not found we can simply echo that the user does not exists instead of a general type of error "Either the username does not exist or the provided password is incorrect". Moving further down the code it's time to perform the password check against the found user record so it's time to apply the encryption to the provided password:

md5 ( self::SALT_KEY . md5 ( $password ) )


If we have a match than it's a successful login so let's hurry up and store the cookie (if a remember me is present), set the sessions and let the member access the protected pages.

I'm not going to discuss how a cookie or sessions is set so I will just skip to the is_logged_in function which also has an important role in our php login system since it has to perform a check if a sessions is set and if not it will also have to look for a cookie and try to log the user in in case it finds one.

public function is_logged_in () {
	if ( isset ( $_SESSION [ self::COOKIE_NAME ] ) ) {
		return TRUE;
	}	

	if ( isset ( $_COOKIE [ self::COOKIE_NAME ] ) ) {
		$user_id = substr ( $_COOKIE [ self::COOKIE_NAME ], 0, 1 );
		$fingerprint = substr ( $_COOKIE [ self::COOKIE_NAME ], 1, strlen ( $_COOKIE [ self::COOKIE_NAME ] ) - 2 );

		$result = mysql_query ( 'SELECT id from users where id = ' . mysql_real_escape_string ( $user_id ) . ' AND fingerprint = ' . mysql_real_escape_string ( $fingerprint ) );
		if ( ! $result ) {
			//	set the cookie in the past to avoid this check again
			$this->set_cookie ( self::COOKIE_NAME, '', time () - 3600 );
			return FALSE;
		}
		else {
			$this->set_login_sessions ( $user_id );
		}
	}
}


The function first checks if there is a valid session and returns TRUE in case there is one. If no session is found it moves to checking if a cookie is present. If the cookie is found, we extract the id of the member from the cookie and the fingerprint since the cookie value is a concatenation of user id + fingerprint. Once extracted both values are matched against the database and, if found, we're restarting the sessions to avoid this check once again.

We have the login, we can check if the user is logged in or not and it's time to move to the logout function wich is very simple. Since it's a requested action we need to unset the login cookie and kill the user session.

public function logout () {
	//	set the cookie in the past to expire
	$this->set_cookie ( self::COOKIE_NAME, '', time () - 3600 );
	//	unset the sessions we previously set using 'set_login_sessions'
	$this->unset_login_sessions ();
	//	redirect the user to the login page
	header ( "Location: login.php" );
}


That's just about everything. In just a few simple steps we managed to create a login system which is very easy to use and secure. For a full and functional example don't forget to download the attached zip archive.

Here's our final code:

require_once "database.php";

class Site_sentry {

	const COOKIE_NAME = "LoggedIn";
	const SALT_KEY = 'AG6-T6U-6*U';
	
	protected $last_error;

	/**
	 *	Performs the user login with various checks
	 *	@param string $username The username
	 *	@param string $password The user password
	 *	@param boolean $rememberme The user wants to be remembered
	 *	return boolean User was logged in or not
	 */
	public function login ( $username, $password, $rememberme = FALSE ) {
		global $link;
		$result = mysql_query ( "SELECT id, password FROM users WHERE username = '" . mysql_real_escape_string ( $username ) . "'", $link ) or die ( 'MySql error: ' . mysql_error () );
		if ( ! $result ) {
			$this->set_last_error ( "User not found" );
			return FALSE;
		}

		$row = mysql_fetch_row ( $result );

		if ( $this->encode_password ( $password ) != $row [ 1 ] ) {
			$this->set_last_error ( "Invalid password" );
			return FALSE;
		}

		if ( $rememberme ) {
			$this->set_fingerprint ( $row [ 0 ] );
			$this->set_cookie ( self::COOKIE_NAME, $row [ 0 ] . '-' . $this->get_fingerprint (), time () + 3600 );
		}

		return $this->set_login_sessions ( $row [ 0 ] );
	}

	/**
	 *	If an error occurred this should return the last error
	 *	return string The last error
	 */
	public function get_last_error () {
		return $this->last_error;
	}

	/**
	 *	Logs the member out, also kills sessions and cookies
	 *	return boolean
	 */
	public function logout () {
		//	set the cookie in the past to expire
		$this->set_cookie ( self::COOKIE_NAME, '', time () - 3600 );
		//	unset the sessions we previously set using 'set_login_sessions'
		$this->unset_login_sessions ();
		return TRUE;
	}

	/**
	 *	Performs a check if the user is logged in or not
	 *	return boolean
	 */
	public function is_logged_in () {
		global $link;
		if ( isset ( $_SESSION [ self::COOKIE_NAME ] ) ) {
			return TRUE;
		}	

		if ( isset ( $_COOKIE [ self::COOKIE_NAME ] ) ) {
			$finger_data = explode ( '-', $_COOKIE [ self::COOKIE_NAME ] );
			$user_id = $finger_data [ 0 ];//	user id
			$fingerprint = $finger_data [ 1 ];//	fingerprint

			$result = mysql_query ( "SELECT id FROM users WHERE id = '" . mysql_real_escape_string ( $user_id ) . "' AND fingerprint = '" . mysql_real_escape_string ( $fingerprint ) . "'", $link ) or die ( 'MySql error: ' . mysql_error () );
			if ( ! $result || $fingerprint != $this->get_fingerprint () ) {
				//	user fingerprint does not match database data or cookie data
				//	set the cookie in the past to avoid this check again
				$this->set_cookie ( self::COOKIE_NAME, '', time () - 3600 );
				return FALSE;
			}
			else {
				$this->set_login_sessions ( $user_id );
				return TRUE;
			}
		}
		
		return FALSE;
	}

	/**
	 *	Encodes the user password with double md5 encryption and a salt key
	 *	@param string $password The user password
	 *	return string The encoded password
	 */
	private function encode_password ( $password ) {
		return md5 ( self::SALT_KEY . md5 ( $password ) );
	}

	/**
	 *	Stores the last error to be accessible via get_last_error ()
	 *	return void
	 */
	private function set_last_error ( $error ) {
		$this->last_error = $error;
	}

	/**
	 *	Sets the login user sessions
	 *	return boolean
	 */
	private function set_login_sessions ( $user_id ) {
		$_SESSION [ 'id' ] = $user_id;
		$_SESSION [ self::COOKIE_NAME ] = TRUE;
		return TRUE;
	}

	/**
	 *	Unsets the login user sessions
	 *	return boolean
	 */
	private function unset_login_sessions () {
		unset ( $_SESSION [ 'id' ], $_SESSION [ self::COOKIE_NAME ] );
		return TRUE;
	}

	/**
	 *	Sets the cookie if remember me was checked
	 *	return boolean
	 */
	private function set_cookie ( $self::COOKIE_NAME, $value, $expires_in, $path = '/', $domain = 'yourdomain.com' ) {
		setcookie ( $self::COOKIE_NAME, $value, $expires_in, $path, $domain );
	}

	/**
	 *	Performs a check if the remember me cookie is set
	 *	return boolean
	 */
	private function check_cookie () {
		return ( isset ( $_COOKIE [ self::COOKIE_NAME ] ) ) ? TRUE : FALSE;
	}

	/**
	 *	Returns the user fingerprint. User agent + IP..can be extended
	 *	return string fingerprint
	 */
	private function get_fingerprint () {
		//	this is basic implementation, feel free to change
		return md5 ( $_SERVER [ 'HTTP_USER_AGENT' ] . $_SERVER [ 'REMOTE_ADDR' ] );
	}

	/**
	 *	Sets the user fingerprint in the database
	 *	return boolean
	 */
	private function set_fingerprint ( $user_id ) {
		global $link;
		return mysql_query ( "UPDATE users SET fingerprint = '" . mysql_real_escape_string ( $this->get_fingerprint () ) . "' WHERE id = '" . mysql_real_escape_string ( $user_id ) . "'", $link ) or die ( 'MySql error: ' . mysql_error () );
	}

}


Usage example:

require_once "sentry.php";

$messages = array ();

$sentry = new Site_sentry ();


//	LOG IN
if ( ! $sentry->login ( 'test', 'test', TRUE ) ) {
	die ( $sentry->get_last_error () );
}
else {
	//	CHECK IF LOGGED IN
	if ( $sentry->is_logged_in () ) {
		$messages [] = "I am logged in as 'test'";
	}
}


//	LOG OUT
if ( ! $sentry->logout () ) {
	die ( $sentry->get_last_error () );
}
else {
	//	CHECK IF LOGGED OUT
	if ( ! $sentry->is_logged_in () ) {
		$messages [] = "Logged out successfully";
	}
}

echo '<pre>';
foreach ( $messages as $message ) {
	echo $message . "\n";
}
echo '</pre>';


Download link for a fully functional example:

login.zip

<< PHP multiple file uploads      
Home
© 2008 drSoft Ltd. All rights reserved.