Storing, securing and validating passwords in PHP has historically been a messy business. Which hashing function should I use? What is a salt, and how should I use it? Do I need to worry about timing attacks? The ubiquity of these problems in web development meant a web language like PHP needed a user friendly solution, and this arrived in PHP 5.5 with the password hashing functions. These functions provide sensible defaults and a straightforward workflow to make it easy to look after your users’ credentials.
The Workflow
Your interaction with your users’ passwords can be broken into three broad areas: storing a password; verifying a password; and keeping a password secure in the long term. These areas map neatly to three built in PHP functions.
Storing A Password
When storing a password, it’s important to choose a mechanism that does not allow attackers to learn the password easily in the event of a breach of your server. The best approach is to “hash” that password; convert it to a string of seemingly random characters. This means that, should an attacker gain access to your database, they will be unable to use the password to get access to your app as one of your users. The best way to do this in PHP is with the function password_hash
. You can use this when your user registers, before storing the $hash
to your database:
$hash = password_hash($_POST['password'], PASSWORD_DEFAULT);
There are other arguments available for password_hash
, but the simple approach above will work for most circumstances.
Verifying A Password
When your user comes back to your site and wishes to log in, you need to check that the password they give matches the password they registered with. You can’t simply compare their login to the registered password since you don’t store it, you only store the hash. However, the hashing process is repeatable; if they enter the same password, you can perform the same process and compare the result to what you have stored. PHP takes care of all these steps in one function: password_verify
.
$login_ok = password_verify($_POST['password'], $hash);
password_verify
will return true
if the submitted password matches the stored hash, allowing you to verify the user without knowing their password directly.
Keeping Passwords Secure
The hashing algorithms used to hash passwords for storage can become obsolete over time as computers get faster or weaknesses are found in the algorithms. Good password management requires updating the hashing algorithm and the stored password hashes to keep up. A useful function for this is password_needs_rehash
.
// Run after a successful login
$needs_rehash = password_needs_rehash($hash, PASSWORD_DEFAULT);
if ($needs_rehash) {
$new_hash = password_hash($_POST['password'], PASSWORD_DEFAULT);
}
PHP will update the PASSWORD_DEFAULT
constant over time to reference the most up to date hashing algorithm available. Using it with password_needs_rehash
ensures your hashes will be kept up to date.
Putting it all together
The code below shows an example of the functions above being used together to keep your users’ passwords secure. It assumes a MySQL database with the following table:
CREATE TABLE user_auth (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) UNIQUE,
password VARCHAR(255)
);
class UserAuth
{
private PDO $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
/**
* Register a new user
*
* Add the user's username and hashed password to the database, and return the user ID.
*
* @param string $username_from_form the submitted username
* @param string $password_from_form the submitted password
*
* @return int the user ID
*/
public function register(string $username_from_form, string $password_from_form): int
{
$hash = password_hash($password_from_form, PASSWORD_DEFAULT);
$q = $this->db->prepare('INSERT INTO user_auth (username, password) VALUES (?, ?)');
$q->execute([$username_from_form, $hash]);
return (int) $this->db->lastInsertId();
}
/**
* Log in a user
*
* If the passed username and password match a user, return the user ID.
* This method also ensures that the password hash is up to date.
*
* @param string $username_from_form the submitted username
* @param string $password_from_form the submitted password
*
* @return int the user ID
*
* @throws Exception if the user does not exist or the password does not match
*/
public function login(string $username_from_form, string $password_from_form): int
{
$q = $this->db->prepare('SELECT id, password FROM user_auth WHERE username = ?');
$q->execute([$username_from_form]);
$row = $q->fetchObject();
if (!$row) {
throw new Exception('User not authenticated');
}
$login_ok = password_verify($password_from_form, $row->password);
if (!$login_ok) {
throw new Exception('User not authenticated');
}
$needs_rehash = password_needs_rehash($row->password, PASSWORD_DEFAULT);
if ($needs_rehash) {
$new_hash = password_hash($password_from_form, PASSWORD_DEFAULT);
$q = $this->db->prepare('UPDATE user_auth SET password = ? WHERE id = ?');
$q->execute([$new_hash, $row->id]);
}
return (int) $row->id;
}
}
You can use this class in your registration script to store your user’s authentication details; and again in your login script, to check their submitted details and update their stored hash if need be.