Hello! Earlier in the course, you worked with form data and database records. A password field brings those pieces together with a particular security rule: the application must be able to check a password later, but it must not store the password itself.
In this lesson, you’ll use PHP’s password API to create a hash at registration and verify a submitted password at login. You’ll also see why generating a second hash and comparing the two strings does not work. Allow about 40 minutes, including the short video, manual readings, and time to run the code.
Why passwords need a dedicated hash
Encryption is designed to let someone with the right key recover the original data. Password storage has a different goal: check a future guess without recovering the original password. A password hash supports that check, but it is not a guarantee that a weak password is safe. If a database is stolen, an attacker can still try guesses against its hashes.
Password hashing algorithms make each guess relatively expensive. PHP’s password_hash() also generates a random salt for each hash. Consequently, two accounts using the same password will ordinarily have different stored hash strings.
PHP password hashing explained
Watch “PHP password hashing explained” from Bro Code for a compact demonstration of creating a hash and checking a login attempt.
Watch the demonstration, stopping before the closing recap. Focus on which value would be stored and on the argument order in password_verify(). Treat its printed passwords and hashes as teaching aids, not production logging practices. Also qualify the video’s security claim: a hash does not reveal a password directly, but an attacker may still guess a weak one.
PHP provides general-purpose hash functions, but a fast hash such as SHA-256 or MD5 is not a substitute for password_hash() when storing passwords. Speed is useful for checksums; it helps an attacker test password guesses quickly.
Registration: create the value you will store
At registration, take the password string supplied by the user and pass it to password_hash(). For an introductory application, PASSWORD_DEFAULT lets PHP choose its default password algorithm rather than fixing one in your code.
Read the PHP Manual’s “password_hash” entry to understand the algorithm choice, automatic salt, and contents of the returned string.
In Description, read the algorithm list, paying particular attention to PASSWORD_DEFAULT and PASSWORD_BCRYPT. Under Supported options for PASSWORD_BCRYPT, read the salt and cost guidance. Finally, in Parameters note the bcrypt limit; in Return Values, read what the hash contains.
Create a file named password_demo.php:
<?php
declare(strict_types=1);
$registrationPassword = 'demo-only-passphrase';
$storedHash = password_hash(
$registrationPassword,
PASSWORD_DEFAULT
);
Here $storedHash represents the value to save in a user record. Do not save $registrationPassword alongside it. The stored hash includes the information PHP needs for verification, including the algorithm settings and salt; you do not need separate database columns for them.
Try hashing the same demonstration password a second time:
$anotherHash = password_hash(
$registrationPassword,
PASSWORD_DEFAULT
);
var_dump($storedHash === $anotherHash); // Normally false
The different strings are expected because each call generates a new salt. Store the complete hash string, not just a portion that appears to represent the hash output. Give the database column room for future algorithm changes; the PHP Manual suggests a column capable of holding 255 bytes rather than one sized only for today’s bcrypt output.
PASSWORD_DEFAULT currently uses bcrypt, but its algorithm—and PHP’s default cost settings—can change with PHP versions. Bcrypt also uses at most the first 72 bytes of a password. Bytes are not necessarily the same as visible characters, so that limit matters when choosing a password policy or an alternative supported algorithm. For now, avoid hard-coding a cost merely because an example uses one; higher cost also makes legitimate logins slower.
Login: verify, do not hash and compare
On login, you have two values: the newly submitted password and the complete hash retrieved from the account record. Pass them to password_verify() in that order. It returns a boolean. There is no “unhash” step, and you should not call password_hash() again and compare the resulting strings—the fresh salt would ordinarily make them different.
Read the PHP Manual’s “password_verify” entry for its argument order, return value, and explanation of how it uses a stored hash.
In Description, read the explanation. Then check Parameters to distinguish the submitted password from the stored hash, and Return Values for the boolean result.
Add this to password_demo.php after the earlier code:
$submittedPassword = 'demo-only-passphrase';
if (password_verify($submittedPassword, $storedHash)) {
echo "Accepted\n";
} else {
echo "Rejected\n";
}
if (password_verify('incorrect-guess', $storedHash)) {
echo "Accepted\n";
} else {
echo "Rejected\n";
}
Run php password_demo.php from the command line. You should see bool(false) from the comparison of two freshly generated hashes, followed by Accepted and Rejected. The first comparison fails even though the original passwords match; both password_verify() calls produce the intended results because the function interprets the stored hash’s embedded settings.
The manual describes password_verify() as safe against timing attacks during its password check. That property does not make an entire login endpoint timing-safe or protect it from unlimited online guesses. Login rate limiting and careful handling of account-not-found responses remain separate concerns.
Putting the calls at the database boundary
You have already used PDO and parameterized queries in the course. Password hashing fits immediately before inserting a new account: the query receives the hash, never the submitted password. In this sketch, assume $pdo is connected and $email and $password have been obtained and validated:
$hash = password_hash($password, PASSWORD_DEFAULT);
$insert = $pdo->prepare(
'INSERT INTO users (email, password_hash)
VALUES (:email, :hash)'
);
$insert->execute([
'email' => $email,
'hash' => $hash,
]);
For login, retrieve the account’s stored hash, then verify the submitted password against it:
$query = $pdo->prepare(
'SELECT id, password_hash FROM users WHERE email = :email'
);
$query->execute(['email' => $email]);
$user = $query->fetch(PDO::FETCH_ASSOC);
if (
$user === false
|| !is_string($user['password_hash'])
|| !password_verify($password, $user['password_hash'])
) {
http_response_code(401);
echo 'Invalid credentials';
return;
}
// The password check succeeded. Establish the authenticated session here.
The same public failure message covers an unknown account and a wrong password. A successful check is the point at which the application can establish an authenticated session—not a reason to reveal or retain the submitted password. In production, do not print passwords or hashes for debugging, and do not expose internal hashing failures to users.
One maintenance detail is worth keeping in mind: because the stored string records its algorithm and settings, existing hashes can still be verified when your application’s preferred settings change. PHP also offers password_needs_rehash() to help migrate hashes after a successful login; you do not need it to perform the basic hash-and-verify workflow.
Key takeaways
Use password_hash($password, PASSWORD_DEFAULT) when creating an account, store its entire returned string, and use password_verify($submittedPassword, $storedHash) when checking a login. Leave salt generation to PHP. Never store plaintext passwords or compare two newly generated password hashes as a login test.
Password verification establishes whether the submitted secret matches the stored hash; it does not protect every action taken after login. The next lesson addresses a different risk: using a session-backed CSRF token to protect a state-changing form.