Creating a Secure PHP Login Script

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

Nice...

I enjoyed the article shaggy, but you have an error.
function getIP() { if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { return $_SERVER['HTTP_X_FORWARDED_FOR']; } else { return $_SERVER['REMOTE_ADDR']; }}
That is less than perfect, sometimes HTTP_X_FORWARDED_FOR returns 2 IP address, like so
x.x.x.x, x.x.x.x
You should check the length of HTTP_X_FORWARDED_FOR, and if it is longer then 15 characters, explode at ', ' and then use the first array item.
I know it's rare, but it happens, and will cause your script to do weird things.
-J

I didn't really know about that, I haven't seen it.

X-Forwarded-For

After thinking about it for a while I decided that X-Forwarder-For only makes it more insecure because it's a HTTP header, and HTTP headers tend to be easier to spoof than IP addresses.

yup..

That's another point. But, just because something can be spoofed doesn't make it insecure, you just have to make sure you validate the header correctly.
Lazy PHP coding is why any scripts are so insecure, just put in a little extra effort and it's no problem.

Well it's less secure so corrected it.
Btw usually it's easier to spot the little problems rather than the great idea behind someone's code.

Yeah...

That's true, I was going to make axion-network open-source but I decided not to. Too much of a risk. I try to write all my scripts securely but it is easy to miss something and most people will not inform you of an security hole. They will just mess your site up :( Alot of bad people around I regret to say.
btw, I like how you changed the layout a little. The design shows off your content better now, it doesn't look as plain :)

Open Source

Look at this nice book by Eric Raymond - The Cathedral and the Bazaar, it convinced Netscape Communications to release mozilla's source.
Although you should have a really nice piece of code to make use of open source.

Understanding of the script

I'm a PHP newbie and I've found your article very interesting but I haven't understand some things.I don't know if the login process runs in this way:a user registers with a form, getting his username and password;the user logs in;_checkLogin is called;if the user isn't found in the DB failed is set to true so I can check it in my script and print an error message;if the user is found _setSession is called;if ($remember) updateCookie is called: where do you initialize DB value for cookie ($values->cookie)???if ($init) session values are stored in the DB (when the user logs in);why updateCookie does $_SESSION['cookie'] = $cookie; already done by _setSession and why it has $save parameter, that is when I will call updateCookie($cookie,false)?
next time the user opens a page in the same session, when I do "$user = new User($db);" the constructor calls _checkRemembered if the user choosed "remember my profile" else _checkSession is called;
when I have to use $_SESSION['remember']?var $date; is useful only for statistics or log files?
I have understand the functioning or not?Please reply to my e-mail address also thanks Luigi.

Re: Understanding of the script

Well the code seems a bit confusing because it's missing some parts like user registration and password change, it would have made the article a lot longer and probably unreadable though.
When the user registers the cookie field in the database is initialized. On password change updateCookie is called to regenerate it, that's why it is needed the second parameter (if the visitor doesn't like cookies.

nice

Very good article indeed.

Some more questions

Thanks for your explanation! Now I've some more questions:
function _checkRemembered($cookie) {list($username, $cookie) = @unserialize($cookie);the @unserialize is a special function different from unserialize?
updateCookie is used only to update the expire time or not ?I think serialize(array($_SESSION['username'],$cookie)) doesn't use the new password to generate the new cookie value when the user changes password;
moreover when the user is logging in through a cookie:
in _checkRemembered you do list($username, $cookie) = @unserialize($cookie);
_setSession calls updateCookie(the Same Value Of $cookie Unserialized Before,true);
updateCookie does serialize(array($_SESSION['username'],the Same Value Of $cookie Unserialize dBefore)) that brings to the value $cookie that was argument of @unserialize)!!!
It's right???
When the user registers the cookie field in the database is initialized to what value: serialize(array($_SESSION['username'], ?????) ); ?
var $date; is useful only for statistics or log files?
Thanks in advance for your help, Luigi.

SMTP problem

hi martin, its a nice login as i was looking for, but i have some problems, when i register a user it appears two messages that tell me errors,(i ckecked yesterday, so i dont remember them exactly) and dont send the email, and i have to setup users manually, can u give me a hand, im a begginer in php
good work!!!

Great help

Martin:
Despite the clutter of angry little folks who seem to be in over (and under) their heads, I think you've done a fantastic job of explaining both the risks and some potential solutions for secure logins.
From one geek to another, thanks.
:-j

Hey look, it worked!

