Creating a Secure PHP Login Script

  • : Function ereg() is deprecated in /f2/mtdev/public/includes/file.inc on line 649.
  • : Function ereg() is deprecated in /f2/mtdev/public/includes/file.inc on line 649.
  • : Function ereg() is deprecated in /f2/mtdev/public/includes/file.inc on line 649.
  • : Function ereg() is deprecated in /f2/mtdev/public/includes/file.inc on line 649.
  • : Function ereg() is deprecated in /f2/mtdev/public/includes/file.inc on line 649.
  • : Function ereg() is deprecated in /f2/mtdev/public/includes/file.inc on line 649.

Explains how to create a secure PHP login script that will allow safe authentication. Features remember-me function using cookies, validates logins on each request to prevent session stealing.

How does this work

This is a short explanation why I have chosen these authentication methods.

Users with shell access to the web server can scan valid session id's if the default /tmp directory is used to store the session data.

The protection against this kind of attack is the IP check.

Somebody who has a site (on a shared host with you) can generate valid session for your site.

This is why the checkSession method is used and the session id is recorded in the database.

Somebody may sniff network traffic and catch the cookie.

The IP check should eliminate this problem too.

Preparation

You need first to decide what information to store about members, the examples provided will assume almost nothing to make it easier to read.

I will use the PHP 4.1 super global arrays like $_SESSION, $_GET, etc. If you want to make it work on an earlier version of PHP you will have to substitute these with $GLOBALS['HTTP_SESSION_VARS'].

Database schema

This is only an example bare structure suitable for online administration, if you want to have registered members you should add more columns.

The schema is somewhat MySQL specific, I have yet to use another database other than MySQL and PostgreSQL but if you are using PostgreSQL you can convert the schema with the example script provided in my article Converting a database schema from MySQL to PostgreSQL.

CREATE TABLE member (
  id int NOT NULL auto_increment,
  username varchar(20) NOT NULL default '',
  password char(32) binary NOT NULL default '',
  cookie char(32) binary NOT NULL default '',
  session char(32) binary NOT NULL default '',
  ip varchar(15) binary NOT NULL default '',
  PRIMARY KEY  (id),
  UNIQUE KEY username (username)
);

The password and cookie fields are md5 hashes which are always 32 octets long. Cookie is the cookie value that is sent to the user if he/she requests to be remembered, session and ip are respectively the session id and the current IP of the visitor.

Connecting to the database

function &db_connect() {
	require_once 'DB.php';

	PEAR::setErrorHandling(PEAR_ERROR_DIE);

	$db_host = 'localhost';
	$db_user = 'shaggy';
	$db_pass = 'password';
	$db_name = 'shaggy';

	$dsn = "mysql://$db_user:$db_pass@unix+$db_host/$db_name";

	$db = DB::connect($dsn);

	$db->setFetchMode(DB_FETCHMODE_OBJECT);
	return $db;
}

This function connects to the database returning a pointer to a PEAR database object.

Session variables

To ease access to the current user's information we register it as session variables but to prevent error messages and set some defaults we use the following function.

function session_defaults() {
	$_SESSION['logged'] = false;
	$_SESSION['uid'] = 0;
	$_SESSION['username'] = '';
	$_SESSION['cookie'] = 0;
	$_SESSION['remember'] = false;
}

... with a check like:

if (!isset($_SESSION['uid']) ) {
	session_defaults();
}

to set the defaults. Of course session_start must be called before that.

To the core of the script

To allow easier integration with other scripts and make things more modular the core script is an object with very simple interface.

class User {
	var $db = null; // PEAR::DB pointer
	var $failed = false; // failed login attempt
	var $date; // current date GMT
	var $id = 0; // the current user's id

