CHAPTER 12

image

Password Protection

One of the last things you need to do before you can call your blog “web-ready” is to hide the administration module from users who aren’t authorized to see it. In this chapter, you’ll learn how to build a system that lets you create new administrators and require administrators to log in with a password before they can create, edit, and delete entries on the blog. Creating this system requires that you perform the following tasks:

  • Create an admin table in the simple_blog database
  • Use one-way encryption of passwords
  • Create an HTML form for creating new administrators
  • Insert one or more administrators into the admin table
  • Create a login form for administrators
  • Hide administration module from unauthorized users
  • Use sessions to persist a login state across multiple HTTP requests

Creating an admin_table in the Database

Enabling authorized administrators for your site requires that you create a table to store their data. I have created an admin table to store the following information about administrators:

  • admin_id: a unique number identifying one administrator
  • email: the administrator’s e-mail address
  • password: the administrator’s password

You will encrypt the password with the so-called MD5 algorithm into a string of 32 characters. Consequently, the password column can use the VARCHAR data type and limit input to 32 characters.

To create the admin table, navigate to http://localhost/phpmyadmin in a browser, select your simple_blog database and open the SQL tab. Enter the following command to create your table:

create table admin(
    admin_id INT NOT NULL AUTO_INCREMENT,
    email TEXT,
    password VARCHAR(32),
    primary key (admin_id)
)

Encrypting Passwords with MD5

Once you get around to inserting new users into your admin table, you will see how to encrypt a string with the MD5 algorithm. The MD5 algorithm provides so-called one-way encryption.

Encrypting passwords is a good practice, because an encrypted password is harder to steal. Let’s imagine your password is test. Run it through MD5, and test becomes 098f6bcd4621d373cade4e832627b4f6. No matter how long or how short your password is, it becomes a 32-character string, once it has been through MD5.

Even if your database suffers a serious attack, and all usernames and passwords are exposed, attackers are still unable to misuse usernames and passwords.

One-Way Encryption

Passwords are often protected through so-called one-way encryption. If you encrypt a password with a one-way encryption algorithm, it should be practically impossible to decrypt. So, if you have an encrypted password such as 098f6bcd4621d373cade4e832627b4f6, it should be practically impossible to figure out that the unencrypted password was test. This means that even if your database is compromised, passwords are still protected.

Any system that can recover a lost password is inherently unsafe. If your original password can be recovered, the system must somehow remember your password. If your password is remembered by the system, it is likely that system administrators can see your password. If your password can be seen, it is vulnerable. A secure system would only store encrypted passwords. If you should forget your password, a secure system would provide you with an opportunity to reset your password. With one-way encryption, sensitive data such as passwords can be kept hidden from system administrators.

In the context of your blogging system, it means that administrator passwords are protected from you or other blog administrators. If a user loses her password, you cannot send it to her again. You can offer her a chance to reset her password, but the password is completely safe—as it should be.

Sufficient Security

Assume that even the most secure systems can be hacked. The question is how much time and effort is required from the attacker. Your task is to provide enough security to discourage attackers from your content. If your content is very valuable, you need very serious security. Most personal blogs are probably not particularly attractive targets for IT criminals.

MD5 is a one-way encryption algorithm. It is often used in PHP, because it is very easy to use in PHP applications. By itself, MD5 is not sufficient to create optimal password security. Despite all its merits, MD5 has been compromised. But for our purposes, it will suffice. You have already taken measures against SQL injection attacks. Soon, you will also have encrypted passwords, so you have probably deterred any attackers from your blogging system.

Image Note  There is another common type of attack known as a cross-site scripting (XSS) attack. You could consider doing a bit of Internet research about preventing XSS attacks on PHP sites.

You should know that there is more to learn about building secure PHP applications. You cannot expect MD5 to keep attackers at bay, if you develop a system that holds valuable data. You might consider consulting Chris Snyder’s Pro PHP Security (Apress, 2010), to learn much more about this topic.

Adding Administrators in the Database

You have a database table to save administrator credentials; you’re ready to start creating blog administrators. Your first step is to create a form that allows you to enter an e-mail address and a corresponding password. Once you accomplish this, you must store the information in the database for later use.

Building an HTML Form

As you have already learned, it is best to create forms that provide feedback to users. You might as well prepare the form for user feedback right away. Create a new file in views/admin/new-admin-form-html.php. Here’s what my form looks like:

<?php
//complete code for views/admin/new-admin-form-html.php
if( isset($adminFormMessage) === false ) {
    $adminFormMessage = "";
}
 
