This is part 2 of a multi-part series on the design of a complete application framework written in PHP. In part 1, we covered the basic class structure of the framework and laid out the scope of the project. This part adds session handling to our application and illustrates ways of managing users.
Sessions
HTTP is a stateless protocol and, as such, does not maintain any information about the connections made to the server. This means that, with HTTP alone, the web server cannot know anything about the users connected to your web application and will treats each page request as a new connection. Apache/PHP gets around this limitation by offering support for sessions. Conceptually sessions are a fairly simple idea. The first time a web user connects to the server, he is assigned a unique ID. The web server maintains session information in a file, which can be located using this ID. The user also has to maintain this ID with each connection to the server. This is typically done by storing the ID in a cookie, which is sent back to the server as part of the typical Request-Response1 sequence. If the user does not allow cookies, the session ID can also be sent to the server with each page request using the query string (the part of the URL after the ?). Because the web client is disconnected, the web server will expire sessions after predefined periods of inactivity.
We will not go over configuring Apache/PHP in this article but will utilize sessions to maintain user information in our application. It is assumed that session support is already enabled and configured on your server. We will pick up where we left off in part 1 of this series when we described the system base class. You may recall that the first line in class_system.php is session_start(), which starts a new user session if none exists or does nothing otherwise. Depending on how your server is configured, this will cause the session ID to be saved in the clients cookie file or passed as part of the URL. The session ID is always available to you by calling the build in function session_id(). With these tools at hand, we can now build a web application that can authenticate a user and maintain the users information as he is browsing the different pages on the site. Without sessions, we would have to prompt the user for their login credentials every single time they request page.
So what will we want to store in the session? Lets start with the obvious like the users name. If you take a look at class_user.php you will see the rest of the data being stored. When this file is included, the first thing that is checked is whether a user is logged in (default session values are set if the users id is not set). Note that the session_start() must have already been called before we start playing with the $_SESSION array which contains all our session data. The UserID will be used to identify the user in our database (which should already be accessible after part one of this series). The Role will be used to determine whether the user has sufficient privileges to access certain features of the application. The LoggedIn flag will be used to determine if the user has already been authenticated and thePersistent flag will be used to determine whether the user wants to automatically be logged in based on their cookie content.
//session has not been established
if (!isset($_SESSION['UserID']) ) {
set_session_defaults();
}
//reset session values
function set_session_defaults() {
$_SESSION['UserID'] = '0'; //User ID in Database
$_SESSION['Login'] = ''; //Login Name
$_SESSION['UserName'] = ''; //User Name
$_SESSION['Role'] = '0'; //Role
$_SESSION['LoggedIn'] = false; //is user logged in
$_SESSION['Persistent'] = false; //is persistent cookie set
}
User Data
We store all the user data in our database in table tblUsers. This table can be created using the following SQL statement (mySQL only).
CREATE TABLE `tblUsers` (
`UserID` int(10) unsigned NOT NULL auto_increment,
`Login` varchar(50) NOT NULL default '',
`Password` varchar(32) NOT NULL default '',
`Role` int(10) unsigned NOT NULL default '1',
`Email` varchar(100) NOT NULL default '',
`RegisterDate` date default '0000-00-00',
`LastLogon` date default '0000-00-00',
`SessionID` varchar(32) default '',
`SessionIP` varchar(15) default '',
`FirstName` varchar(50) default NULL,
`LastName` varchar(50) default NULL,
PRIMARY KEY (`UserID`),
UNIQUE KEY `Email` (`Email`),
UNIQUE KEY `Login` (`Login`)
) TYPE=MyISAM COMMENT='Registered Users';
This statement creates a bare-bones user table. Most of the fields are self explanatory. We need the UserID field to uniquely identify each user. The Login field, which must also be unique, stores the user’s desired login name. The Password field stores the MD5 hash of the user’s password. We are not storing the actual password for security and privacy reasons. Instead we can compare the MD5 hash of the password entered with the value stored in this table to authenticate the user. The user’s Role will be used to assign the user to a permission group. Finaly, we will use the LastLogon, SessionID, and SessionIP fields to track the user’s usage of our system including the last time the user logged in, the last PHP session ID the user had, and the IP address of the user’s host. These fields are updated each time the user successfully logs in using the _updateRecord() function in the user system class. These fields are also used for security in preventing cross-site scripting attacks.
//Update session data on the server
function _updateRecord () {
$session = $this->db->quote(session_id());
$ip = $this->db->quote($_SERVER['REMOTE_ADDR']);
$sql = "UPDATE tblUsers SET
LastLogon = CURRENT_DATE,
SessionID = $session,
SessionIP = $ip
WHERE UserID = $this->id";
$this->db->query($sql);
}
Security Issues
This seems like a logical place to address several security issues that come up when developing web applications. Since security is a major aspect of user management, we need to be very careful not to leave any careless bugs in this part of our code.
The first issue that needs to be addressed is the potential for SQL injection in any web application that uses posted web data to query a database. In our case, we use the login name and password supplied by the user to query the database and authenticate the user. A malicious user can submit SQL code as part of input field text and may potentially achieve any of the following: 1) login without having a valid account, 2) determine the internal structure of our database or 3) modify our database. The simplest example of this is the SQL code used to test if the user is valid.
$sql = "SELECT * FROM tblUsers
WHERE Login = '$username' AND Password = md5('$password')";
Suppose the user enters admin'-- and leaves the password blank. The SQL code executed by the server is: SELECT * FROM tblUsers WHERE Login = 'admin'--' AND Password = md5(''). Do you see the problem? Instead of checking the login name and password, the code only the checks the login name and the rest is commented out. As long as there is a user admin in the table, the query will return a positive response. You can read about other SQL injection exploits in David Litchfield’spublication.
How do you protect yourself from this kind of threat. The first step is to validate any data sent to the SQL server that comes from an untrusted source (i.e. the user). PEAR DB provides us with this protection using the quote() function which should be used on any string sent to the SQL server. Our login() function shows other precautions that we can take. In the code, we actually check the password in both the SQL server and in PHP based on the record returned. This way, the exploit would have to
work for both the SQL server and PHP for an unauthorized user to get in. Overkill you say? Well, maybe.
Another issue that we have to be aware of is the potential for session stealing and cross site scripting (XSS). I won’t get into the various ways that a hacker can assume the session of another authenticated user but rest assured that it is possible. In fact, many methods are based on social engineering rather than bugs in the actual code so this can be a fairly difficult problem to solve. In order to protect our users from this threat, we store the Session IP and Session ID of the user each time he logs in. Then, when any page is loaded, the users current Session ID and IP address are compared to the values in the database. If the values don’t match then the session is destroyed. This way, if a hacker gets a victim to log in from one machine and then tries to use that active session from his own machine, the session will be closed before any harm can be done. The code to implement this is bellow.
//check if the current session is valid (otherwise logout)
function _checkSession() {
$login = $this->db->quote($_SESSION['Login']);
$role = $this->db->quote($_SESSION['Role']);
$session = $this->db->quote(session_id());
$ip = $this->db->quote($_SERVER['REMOTE_ADDR']);
$sql = "SELECT * FROM tblUsers WHERE
Login = $login AND
Role = $role AND
SessionID = $session AND
SessionIP = $ip";
$result = $this->db->getRow($sql);
if ($result) {
$this->_setSession($result);
} else {
$this->logout();
}
}
Authentication
Now that we understand the various security issues involved, lets look at the code for authenticating a user. The login() function accepts a login name and password and returns a Boolean reply to indicate success. As stated above, we must assume that the values passed into the function came from an untrusted source and use the quote() function to avoid problems. The complete code is provided below.
//Login a user with name and pw.
//Returns Boolean
function login($username, $password) {
$md5pw = md5($password);
$username = $this->db->quote($username);
$password = $this->db->quote($password);
$sql = "SELECT * FROM tblUsers WHERE
Login = $username AND
Password = md5($password)";
$result = $this->db->getRow($sql);
//check if pw is correct again (prevent sql injection)
if ($result and $result['Password'] == $md5pw) {
$this->_setSession($result);
$this->_updateRecord(); //update session info in db
return true;
} else {
set_session_defaults();
return false;
}
}
To logout, we have to clear the session variables on the server as well as the session cookies on the client. We also have to close the session. The code below does just that.
//Logout the current user (reset session)
function logout() {
$_SESSION = array(); //clear session
unset($_COOKIE[session_name()]); //clear cookie
session_destroy(); //kill the session
}
In every page that requires authentication, we can simply check the session to see if they user is logged in or we can check the user’s role to see if the user has sufficient privileges. The role is defined as a number with the larger numbers indicating more rights. The code below checks to see if the users has enough rights using the role.
//check if user has enough permissions
//$role is the minimum level required for entry
//Returns Boolean
function checkPerm($role) {
if ($_SESSION['LoggedIn']) {
if ($_SESSION['Role']>=$role) {
return true;
} else {
return false;
}
} else {
return false;
}
}
Login/Logout Interface
Now that we have a framework for handling sessions and user accounts, we need an interface to allow the user to login and out. Using our framework, creating this interface should be fairly easy. Let us start with the simpler logout.php page which will be used to log a user out. This page has no content to display to the user and simply redirects the user to the index page after having logged him out.
define('NO_DB', 1);
define('NO_PRINT', 1);
include "include/class_system.php";
class Page extends SystemBase {
function init() {
$this->user->logout();
$this->redirect("index.php");
}
}
$p = new Page();
First we define the NO_DB and NO_PRINT constants to optimize the loading time of this page (as described in Part 1 of this series). Now all we have to do is use theuser class to log the user out and redirect to another page in the page’s initialization event.
The login.php page will need an interface and we will use the system’s form handling abilities to simplify the implementation process. Details of how this works will be described in Parts 3 and 4 of this series. For now, all we need to know is that we need an HTML form that is linked the application logic. The form is provided below.
<form action="<?=$_SERVER['PHP_SELF']?>" method="POST" name="<?=$formname?>">
<input type="hidden" name="__FORMSTATE" value="<?=$_POST['__FORMSTATE']?>">
<table>
<tr>
<td>Username:</td>
<td><input type="text" name="txtUser" value="<?=$_POST['txtUser']?>"></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" name="txtPW" value="<?=$_POST['txtPW']?>"></td>
</tr>
<tr>
<td colspan="2">
<input type="checkbox" name="chkPersistant" <?=$persistant?>>
Remember me on this computer
</td>
</tr>
<tr style="text-align: center; color: red; font-weight: bold">
<td colspan="2">
<?=$error?>
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" name="Login" value="Login">
<input type="reset" name="Reset" value="Clear">
</td>
</tr>
</table>
</form>
Now we need the code to log a user in. This code sample demonstrates how to use the system framework to load the above form into a page template, handle the form events, and use the user class to authenticate the user.
class Page extends SystemBase {
function init() {
$this->form = new FormTemplate("login.frm.php", "frmLogin");
$this->page->set("title","Login page");
if (!isset($_POST['txtUser']) && $name=getCookie("persistantName")) {
$this->form->set("persistant","checked");
$_POST['txtUser']=$name;
}
}
function handleFormEvents() {
if (isset($_POST["Login"])) {
if($this->user->login($_POST['txtUser'],$_POST['txtPW'])) {
if (isset($_POST['chkPersistant'])) {
sendCookie("persistantName", $_POST['txtUser']);
} else {
deleteCookie("persistantName");
}
$this->redirect($_SESSION['LastPage']);
} else
$this->form->set("error","Invalid Account");
}
}
}
$p = new Page();
On page initialization, the form is loaded into the page template, the page’s title is set and the user’s login name is pre-entered into the input field if the persistent cookie is set. The real work happens when we handle the form events (i.e. when the user presses a button to submit the page). First we check if the login button was clicked. Then we use the login name and password submitted to authenticate the user. If authentication is successful, we also set a cookie to remember the users name for the next time. If the authentication fails, an error is displayed on the page.
Summary
So far, we have laid the foundation for how our application will behave and how the framework will be used. We added user management capabilities to our application and covered several security issues. Read the next part to see how to implement page templates and separate application logic from the presentation layer.
1. The Request-Response sequence is the key behind many network protocols including HTTP. A user request is made by sending the server the URL of the desired page over a specific port (typically port 80 for HTTP and 443 for HTTPS). The web server listens on the specified port and generates a response, which is composed of a header section and a body section separated by two new line characters. The header section contains descriptive information about the data being sent to the user including the content type/encoding and content length. The body section contains the desired data, which is typically the HTML page (of course it could be any file that the web server is configured to send). When using server-side programming languages such as PHP, the web server has to pass the page request to the languages interpreter, which will run the page on the server and generate the HTML output. For more information about the specification of the header and body sections visit the World Wide Web Consortium.