Upgrade To Better Passwords in PHP

The password features in PHP aren’t exactly new, but I see lots of applications from “before” which aren’t being migrated to better practices. I have some strategies for doing these migrations so I thought I’d share my main approach, plus a similar-but-different one I saw in the wild (OK it was in CakePHP, so not too wild!).

The examples here assume that you currently have either unsalted or all-with-the-same-salt passwords stored in your database, hashed with md5 or sha1 or something. This is a Very Bad Idea (TM) since it’s trivial to recover unsalted passwords and not all that hard to figure out same-salted ones.

Enter the password_hash() and password_verify() functions which were included by default in PHP 5.5 but are also available for PHP 5.3.9+ via a userland implementation (see https://github.com/ircmaxell/password_compat). All new applications should use this but converting your existing application can be tricky so here’s my process.

How to Convert To Using PHP’s Password Features

Essentially, we’re going to re-hash the existing password database. We don’t have everyone’s plaintext passwords, so we’ll have a properly-hashed value containing a hashed value … we’ll handle this in code and clean up after ourselves later. I like this approach because it means you’re immediately protected rather than only being able to update passwords when people log in.

Update Login Code

Before we do anything to the password database, we’ll update the login code so that we’re still able to log in later! If you’re using an md5 hash then you probably have an SQL query that selects users with matching usernames and passwords, something like:

SELECT * FROM users u
WHERE u.username = :username AND u.password = :password

This doesn’t work with the new password features because the algorithm and salt used are part of the stored value; we will need to get the hashed value, then ask PHP to work out if it matches the password that the user supplied. So the first thing to do here is to amend the code so that we:

  1. Fetch users with a matching username, including the hashed password value that is stored in the database
  2. Pass the supplied password value from the form along with the stored hash to password_verify() to find out if the passwords matched
  3. If they don’t match, then hash the supplied password value with the old password algorithm (and salt if used), and try that instead!

This will allow us to hash the password database, and be able to log in.

Hash Existing Passwords

To do this, we will need to loop through the MySQL table and update all the values. I do this with a one-off PHP script and work through updating one row at a time, since I need to calculate the new hashed value using PHP’s password_hash() function.

At this point, we’ve got a lot of values flying around, but they’re visually distinct so let me recap:

  • There’s the original user password e.g. qwerty, this wasn’t ever stored anywhere
  • There’s the old stored password, if you use md5() then it would be d8578edf8458ce06fbc5bb76a58c5ca4
  • There’s the new hash of the password. This has quite a different format and will look something like: $2y$10$qxnaCfygnb/z6bpqPf/S4e3DsjKZ6bqcR3tsWLwc4zcJ3l.tVA2La. It’s longer because it has the algorithm and salt included in it

OK so: database now holds new format password hashes BUT they are hashes of hashes, which is more overhead than we need, so let’s fix that.

Update Registration Code

Make sure that newly-registered users get their password stored in the new format when they pick a password. Our existing changes to the login code should then allow them to log in.

Usually I find that there are also other places that need changing, such as when a user changes or forgets their password – grep your code for whatever the name of the password column in the database is to find them all!

Stored Hashed-Once Passwords On Login

Now, when a user logs in, if they have a password that has been hashed once, we’ll accept their login. Then as we set up in our first step, if they fail that check, we have a second attempt to log them in, by hashing their password to match our old algorithm, and trying that. This allows the existing users whose passwords we then doubly-hashed to log in, but it’s a lot of processing just to log in a user, so we’ll amend the login code to add one final step:

  1. While we have the user’s plaintext password, hash it properly and store it – so next time they can log straight in

Over time, users will eventually get their passwords into the proper format when they log in – you could also add a column to indicate what format password they have or whether it’s been properly rehashed if you want to know who has what.

Alternative Approaches

There certainly are alternatives, most of them involve changing user’s passwords when they log in, and/or forcing all users to go through a password change on their next login. An elegant solution I saw recently is in the CakePHP3 migration guide. This allows the definition of two password handlers: a real one, and a fallback. The real one will be used – but if password verification fails then the fallback one will be tried as well. This allows for easy migration to a better password strategy and works particularly well since the older versions of Cake did use a single salt plus SHA-1, so I was very happy to see that they had really thought about how users could upgrade, and made it very easy both technically and by including it in their migration guide.

9 thoughts on “Upgrade To Better Passwords in PHP

  1. The password_hash function is not available on my server because the PHP version is still 5.4. Perhaps you can suggest alternative methods for those of us that are still using old versions of PHP (which is quite a large part of the PHP user base).

    • Thanks for mentioning that, I’ve been having some issues with my syntax highlight plugin but I think I’ve really fixed it this time (famous last words…)

Leave a Reply

Please use [code] and [/code] around any source code you wish to share.

This site uses Akismet to reduce spam. Learn how your comment data is processed.