	function User(&$db) {
		$this->db = $db;
		$this->date = $GLOBALS['date'];

		if ($_SESSION['logged']) {
			$this->_checkSession();
		} elseif ( isset($_COOKIE['mtwebLogin']) ) {
			$this->_checkRemembered($_COOKIE['mtwebLogin']);
		}
	}

This is the class definition and the constructor of the object. OK it's not perfectly modular but a date isn't much of a problem. It is invoked like:

$date = gmdate("'Y-m-d'");
$db = db_connect();
$user = new User($db);

Now to clear the code purpose, we check if the user is logged in. If he/she is then we check the session (remember it is a secure script), if not and a cookie named just for example mtwebLogin is checked - this is to let remembered visitors be recognized.

Logging in users

To allow users to login you should build a web form, after validation of the form you can check if the user credentials are right with $user->_checkLogin('username', 'password', remember). Username and password should not be constants of course, remember is a boolean flag which if set will send a cookie to the visitor to allow later automatic logins.

	function _checkLogin($username, $password, $remember) {
		$username = $this->db->quote($username);
		$password = $this->db->quote(md5($password));

		$sql = "SELECT * FROM member WHERE " .
			"username = $username AND " .
			"password = $password";

		$result = $this->db->getRow($sql);

		if ( is_object($result) ) {
			$this->_setSession($result, $remember);
			return true;
		} else {
			$this->failed = true;
			$this->_logout();
			return false;
		}
	}

The function definition should be placed inside the User class definition as all code that follows. The function uses PEAR::DB's quote method to ensure that data that will be passed to the database is safely escaped. I've used PHP's md5 function rather than MySQL's because other databases may not have that.

The WHERE statement is optimized (the order of checks) because username is defined as UNIQUE.

No checks for a DB_Error object are needed because of the default error mode set above. If there is a match in the database $result will be an object, so set our session variables and return true (successful login). Otherwise set the failed property to true (checked to decide whether to display a login failed page or not) and do a logout of the visitor.

The logout method just executes session_defaults().

Setting the session

function _setSession(&$values, $remember, $init = true) {
   $this->id = $values->id;
   $_SESSION['uid'] = $this->id;
   $_SESSION['username'] = htmlspecialchars($values->username);
   $_SESSION['cookie'] = $values->cookie;
   $_SESSION['logged'] = true;

   if ($remember) {
      $this->updateCookie($values->cookie, true);
   }

   if ($init) {
      $session = $this->db->quote(session_id());
      $ip = $this->db->quote($_SERVER['REMOTE_ADDR']);

      $sql = "UPDATE member SET session = $session, ip = $ip WHERE " .
         "id = $this->id";
      $this->db->query($sql);
   }
}

This method sets the session variables and if requested sends the cookie for a persistent login, there is also a parameter which determines if this is an initial login (via the login form/via cookies) or a subsequent session check.

Persistent logins

If the visitor requested a cookie will be send to allow skipping the login procedure on each visit to the site. The following two methods are used to handle this situation.

function updateCookie($cookie, $save) {
   $_SESSION['cookie'] = $cookie;
   if ($save) {
      $cookie = serialize(array($_SESSION['username'], $cookie) );
      set_cookie('mtwebLogin', $cookie, time() + 31104000, '/directory/');
   }
}

Checking persistent login credentials

If the user has chosen to let the script remember him/her then a cookie is saved, which is checked via the following method.

function _checkRemembered($cookie) {
	list($username, $cookie) = @unserialize($cookie);
	if (!$username or !$cookie) return;

	$username = $this->db->quote($username);
	$cookie = $this->db->quote($cookie);

	$sql = "SELECT * FROM member WHERE " .
		"(username = $username) AND (cookie = $cookie)";

	$result = $this->db->getRow($sql);

	if (is_object($result) ) {
		$this->_setSession($result, true);
	}
}

This function should not trigger any error messages at all. To make things more secure a cookie value is saved in the cookie not the user password. This way one can request a password for areas which require even higher security.

Ensuring valid session data

function _checkSession() {
	$username = $this->db->quote($_SESSION['username']);
	$cookie = $this->db->quote($_SESSION['cookie']);
	$session = $this->db->quote(session_id());
	$ip = $this->db->quote($_SERVER['REMOTE_ADDR']);

	$sql = "SELECT * FROM member WHERE " .
		"(username = $username) AND (cookie = $cookie) AND " .
		"(session = $session) AND (ip = $ip)";

	$result = $this->db->getRow($sql);

	if (is_object($result) ) {
		$this->_setSession($result, false, false);
	} else {
		$this->_logout();
	}
}

So this is the final part, we check if the cookie saved in the session is right, the session id and the IP address of the visitor. The call to setSession is with a parameter to let it know that this is not the first login to the system and thus not update the IP and session id which would be useless anyway.

Comments

Holy

man I dont know but it works fine for me. Thanks a lot for this cool script I use it on many sites now, and it works with SSL too!!!! Very secure in my opinion.

Taking the credit for another persons work ?

http://www.devshed.com/c/a/PHP/Creating-a-Secure-PHP-Login-Script/
Not knocking this tute, but I spotted it at the above URL, and wondered, if they copied you, or you copied them, or both are your work ?

I hope my last question ;-)

