In this article Martin 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, and more.
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 PHP4.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'];
i f ($_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.
|