Johan Broddfelt
/* Comments on code */

Authentication and users

Now when we got som security in place it is time to take a look at creating users so that you only can access the list and edit views if you are logged in. I would love to talk about all the things we need for this to work. But let me show it right away instead. First we need a user table in the database. In that we need a username and password, and then we might want to through some extra stuff in there like name, surname, birthdate, mail, phone.. Stop, stop, STOP. First we think about what kind of system we ar building and what we need to know about our users. We can easily add more data later. Right now we only want some login credentials, mail, a last active data and a status of the user. I'll just throw in a created date as well. That is quite hard to add later.

CREATE TABLE IF NOT EXISTS `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `password` varchar(255) NOT NULL,
  `mail` varchar(50) NOT NULL,
  `status` int(11) NOT NULL,
  `created` datetime NOT NULL,
  `active` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`),
  KEY `mail` (`mail`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

Do not forget the id and that it should be AUTO_INCREMENT and set as primary index. Now we can acually go and create a user just by typing index.php? module=user&view=edit. Is'nt it amazing, we did not write a single line of code. But as you can see the password is shown in clear text and it is stored as clear text in the database. This is a really bad thing, so we need to fix this. First run index.php?module=user&view=edit&generate=code in order to generate the views/user/edit.php. Now we can modify the password field to be of type password. This will add dots when you type in that field. We also remove the value of the input field, because we never want to display the password. That field should only be used to update the password. In some cases you might even want to make a separate page for just that and make sure that the user repeats the password twize.

            <tr>
                <th>
                    Password
                </th>
                <td>
                    <input 
                           type="password"
                           value=""
                           name="password" 
                        >
                </td>
            </tr>
            <tr>
                <th>
                    Repeat password
                </th>
                <td>
                    <input 
                           type="password"
                           value=""
                           name="pwd_repeat" 
                        >
                </td>
            </tr>

You also want to remove this line in the top of the form, because we do not want to change the value if the password field here.

$obj->password = Http::filter_input(INPUT_POST, 'password');

This is not nearly enough, we also need to make sure that the password is well encrypted in our database, so that if a hacker gains access to our system he at least will have some trouble figuring out the actual passwords used. The current best practice for this in php is to use the password_hash() function. So lets implement that and for that we also need to create a User.php.

<?php
class User extends Db {
    public $table = 'user';
    public $error = '';
    
    function update() {
        // If ther is something posted to the password field we attempt to change it
        if (Http::filter_input(INPUT_POST, 'password') != '') {
            // But only if it is equal to the pwd_repeat field.
            if (Http::filter_input(INPUT_POST, 'password') == Http::filter_input(INPUT_POST, 'pwd_repeat')) {
                $this->password = password_hash($this->password, PASSWORD_BCRYPT, ["cost" => 10]);
            } else {
                // Alert the user that there is a missmatch in the password
                $this->error = 'The two passwords do not match!';
            }
        }
        // parent::update() will forward the function call to Db::update() that will cotinue to save the post as usual
        parent::update();
    }

When we now save a password we can see that it is encrypted in the database. Now we want to create a login form. We can do that by creating a views/user/login.php like this

<?php
    if ($ad->user->id > 0) {
        ?>
        <div class="information">
            You are logged in!
        </div>
        <?php
    } else {
        if (Http::filter_input(INPUT_POST, 'login', FILTER_SANITIZE_URL) != '') {
            ?>
            <div class="warning">
                You failed to login!
            </div>
            <?php
        }
    }
?>
<div>
<h1>Login</h1>
<form method="post" action="">
    <table class="form">
            <tr>
                <th>
                    Username
                </th>
                <td>
                    <input 
                           type="text"
                           value="<?php echo $obj->username; ?>"
                           name="username" 
                        >
                </td>
            </tr>
            <tr>
                <th>
                    Password
                </th>
                <td>
                    <input 
                           type="password"
                           value=""
                           name="password" 
                        >
                </td>
            </tr>
            <tr>
                <th></th>
                <td>
                    <input type="submit" name="login" value="Login" class="button">
                </td>
            </tr>
    </table>
</form>
</div>

And we also need to add the login() function to our User.php:

    function login() {
        // We start by trying to find the user in our database
        $qry = 'SELECT id, username, password FROM ' . $this->table . ' WHERE username LIKE '' . $this->username . ''';
        $res = Db::query($qry);
        $row = Db::fetch_array($res);
        if (password_verify($this->password , $row['password'])) {
            return true;
        }
        return false;
    }

But as you can see we do not stay logged in. In order for a user to login and stay logged in as the browser loads new page, we need to store something that identifies the user in the session or in a cookie. The first that comes to mind is to store the username and password. But then the user credentials are basically in clear text except for maby an encrypted password. But that can be hacked. So what about just storing the id of the user then, maby scrambled? No that is not a good idea either. It is also something that a hacker could use to gain access to your system or other users accounts. We need something separate, that we can change without having to update the user profile. Lets create a session table in our database to stor login sessions. that give us the posibillity to close the access for a session_key directly from the server without having to close the user account.

CREATE TABLE IF NOT EXISTS `session` (
  `session_key` varchar(32) CHARACTER SET latin1 NOT NULL,
  `created` datetime NOT NULL,
  `closed` datetime NOT NULL,
  `remote_address` varchar(15) CHARACTER SET latin1 NOT NULL,
  `user_id` int(11) NOT NULL,
  `user_agent` varchar(500) NOT NULL,
  UNIQUE KEY `session_key` (`session_key`),
  KEY `user_id` (`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

We need the user id in order to create a new session, and we get that in our login function. So lets start by taking a look at the login and logout functions in User.php.

    function login() {
        if (String::slashText(Http::filter_input(INPUT_GET, 'user')) == 'logout') {
            $this->logout();
        }
        // We start by checking if you already are logged in
        $s = new Session();
        if ((int)$s->userId == 0) {
            // We start by trying to find the user in our database
            $qry = 'SELECT id, username, password FROM ' . $this->table 
                . ' WHERE username LIKE '' . String::likeSafe(Http::filter_input(INPUT_POST, 'username')) . ''';
            $res = Db::query($qry);
            $row = Db::fetch_array($res);
            if (!password_verify(Http::filter_input(INPUT_POST, 'password') , $row['password'])) {
                return false; // The user is not known so the login attempt fails here
            }
            $s->userId = $row['id'];
            $s->update();
        }
        // The user is known so we activate the user
        $this->fetchObjectById($s->userId);
        return true;
    }
    
    function logout() {
        $s = new Session();
        $s->close();
        
        $this->fetchEmpty();
        
        header("location: ./?"); // This will remove the logout command from the url, so that you are not logged out next time you try to login
        exit;
    }

There are some calls to the Session class here, so we also need to take a look at that in order to get the whole picture.

<?php
class Session {
    public $sessionKey = '';
    public $userId = 0;
    
    public $sessionTimeOut = 0;
    public $sessionCleanUp = 0;
    public $sessionAttempts = 0;
    
    public $table = 'session';
    
    function __construct($sessionKey='') {
        $this->sessionTimeOut = (3600 * 24 * 7); // 7 days
        $this->sessionCleanUp = (3600 * 24 * 60); // 2 months. Remove this line if you do not want to delete old sessions
        if ($sessionKey != '') {
            $this->sessionKey = $sessionKey;
        }
        if ($this->sessionKey == '') {
            $this->sessionKey = $_COOKIE['session_key'];
            setcookie('session_key', $this->sessionKey, time() + $this->sessionTimeOut); // This will extend the cookie for [timeout time] from now
        }
        $this->getKey($this->sessionKey);
    }

    function getKey($sessionKey='') {
        $activate = true;
        if (!$this->validSession($sessionKey, $activate)) {
            $this->createKey();
        }
    }
    
    function update() {
        // if you do not have a key we will generate one for you
        if ($this->sessionKey == '') {
            $this->createKey();
        }
        if ($this->created == '0000-00-00 00:00:00') {
            //Log::err('no created date: ' . $this->created);
            $this->created = date('Y-m-d H:i:s');
        }
        if ($this->closed == '') {
            $this->closed = '0000-00-00 00:00:00';
        }
        if (!$this->validSession($this->sessionKey)) {
            $sql = 'INSERT INTO session
                  (
                    user_id, 
                    session_key, 
                    created, 
                    closed, 
                    remote_address,
                    user_agent
                  )
                  VALUES
                  (
                    ' . $this->userId . ', 
                    '' . $this->sessionKey . '', 
                    '' . $this->created . '', 
                    '' . $this->closed . '',
                    '' . $this->remoteAddress . '',
                    '' . $this->userAgent . ''
                  )
                  ';
            if (!Db::query($sql)) {
                //Log::warn('Session key kollition get a new one: ' . $sql . ' - ' . Db::error());
                // This could create an infinite loop if the database fails, so we might want to
                // add a counter here so we try this max 10 times or so
                $this->sessionAttempts++;
                if ($this->sessionAttempts < 10) {
                    $this->createKey();
                }
                return false;
            }
        } else {
            $sql = 'UPDATE session
                    SET
                        user_id=' . $this->userId . ',
                        created='' . $this->created . '',
                        closed='' . $this->closed . '',
                        remote_address='' . $this->remoteAddress . '',
                        user_agent='' . $this->userAgent . ''
                    WHERE
                        session_key='' . $this->sessionKey . ''
                        ';
            Db::query($sql);
        }
        return true;
    }

    function createSession($sessionKey) {
        $this->sessionKey = $sessionKey;
        $this->userId = 0;
        $this->created = date('Y-m-d H:i:s');
        $this->remoteAddress = $_SERVER['REMOTE_ADDR'];
        $this->userAgent = $_SERVER['HTTP_USER_AGENT'];
    }

    function validSession($sessionKey, $activate=false) {
        if ($sessionKey == '') {
            return false;
        }
        $sql = 'SELECT session_key, user_id, closed, remote_address, created
                FROM session 
                WHERE session_key='' . String::slashText($sessionKey) . ''
                ';
        $res = Db::query($sql);
        $row = Db::fetch_assoc($res);
        if ($sessionKey == $row['session_key'] and $row['closed'] == '0000-00-00 00:00:00') {
            if ($activate) {
                $this->sessionKey = $row['session_key'];
                $this->userId = $row['user_id'];
                $this->created = $row['created'];
                $this->closed = $row['closed'];
                $this->remoteAddress = $_SERVER['REMOTE_ADDR'];
                $this->userAgent = $_SERVER['HTTP_USER_AGENT'];
            }
            return true;
        }
        return false;        
    }
    
    function createKey() {
        $valid = false;
        $cnt = 0;
        // make sure that it does not exist already
        while (!$valid) {
            list($usec, $sec) = explode(" ", microtime());
            $sessionKey = md5(date('Y-m-d H:i:s') . $usec . $cnt);
            $sql = 'SELECT session_key FROM session WHERE session_key='' . $sessionKey . ''';
            $res = Db::query($sql);
            if (Db::num_rows($res) == 0) {
                $this->createSession($sessionKey);
                if ($this->update()) {
                    $valid = true;
                }
            }
            $cnt++;
        }
        setcookie('session_key', $this->sessionKey, time() + $this->sessionTimeOut); // The cookie will expire after [timeout time] from creation
        if ($this->sessionCleanUp > 0) {
            $clean = 'DELETE session WHERE `created` < '' . date('Y-m-d H:i:s', strtotime('today - ' . $this->sessionCleanUp . ' seconds')) . ''';
            Db::query($clean);
        }
    }

    function close() {
        $this->closed = date('Y-m-d H:i:s');
        $this->update();
        setCookie('session_key', '', time()-1);
    }
}

Even though this will set the session in our browser it still does not let us in to other pages. Or should I say prevent us from accessing other pages. So we need to make sure that we create a $user that we can check in our code to se if we have an active user. So we add that to the globa.php. Because it is called in the begining of every page in our system.

// The default communication is acually latin1 so we need to set it to utf8 if we really want to use it in the database.
$ad = ActiveData::getInstance();
$ad->mysqli->set_charset("utf8");

// Create a session and if a user is logged in add that user to the ActiveData object. So that wi have access to the user.
$user = new User();
$user->login();
$ad->user = $user;

And we also want a login/Log out button in our menu. So we update the menu like this, in our main_template.php. We could also use icons instead of text. But we keep it simple for now.

        <div id="menu"><div>
            <a href="index.php?module=post&view=items">Posts</a>
            <a href="">Tags</a>
            <a href="index.php?module=page&view=links">Links</a>
            <a href="index.php?module=page&view=about">About</a>
            <?php if ($ad->user->id > 0) { ?>
            <a href="?user=logout" class="right">Logout</a>
            <?php } else { ?>
                <a href="index.php?module=user&view=login" class="right">Login</a>
            <?php } ?>
        </div></div>

I want the login button on the right therefor we add the following css to our style.

#menu .right {
  right: 20px;
  position: absolute;
}

Now we only need to modyfy the index.php printContent() so that we prevent access to all but public pages. On the first line we need to reactivate the $user variable. Then we create an array of allowed pages.

function printContent() {
    $ad = ActiveData::getInstance();
    $user = $ad->user;
    $publicPages = array(
        'post_items',
        'post_item',
        'user_login',
        'page_*'
    );
    
    $module = Http::filter_input(INPUT_GET, 'module', FILTER_SANITIZE_URL);
    $view = Http::filter_input(INPUT_GET, 'view', FILTER_SANITIZE_URL);
    $id = Http::filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);
    $obj = new stdClass();
    if ($module != '') {
        $db = new Db();
        $className = $db->className($module);
        if (is_file('classes/' . $className . '.php')) {
            $obj = new $className((int)$id);
        } else if ($module != '' and Db::tableExist($module)) {
            $obj = new Generic((int)$id);
        }
    }
    // Get the page we want to display
    if ($module == '') {
        include('views/start/main.php');
    } else {
        // Check if the user have access
        if ($user->id > 0 
            or in_array($module . '_' . $view, $publicPages)
            or in_array($module . '_*', $publicPages)
                ) {
            // Check if a table exists and if the file does not
            if (!is_file('views/' . $module . '/' . $view . '.php') and Db::tableExist($module)) {
                $generate = Http::filter_input(INPUT_GET, 'generate', FILTER_SANITIZE_URL);
                if ($generate != '') {
                    $generate = '_' . $generate;
                }
                if (is_file('views/generic/' . $view . $generate . '.php')) {
                    include('views/generic/' . $view . $generate . '.php');
                } else {
                    echo '<div class="warning">The file ' . $view . '.php' . ' is missing!</div>';
                }
            } else {
                include('views/' . $module . '/' . $view . '.php');
            }
        } else {
            echo '<div class="warning">Access denied! You need to login.</div>';
            include('views/user/login.php');
        }
    }
}

Now we have the basic structure to handle users. What we have not implemented so far is the status of the user. We might to give different rights to different users. There are several ways to do that. But I will cover that in a later session. Basically it is three ways. The first is as we have it now. Where you either have a user or you do not. The next level is to use a fixed statuses of the user where you assigne different levels to different numbers. For example -2: Blocked, -1: Closed, 0: new not activated, 1: regular user, 2: premium user and 3: administrator. You might create as many levels as you need. And my goal is to implement this into my blog
For more complex solutions you might want to individually give access to different parts or elements of the site. So then you need a table with a list of rights and then a link table that connects users to the rights they should have. You might also want to add a group that you can assign to a user, and the group have a predefined set of rights.
Now you might also consider having a register page so that users can register them selvs or if it only should be posible for you to create users.

- Framework, PHP, User

Follow using RSS

<< Security is important Snap menu on top >>

Comment

Name
Mail (Not public)
Send mail uppdates on new comments
0 comment