According to your last answer UpdateCookie has to be modified in this way:
function updateCookie($cookie, $save) { /code to store new cookie value generated with md5(uniqid(mt_rand(1, mt_rand_getmax()))); in DB/ $_SESSION['cookie'] = $cookie; if ($save) { $cookie = serialize(array($_SESSION['username'], $cookie) ); set_cookie('mtwebLogin', $cookie, time() + 31104000, '/directory/'); }}
If so when a user logs in or changes his password a new cookie is stored in the DB and in the session variable.If somebody is sniffing my network traffic why he can login as me? He can "stole" my userName and Password?If yes why he cannot change my password after having logged in with my userName and Password?I haven't understand how generating a new cookie can protect my password from being changed if someone listen to my network traffic either if I enable or disable cookies. Could you gently explain me the two cases?Thanks and excuse me.

Another guy?

Have you noticed that the article on DevShed says Contributed by Martin Tsachev?

Against the brut force attempt you can log the following point and then check them after :
- the last bad login attempt time.
- increase the number of bad attempts if the delta-time between last bad login and now is below a lap of time.
If the number of attempts is greater than 3 (it can be more of course), then refused all login attempt from that IP.
If the delta-time is greater than the fixed delay, just reset the number of attempts.

You can also do the same thing with the correct login but bad password, in this case I suggest to block only this login for a period. Then it can prevent hacking by using differents IP (like spoofing).

In either case, the aim is only to delay the hack of login.

Another hint to be sure that that session isn't stoled.
You can save in the session var like that :
- $_SERVER["HTTP_USER_AGENT"]
- $_SERVER["HTTP_ACCEPT_LANGUAGE"]
and then check them back everytime you want to check session authenticity.
It doesn't take much time and can improve security.

This topic was very good, but I still can't understand some things. There's a lot of functions, but I need to understand how they're connecting and how it's working. Can anyone write simple structure (Example: login.php(including function &db_connect()), chech.php...). That would be very nice. I'm only beginner, who wants to understand PHP :)

Hello! The login script is brilliant, thanks!
My question is now that I have the login script and have members on the database, what would be its main job? In my situation, I want the visitors (who are not members) to have limited access to the pages on my site and the members to have the full access. Now how can I do that? Can someone give me a brief explanation how to do it? Forgive me masters for my ignorance, I'm still a newbie! But I learn quick so you won't waste your time, I promise:)

I'm having a bit of trouble with this code getting the _checkLogin function to run. I think I'm getting stuck when you say "The function definition should be placed inside the User class definition as all code that follows." Can someone help me with this?

