User authentication - login

This chapter is about means to protect your application from anonymous users who could deliberately delete random data or pollute your application with spam messages. Until now, your application was publicly available to online audience without any possibility to control who is working with stored data. I want to show you how to store user account data securely (especially passwords) and how to verify (authenticate) a user which is trying to log into your application. I will not talk about different levels of user permissions because it would complicate things a lot – such feature is called authorisation.

Storing users’ passwords

It is not a safe approach to store passwords in their plain-text form. Such passwords can be viewed by anybody who has access to your database (maybe now it is only you, but in future it can be some of your colleagues or even your employees). A password is always saved in hashed form (a hash is a result of function which outputs a unique strings for different inputs and it is not trivial to reverse this process – i.e. to calculate original password from a hash).

Examples of hash function outputs in PHP for word “cat”:

  • md5(‘cat’) = d077f244def8a70e5ea758bd8352fcd8 (always 32 characters)
  • sha1(‘cat’) = 9d989e8d27dc9e0ec3389fc855f142c3d40f0c50 (always 40 characters)
  • password_hash(‘cat’) = $2y$10$5iA8dvLAzWzl.cepri1xxuINCQBHKNANmEfx4nT/jjCV4hWcUTW.y (up to 255 characters according to selected hashing algorithm)

Function password_hash() is a bit different because its output is not always same like with the md5 or sha1 functions. It is caused by a salt which is a sequence of random characters. The salt is used to make hashes unique to avoid attackers from searching hashes online or using vocabulary attacks (try to search for md5 hash e246c559bf94965d89cf207fc45905bc using Googled’oh). A salt is stored directly in the result of password_hash() function. To verify a password generated by password_hash() use password_verify() function.

You can also use a salt with the md5 or sha1 but you have to handle it by yourself (you will need another column in your database table to store it).

Task – try to calculate hash with random salt and verify it

To store a password during registration process:

hash = sha1(randomSalt + registrationPassword)

To verify a password when user tries to log-in (first fetch hash and salt from database):

if(sha1(databaseSalt + providedPassword) == databaseHash) {...OK...}
<?php
//some salt, this would be stored in your database
$salt = "abc123";
//hash of string "abc123squirrel", this would be also stored in your database
//password is ofcourse squirrel
$hashStored = "eb5c28c5a881fff827014ad530c8a580bd7ac42e";
$hashVerify = sha1($salt . $_GET['pass']);
//try to run this script with query parameter pass set to various values
if($hashStored == $hashVerify) {
    //hash-salt.php?pass=squirrel
    echo "hashes do match";
} else {
    //hash-salt.php?pass=hippopotamus
    echo "hashes do NOT match";
}

The probability that concatenation of user’s password and a random string used as a salt will yield results in vocabulary search is very small. Yet use of sha1() or md5() functions is strongly discouraged because powerful CPUs or GPUs can generate millions of hashes per second and they are capable of breaking md5 or sha1 hashes within reasonable time (days). Recommended function password_hash() is also quiet slow (by design – this is one of few instances when we want our algorithms to be slow).

One of the reasons why any web application sends you a new password instead of yours when you forgot it, is hashing. They do not store your password in plain text form.

Task – create a table to store user data in database

Create a table with login or email column and a column to store password in hashed format. If you plan to use password_hash() function, you do not need another column for salt. Remember that login or email column must have unique constraint to prevent duplicate user accounts.

For PostgreSQL:

CREATE TABLE account (
  id_account serial NOT NULL,
  login character varying(100) NOT NULL,
  password character varying(255) NOT NULL
);
ALTER TABLE account ADD CONSTRAINT account_login_key UNIQUE (login);
ALTER TABLE account ADD CONSTRAINT account_pkey PRIMARY KEY (id_account);

For MySQL:

CREATE TABLE `account` (
  `id_account` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `login` varchar(100) COLLATE utf8_czech_ci NOT NULL,
  `password` varchar(255) COLLATE utf8_czech_ci NOT NULL,
  PRIMARY KEY (`id_account`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci;

Registration process

Registration is actually not very much different from any other record insertion. Only difference is that you have to validate match of the passwords and calculate the hash. After successful insertion of the account record redirect the visitor to the login page.

Task – create a form for user registration

It should have an input for login or email and two inputs for password verification (all inputs are required). You can use Bootstrap CSS styles. Prepare a place to display error message and remember to prepare form data array to display values in case of failure.

File templates/register.latte:

{extends layout.latte}

{block title}Register{/block}

{block body}
    <h1>Register</h1>
    {if $message}
        <p>{$message}</p>
    {/if}
    <form action="{link do-register}" method="post">
        <label>Login:</label>
        <input type="text" name="login" value="{$form['login']}" required/>
        <br/>
        <label>Password:</label>
        <!--
            Passwords are not usually send back and forth over internet.
            Hence there is no default value for password inputs.
        -->
        <input type="password" name="pass1" required/>
        <br/>
        <label>Password (verification):</label>
        <input type="password" name="pass2" required/>
        <br/>
        <input type="submit" value="Register"/>
    </form>
{/block}

Add a GET route to display this template.

File src/routes.php:

<?php
$app->get('/register', function(Request $request, Response $response, $args) {
    return $this->view->render($response, 'register.latte', [
        'message' => '',
        'form' => [
            'login' => ''
        ]
    ]);
})->setName('register');

Task – process registration with PHP script

Make a new POST route to process registration of new users. Use password_hash() function. Read the documentation because this function requires actually two input parameters. Second one is the algorithm which is used for password hash calculation.

File src/routes.php:

<?php
$app->post('/register', function(Request $request, Response $response, $args) {
    $tplVars = [
        'message' => '',
        'form' => [
            'login' => ''
        ]
    ];
    $input = $request->getParsedBody();
    if(!empty($input['login'] && !empty($input['pass1']) && !empty($input['pass2']))) {
        if($input['pass1'] == $input['pass2']) {
            try {
                //prepare hash
                $pass = password_hash($input['pass1'], PASSWORD_DEFAULT);
                //insert data into database
                $stmt = $this->db->prepare('INSERT INTO account (login, password) VALUES (:l, :p)');
                $stmt->bindValue(':l', $input['login']);
                $stmt->bindValue(':p', $pass);
                $stmt->execute();
                //redirect to login page
                return $response->withHeader('Location', $this->router->pathFor('login'));
                exit;
            } catch (PDOException $e) {
                $this->logger->error($e->getMessage());
                $tplVars['message'] = 'Database error.';
                $tplVars['form'] = $input;
            }
        } else {
            $tplVars['message'] = 'Provided passwords do not match.';
            $tplVars['form'] = $input;
        }
    }
    return $this->view->render($response, 'register.latte', $tplVars);
})->setName('do-register');

After successful registration, a record representing user account in the database should look like this:

User account in database

User verification and login

User verification is also not a big problem – a person who wishes to log-in into your application has to visit login page with a simple two-input form. After he fills and submits the form, he is verified against the database. If an existing account is found and passwords match, your application can trust this user.

Actually there were cases when a user logged into another user’s account by a mistake – two different accounts had same passwords (not even salt can solve this situation). There are also online identity thefts when user’s password is compromised and used by someone else to harm original person. You can add another tier of user authentication, e.g. send an SMS to his cell phone to retype a verification code or distribute user certificates.

Task – create a form for user login and Slim routes to handle it

Create a login form and a Slim routes to process login information. You can make error message a bit confusing to obfuscate existence of user accounts (sometimes you do not wish to easily reveal users of your app – especially when you use email address as login). For now, do not bother yourself by the fact that the confirmation is displayed only when the user sends his credentials. We will handle persistence of authentication flag later.

File templates/login.latte:

{extends layout.latte}

{block title}Login{/block}

{block body}
    <h1>Login</h1>
    <form action="{link do-login}" method="post">
        <label>Login:</label>
        <input type="text" name="login" required>
        <br>
        <label>Password:</label>
        <input type="password" name="pass" required>
        <br>
        <input type="submit" value="Login">
    </form>
{/block}

File src/routes.php:

<?php
$app->get('/login', function(Request $request, Response $response, $args) {
    return $this->view->render($response, 'login.latte', ['message' => '']);
})->setName('login');

Now we need to verify user information against the database and display errors when there are some. We will use the password_hash() counterpart function password_verify() similarly as in registration route:

File src/routes.php:

<?php
$app->post('/login', function(Request $request, Response $response, $args) {
    try {
        //retrieve login and password from request
        $login = $request->getAttribute('login');
        $pass = $request->getAttribute('pass');
        //find user by login
        $stmt = $this->db->prepare('SELECT * FROM account WHERE login = :l');
        $stmt->bindValue(':l', $login);
        $stmt->execute();
        $user = $stmt->fetch();
        if ($user) {
            //verify if hash from database matches hash of provided password
            if (password_verify($pass, $user["password"])) {
                echo "USER VERIFIED";
                exit;
            }
        }
        //do not reveal if account exists or not
        $tplVars['message'] = "User verification failed.";
    } catch (PDOException $e) {
        $tplVars['message'] = $e->getMessage();
    }
})->setName('do-login');

Add a message slot to display errors in the template.

File templates/login.latte:

{extends layout.latte}

{block title}Login{/block}

{block body}
    <h1>Login</h1>
    {if $message}
        <p>{$message}</p>
    {/if}
    <form action="{link do-login}" method="post">
        <label>Login:</label>
        <input type="text" name="login" required>
        <br>
        <label>Password:</label>
        <input type="password" name="pass" required>
        <br>
        <input type="submit" value="Login">
    </form>
{/block}

Persisting data between HTTP requests - $_SESSION

You probably noticed that there is no way to tell if a user has authenticated in subsequent HTTP requests due to stateless nature of HTTP protocol. To safely store login information you would probably want to logically connect subsequent HTTP request from one client (internet browser) and associate these requests with some kind of server storage. That is exactly what sessions are used for. A session is a server-side storage which is individual for each client. Client holds only unique key to this storage on its side (stored in cookies). Client is responsible for sending this key with every HTTP request. If the client “forgets” the key, data stored in session is lost. The key is actually called session ID.

To initiate work with session storage you have to call PHP function session_start() in the beginning of each of your script (before you send any output, because session_start() sends a cookie via HTTP headers).

In PHP, there is as superglobal $_SESSION array which is used to hold various data between HTTP request. These data are stored on a server and cannot be modified by will of a visitor – it has to be done by your application’s code. The $_SESSION variable is initialized and eventually filled by session_start() function.

Task – store information about authenticated user

Use $_SESSION variable to store authenticated user’s data after login. Note that there is already a line with session_start(); function in the public/index.php script.

File src/routes.php (final version):

<?php
$app->post('/login', function(Request $request, Response $response, $args) {
    try {
        $input = $request->getParsedBody();
        //find user by login
        $stmt = $this->db->prepare('SELECT * FROM account WHERE login = :l');
        $stmt->bindValue(':l', $input['login']);
        $stmt->execute();
        $user = $stmt->fetch();
        if ($user) {
            //verify if hash from database matches hash of provided password
            if(password_verify($input['pass'], $user["password"])) {
                $_SESSION["user"] = $user;
                return $response->withHeader('Location', $this->router->pathFor('profile'));
            }
        }
    } catch(PDOException $e) {
        $this->logger->error($e->getMessage());
    }
    //do not reveal if account exists or not
    return $this->view->render($response, 'login.latte',[
        'message' => 'User verification failed.'
    ]);
})->setName('do-login');

Preventing unauthenticated users from performing actions

You can prevent anonymous users to access all your application’s functions or just selected ones. If a visitor tries to access prohibited function without authentication, he should be redirected to the login page.

Task – protect your application

Write a middleware function which will verify presence of user’s data in $_SESSION array and redirect to login route if no such data is found. The is automatically executed before (or after) a route. You can choose to have a global (application) middleware for all routes or a middleware for a group of routes or a middleware for a particular route. We do not want to block all routes and therefore we will create a middleware only for a certain group of routes.

The middleware handler is provided with three input parameters, two of them are already familiar, it is the $request and $response objects. The third one is the $next callback which represents the next action (a route that should be called according to URL or another layer of middleware). The middleware can decide whether to call the $next callback or return $response like you do in ordinary route. You have to pass the $request and $response objects to the $next callback. The middleware can also be executed after the route itself – all you have to do is call $next handler beforehand and then modify returned value which is a modified $response.

Here is an example how to protect a new route with user profile information using such middleware:

File src/routes.php:

<?php
$app->group('/auth', function() use($app) {
    $app->get('/', function(Request $request, Response $response, $args) {
        //URL: /auth/
        //...
    })->setName('index');
    $app->get('/profile', function(Request $request, Response $response, $args) {
        //URL: /auth/profile
        return $this->view->render($response, 'profile.latte', [
            'user' => $_SESSION['user']
        ]);
    })->setName('profile');
})->add(function($request, $response, $next) {
    if(!empty($_SESSION['user'])) {
        return $next($request, $response);
    } else {
        return $response->withHeader('Location', $this->router->pathFor('index'));
    }
});

$app->route('/', function(Request $request, Response $response, $args) {
    //redirect to index (can divert to login without authorisation data in $_SESSION)
    return $response->withHeader('Location', $this->route->pathFor('index'));
});

File templates/profile.latte:

{extends layout.latte}

{block title}Login{/block}

{block body}
    <pre>
        id: {$user['id_account']}
        login: {$user['login']}
    </pre>
{/block}

Another option is to store the middleware handler into a variable and add it to selected routes manually:

$authMiddleware = function($request, $response, $next) { ... return $next($request, $response); ... };

$app->get('/route1', function(...) {...})->add($authMiddleware);
$app->get('/route2', function(...) {...})->add($authMiddleware);

Now you can see why is it important to give names to your routes. You can transfer all routes into this “auth” group without additional changes. Result of this operation is that all these route URLs now start with /auth string. Because you used named routes and {link ...} macro, you do not have to change URLs in templates at all.

Because route / named index is on URL /auth/ (/auth + /), you can create a redirect route which tells new visitor’s browser to redirect either to index or login route.

Logout

Finally, we have to give our users an option to leave our application. A logout action is usually just deletion of all user related data from $_SESSION variable on server.

Sometimes you wish to leave some data in the $_SESSION variable – the contents of shopping cart, for example.

Task – Create a logout route

Make a POST route which will handle logout. It is safer to use POST method because GET logout route can be easily exploited via XSS and CSRF. Use session_destroy() function. Redirect user to a public route of your application after logout.

File src/routes.php:

<?php
$app->group('/auth', function() use($app) {
    $app->post('/logout', function(Request $request, Response $response, $args) {
        session_destroy();
        return $response->withHeader('Location', $this->router->pathFor('login'));
    })->setName('logout');
})->add(function($request, $response, $next) {
    //...
});

Here is an example of logout form with a single button:

File templates/profile.latte:

{extends layout.latte}

{block title}Login{/block}

{block body}
    <pre>
        id: {$user['id_account']}
        login: {$user['login']}
    </pre>
    <form action="{link logout}" method="post">
        <input type="submit" value="Logout">
    </form>
{/block}

Make a link to logout route from main menu. You can display user’s login name inside the link. You can also pass user information into the template inside the middleware. Remember to hide the logout button when there is actually no user signed in.

Conclusion

User authentication or even authorisation is complicated. I demonstrated one of the easiest ways how to do it – you can also hardcode passwords into your source code (do not do that!). Keep in mind that weakest point of application is probably its user because people are lazy to fabricate new passwords for each website and they share some passwords with their family or friends. Security measures should also be designed according to the type of application you are developing (a bank account management application VS discussion board).

Passwords are sent over the network in plain text, it is a good idea to use HTTPS for (at least) registration and login pages to prevent attackers to sniff the password from network traffic. Still this does not prevent targeting users of your application by scam login pages which are used to steal their authentication information.

There is a lot more to explore: you probably know sites where you can persist your authentication for duration of days or even weeks – ours is forgotten as soon as the visitor closes his browser’s window. That can be achieved by setting special attributes to cookies which hold keys to a sessions. You also probably seen that some sites use global services like Facebook or Google to provide login functionality, which is good for users who do not want to remember too many passwords. Totally different approach of authentication is used for single page applications.

Remember that you are responsible for security of your application and also for the data of your users.

New Concepts and Terms

  • authentication
  • hash & salt
  • $_SESSION
  • login
  • logout