LearnContact
Lesson 6320 min read

Password Hashing

Learn why passwords must never be stored in plain text, and how password_hash() and password_verify() keep stored credentials safe.

Introduction

In the previous lesson, a login script called password_hash() and password_verify() without much explanation. This lesson looks at those two functions closely, along with password_needs_rehash(), and explains the reasoning behind PHP's modern password-handling API.

Password hashing is one of the few areas of web development where "just write it yourself" is almost always the wrong instinct. PHP's built-in functions exist precisely so you never have to design your own hashing scheme.

What You Will Learn
  • Why passwords must never be stored in plain text.
  • How password_hash() creates a secure, salted hash.
  • How password_verify() checks a submitted password against that hash.
  • How password_needs_rehash() helps you upgrade hashing algorithms over time.
  • Why hashing and encryption are fundamentally different operations.

Why Not Store Passwords as Plain Text?

If a database storing plain-text passwords is ever leaked — through a breach, a misconfigured backup, or an insider — every account on it is instantly compromised, and because many people reuse passwords, accounts on other sites become vulnerable too.

Hashing solves this by transforming the password into a fixed-length string that cannot practically be reversed back into the original password. The database stores only that hash. Even the application itself never needs to "read back" the real password — it only ever needs to check whether a newly submitted password produces a matching result.

Never Do This

INSERT INTO users (username, password) VALUES ('alex', 'hunter2') — storing the raw password string is a critical security flaw, full stop, regardless of how small or "internal" the project is.

Hashing with password_hash()

PHP's password_hash() function takes a plain-text password and returns a secure hash string, using bcrypt by default (PASSWORD_DEFAULT). Bcrypt automatically generates and embeds a random "salt" into the resulting hash, so two users with the identical password still get completely different stored hashes.

Hashing a password
<?php
$plainPassword = 'MySecret123';

$hash = password_hash($plainPassword, PASSWORD_DEFAULT);

echo $hash;
Example Output (yours will differ every time)
$2y$10$uW9x1eIRlv1v8k4hT.d3Ee3vFZ5J2z8y1uQxWn0nD0p3G8kQxJm9K
Always Use PASSWORD_DEFAULT

PASSWORD_DEFAULT tells PHP to use whatever algorithm the PHP team currently considers the strongest reasonable default (bcrypt today). This value can change in future PHP releases as stronger algorithms become the standard, and your code automatically benefits without changes.

Every call to password_hash() with the same input produces a different-looking hash, because a new random salt is generated each time. That is expected and by design — it does not mean the function is broken.

Verifying with password_verify()

To check a login attempt, you never hash the submitted password and compare it to the stored hash with ==. Instead, password_verify() takes the plain submitted password and the stored hash, and internally re-derives whether they match — including using the same salt that was embedded in the original hash.

Verifying a login attempt
<?php
$submittedPassword = 'MySecret123';
$storedHash = '$2y$10$uW9x1eIRlv1v8k4hT.d3Ee3vFZ5J2z8y1uQxWn0nD0p3G8kQxJm9K'; // fetched from the database

if (password_verify($submittedPassword, $storedHash)) {
    echo 'Password is correct.';
} else {
    echo 'Password is incorrect.';
}
Output
Password is correct.
Never Compare Hashes Manually

Writing your own comparison (like re-hashing the input and checking with ===) is unnecessary and error-prone. password_verify() exists specifically to do this correctly, including safe, timing-attack-resistant comparison internally.

Upgrading Hashes: password_needs_rehash()

Hashing algorithms and their recommended settings (like bcrypt's "cost" factor) improve over time. password_needs_rehash() lets an application check, at login time, whether a previously stored hash was created with older or weaker options than the ones currently configured — and if so, re-hash and update it.

Rehashing an outdated hash after successful login
<?php
if (password_verify($submittedPassword, $storedHash)) {
    if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
        $newHash = password_hash($submittedPassword, PASSWORD_DEFAULT);
        // Save $newHash to the database, replacing $storedHash
    }

    $_SESSION['user_id'] = $user['id'];
}

This check only makes sense right after a successful password_verify() call, because rehashing requires the plain-text password, which is only available at the moment the user typed it in.

