PHP password_hash() and password_verify() for Beginners
Learn PHP password_hash and password_verify step by step. Store passwords safely, build sign-up and login scripts, and avoid common beginner mistakes.
Learn PHP password_hash and password_verify step by step. Store passwords safely, build sign-up and login scripts, and avoid common beginner mistakes.
How do you store a password safely in PHP? Never save the password itself. Turn it into a scrambled string with the built-in password_hash() function, save that string, and later check a login attempt with password_verify(). This guide shows you how, step by step, with no experience needed.
If you have followed our earlier lessons on handling forms and inserting form data into a database, this is the natural next step: a sign-up form that keeps user passwords safe.
Imagine your database is stolen. If it holds real passwords, the thief can log in as every user. Worse, many people reuse the same password on other sites, so the damage spreads. This is why websites store a hash instead.
A hash is the result of a one-way mathematical scramble. You can turn a password into a hash, but you cannot turn the hash back into the password. Even you, the site owner, cannot read it.
| Method | Can it be reversed? | Safe for passwords? |
|---|---|---|
| Plain text | Not needed, it is readable | No, never |
| Encryption | Yes, with a key | No, the key can leak |
| Fast hash (md5, sha1) | Hard, but easy to guess at scale | No, too fast to attack |
| password_hash() | No, one-way and slow on purpose | Yes, use this |
Old tutorials still show md5() for passwords. Do not use it. It is very fast, which helps attackers try billions of guesses. password_hash() is slow on purpose and adds random data called a salt, so two users with the same password get different hashes.
<?php
$password = 'MySecret123';
$hash = password_hash($password, PASSWORD_DEFAULT);
echo $hash;Line 2 stores the plain password in a variable (in real code it comes from a form). Line 3 calls password_hash(). The first argument is the password. PASSWORD_DEFAULT tells PHP to pick the best algorithm available, so your code improves when PHP updates. Line 4 prints the result, which looks like $2y$10$abc... and is about 60 characters long. Run it twice and you will see two different hashes for the same password. That is the salt at work.
<?php
$hash = '$2y$10$exampleHashFromDatabase';
$attempt = 'MySecret123';
if (password_verify($attempt, $hash)) {
echo 'Correct password!';
} else {
echo 'Wrong password.';
}password_verify() takes the password the user just typed and the saved hash. It returns true if they match and false if not. Notice you never compare the two strings yourself with ==. The function knows how to read the salt inside the hash. Use it every time you check a login.
You need a place to save hashes. Here is a simple table. The password column must be long enough for the hash, so use at least 255 characters. This works in MySQL and PostgreSQL (in PostgreSQL, write SERIAL PRIMARY KEY instead of AUTO_INCREMENT).
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL
);Each user has an id, an email that must be unique (so nobody registers twice), and a password_hash column. We name it password_hash on purpose so you never forget it does not hold the real password.
<?php
$pdo = new PDO('mysql:host=localhost;dbname=demo', 'user', 'pass');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email']);
$password = $_POST['password'];
if (strlen($password) < 8) {
die('Password must be at least 8 characters.');
}
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare('INSERT INTO users (email, password_hash) VALUES (?, ?)');
$stmt->execute([$email, $hash]);
echo 'Account created!';
}Line 2 connects to the database with PDO. Line 4 runs the code only when the form was sent with POST. Lines 5 and 6 read the email and password; trim() removes stray spaces from the email. We do not trim the password, because spaces can be part of it. Lines 8 to 10 reject short passwords. Line 12 creates the hash. Lines 14 and 15 save it using a prepared statement, where the question marks are placeholders filled in safely. This stops SQL injection, an attack where someone types database commands into your form.
The matching HTML form is short:
<form method="post">
<input type="email" name="email" required>
<input type="password" name="password" required>
<button type="submit">Sign up</button>
</form>Each input has a name, and PHP reads it from $_POST using that same name. type="password" hides the characters as you type.
<?php
session_start();
$pdo = new PDO('mysql:host=localhost;dbname=demo', 'user', 'pass');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = ?');
$stmt->execute([trim($_POST['email'])]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($_POST['password'], $user['password_hash'])) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
echo 'Welcome back!';
} else {
echo 'Invalid email or password.';
}
}session_start() lets PHP remember the visitor between pages (see our guide to PHP sessions). We look up the user by email, then fetch() returns one row or false. The if checks two things: the user exists, and password_verify() says the password matches. If so, session_regenerate_id(true) creates a fresh session ID, which blocks session fixation attacks, and we store the user id in the session. The error message is the same whether the email or the password was wrong, so attackers cannot learn which emails are registered.
<?php
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
$newHash = password_hash($_POST['password'], PASSWORD_DEFAULT);
// save $newHash to the database for this user
}Over time PHP may choose a stronger default algorithm. After a successful login, password_needs_rehash() tells you whether the saved hash is out of date. If it returns true, hash the password again and save the new hash. Users never notice, and your site stays secure.
Call password_hash($password, PASSWORD_DEFAULT). It returns a long string that includes a random salt. Save that string in your database instead of the real password.
No. Hashing is one-way, so there is no way to get the original password back. To check a login, use password_verify(), which compares the typed password with the saved hash.
It adds a random salt every time it runs. This is normal and good, because two users with the same password get different hashes. password_verify() reads the salt from the hash, so checking still works.
No. md5 is very fast and has been broken for security use, so attackers can test huge numbers of guesses quickly. Use password_hash() with PASSWORD_DEFAULT instead.
Use VARCHAR(255). Current hashes are about 60 characters, but PHP may switch to longer ones in the future, and 255 leaves plenty of room.
Want to understand how logged-in users are remembered between pages? Read our beginner guide next.
Read: PHP Sessions ExplainedBuilding a website or app that needs secure user accounts? The Fepiq team can help.
Talk to FepiqOccasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.
Keep reading
Learn SQL CASE WHEN step by step. This beginner guide shows how to label, group and sort data with simple examples you can copy and run today.
PostgreSQL ILIKE explained for beginners: search text without worrying about case, learn % and _ wildcards, and copy simple examples you can run today.
Let's build something
Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.