I've tried it exactly like it is in the article I've tried it like below, and I just can't seem to get it to run that function. $user->_checkLogin($_REQUEST['username'],$_REQUEST['password'],remember);

I guess I'm asking where should I put:
$user->_checkLogin('username','password',remember);

Thanks in advance!

Okay, for all those that find this site and it hasn't been updated for a month like I did the solution to my problem was that everything spoken of after the User class is suppose to be within that user class. In other words don't close the user class until the functions:

_checkLogin,
_setSession,
updateCookie, and
_logout (which you need to create, just make it call the function session_defaults();

I hope this helps someone. Peace out!

FD:
It's a long time later, but thanks, mate. I worked out my cookie problem so long ago I can't even remember what the solution was, but your explanation is appreciated.

Ok I have a download for you.

http://www.azpixels.com/php_login.zip

I will leave it up for as long as possible. It has just about everything you need (but not html) including the login page, examples of use of backend pages, and query for starting your database with the right table. Sorry about the last couple of posts I made. The code I was trying to include was being truncated because this board was trying to process it up to a certain point.

Thanks!

Thank you so much Martin, and Brad as well for making the download available. This is a great help and starting point to tweak the script further.

Thks Brad :D

Logins

He can login as you but cannot change your password if you have used the remember me function.
You are supposed to enter your old password if you want to change it to a new one.

Hi Martin

Your work is Incredible. I have one question. According to following code written in login.php, one user cannot login in two browsers in same PC, but i found that I can login in second browser (off-course with same user/password). Actually, here what is happening is that when i logged in second browser, I logged out from first browser. Is it correct? because in this code, it is written taht if you are already logged in, you cannot login on second machine or second browser.
function failed(&$form) {
begin_html();
echo "You could not be logged in, $_SESSION[login] attempts left.
Possible reasons for this are:

Your username and/or password is not correct.
Check your username and password and then try again.
You haven't " .
'registered yet
Your account is temporarily disabled.
You are trying to login simultaneously from two different computers or
two browsers on the same computer.
';

$form->display();
}

Can you please check and let me know?

regards
Dharam

If you login from two browsers the first one will be logged out automatically.

thanks martin.

same happening here
regards
dharam

Great work Martin. Thank you.

Just stopped by to visit and got the crunch on your stuff in here - bravo!

The stuff on this web site is really witty and cool wise

Lovely. Made my day (which is saying something)

I must say that I was surprised to find this web page, but - - - Good Job.

Boy, this is some high-class site

Hi.. as many others here, I like the code.

I might wanna base my next login-script on it, however, what I fail to fully understand is, if the login-info (stored in a session) is compared to data in database each time a page is requested? _checkSession() is the function, right?

What's your performance-thoughts about this? I feel it might be pretty cpu-tick consuming. But I guess it's your way of making it more secure, right?

If u don't answer, then hey... thanks for the inspiration :-)

This Script

This script isn't entirely helpful. Sure, it provides the front-end for the login, but it's buggy as hell. For one, even when choosing not to remember your login, it STILL does regardless.
Also, it's VERY insecure, as it's not encrypted well at all. If you're going to use encryption, make your own method. My method has so far been uncrackable, and is contest winning software in several places.
It's pretty nice for beginners, though.

It's always a good idea to verify session data as someone who has shell access to the server might fetch a list of available sessions and try to hijack one. Instead of the IP now I think it would be a better idea to save the user agent string or a hash value of it.

Thanks for the article, Martin.

What IDE do you use to write PHP code?

Oh, I never liked PHP IDEs, I use Kate on Linux, EditPlus 2 on Windows, TextWrangler on OS X. I also like SciTE (multi OS).

Ok. Then my questions is: Is there any way to debug PHP code, just like you can in an Eclipse environment?

I'm a Java developer and work in Eclipse. Going from that to a basic editor with no debug capabilities slows me down. Do you have any tips on how to set up an environment (OS, applications, etc) so that writing PHP code is a bit more easy?