Jay Blanchard, LLC

Website & Web Application Development & Design

Proper Password Preparation with PHP

May 2015

  • Required: a webserver running PHP and a database of your choice. MySQL is used in all of the examples. If you use another database you will need to change the connection string and perhaps update queries accordingly.
  • Nice to know: PHP basics, SQL basics.
  • Download: Project Files

Caution!

There are many libraries written for taking care of security properly and many frameworks which include these libraries. This article is intended to teach you how to use specific PHP functions correctly but does not take into account all of the issues surrounding password and website security. Proceed with caution.

Send me your raspberries now, I went for the alliteration in the title. It really should have been DeMystifying Password Hashing with PHP but I was in a funky mood when I started writing. The goal of this article is to show you how to properly use PHP's password_hash() and password_verify() functions while gaining an understanding of how they work. We'll combine the code used here with a PDO function created in a previous post, Demystifying PHP's Data Objects (PDO), to make our coding simpler and to avoid distraction. In addition we're not going to limit our user's passwords (it is a bad idea).

But first, some descriptions and definitions.

A hash is a substitution of one thing for another thing in order to make the first thing hidden or undecypherable. You likely created your first hash, a two-way cypher, as a kid when you wrote the alphabet out and then wrote another line below your first with alphabet being backwards.

                ABCDEFGHIJKLMNOPQRSTUVWXYZ
                ZYXWVUTSRQPONMLKJIHGFEDCBA
                

'Z' is now 'A' and we can send coded messages to our friends and they can decode them! How clever are we?!?! We find out we're not clever at all when our little sister intercepts our coded notes and begins to translate them, foiling our plans for total tree-house domination. We need to find a way to modify our hash. How do we make the modification? We add salt!

A salt is some data used as an additional input to change our hash in order to make it more secure. We tell our friends to 'shift by 3' or maybe we include a number somewhere in the message which indicates the shift. Suddenly our hash is a little more complex:

                ABCDEFGHIJKLMNOPQRSTUVWXYZ
                CBAZYXWVUTSRQPONMLKJIHGFED
                

'C' is now 'A' with the whole backwards alphabet shifted three characters. The goal of salts in the real world is to defend against things like dictionary and rainbow table attacks.

Finally there is the cost which refers to the algorithmic cost of performing the hash given the salt and the hash method. Since our friend Bobby was really good with the alphabet he was in charge of making the encrypted notes. If he did the encryption twice (shift and shift again) it doubled the cost of the hash, both in time and in effort. Three times would triple the cost and so on.

                ABCDEFGHIJKLMNOPQRSTUVWXYZ
                CBAZYXWVUTSRQPONMLKJIHGFED
                FEDCBAZYXWVUTSRQPONMLKJIHG
                

'F' is now 'A'. It's much more secure but it may not have been worth it if the message took too long to generate and too long to decypher. For our computers and servers the cost is much more complex but in many cases we have the option of setting a value which relates to a factor of the cost. It takes some fine tuning to get the balance, speed vs. effort, correct and is best left to the experts.

PHP's password_hash() function doesn't perform a two-way cypher though, it is one-way only. You cannot reverse engineer the hash to figure out the password, unlike our attempts at secrecy above. But keep our examples in the back of your mind. We'll refer back to them as we move forward.

First let's create a database for our user's. (All scripts used in this article are available in the download.)

                CREATE TABLE `users` (
                  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
                  `fname` varchar(32) DEFAULT NULL,
                  `lname` varchar(64) DEFAULT NULL,
                  `uname` varchar(64) DEFAULT NULL,
                  `password` text DEFAULT NULL,
                  PRIMARY KEY (`id`),
                  UNIQUE (`uname`)
                ) ENGINE=MyISAM DEFAULT CHARSET=latin1;
                

The important thing to note here is the password field is huge. The password_hash() can generate some very lengthy text (the current default is 60 characters), so making the field larger now will allow for the length needed. Secondly the PHP team is adding more algorithms to the method which means the hash can and will grow. We also do not want to limit our user's ability to use the password or passphrase of their choice. It's best to leave room for the changes.

We've also set the user name field (uname) to be a unique value in the database because we do not want to allow duplicate user names. Handling duplicate user names (or any other field specified to be unique) is beyond the scope of this article.

We're going to need two forms, one for registration and one for logging in. We will place them all in one page called register_login.html.

                <!DOCTYPE html>
                <html>
                <head>
                    
                    Register / Login
                    
                </head>
                <body>
                    
 REGISTER 




 LOGIN 


</body> </html>

Nothing special here, just a couple of forms each pointed to a different action.

