Password Security
By the end of this lesson you'll store passwords the way every secure app does: hashed with password_hash() , checked with password_verify() , upgraded over time, and shielded by constant-time comparison, rate limiting, and breach checks.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ Why Plain Text, MD5 & SHA1 Are a Disaster
Sooner or later, databases leak. The whole game of password security is making the stored value useless to whoever steals it. Storing the password in plain text obviously fails — the attacker reads it straight off. Encrypting it fails too, because encryption is reversible by design: whoever steals the database usually steals the key with it. And MD5/SHA1 — the classic "I hashed it!" mistake — fail because they are fast checksum functions, not password functions: a modern GPU tries billions of guesses per second, and because they're unsalted and deterministic, the same password always yields the same hash, so attackers reverse leaks instantly with precomputed rainbow tables .
A hash is a one-way function: easy to compute forwards, infeasible to reverse. The right hash for passwords is also deliberately slow and salted — the exact opposite of MD5. That's what PHP's password_* functions give you.
2️⃣ The Right Way: password_hash() & password_verify()
PHP gives you exactly two functions for the whole job. password_hash() turns a password into a slow, salted, self-describing hash you save in your database. password_verify() takes the password a user typed at login plus that stored hash and tells you true or false . You never write the comparison yourself, and — crucially — the salt is automatic : it's generated randomly and stored inside the hash, which is why the same password hashes to two different strings.
Read that "two hashes equal? false" line again — it's the heart of why this is secure. Each hash carries its own random salt, so identical passwords look completely different in storage, and password_verify() still matches because it reads the salt back out of the stored hash for you.
3️⃣ Algorithms, the Cost Factor & Rehashing
You also pick how hard the hash is to compute. With PASSWORD_BCRYPT the cost sets the number of rounds — each +1 doubles the work, and 10–12 is a sensible 2026 default. PASSWORD_ARGON2ID is the modern recommendation: it's memory-hard , so it defeats the cheap GPU and ASIC farms that bcrypt struggles against. Because hardware keeps getting faster, you raise these costs over time — and password_needs_rehash() lets you upgrade a user's stored hash silently, right after they log in , with no password reset.
4️⃣ Your Turn: Register & Log In
Time to wire it up yourself. The script below is almost complete — fill in each ___ using the 👉 hint, then run it and check it against the Output panel. This is the exact register-then-login flow you'll write in every real app.
One more. This time you'll upgrade a weak legacy hash on login — the everyday job of password_needs_rehash() .
5️⃣ Defences Around the Hash
Strong hashing protects a leaked database, but you also have to protect the live login form . Three more tools do that. hash_equals() compares secret strings (reset tokens, API keys) in constant time , so an attacker can't time your == to guess a token byte by byte — a timing attack . Rate limiting caps how many guesses each account gets, killing online brute force. And checking new passwords against the Have I Been Pwned breach database (using k-anonymity, so the password never leaves your server) stops users picking a password attackers already have.
📋 Quick Reference — Password Security
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is exactly the hash-then-verify loop every real authentication system runs.
Practice quiz
Why are md5() and sha1() unsafe for storing passwords?
- They are too slow to compute
- They cannot hash strings longer than 8 characters
- They are fast, unsalted, and deterministic, so attackers reverse leaks with rainbow tables
- They require an internet connection
Answer: They are fast, unsalted, and deterministic, so attackers reverse leaks with rainbow tables. md5/sha1 are fast checksum functions: a GPU tries billions of guesses per second, and because they're unsalted the same password always hashes the same.
Which two functions are the correct way to store and check a password in PHP?
- password_hash() and password_verify()
- md5() and strcmp()
- encrypt() and decrypt()
- hash() and hash_equals()
Answer: password_hash() and password_verify(). password_hash() creates a slow, salted hash to store; password_verify() checks a typed password against that stored hash.
Do you need to generate and store a salt yourself when using password_hash()?
- Yes, you must pass a salt as the third argument
- Yes, store the salt in a separate column
- Only when using Argon2id
- No — it generates a random salt and stores it inside the hash automatically
Answer: No — it generates a random salt and stores it inside the hash automatically. password_hash() generates a secure random salt and bakes it into the hash, which is why the same password hashes to two different strings.
Why does hashing the same password twice with password_hash() give two different strings?
- The function is broken
- Each call uses a different random salt
- It alternates between bcrypt and Argon2
- It includes the current timestamp as the hash
Answer: Each call uses a different random salt. A unique random salt per hash makes identical passwords look completely different in storage, defeating rainbow tables.
For PASSWORD_BCRYPT, what does increasing the cost by 1 do?
- Doubles the work (time) required to compute the hash
- Halves the hashing time
- Adds one more salt
- Has no measurable effect
Answer: Doubles the work (time) required to compute the hash. Each +1 to the bcrypt cost doubles the work; 10–12 is a sensible default — slow for attackers, fast enough for login.
What makes PASSWORD_ARGON2ID stronger than bcrypt against modern cracking?
- It is faster
- It never needs a salt
- It is memory-hard, defeating cheap GPU/ASIC farms
- It produces a shorter hash
Answer: It is memory-hard, defeating cheap GPU/ASIC farms. Argon2id is memory-hard, so it resists the cheap GPU and ASIC farms that bcrypt is more vulnerable to.
When should you call password_needs_rehash() and rehash a password?
- On every failed login
- After a successful verify, to silently upgrade a hash below today's standard
- Before checking the password
- Only once a year on a schedule
Answer: After a successful verify, to silently upgrade a hash below today's standard. Only rehash after a successful password_verify(); needs_rehash() flags a below-standard hash so you upgrade it without a password reset.
Why use hash_equals() instead of == when comparing a reset token you check yourself?
- It is faster than ==
- It hashes the token first
- It is required for password_verify()
- It compares in constant time, preventing timing attacks
Answer: It compares in constant time, preventing timing attacks. A normal == returns as soon as two bytes differ, leaking timing info; hash_equals() always takes the same time, blocking timing attacks.
Do you need hash_equals() to compare a password against its stored hash?
- Yes, always wrap password_verify() in hash_equals()
- No — password_verify() is already constant-time
- Only for Argon2id hashes
- Only when the password is over 72 bytes
Answer: No — password_verify() is already constant-time. password_verify() is already constant-time, so you don't need hash_equals() for passwords — only for tokens you compare by hand.
How does the Have I Been Pwned range API let you check a password without sending it?
- It encrypts the password before sending it
- It sends the password over HTTPS so it is safe
- You send only the first 5 chars of the SHA-1 (k-anonymity) and match the rest locally
- It only works with bcrypt hashes
Answer: You send only the first 5 chars of the SHA-1 (k-anonymity) and match the rest locally. Using k-anonymity you send just the first 5 hex chars of the SHA-1; the API returns matching suffixes and you check locally, so the password never leaves your server.