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.
1. 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.
2. 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'].
3. 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.
4. 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.
5. 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.
6. 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.
7. 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().
8. 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.
9. 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/');
}
}
10. 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.
11. 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.
|