How to hash passwords in PHP
The complete step-by-step tutorial

Today, I’ll show you exactly how to hash passwords in PHP.
In this step-by-step tutorial you will learn:
- Why hashes like MD5 are not secure
- How to create secure password hashes with password_hash()
- How to verify passwords with password_verify()
- 2 ways to increase hash security
- BONUS tutorial: how to automatically convert old hashes
So, if you want to learn how to encrypt passwords in PHP, this is the tutorial for you.
Let’s dive in.
CONTENTS
Looking for a complete login and authentication tutorial?
Here it is: PHP login and Authentication Tutorial
PHP password encryption security
As a PHP developer, you must know how to store passwords securely.
For the following reasons:
- Many web attacks aim at stealing your users’ passwords.
If passwords are stolen, you must make sure the attacker cannot decrypt them. - Today’s strict privacy regulations require sensitive data, like passwords, to be protected. Failing to comply can result in fines.
- Password security is one of the basic PHP security features your clients expect from you.
If you learn how to do it properly, your reputation will improve and your clients will trust you.
So, let’s start from this question:
Are MD5 and SHA hashes secure?
(Short answer: no)
Back in the day, passwords were stored using an MD5 or SHA1 hash.
Like this:
/* User's password. */
$password = 'my secret password';
/* MD5 hash to be saved in the database. */
$hash = md5($password);
However, this technique is not safe enough.
For two reasons:
- MD5 and SHA algorithms are too weak for today’s computational power.
- Simple, not-salted hashes are vulnerable to “rainbow tables” and dictionary attacks.
If an attacker steals an MD5 or SHA hash, he or she can easily find out the original password too.
In other words, these hashes are almost as insecure as plain text passwords.
The solution is to use a secure hashing function: password_hash().
Let’s see how it works.
password_hash()
The password_hash() function creates a secure hash of your password.
This is how you can use it:
/* User's password. */
$password = 'my secret password';
/* Secure password hash. */
$hash = password_hash($password, PASSWORD_DEFAULT);
The result hash from password_hash() is secure because:
- It uses a strong hashing algorithm.
- It adds a random salt to prevent rainbow tables and dictionary attacks.
Once you have the password hash, you can save it directly in the database.
Let’s see how with the next examples.
How to use password_hash()
First, you need a database users table.
For example, let’s use a simplified version of the “accounts” table from my Authentication Tutorial.
This table has the following columns:
- account_id: the unique identifier of the account.
- account_name: the account username.
- account_passwd: the password hash.
This is the SQL code to create the table (you can use it with PhpMyAdmin to create the table on your development environment):
CREATE TABLE `accounts` (
`account_id` int(10) UNSIGNED NOT NULL,
`account_name` varchar(255) NOT NULL,
`account_passwd` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ALTER TABLE `accounts`
ADD PRIMARY KEY (`account_id`);
ALTER TABLE `accounts`
MODIFY `account_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
Important:
Be sure to set the password column as a varchar.
(A varchar is a text column of variable length.)
The reason is that the size of the hash from password_hash() can change (more details on this later).
If you need help with SQL, you can find all you need here: How to use PHP with MySQL
Now, you need to connect to the database from your PHP script.
If you don’t know how, here is a simple PDO connection script you can use right away.
Just edit the connection parameters to make it work with your own environment:
/* Host name of the MySQL server. */
$host = 'localhost';
/* MySQL account username. */
$user = 'myUser';
/* MySQL account password. */
$passwd = 'myPasswd';
/* The default schema you want to use. */
$schema = 'mySchema';
/* The PDO object. */
$pdo = NULL;
/* Connection string, or "data source name". */
$dsn = 'mysql:host=' . $host . ';dbname=' . $schema;
/* Connection inside a try/catch block. */
try
{
/* PDO object creation. */
$pdo = new PDO($dsn, $user, $passwd);
/* Enable exceptions on errors. */
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch (PDOException $e)
{
/* If there is an error, an exception is thrown. */
echo 'Database connection failed.';
die();
}
Now you are ready to add a new user to the table.
Here is a full example (pdo.php is the script containing the previous database connection snippet):
/* Include the database connection script. */
include 'pdo.php';
/* Username. */
$username = 'John';
/* Password. */
$password = 'my secret password';
/* Secure password hash. */
$hash = password_hash($password, PASSWORD_DEFAULT);
/* Insert query template. */
$query = 'INSERT INTO accounts (account_name, account_passwd) VALUES (:name, :passwd)';
/* Values array for PDO. */
$values = [':name' => $username, ':passwd' => $hash];
/* Execute the query. */
try
{
$res = $pdo->prepare($query);
$res->execute($values);
}
catch (PDOException $e)
{
/* Query error. */
echo 'Query error.';
die();
}
In this example we skipped the validation steps, including:
- checking the username and password length
- checking for invalid characters
- checking if the username already exists
And so on.
Validation is out of the scope of this tutorial, but remember that you always need to validate your input variables.
You can refer to my Login and Authentication Tutorial for more details and examples.
If you want to learn more about PHP security, take a look at my PHP Security course.
How to change a user’s password
The next example shows how to change the password of an existing user.
First, get the new password and create its hash with password_hash():
/* New password. */
$password = $_POST['password'];
/* Remember to validate the password. */
/* Create the new password hash. */
$hash = password_hash($password, PASSWORD_DEFAULT);
Then, update the table row having the same account ID of the current user and set the new hash.
Note: we assume the $accountId variable contains the account ID.
/* Include the database connection script. */
include 'pdo.php';
/* ID of the account to edit. */
$accountId = 1;
/* Update query template. */
$query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
/* Values array for PDO. */
$values = [':passwd' => $hash, ':id' => $accountId];
/* Execute the query. */
try
{
$res = $pdo->prepare($query);
$res->execute($values);
}
catch (PDOException $e)
{
/* Query error. */
echo 'Query error.';
die();
}
How to use password_verify()
To verify the password provided by a remote user, you need to use the password_verify() function.
password_verify() takes two arguments:
- the password you need to verify, as first argument
- the hash from password_hash() of the original password, as second argument
If the password is correct, password_verify() returns true.
Here is an example:
/* Include the database connection script. */
include 'pdo.php';
/* Login status: false = not authenticated, true = authenticated. */
$login = FALSE;
/* Username from the login form. */
$username = $_POST['username'];
/* Password from the login form. */
$password = $_POST['password'];
/* Remember to validate $username and $password. */
/* Look for the username in the database. */
$query = 'SELECT * FROM accounts WHERE (account_name = :name)';
/* Values array for PDO. */
$values = [':name' => $username];
/* Execute the query */
try
{
$res = $pdo->prepare($query);
$res->execute($values);
}
catch (PDOException $e)
{
/* Query error. */
echo 'Query error.';
die();
}
$row = $res->fetch(PDO::FETCH_ASSOC);
/* If there is a result, check if the password matches using password_verify(). */
if (is_array($row))
{
if (password_verify($password, $row['account_passwd']))
{
/* The password is correct. */
$login = TRUE;
}
}
Important:
You cannot just compare two different hashes to see if they match.
The reason is that password_hash() creates salted hashes.
Salted hashes include a random string, named “salt”, as a protection against rainbow tables and dictionary attacks.
Therefore, every hash will be different even if the source password is the same.
Try the following code. You will see that the two hashes are different, even if the password is the same:
$password = 'my password';
echo password_hash($password, PASSWORD_DEFAULT);
echo '<br>';
echo password_hash($password, PASSWORD_DEFAULT);
Note:
password_verify() only works with hashes created by password_hash().
You cannot use it to check a password against a MD5 or SHA hash.
How to increase hash security
The hash generated by password_hash() is very secure.
But you can make it even stronger with two simple techniques:
- Increasing the Bcrypt cost.
- Automatically updating the hashing algorithm.
Bcrypt cost
Bcrypt is the current default hashing algorithm used by password_hash().
This algorithm takes an option parameter named “cost”. The default cost value is 10.
By increasing the cost, you can make the hash more difficult to compute. The higher the cost, the longer the time needed to create the hash.
A higher cost makes more difficult to break the hash. However, it also makes the hash creation and check longer, too.
So, you want to find a compromise between security and server load.
This is how you can set a custom cost value for password_hash():
/* Password. */
$password = 'my secret password';
/* Set the "cost" parameter to 12. */
$options = ['cost' => 12];
/* Create the hash. */
$hash = password_hash($password, PASSWORD_DEFAULT, $options);
But what cost value should you set?
A good compromise is a cost value that lets your server create the hash in about 100ms.
Here is a simple test to find this value:
/* 100 ms. */
$time = 0.1;
/* Initial cost. */
$cost = 10;
/* Loop until the time required is more than 100ms. */
do
{
/* Increase the cost. */
$cost++;
/* Check how much time we need to create the hash. */
$start = microtime(true);
password_hash('test', PASSWORD_BCRYPT, ['cost' => $cost]);
$end = microtime(true);
}
while (($end - $start) < $time);
echo 'Cost found: ' . $cost;
Once you have found your cost, you can use it every time you execute password_hash() like in the previous example.
Keeping your hashes up to date with password_needs_rehash()
To understand this step, let’s see how password_hash() works.
password_hash() takes three arguments:
- The password you need to hash
- The hashing algorithm you want to use
- An array of options to pass to the hashing algorithm
PHP supports different hashing algorithms, but you usually want to use the default one.
You can select the default algorithm by using the PASSWORD_DEFAULT constant, as you have seen in the previous examples.
As of June 2020, the default algorithm is Bcrypt.
However, PHP can change the default algorithm in the future, if a better and more secure algorithm is implemented.
When that happens, the PASSWORD_DEFAULT constant will point to the new algorithm. So, all the new hashes will be created using the new algorithm.
But what if you want to take all your old hashes, made with the previous algorithm, and automatically create them again with the new one?
This is where password_needs_rehash() comes into play.
This function checks if a hash has been created with a given algorithm and parameters.
For example:
/* Password. */
$password = 'my secret password';
/* Set the "cost" parameter to 10. */
$options = ['cost' => 10];
/* Create the hash. */
$hash = password_hash($password, PASSWORD_DEFAULT, $options);
/* Now, change the cost. */
$options['cost'] = 12;
/* Check if the hash needs to be created again. */
if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options))
{
echo 'You need to rehash the password.';
}
If the current default hashing algorithm is different from the algorithm used to create the hash, password_needs_rehash() returns true.
password_needs_rehash() also checks if the options parameter is different.
This is very handy if you want to update your hashes after you change a parameter like the Bcrypt cost.
This example shows how you can automatically check a password hash and update it if needed, when a remote user logs in:
/* Include the database connection script. */
include 'pdo.php';
/* Set the "cost" parameter to 12. */
$options = ['cost' => 12];
/* Login status: false = not authenticated, true = authenticated. */
$login = FALSE;
/* Username from the login form. */
$username = $_POST['username'];
/* Password from the login form. */
$password = $_POST['password'];
/* Remember to validate $username and $password. */
/* Look for the username in the database. */
$query = 'SELECT * FROM accounts WHERE (account_name = :name)';
/* Values array for PDO. */
$values = [':name' => $username];
/* Execute the query */
try
{
$res = $pdo->prepare($query);
$res->execute($values);
}
catch (PDOException $e)
{
/* Query error. */
echo 'Query error.';
die();
}
$row = $res->fetch(PDO::FETCH_ASSOC);
/* If there is a result, check if the password matches using password_verify(). */
if (is_array($row))
{
if (password_verify($password, $row['account_passwd']))
{
/* The password is correct. */
$login = TRUE;
/* Check if the hash needs to be created again. */
if (password_needs_rehash($row['account_passwd'], PASSWORD_DEFAULT, $options))
{
$hash = password_hash($password, PASSWORD_DEFAULT, $options);
/* Update the password hash on the database. */
$query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
$values = [':passwd' => $hash, ':id' => $row['account_id']];
try
{
$res = $pdo->prepare($query);
$res->execute($values);
}
catch (PDOException $e)
{
/* Query error. */
echo 'Query error.';
die();
}
}
}
}
How to automatically convert old hashes
In this example, you will implement a simple script to automatically convert old, MD5-based hashes to secure hashes created with password_hash().
This is how it works:
- When a user logs in, you first check its password with password_verify().
- If the login fails, check if the hash in the database is the MD5 hash if the password.
- If it is, then you update the hash with the one generated by password_hash().
Here is the script:
/* Include the database connection script. */
include 'pdo.php';
/* Set the "cost" parameter to 12. */
$options = ['cost' => 12];
/* Login status: false = not authenticated, true = authenticated. */
$login = FALSE;
/* Username from the login form. */
$username = $_POST['username'];
/* Password from the login form. */
$password = $_POST['password'];
/* Remember to validate $username and $password. */
/* Look for the username in the database. */
$query = 'SELECT * FROM accounts WHERE (account_name = :name)';
/* Values array for PDO. */
$values = [':name' => $username];
/* Execute the query */
try
{
$res = $pdo->prepare($query);
$res->execute($values);
}
catch (PDOException $e)
{
/* Query error. */
echo 'Query error.';
die();
}
$row = $res->fetch(PDO::FETCH_ASSOC);
/* If there is a result, check if the password matches using password_verify(). */
if (is_array($row))
{
if (password_verify($password, $row['account_passwd']))
{
/* The password is correct. */
$login = TRUE;
/* You can also use password_needs_rehash() here, as shown in the previous example. */
}
else
{
/* Check if the database contains the MD5 hash of the password. */
if (md5($password) == $row['account_passwd'])
{
/* The password is correct. */
$login = TRUE;
/* Update the database with a new, secure hash. */
$hash = password_hash($password, PASSWORD_DEFAULT, $options);
$query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
$values = [':passwd' => $hash, ':id' => $row['account_id']];
try
{
$res = $pdo->prepare($query);
$res->execute($values);
}
catch (PDOException $e)
{
/* Query error. */
echo 'Query error.';
die();
}
}
}
}
Conclusion
In this tutorial you learned how to use password_hash() and password_verify() to create secure hashes of your passwords (and why you should not use MD5).
You also learned how to make your password hashes more secure by setting a proper Bcrypt cost and automatically rehashing your passwords as needed.
Now it’s your turn: leave your questions and your comments below.
Images designed by Freepik.
MySQLi vs PDO: which one is best for you?
Get my weekly PHP tips and the bonus PDF, absolutely free.
Thanks for the great content, Alex. I am a beginner when it comes to all this, and I appreciate your work.
You have shared very good information. Thank you!
Thanks Sir nice article
thank you for the professional in depth and yet comprehensive article.
Thank you, I’m glad you liked it.
Thanks Alex for this amazing article.
You’re welcome, thank you for your comment.
I’m a total newbie at all this Alex and just wanted to say that what I’ve read so far gives me confidence I’m going to like reading more. Thank you.
Thank you Adam, I’m happy to hear that!
I am really delighted to read this webpage posts which includes tons of helpful facts, thanks for providing such information.
Thank you Alex sir for your step by step tutorial.
You’re very welcome. Thank you for your commnent!
nice article. It helped me a lot.
can password hash really protects one’s account if used anywhere?
Glad to know it helped!
Using password_hash() is the most secure way to store a password hash, minimizing the risk of the password being “guessed” or found. This protect the users’ passwords in case of data leaks, which unfortunately still happen too often.
There are other factors that come into play when talking about account security though, such as using 2-Factor authentication and anti-CSRF techniques.
Thanks for this useful Post
Alex,
Others have said it before; this a great explanation of how this works and how we can use it.
Thank you!
As my and many other MVC-frameworks use foreword slashes [/] in their urls, it’s a pain the back that the php hash comes out regularly with slashes in their result.
Is there a way to avoid that?
I know, there’s always a workaround; but that’s not elegant.
Hey Jan, thank you for your comment.
I don’t think that PHP hashes include slashes. Even so, hashes are not meant to be used in URLs so that should not be an issue anyway.
Am I missing something?
Thanks Alex for this amazing article.
Very interesting and informative article. Thanks for share such type of precious article.
Amazing Information. Keep it up.
My rating: ⭐⭐⭐⭐⭐⭐ out of 5! I also give this the Unicorn distinction, because it’s truly one of a kind. I’ve been trying to get this working for days and this is the ONLY resource that delivered!
This is one instance where StackOverflow was completely worthless! I love that site but I truly resent the attitude of some of the users that post responses. Everybody talks around the problem or just says “do this” but only describes an approach instead of giving you a concrete/complete example. For every person on there that understands it, there are probably dozens that just don’t get it. I know, because before I found this tutorial, I was one of the ones that DIDN’T. But I finally got it working earlier today because of this content. Fortunately, you must do a pretty good job of SEO because this page ranks very highly. It would have solved my problem much sooner, but when I first found it, the site wouldn’t load for some reason. I think your site may have been down for an extended period a few days ago. Fortunately, I tried it again yesterday and got to it. Whew! This is the ONLY resource I’ve found that has example code that actually works.
Thanks again,
Paul
Thank you for your kind words, Paul!
I’m really happy to know that my article has been helpful!
Thanks sir,
You have published very good article. Reading this article helped me a lot.
I always read your article and share it with my friends, you work very hard in making articles
Suggestway.com
Thank you Yash, I’m glad that this article has been helpful to you.
This a GREAT resource…THANKS!
A couple of quick questions…if you have to transfer DB/website to a new server…will the verify still work or do you have to create new hashes for all existing passwords on the new server?
Would using a password_needs_rehash() check on every login help with this? Or is that overkill?
Hi Peter,
If you transfer the DB, the passwords will still work without the need to create them again, because the salt and the algorithm info are stored together with the hash itself.
I think you can use password_needs_rehash() at every login, it doesn’t add much overhead to the whole login process.
Hi, thanks for this tutorial. Please can you make a full change password PHP code with password_hash without having to break them into segments? By the way, you don’t have a contact email on your website.
Thank you.
What do you mean exactly with “without break”? If you mean using few lines of code as possible, then you can refer to the last example. You could make it even more compact but that would make the code poorly readable.
About the contact page, you are right. I need to add a contact page…
Hi,
thank you for this tutorial, very usefull.
I’m a bit confused about the password_needs_rehash though.
You write:
If the current default hashing algorithm is different from the algorithm used to create the hash, password_needs_rehash() returns false.
But you check when the return value is TRUE.
/* Check if the hash needs to be created again. */
if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options))
{
echo ‘You need to rehash the password.’;
}
Also, I used your code and if no $options are used, the password_needs_rehash always return true. Why is that?
Hi Thomas,
Thank you, I made a mistake. password_needs_rehash() returns *true* if the algorithm is different. I have fixed the article.
About your other question, if your hash has been created with non-default options (using the $options parameter with password_hash), then password_needs_rehash will always return true if the same $options are not used, as the hash itself will differ.
Thankyou. It is really great article and well explained. Also i have bookmarked it. Thanks.
Thank you!
This is a really clear and simple tutorial, congrats on simplifying an often confusing task.
password_hash() and family are a great set of functions and really make life easy. My one comment would be not to underestimate the difficulty of writing secure password handling code.
Also worth considering a pepper, so in a typical grab-and-run attack where db tables are stolen, they are useless. Again, fantastic article.
Thank you for your feedback, Brian.
Using a pepper is a good idea, it will definitely be in the next article update. Thanks again!
Also dint forget to use getMessage() in your catch blocks:
echo $e->getMessage();
123
This is an excellent post. This is really helpful for my business websites. It saved a lot of time.
Thank you for your feedback. I’m glad this tutorial has been helpful to you.
Hello Sir I am working on a student management system,
And everything has been moving on so well not until I was creating a change password file in php but the new passwords are not matching yet I am using the same password.
So I am asking a how can change a hashed password
Hello,
to change an existing password, you still need to use password_hash() to create the secure hash. Then, you need to update the row of the user on the database with the new hash, instead of adding a new row.
You can find a complete code example in this tutorial, just search for “How to change a user’s password”. Let me know if you have more questions.
I’m delighted to become part of people who followed your blog and post.
It’s makes me learned and understand many things in php.
Thank you for your time, energy and dedication to educate upcoming programmers like me.
Thank you for your kind comment 🙂
Great work
Awesome work sir
thanks a lot Mr. Alex …………your writeup helped me a lot
Thank you, Stephen.
Thanks a a lot you already make it very easy to use 🙂
Thank you 🙂
Thanks Sir this article is really helpful,you are the best.
Thank you Diran.
First of all thank you so much for this helpful. I am really appreciate after reading your post. You are the best writer ever. I also share your post with my friends and at my social media platforms. Keep it up this good work in future.
Veramente un ottimo articolo. Stai svolgendo un lavoro di grande qualità!
Grazie mille Gaetano 🙂
There is a small problem in the example that checks a password hash – $hash should be $row[‘account_passwd’]:
/* Check if the hash needs to be created again. */
if (password_needs_rehash($row[‘account_passwd’], PASSWORD_DEFAULT, $options))
Thank you Neil! I just fixed it.
As always, brilliant. You are not only a great teacher but a great presenter, too.
Thank you for you kind comment!
Thanks a million, Alex.
This has been so helpful. You’re a great resource.
Deeply appreciate.
Thank you!