return "<form method='post' action='admin.php?page=users'>
    <fieldset>
        <legend>Create new admin user</legend>
        <label>e-mail</label>
        <input type='text' name='email' required/>
        <label>password</label>
        <input type='password' name='password' required/>
        <input type='submit' value='create user' name='new-admin'/>
    </fieldset>
    <p id='admin-form-message'>$adminFormMessage</p>
</form>
";

To display the form, you need a controller to load it and return it to admin.php. You can create a new navigation item and a corresponding controller. Begin by updating your navigation with an additional link, as follows:

<?php
//complete code for views/admin/admin-navigation.php
 
//new code: notice item added for a 'Create admin user' page
return "
<nav id='admin-navigation'>
    <a href='admin.php?page=entries'>All entries</a>
    <a href='admin.php?page=editor'>Editor</a>
    <a href='admin.php?page=images'>Image manager</a>
    <a href='admin.php?page=users'>Create admin user</a>
</nav>
";

With the form’s view and a new navigation item created, the next step could be to create a controller to load the view and have it displayed. In the previous example, I created an href for the new link that requests a new page called users. So, my system expects a controller called controllers/admin/users.php. Create such a file and write the following code:

<?php
//complete code for controllers/admin/users.php
$newAdminForm = include_once "views/admin/new-admin-form-html.php";
return $newAdminForm;

Save your work and load http://localhost/blog/admin.php?page=users in your browser. You should see an unstyled, yet valid, HTML form. You can’t really use it yet: it cannot insert new administrators into the database table.

Saving New Administrators in the Database

Inserting new rows into a database table is a job for a model script. You have already made good use of the table data gateway design pattern a couple of times. You have even prepared a base Table class, which you can extend to create a table data gateway for the admin table.

At this point, you don’t need to be able to do much: you want to avoid name conflicts, so before a new admin user is created, the code should check whether the e-mail is already used in the table. If the e-mail is not in use, the model script should insert a new entry into the admin table. Create a new file, models/Admin_Table.class.php, and write a new class definition, as follows:

<?php
//complete code for models/Admin_Table.class.php
//include parent class' definition
include_once "models/Table.class.php";
 
class Admin_Table extends Table {
 
    public function create ( $email, $password ) {
        //check if e-mail is available
        $this->checkEmail( $email );
        //encrypt password with MD5
        $sql = "INSERT INTO admin ( email, password )
                VALUES( ?, MD5(?) )";
        $data= array( $email, $password );
        $this->makeStatement( $sql, $data );  
    }
    
    private function checkEmail ($email) {
        $sql = "SELECT email FROM admin WHERE email = ?";
        $data = array( $email );
        $this->makeStatement( $sql, $data );
        $statement = $this->makeStatement( $sql, $data );
        //if a user with that e-mail is found in database
        if ( $statement->rowCount() === 1 ) {
            //throw an exception > do NOT create new admin user
            $e = new Exception("Error: '$email' already used!");
            throw $e;
        }
    }
}

The preceding code is just waiting to be called from a controller. The create() method takes two arguments for a new e-mail and the associated password. The private method checkEmail() is used to check whether the provided e-mail already exists in the database. If it does, an Exception object is created with a relevant message, and the created exception is thrown. This means you can use a try-catch statement in your controller. You will soon write code that will try to create a new admin user. If the operation fails, your code will fail gracefully, by catching the exception and providing relevant feedback to the user.

Take a look at the $sql variable in the create() method. See how the received password is encrypted with MD5 as it is inserted into the database? That’s possible because the SQL language has a built-in MD5 function. Let’s go back to the controller script in controllers/admin/users.php and write some code, so that input from the form will be used to insert new admin users in the database:

<?php
//complete code for controllers/admin/users.php
//new code starts here
include_once "models/Admin_Table.class.php";
 
//is form submitted?
$createNewAdmin = isset( $_POST['new-admin'] );
//if it is...
if( $createNewAdmin ) {
    //grab form input
    $newEmail = $_POST['email'];
    $newPassword = $_POST['password'];    
    $adminTable = new Admin_Table($db);
    try {
        //try to create a new admin user
        $adminTable->create( $newEmail, $newPassword );
        //tell user how it went
        $adminFormMessage = "New user created for $newEmail!";
    } catch ( Exception $e ) {
        //if operation failed, tell user what went wrong
        $adminFormMessage = $e->getMessage();
    }
}
//end of new code
 
$newAdminForm = include_once "views/admin/new-admin-form-html.php";
return $newAdminForm;

Save your code and test it by running http://localhost/blog/admin.php?page=users in your browser. Try to create an admin user. You should get a confirmation message displayed in the form. Check in http://localhost/phpmyadmin to see whether the admin user was in fact inserted into the admin database table.