Martin:
So I just implemented this system on a client site, after some minor tweaking (in particular, there's no _logout() member function).
It works like a dream. Not bad considering I only spent about an hour of my time and... oh yeah, zero dollars.
Once again, nice work.
:-j

study php nd mysql

thank's give me explain

Thanks

Howz that code !!!!
Martin, I owe you man! thanks!!
I've not got anything working yet, but your explanation and examples have made life a lot easier!!
So now I'll go away and copy that code and make it work for my shit!! Hope you don't mind!!
Later

good

respect,
yu've wrote a good login script! but i have got a question: does this script has got an protection against brute-force? and is the script seeing at the top the actually version or exists a nearer?

Secure Login Script

Martin, job well done. I have to hand it to you, it takes a lot of good will in a person to go as far with this as you have. Hats off to you.
This script, while simple and basic, is secure and easy to modify for a variety of uses. I myself use a modified version of it to allow users to login to the main areas of a website using the same login/password/authorities that were set when the user registered at that site's forum board (yabbse 1.5.5). I use the forum registration scripts to register new members, and modified the table/database to point to the existing forum database instead. The passwords and incrypted using the exact same method, which is the most important part of the process to make them compatible.
A moderate understanding of php and mysql is helpful, of course, but this is a simple and effective script that everyone should find useful.

brute force cracking

Why are you worried about a password being cracked by brute force? Do you have any idea how long that would take? The universe will be dead long before then..

Persistant logins

Hi, just setting up a login based on this code on my site - it all works fine apart from the persistant logins part - I can't see any code anywhere that actually sets the cookie value in the database?
_checkLogin performs a SELECT on the database selecting the "cookie" field amongst others (which is empty initially!), this then calls _setSession and in turn updateCookie - but where does the value of "cookie" actually get set in the database so it can be used for persistant logins?
Am i missing something?
Thanks for a great script!

AOL

This script doesn't work with AOL clients - could it be the IP check part?

And more answers...

When you use @ in front of a function it supresses error messages.
UpdateCookie updates the cookie value stored in the database and if required by the visitor updates the cookie on his/her browser. Its value is generated: md5(uniqid(mt_rand(1, mt_rand_getmax()))); It has nothing to do with your password, if somebody listens to your network traffic he/she can login as you but cannot change your password.
$date should have been used to update the last_logged date but it didn't make it into the article.

Error message(REQUEST_URI ) on index.php

Hello Martins, Lovely jod u're doing out here. I got your script and tried working aroun it. Please I get this error each time I load the index.php page:
Welcome to the login page. Notice: Undefined index: REQUEST_URI in e:\inetpub\wwwroot\user.php on line 234
This page is available only to registered members, you have to login first, if you haven't registered yet you can do that for free.
I have tried opening the signup.php and login.php pages directly which opens but takes no action when I click the submit button.
Please, help me out with any tutorial or documentation. I have a deadline to met.
Thanks,

SecurePHP

There's a good site that offers some more information on creating secure login scripts and PHP security in general. Check it out: <a href="http://securephp.damonkohler.com/">SecurePHP</a>

Firefox issues

When you open a new tab in FireFox the same session_id() will be used, will it still use the same $_SESSION['username']? Because say an Admin logs on, $_SESSION will set the user name to Admin, and a regular use logs on setting the $_SESSION to user. The Admin's $_SESSION username is user now. I haven't tried this script, but will it prevent that?

comment about Creating a Secure PHP Login Script

can you give me solution about creating a Secure PHP Login Script more simple becouse your's is complexxx or give me code can run direct in my computer
thanks

stuff

"i dont want to do any work, you do it for me and ill do something for you that you dont give a crap about" lol.
hey people, learn it on your own. i dont understand a THING on how to access php with mySQL, dont understand anything but the basic function and purpose of sessions, dont understand how they work at all, yet im not gonna come here and ask someone to teach me a language or do my work for me.
this dude cant help everyone who is a noob to the language, dont overload him! we dont want ppl that are willing to help to just give up and quit cuz of the flood of requests do we? lol.

Is it possible...

Several questions from a newbie...
1. Would it be possible for whatever PHP/MySQL login script to use the Windows integrated authentication method (I have IIS 5)? My scripts all use the same MySQL user account. I know that this authetication method would not be recommended as many "outside" users just don't use MSIE or aren't set up to "hand-shake" with that type of authentication, but it would be used behingd closed doors, within an intranet... Thanks for your great script!!
A special note: for those who would prefer a full code without any outside classes or packages (as PEAR), I would humbly suggest to re-consider : as a computer scientist, of course it's more satisfactory (and maybe adaptive) to build up your own code libraries... But it could turn out to be counter-productive, especially when you are an independant consultant.
More-over, as a complete total definitive newbie regarding PHP and MySQL, your script permitted me to dig deeper into PHP and globally into the PHP/MySQL community. The result - in less than two weeks I've been able to:
1. fully intall MySQL and PHP with tight security
2. create an full-blown WebApp by assembling pieces of codes / classes / extensions from here and there (a Photo Catalog with Administrative interface, including all goodies you could ask for that: infinite categories, multi-categorization for one item, EFIX/IPCT, dynamic layout of tables given user preferences, grouping of pictures (like a shoping basket or a favorite list) for users, optional gateway to paypal (for one/several items or one/several groups of items selected by the user), etc...
YOU WERE THE FIRST SCRIPT I'VE IMPLEMENTED AND STUDIED (after some modifications), and doing so forced me to dig into classes and such...
I used to work with JavaScrip, some ASP, mostly ColdFusion (which is much better than ASP imho), Perl. NOW, PHP ROCKS (still, ColdFusion is much better in terms of security and developpment speed)!
One week... I'm still flabergasted to have discovered such a language, more over such a community. I WILL NEVER BE ABLE TO THANK YOU ENOUGH!!! :-))

login script

hello martin...
i wonder if you still go through these posts, coz man, u hav helluva lotta patience. anyways...
first off i'd like to thank you for the script.. realli helped me understand some basics...
i hav implemented a login script at http://markiv.thefreebizhost.com/forum/forum.php
i store a cookie with the encrypted session info when the user logs in and check it against the session id stored in the database (mysql) when the user wants to post a message...
it doesn't work in all comps coz of some browser cookie settings... is there any way to bypass this... the trouble seems to be specific to IE...
and if there wuz som1 tryin to hack the site... is there som dumb thing ive overlooked that wuld make it a walk in the park for the hacker?
thanks

unsecure

why the fuck would someone go to all the trouble of
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.
Somebody who has a site (on a shared host with you) can generate valid session for your site.
Somebody may sniff network traffic and catch the cookie.
If all they have to do is wait for a user to submit the form and then look at the username and password in the packet that was sent?