Hashing vs Encryption

Hashing and encryption are often confused, but they solve different problems and are not interchangeable.

Hashing (One-Way)

  • Cannot be reversed back to the original value
  • Used for passwords, where you only ever need to verify, never retrieve
  • password_hash() and password_verify() are PHP's tools for this
  • The same input can produce different-looking output (thanks to salting)

Encryption (Two-Way)

  • Designed to be decrypted back to the original value with a key
  • Used for data you need to read again later, like a stored credit card token
  • Uses functions like openssl_encrypt() / openssl_decrypt()
  • Losing the key means losing access to the original data forever
Rule of Thumb

If you will ever need to see the original value again, it needs encryption. If you only ever need to check whether a newly submitted value matches, it needs hashing. Passwords always fall into the second category.

Putting It Together

Here is the registration-and-login pair from the previous lesson, now fully explained in terms of the three functions covered here.

Registration
<?php
$hashedPassword = password_hash($_POST['password'], PASSWORD_DEFAULT);
// Save $hashedPassword to the database — never the raw password
Login
<?php
if (password_verify($_POST['password'], $user['password'])) {
    if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
        $updatedHash = password_hash($_POST['password'], PASSWORD_DEFAULT);
        // Update the stored hash in the database
    }

    $_SESSION['user_id'] = $user['id'];
}

Common Mistakes

Avoid These Mistakes
  • Storing passwords with md5() or sha1() — these are fast general-purpose hashes, not designed for passwords, and are crackable at scale.
  • Comparing a freshly computed hash to a stored one with == or === instead of password_verify().
  • Hard-coding a specific algorithm constant like PASSWORD_BCRYPT instead of PASSWORD_DEFAULT, which prevents automatically benefiting from future improvements.
  • Assuming hashing and encryption are the same thing, or that either one can substitute for the other.
  • Trying to write a custom salting or hashing scheme instead of using the built-in functions.

Best Practices

  • Always use password_hash() with PASSWORD_DEFAULT to store new passwords.
  • Always use password_verify() to check submitted passwords — never compare hashes directly.
  • Call password_needs_rehash() right after a successful login to keep hashes up to date.
  • Never log, print, or store a plain-text password anywhere, even temporarily.
  • Reach for encryption functions only when you genuinely need the original value back later.

Frequently Asked Questions

Why does password_hash() produce a different result every time for the same input?

It generates a new random salt on every call and embeds it in the output. This protects against precomputed "rainbow table" attacks, since identical passwords never produce identical stored hashes.

Can I decrypt a password hash to see the original password?

No. Hashing is one-way by design. There is no function or key that turns a hash back into the original password — you can only check whether a given password produces a matching hash.

What does PASSWORD_DEFAULT actually use right now?

As of current PHP versions, PASSWORD_DEFAULT maps to bcrypt. PHP may change this default in a future release as stronger algorithms become standard, which is exactly why you should use the constant instead of hardcoding an algorithm.

Do I need to store the salt separately?

No. password_hash() embeds the salt directly inside the returned hash string, and password_verify() automatically extracts and uses it — you never manage salts yourself.

Key Takeaways

  • Passwords should always be stored as a hash, never as plain text.
  • password_hash($password, PASSWORD_DEFAULT) creates a secure, salted hash using bcrypt by default.
  • password_verify($password, $hash) is the correct way to check a login attempt.
  • password_needs_rehash() lets you upgrade stored hashes as PHP's default algorithm improves.
  • Hashing is one-way and used for passwords; encryption is two-way and used for data you need back later.

Summary

PHP's password_hash() and password_verify() functions give you a secure, battle-tested way to store and check passwords without ever designing your own cryptography. password_needs_rehash() keeps those hashes current as security recommendations evolve.

With authentication and password handling covered, the next lesson turns to a different everyday need: sending email from a PHP application.

Lesson 63 Completed
  • You understand why plain-text passwords are never acceptable.
  • You can hash a password with password_hash() and verify it with password_verify().
  • You know when and why to use password_needs_rehash().
  • You can explain the difference between hashing and encryption.
Next Lesson →

Email Sending with PHP