Once you have created a new admin user, you could try to create one more admin user using the same e-mail address. It shouldn’t be allowed, and you should receive an error message in the form. To be more specific, you should see the message of the Exception thrown from the checkEmail() method of the Admin_Table class.

Planning Login

The rest of this chapter focuses on restricting access to the administration module. Only authenticated users should be allowed to create, edit, and delete blog entries.

You can continue to use the MVC idea for organizing the code. You’ll need two views: A login form and a logout form. You’ll need a controller to handle user interactions received from these two views and a model to actually perform login and logout. The model should also remember a state: it should remember if the user is logged in or not.

Creating a Login Form

Begin with a login form. You should end up with a system in which a user must provide a valid e-mail and a matching password to be allowed access to the blog administration module. So, you need a form with e-mail and password fields. Create a new file in views/admin/login-form-html.php:

<?php
//complete code for views/admin/login-form-html.php
return " <form method='post' action='admin.php'>
    <p>Login to access restricted area</p>
    <label>e-mail</label><input type='email' name='email' required />
    <label>password</label>
    <input type='password' name='password' required />
    <input type='submit' value='login' name='log-in' />
</form>";

You should also create a simple controller. At this point, it should only load the view and output it. Create a new file in controllers/admin/login.php:

<?php
//complete code for controllers/admin/login.php
$view = include_once "views/admin/login-form-html.php";
return $view;

The admin navigation does not provide a menu item for the login, and it shouldn’t. You want the login form to be displayed as the default view of admin.php. Only when a user is authenticated and logged in should the user be allowed to see the blog administration module.

You can create a class to represent an admin user. This class should remember whether the current visitor is logged in. Initially, you can safely assume that a user is not logged in. It should be possible for users to log in and log out. You can represent the state (that is, whether the user is logged in or not) with a property. You’ll need methods to log in and log out. Each method should manipulate the login state represented by a property. Create a new file in models/Admin_User.class.php, as follows:

<?php
//complete code for models/Admin_User.class.php
 
class Admin_User {
    private $loggedIn = false;
    
    public function isLoggedIn(){
        return $this->loggedIn;
    }
 
    public function login () {
        $this->loggedIn = true;
    }
 
    public function logout () {
        $this->loggedIn = false;
    }
          
}

Hiding Controls from Unauthorized Users

It is very important that a login can actually hide parts of a system from unauthorized users. You might argue that this is the whole point of a login. So far, the administration module has been readily available to anybody visiting admin.php. You need to make a few changes here.

  1. Create an Admin_User object to remember login state.
  2. If a visitor is not logged in, show only the login form.
  3. If a user is logged in, show the admin navigation and the administration module.

To do that, you have to make a few changes in admin.php. Some of the changes will involve commenting out, or deleting, existing code. But you also have to add some new lines of code, as follows:

<?php
//complete code for blog/admin.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
 
include_once "models/Page_Data.class.php";
$pageData = new Page_Data();
$pageData->title = "PHP/MySQL blog demo";
$pageData->addCSS("css/blog.css");
$pageData->addScript("js/editor.js");
//code changes start here: comment out navigation
//$pageData->content = include_once "views/admin/admin-navigation.php";
 
$dbInfo = "mysql:host=localhost;dbname=techreview_blog";
$dbUser = "root";
$dbPassword = "";
$db = new PDO( $dbInfo, $dbUser, $dbPassword );
$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
//code changes here: comment out some of the existing code in admin.php
//$navigationIsClicked = isset( $_GET['page'] );
//if ($navigationIsClicked ) {
//    $contrl = $_GET['page'];
//} else {
//    $contrl = "entries";
//}
 
//new code added below
include_once "models/Admin_User.class.php";
$admin = new Admin_User();
//load the login controller, which will show the login form
$pageData->content = include_once "controllers/admin/login.php";
 
//add a new if statement
//show admin module only if user is logged in
if( $admin->isLoggedIn() ) {
    $pageData->content .=include "views/admin/admin-navigation.php";
    $navigationIsClicked = isset( $_GET['page'] );
    if ($navigationIsClicked ) {
        $controller = $_GET['page'];
    } else {
        $controller = "entries";
    }
    $pathToController = "controllers/admin/$controller.php";    
    $pageData->content .=include_once $pathToController;
} //end if-statement
//end of new code
 
$page = include_once "views/page.php";
echo $page;

With this code, you’re ready to test your login. If you navigate your browser to http://localhost/blog/admin.php, you should expect to see nothing but the login form. That is as it should be, as the loggedIn property of the $admin object is false by default, and when a user is not logged in, the user should not be allowed into the administration area.