Because we're using PDO to connect to our database we'll use the same connection and query execution function created in the article Demystifying PHP's Data Objects (PDO). Neat to have a little bit of code we can use over and over again. One small change was made to the code in pdo_connect.php: we added an else statement to get the number of affected rows from our inserts.

                if($queryResults != null && 'SELECT' == $queryType[0]) {
                    $results = $queryResults->fetchAll(PDO::FETCH_ASSOC);
                    return $results;
                } else { // line added
                    return $queryResults->rowCount(); //line added
                } // line added
                

This little bit of code will come in handy should we use the function for updating and deleting records too. The completed function is available in the download for this article.

We'll include the file with the connection and query execution function in our register and login processes. Let's take care of the register process first and call it register.php. (Saved in the same folder as our other PHP files.)

                <?php
                include 'pdo_connect.php';

                if(isset($_POST['submit'])) {
                    $fname = $_POST['fname'];
                    $lname = $_POST['lname'];
                    $uname = $_POST['uname'];
                    $upassword = password_hash($_POST['upassword'], PASSWORD_DEFAULT);

                    $query = 'INSERT INTO `users` (`fname`, `lname`, `uname`, `password`) VALUES (?,?,?,?)';
                    $params = array($fname, $lname, $uname, $upassword);
                    $results = dataQuery($query, $params);

                    // for testing only
                    echo 1 == $results ? 'success' : 'failure';
                }
                ?>
                

Because we're keeping things simple and not taking into account everything else we would need to do if we were building a true authentication system we can skip right to the heart of the matter in this code. If the submit button has been clicked we gather our inputs and assign them to variables for the sake of ease later in the code. During the assignment of the password you will apply the password_hash() function using the default parameters. Once done the query and parameters can be gathered and sent to the function which will run the query and return the results. We perform a little sanity test at the end to make sure things are working.

Easy enough, right?

Let's go back for a second before we learn how to verify passwords where we discussed salts and costs.

If you use the default options for the password_hash() function PHP will generate a random salt for each password as it is hashed. The random salt is an additional layer of security which makes it exceptionally hard to crack any passwords. Even if two or more users use the same password each of their hashes will be different. Let's look at an example which will also give us a chance to test our form and our data insertion query.

The first person we register is Adam West, Batman from the 1960's television series. He wants the username "bwayne" and his password is "I'm Batman!" (notice the password is a phrase and includes spaces, far better than any standard password you may have used in the past.). The entry, if there is nothing wrong with our script, goes in smoothly.

The next person who registers is Michael Keaton, Batman from the 1989 movie. He also wants the username "bwayne" but trying an already used user name causes an error which we can actually handle more appropriately, but for testing purposes will do just fine:

Michael changed his username but his password is amazingly familiar: "I'm Batman!". Here is where the random salt provided by PHP comes in so handy. Even though the passwords are the same each hash is completely different.

Because of the type of encryption involved the first few characters of the password match but everything beyond those characters is different and unique.

NOTE: We don't want to get into the finer points of who Batman really is but we did have them both go back and change their passwords once testing was complete. No one needs to know the location of the Batcave but us.....um, er.....them.

This where a lot of people get thrown for a loop. "How can this work?", they ask. "What do we have to do to make this match with someone's password?"

It's really not hard at all. Instead of jumping through complicated hoops you only need to use PHP's password_verify() function. Let's create our login script to see how it works.

                <?php

                include 'pdo_connect.php';

                if(isset($_POST['uname'])) {
                    $query = "SELECT `password` FROM `users` WHERE `uname` = ?";
                    $params = array($_POST['uname']);
                    $results = dataQuery($query, $params);
                }

                $hash = $results[0]['password']; // first and only row if username exists;

                echo password_verify($_POST['upassword'], $hash) ? 'password correct' : 'passwword incorrect';

                ?>
                

As we did before we include the script containing the connection and query execution function. If the user name is set we get the password hash from the database for this user and store it in a variable. Finally we compare the password submitted with the hash by letting the password_verify() function do its job. In this case we set up a ternary operator to return the acceptance of the password but you could use a much more complex set of instructions to redirect the user or advise them their password is not correct. Depending on the level of security you desire you have a huge number of options.

Try to login with either of the Batman identities, both should be successful.

How does it work? The password_hash() function returns the algorithm, salt and cost as parts of the hash. Because of this all of the information needed to verify the password's hash is included. No additional information is needed by the function and the comparison can take place. No reverse engineering of the hash is needed, only a comparison to see if the provide password or passphrase matches the previously hased information.

Simple? You bet! Using these functions makes it easy to work with passwords and passphrases in a safe and secure way. Is there more you can do? Absolutely! You can provide verification, serve the site on an SSL, provide a way to reset a user's password and much, much more. The code and techniques in this article are a foundation for doing those things. Make sure to read (and heed!) the documentation for password_hash() and password_verify() as a starting point for creating your own website and web application security checkpoints.

Happy coding!

Comments? Shoot me your thoughts on Twiiter: @jaylblanchard