User Passwords with PHP
Current versions of PHP include functions which help to process user passwords.
The Problem
To put it simply, passwords should never be stored. That is, in any readable format. Instead, a hashed copy of the password should be stored.
Hashing is similar to encryption in that it produces a scrambled version of the original. Where it differs is that the process is irreversible. You cannot unhash a string. Encryption, on the other hand, is designed to be reversible given the correct key, and is useful for transmitting messages in secret.
There are many ways of hashing strings, but, in general, you need a method which:
- can be implemented in PHP
- hasn’t been cracked
- takes long enough to slow down a cracker without blowing all of your resources
Hashing
Hashing is the process of scrambling input to produce a distinct result. It is destructive in that the original input cannot be reverse-engineered from the hash.
The length of a hash also has no relation to the input string. A trivial password like test
should give the same sort of results as entering the Complete Works of Shakespeare, if that’s your idea of having a good time.
Salts
A salt is an artificial string added to your password. This adds to the length and complexity of the password, and makes it less likely to make an appearance in an existing list. It should be a random string, and not reused for other passwords. It doesn’t have to be secret, as, by itself, it reveals no useful information.
PHP Hashing Functions
PHP has a few functions to work with our password hashes. The most important are:
$hash = password_hash($password,$algorithm[,$options]);
This function takes the password, and hashes it according to the given algorithm code. The algorithm code is an integer, with some pre-defined constants to make them more memorable. Unless you have a compelling reason to choose another algorithm, use
PASSWORD_DEFAULT
.The function also generates a random salt. As a result, every time you use the function on the same password, you will get a different salt, and thus a different hash. This is good, but a little unnerving if you’re not expecting it.
$result = password_verify($password,$hash);
Returns a boolean indicating whether the password matches the hash. Since the hash includes all of the relevant details, including the salt, no more information is necessary.
You can read more at http://php.net/manual/en/book.password.php
User Registration and Logging In
This describes the process for storing a password hash, and then checking it for loging in.
User Registration
Typically a user does their own registration, but what follows also applies if the registration is performed by the administrator.
First, there is a database table which includes the user name, hashed password, and a few more details, usually including the email address. If you ensure that the email address is unique, you can use the email address as a user name, simplifying the process.
It is also a good idea to have a password expiry date. This is not so much as to force password changes, but to allow for temporary passwords.
The SQL to create such a table would be something like this:
CREATE TABLE users ( -- mysql
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(64) UNIQUE INDEX,
passwordhash VARCHAR(255),
passwordexpires DATETIME,
-- more fields
);
The passwordhash
field is a varying character field, large enough to store the hash. For now, 255 is well and truly over the top.
The PHP to add a new user to the table would look as follows:
// $password='whatever';
$sql='INSERT INTO users(email,passwordhash,etc) VALUES(?,?,?)';
$prepared=$pdo->prepare($sql);
$data=array($email,password_hash($password,PASSWORD_DEFAULT),$etc);
$prepared->execute($data);
This uses a prepared statement which protects against SQL injection. The etc data is, of course, a place holder for the rest of the user data.
Using the above technique, you have stored a hashed version of the password which cannot be reversed.
Manually Adding a User
If you’re adding a user without using PHP, you will have a problem with the password. In this case, you will need the help, either of a script to hash the password for you, or of an online service.
One such service can be found at: https://ajax.internotes.net/password.php. Just enter your password and copy the result.
Authentication
The main purpose of passwords is, of course, authentication. That is, to allow a user to log in using their user name and password.
Since the password is not to be stored directly, you will need to compare not the passwords, but the hashed version.
The problem here is that you can’t just re-hash the entered password and compare that to the one stored in the database. This is because password_hash()
always generates a new random salt, so the actual hash will also be different.
Instead, we use the following process:
- read the hash stored in the database
- use the
password_very()
function to compare the entered password with the hash retrieved from the database.
The following code illustrates this:
$email='…'; // From login
$password='…'; // From login
$sql='SELECT id,password,etc FROM users WHERE email=?';
$prepared=$pdo->prepare($sql);
$data=array($email);
$prepared->execute($data);
$row=$prepared->fetch();
if($row && password_verify($password,$row['password'])) { // notes
// successful
}
else {
// no good
}
In the line marked notes
:
if($row)
checks that you got something. If the email address doesn’t match, you would getnull
.- The
&&
is, of course, short-circuited, so the next part of the test is only performed if there is a$row
to read. - The
password_verify()
function does the rest. If the password hashes OK, then the result will betrue
.