Logging In a User

It is time to allow users to log in. At this point, I will not really authenticate users. I will not write code to check whether the supplied e-mail and password are valid. I will show you an approach to that later in this chapter. For now, you’ll simply log in anybody who submits the login form. Dealing with such user interactions is a job for the login controller. Update your code in controllers/admin/login.php:

<?php
//complete code for controllers/admin/login.php
//new code here
$loginFormSubmitted = isset( $_POST['log-in'] );
if( $loginFormSubmitted ) {
    $admin->login();
}
//end of new code
$view = include_once "views/admin/login-form-html.php";
return $view;

Changes are small and should be easily accessible for you by now. Try to point your browser to http://localhost/blog/admin.php and log in. You can use any credentials!

It should work like a charm, and you should see the admin navigation and a clickable list of all blog entries. But wait! Click one of the entries to load it into the entry editor. What happens? Oh no! You don’t see the entry editor. Instead, you find yourself looking at the login form once again. Apparently, you were instantly logged out? Why?!?

HTTP Is Stateless

The HyperText Transfer Protocol is the foundation of much data communication on the Internet. It is stateless, which means it treats each new request as a separate transaction. In practical terms, it means that all PHP variables and objects are created from scratch with every new request.

That has some consequences for you. When you submit the login form, you really make an HTTP request. PHP will run, and the $admin object will remember that you are logged in. Next, you click an entry. This will make a new HTTP request, and thus, a new $admin object will be created. PHP will not remember that you just logged in, because the new HTTP request is treated as an independent, separate transaction. Whatever happened with the previous HTTP request is completely forgotten.

Superglobals Revisited: $_SESSION

This stateless HTTP is bad news for your login. No matter how many times you log in, PHP will forget about it with every new request. It is very impractical, and of course, there is a solution: persist state across requests. You need a way to force PHP to remember that a given user is logged in. You need a PHP session.

When a PHP session is started, the visiting user’s browser will be assigned a unique identification number: a session id. The server will create a small, temporary file on the server-side. Any information you require your application to remember across requests should be stored in this file. PHP will handle this temporary file for you. A PHP session has a default lifetime of 20 minutes, and duration can be configured in the php.ini file.

Image Note  Read more at www.php.net/manual/en/intro.session.php.

PHP provides a superglobal to make session handling easier. You’ve already met a few superglobals: $_GET, $_POST, and $_FILES. Now, it’s time for you to meet $_SESSION.

Persisting State with a Session

To use a session, your code must start a session. With that in place, you can create a session variable, which is a variable whose value is stateful, meaning a variable whose value can persist across HTTP requests.

You already have an Admin_User class, which uses a normal property to remember login state. You can use a session variable instead. Here’s how that can be done:

<?php
//complete code for models/Admin_User.class.php
 
class Admin_User {
 
    //declare a new method, a constructor
    public function __construct(){
        //start a session
        session_start();
    }
    
    //edit existing method
    public function isLoggedIn(){
        $sessionIsSet = isset( $_SESSION['logged_in'] );
        if ( $sessionIsSet ) {
            $out = $_SESSION['logged_in'];
        } else {
            $out = false;
        }
        return $out;
    }  
  
    //edit existing method
    public function login () {
        //set session variable ['logged_in'] to true
        $_SESSION['logged_in'] = true;
    }
    
    //edit existing method
    public function logout () {
        //set session variable ['logged_in'] to false
        $_SESSION['logged_in'] = false;
    }
 
}

Save the code changes and load http://localhost/blog/admin.php in your browser. You can still log in with any credentials, but this time, PHP will remember that you are logged in. So, clicking a blog entry will actually load the clicked entry into the entry editor. This is great news: you can log in as an administrator, and once logged in, you can use the administration module. But all is not perfect yet. Anybody can log in, because the e-mail and password aren’t really compared to database records of valid administrators. Also, logged-in users cannot log out. Let’s tackle the easiest task first.

Logging Users Out

It is customary to provide a logout option for logged-in users. It is also a good idea: you don’t want administrators to stay logged in with no option for logging out. What if an administrator has to leave the computer to get a fresh cup of coffee? You wouldn’t want to leave the administration module exposed. Let’s create a new view for logging out. Create a new file in views/admin/logout-form-html.php, as follows:

<?php
//complete code for views/admin/logout-form-html.php
return "
<form method='post' action='admin.php'>
    <label>logged in as administrator</label>
    <input type='submit' value='log out' name='logout' />
</form>";

This view should be displayed whenever a user is logged in. But simply showing a logout form won’t actually log out users. If a user clicks the Logout button, this should run a script to log out the user. These are tasks for the controller. You have to write some code for the login controller in controllers/admin/login.php:

<?php
//complete code for controllers/admin/login.php
 
$loginFormSubmitted = isset( $_POST['log-in'] );
if( $loginFormSubmitted ) {
    $admin->login();
}
 
//new code below
$loggingOut = isset ( $_POST['logout'] );
if ( $loggingOut ){
    $admin->logout();
}
 
if ( $admin->isLoggedIn() ) {
    $view = include_once "views/admin/logout-form-html.php";
} else {
    $view = include_once "views/admin/login-form-html.php";
}
//comment out the former include statement
//$view = include_once "views/admin/login-form-html.php";
//end of code changes
return $view;

You’re making excellent progress! You should be able to log in and log out now, and PHP will remember your current state through a session variable. It is a bit of a problem that anybody can log in using any credentials. The code does not check if the supplied username and password are correct.

Allowing Authorized Users Only

The login is nearly complete. All you have to do is check whether the supplied e-mail and password match exactly one record in the database. The information is available in the admin table, and you already have a table data gateway called Admin_Table. You can create a new method to check whether submitted credentials are valid.

//partial  code for models/admin/Admin_Table.class.php
//declare new method in Admin_Table class
public function checkCredentials ( $email, $password ){
    $sql = "SELECT email FROM admin
            WHERE email = ? AND password = MD5(?)";
    $data = array($email, $password);
    $statement = $this->makeStatement( $sql, $data );
    if ( $statement->rowCount() === 1 ) {
        $out = true;
    } else {
        $loginProblem = new Exception( "login failed!" );
        throw $loginProblem;
    }
    return $out;
}

See how the received password is encrypted with MD5? In the database, you have MD5-encrypted passwords. To compare a password in the database with a password received from the login form, your code will have to encrypt the received password. If you were to compare an encrypted password with an un-encrypted password, users would not be able to login, because test is not identical to 098f6bcd4621d373cade4e832627b4f6.

Note how the method will create a new Exception object and throw it, if the submitted e-mail and password do not match exactly one row of data in the admin table. With this method declared, you’re ready to call it from your login controller whenever a user tries to log in.

<?php
//complete code for controllers/admin/login.php
//new code: include the new class definition
include_once "models/Admin_Table.class.php";
 
$loginFormSubmitted = isset( $_POST['log-in'] );
if( $loginFormSubmitted ) {
    //code changes start here: comment out the existing login call
    //$admin->login();
    //grab submitted credentials
    $email = $_POST['email'];
    $password = $_POST['password'];
    //create an object for communicating with the database table
    $adminTable = new Admin_Table( $db );
    try {
        //try to login user
        $adminTable->checkCredentials( $email, $password );
        $admin->login();
    } catch ( Exception $e ) {
        //login failed
    }
    //end of code changes
}
 
$loggingOut = isset ( $_POST['logout'] );
if ( $loggingOut ){
    $admin->logout();
}
if ( $admin->isLoggedIn() ) {
    $view = include_once "views/admin/logout-form-html.php";
} else {
    $view = include_once "views/admin/login-form-html.php";
}
return $view;

This is a huge improvement in terms of security. With the preceding code saved, you should only be able to log in with valid user credentials. But don’t take my word for it. Try to log in with bad credentials; it should be impossible.

Exercises

You should consider a few additions related to the login. You’ve tried it all in previous chapters, so these additions should be great learning exercises for you.

Your login form uses a required attribute. As you have learned in Chapter 10, different browsers treat the required attribute differently. You could use JavaScript to provide a more consistent behavior across modern browsers. This would improve the usability, and it would also give you an opportunity to practice a little JavaScript.

The login form fails silently at this point. An Exception object is thrown when a user login fails. The exception is even caught, but the exception message is not displayed as feedback to the user. Perhaps you could grab the exception message and display it in the login form. It would be a lot like creating an $adminFormMessage in the beginning of this chapter.

Once you have a basic user feedback implemented, you could try something more advanced for the login form. When a user login fails, check whether the e-mail exists in the database. If it does, let the user know. Wrong password, please try again. If the e-mail does not exist, tell the user The supplied e-mail does not match any record in the system, please try again.

Summary

This was a short chapter. With a one-way encryption for privacy and a session for stateful memory, you have managed to effectively restrict access to the administration module of your blog. I hope you will agree it was very rewarding to implement a login system. I especially enjoyed providing you with extra opportunities for creating forms that communicate with users.

Your blog is web-ready. The next chapter walks you through the process of uploading your project to an online web host, so that your blog can be published on the Internet.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.226.121