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).

It is impossible to show/send user his original forgotten password due to hashing (but the security benefits of hashing are more significant). If you need to have the ability of self managed passwords restoring in your application, you should send a new password to email specified during registration process. You can use PHP’s mail() function. Even better way is to send unique password reset request link to his email, so the password is in fact restored only when the user clicks this link from his email account.

This makes the email address of a user also a very sensitive information. Change of an email address should only be possible after confirmation of ownership of current email address – send unique token to his current email address to authorize changes.

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 quite 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.

File templates/register.latte:

{extends layout.latte}
{block content}
    {if $message}
        <p>{$message}</p>
    {/if}
    <form action="register.php" 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}

Task – process registration with PHP script

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 register.php:

<?php
require 'include/start.php';
$tplVars['form'] = [
    'login' => '', 'pass1' => '', 'pass2' => ''
];
$tplVars['message'] = '';
if(!empty($_POST['login']) && !empty($_POST['pass1']) && !empty($_POST['pass2'])) {
    if($_POST['pass1'] == $_POST['pass2']) {
        try {
            //prepare hash
            $pass = password_hash($_POST['pass1'], PASSWORD_DEFAULT);
            //insert data into database
            $stmt = $db->prepare('INSERT INTO account (login, password) VALUES (:l, :p)');
            $stmt->bindValue(':l', $_POST['login']);
            $stmt->bindValue(':p', $pass);
            $stmt->execute();
            //redirect to login page
            header('Location: login.php');
            exit;
        } catch (PDOException $e) {
            $tplVars['message'] = $e->getMessage();
            $tplVars['form'] = $_POST;
        }
    } else {
        $tplVars['message'] = 'Provided passwords do not match.';
        $tplVars['form'] = $_POST;
    }
}
$latte->render('templates/register.latte', $tplVars);

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 with PHP script

Create a login form and a PHP script 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 content}
    {if $message}
        <p>{$message}</p>
    {/if}
    <form action="login.php" 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}

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 script:

File login.php:

<?php
require 'include/start.php';
$tplVars['message'] = '';
if(!empty($_POST['login']) && !empty($_POST['pass'])) {
    try {
        //find user by login
        $stmt = $db->prepare('SELECT * FROM account WHERE login = :l');
        $stmt->bindValue(':l', $_POST['login']);
        $stmt->execute();
        $user = $stmt->fetch();
        if($user) {
            //verify if hash from database matches hash of provided password
            if(password_verify($_POST['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();
    }
}
$latte->render('templates/login.latte', $tplVars);

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. Insert line with session_start(); function into start.php script.

File login.php (final version):

<?php
//remember to put session_start() call into start.php
require 'include/start.php';
$tplVars['message'] = '';
if(!empty($_POST['login']) && !empty($_POST['pass'])) {
    try {
        $stmt = $db->prepare('SELECT * FROM account WHERE login = :l');
        $stmt->bindValue(':l', $_POST['login']);
        $stmt->execute();
        $user = $stmt->fetch();
        if($user) {
            if(password_verify($_POST['pass'], $user["password"])) {
                //store user data into session and redirect
                $_SESSION["user"] = $user;
                header("Location: person-list.php");
                exit;
            }
        }
        $tplVars['message'] = "User verification failed.";
    } catch (PDOException $e) {
        $tplVars['message'] = $e->getMessage();
    }
}
$latte->render('templates/login.latte', $tplVars);

Extended file start.php:

<?php
//here we start the session
session_start();
require 'latte.php';
$latte = new Latte\Engine;
try {
    $db = new PDO('pgsql:host=localhost;dbname=apv', 'apv', 'apv');
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    exit("I cannot connect to database: " . $e->getMessage());
}
$tplVars['pageTitle'] = 'My First Application';

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 short include-script which will verify presence of user’s data in $_SESSION array and redirect to login.php script if no such data is found. Require this script in all PHP scripts where you want user authentication to be performed before execution of that script itself. Place the require command just below the line where you require start.php script.

File protect.php:

<?php
if(empty($_SESSION['user'])) {
    header("Location: login.php");
    exit;
}

Here is an example how to protect deletion of persons from database with created script:

File delete.php:

<?php
//start_session() is in start.php
require 'include/start.php';
//this script checks if there is a user flag in $_SESSION
require 'include/protect.php';
if(!empty($_POST["id_person"])) {
    try {
        $stmt = $db->prepare("DELETE FROM person WHERE id_person = :idp");
        $stmt->bindValue(":idp", $_POST["id_person"]);
        $stmt->execute();
        header("Location: person-list.php");
        exit();
    } catch(PDOException $e) {
        exit($e->getMessage());
    }
}
$tplVars["pageTitle"] = "Delete a person";
$latte->render('templates/delete.latte', $tplVars);

You can also pass information about authenticated user into templates in protect.php script. This would be useful to modify your templates according to the presence of selected variable – e.g. you can show or hide menu buttons which are not accessible to anonymous user. If you choose to make some modules of your application public, you should pass user related variables in another way because protect.php is not always executed. You can do that in start.php after you start the session. Remember to handle non-existing values (for the case that the user is not logged in yet).

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 script

Use session_destroy() function. Redirect user to public page of your application after logout. Put logout button to your layout.latte template.

File logout.php:

<?php
//we have to include start.php because of session_start()
require 'include/start.php';
session_destroy();
//redirect somewhere or display logout message
header("Location: person-list.php");

Make a link to logout script from main menu. You can display user’s login name inside the link.

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

Control question

  • Why store passwords as hash in database?
  • Why to use salt?
  • Which hashing functions you would not use today?
  • Is it better to use email instead of login?