Choose a version here. If you've not already started the tutorial, just go with the latest one.

30 Aug 2014 Preview version Choose
5 Oct 2014 Explicitly adds a specific charset for htmlspecialchars(), and wraps it with a custom function Choose
16 Oct 2014 Updated redirect function to work with vhost subfolders Choose
4 Nov 2014 Minor improvements: added missing docblock, fixed security issue, CSS tweak. Switched hashing method to DEFAULT instead of BCRYPT, this is best practice. Choose
25 Nov 2014 Improve the notes on getting started, in particular choosing a programmer's editor. Added introduction to mod_rewrite rules. Choose
16 Aug 2018 Some bug fixes, remove compatibility library for earlier version of PHP Choose
OK
NB: There are several versions of this tutorial, each successive one containing additional improvements. If you're in the middle of working through it, please check the versions panel above, to ensure you're not mixing code from different versions.

Make your own blog

Adding a login system

The next thing I decided to implement was a log-on system. This will allow a blog administrator to write, edit and delete posts, and to delete comments. In this set of changes, I add a users table, and allow the installer to create an admin user with a new password each time it is run. So, make the following changes, and then re-create the database:

Expand/contract code area Select previous tab
Select next tab
93
94
95
96
        'http://anotherexample.com/',
        "This is a comment from Jonny"
    )
;
93
94
95
96
97
98
99
100
101
102
103
104
        'http://anotherexample.com/',
        "This is a comment from Jonny"
    )
;
CREATE TABLE user (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    username VARCHAR NOT NULL,
    password VARCHAR NOT NULL,
    created_at VARCHAR NOT NULL,
    is_enabled BOOLEAN NOT NULL DEFAULT true
);

There are two fields in the new user table that I added based on my experience rather than an immediate need. These are created_at, which holds the date and time when the user was first set up, and is_enabled, which allows us to turn users on and off. Most user systems will find a practical use for these simple features during their lifetime.

You'll have noticed a comment in lib/install.php noting that the password is stored in plaintext. This means that, as it stands, passwords would be stored literally, which is considered to be very bad practice indeed. What we should do is to store passwords in an encoded, non-reversible format, so that even if they are stolen they will be nearly impossible to read. This acts as a form of protection should a cracker get through our security and steal our database.

So, let's make that improvement straight away. To do so, we'll need the library I talked about at the start of the tutorial, stored here in vendor/password_compat. The features it provides are so useful they have been built into PHP 5.5, so if you are running this version (or later) you can skip adding the new file and the associated require_once. However if you are running an earlier version, or if you are not sure which version you have, add all of these changes.

Expand/contract code area Select previous tab
Select next tab
1
 
 
2
3
4
113
114
115
116
 
 
 
 
 
 
 
 
 
 
117
118
119
120
121
122
123
124
125
<?php
/**
* Blog installer function
        $error = 'Could not prepare the user creation';
    }
    // We're storing the password in plaintext, will fix that later
    if (!$error)
    {
        $result = $stmt->execute(
            array(
                'username' => $username,
                'password' => $password,
                'created_at' => getSqlDateForNow(),
            )
        );
1
2
3
4
5
6
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<?php
// Load the hashing library from the root of this project
require_once getRootPath() . '/vendor/password_compat/lib/password.php';
/**
* Blog installer function
        $error = 'Could not prepare the user creation';
    }
    if (!$error)
    {
        // Create a hash of the password, to make a stolen user database (nearly) worthless
        $hash = password_hash($password, PASSWORD_BCRYPT);
        if ($hash === false)
        {
            $error = 'Password hashing failed';
        }
    }
    // Insert user details, including hashed password
    if (!$error)
    {
        $result = $stmt->execute(
            array(
                'username' => $username,
                'password' => $hash,
                'created_at' => getSqlDateForNow(),
            )
        );

So, what does the new code do? It makes use of a new function called password_hash(), which takes a password as input and produces what is known as a hash. A hash is a mathematical calculation that is strictly one way, which means that if sometimes steals our database of password hashes, they will find it very difficult indeed to recreate the passwords they were generated from.

Whether or not you are using it, by all means do have a read of the source code in password.php. However, an in-depth exploration of it would be rather advanced at this stage, so for our purposes we will assume it just works.

The next step is to add a login form, and a link from which to access it:

Expand/contract code area Select previous tab
Select next tab
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html>
    <head>
        <title>
            A blog application | Login
        </title>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    </head>
    <body>
        <?php require 'templates/title.php' ?>
        <p>Login here:</p>
        <form
            method="post"
        >
            <p>
                Username:
                <input type="text" name="username" />
            </p>
            <p>
                Password:
                <input type="password" name="password" />
            </p>
            <input type="submit" name="submit" value="Login" />
        </form>
    </body>
</html>

In the next change, we add our familiar block of business logic before the main HTML. In this case, it checks to see if the form has been submitted; if it has, then it turns on the session system (more about that in a minute), creates a hash of the submitted password, and compares it with the hash stored in the database.

If the user gets their username wrong (e.g. it is not found) or the password wrong (the password hash does not match the one for the username supplied) then we regard this as a login failure. It is important for us not to be too helpful here (such as explaining that a username does not exist), as this information might be useful to a system cracker.

If the password matches however, then we call login() to sign in the user, and then we redirect to the home page using redirectAndExit().

So, with that explained, let's add the changes:

Expand/contract code area Select previous tab
Select next tab
121
122
123
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function tryLogin(PDO $pdo, $username, $password)
{
    $sql = "
        SELECT
            password
        FROM
            user
        WHERE
            username = :username
    ";
    $stmt = $pdo->prepare($sql);
    $stmt->execute(
        array('username' => $username, )
    );
    // Get the hash from this row, and use the third-party hashing library to check it
    $hash = $stmt->fetchColumn();
    $success = password_verify($password, $hash);
    return $success;
}
function login($username)
{
    $_SESSION['logged_in_username'] = $username;
}

Now we have a way to determine whether users are logged in or not, let's switch that feature on for all pages. Here we also modify the HTML snippet that contains the page header, so it can show the appropriate login/logout link.

Expand/contract code area Select previous tab
Select next tab
1
2
3
 
 
4
5
6
<?php
require_once 'lib/common.php';
// Connect to the database, run a query, handle errors
$pdo = getPDO();
$stmt = $pdo->query(
1
2
3
4
5
6
7
8
<?php
require_once 'lib/common.php';
session_start();
// Connect to the database, run a query, handle errors
$pdo = getPDO();
$stmt = $pdo->query(

You may have noticed, if you tried the logout link, that this page does not yet exist. So let's add that now in the following set of changes. Whilst we are at it, let's greet the user by their username while they are logged in, as this makes the experience of the site a bit more friendly.

Expand/contract code area Select previous tab
Select next tab
149
150
151
 
 
 
 
 
 
 
 
 
 
152
153
154
    $_SESSION['logged_in_username'] = $username;
}
function isLoggedIn()
{
    return isset($_SESSION['logged_in_username']);
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
    $_SESSION['logged_in_username'] = $username;
}
function logout()
{
    unset($_SESSION['logged_in_username']);
}
function getAuthUser()
{
    return isLoggedIn() ? $_SESSION['logged_in_username'] : null;
}
function isLoggedIn()
{
    return isset($_SESSION['logged_in_username']);

That's all for this chapter, so give your application a good test, especially the new login and logout features. When you're ready, we'll proceed in the next chapter with some small tweaks to improve what we have.