Authentication
By the end of this lesson you'll be able to register and log in users the secure way — hashing passwords with password_hash , checking them with password_verify , storing users with PDO prepared statements, and keeping people signed in with sessions.
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️⃣ Hashing a Password with password_hash
The golden rule of authentication: never store a password you can read back . Instead you hash it — run it through a one-way function that turns "hunter2" into a long scrambled string you can't reverse. PHP gives you exactly one function to do this well: password_hash() . It automatically adds a random salt (extra random data) so the same password produces a different hash every time, which kills entire classes of attack. You store that hash in your database; you never store the password itself.
Notice you never "decrypt" anything. To check a login you call password_verify($typed, $hash) : it re-hashes what the user typed using the salt baked into the stored hash and compares the two safely. Match means the password was right.
2️⃣ Choosing the Algorithm: BCRYPT & Argon2
PASSWORD_DEFAULT currently means bcrypt , a deliberately slow hash — slowness is a feature, because it makes brute-forcing expensive. You can raise the "cost" to make each guess cost more, or choose Argon2id ( PASSWORD_ARGON2ID ), the modern "memory-hard" winner of the Password Hashing Competition. Both are excellent; bcrypt is the safe default everywhere, Argon2id is preferred if your PHP build supports it.
Use PASSWORD_DEFAULT unless you have a reason not to — it lets PHP upgrade the algorithm for you in future versions, and (with the rehash check in section 6) your stored hashes upgrade along with it.
3️⃣ Registration: Storing Users with PDO
Registration is two steps: hash the password, then save the user. To save safely you use a prepared statement — you write the SQL with ? placeholders and pass the real values separately, so a malicious email like '; DROP TABLE users; -- is treated as plain data, never as SQL. This is your defence against SQL injection . Gluing user input straight into a query string is the classic beginner mistake; prepared statements make it impossible.
The stored row contains a bcrypt hash (it starts with $2y$ ), not the password. Even if your whole database leaked tomorrow, attackers still wouldn't have your users' passwords.
4️⃣ Login: Verify, Then Start a Session
Logging in means: look up the user by email, password_verify() their password against the stored hash, and if it matches, record that they're logged in. PHP tracks logged-in state with a session — a small server-side store keyed to a cookie. After session_start() you read and write $_SESSION like an array; here you save the user's id. Two security musts: return one generic failure for both "no such user" and "wrong password" (so you don't reveal which emails exist), and call session_regenerate_id(true) on success to stop session fixation (section covered in Common Errors).
5️⃣ Protecting Pages & Logging Out
Once login state lives in $_SESSION , protecting a page is easy: at the top, check for $_SESSION['user_id'] and, if it's missing, redirect to the login page and exit immediately — forgetting exit is a real bug, because the protected code below would still run. Logging out is the reverse: empty the session array, expire the session cookie, and destroy the session.
Now you try. The script below registers and logs in a user, but two function calls are missing. Fill in each ___ using the 👉 hint, then run it and check it against the Output panel.
One more. This time finish the prepared statement so the email is bound safely instead of pasted into the SQL.
6️⃣ "Remember Me" & Rehashing on Login
A normal session ends when the browser closes. A "remember me" feature keeps users signed in for weeks using a long-lived cookie — but never put the password or a plain user id in it. Instead generate a random token, store a hash of it in the database (treat it like a password), and send the token in an httponly , secure , samesite cookie. Separately, every successful login is a good moment to rehash : password_needs_rehash() tells you if your cost or algorithm has since been strengthened, so you can transparently upgrade the stored hash while you still have the plain password in hand.
Common Errors (and the fix)
- Storing passwords as plain text (or md5/sha1) — the worst and most common mistake. If your database leaks, every account is instantly compromised. md5/sha1 are fast and unsalted, so they're cracked in seconds. Fix: always password_hash() on the way in and password_verify() on the way out — nothing else.
- "Login failed" for a correct password because the column is too short — bcrypt hashes are 60 characters and may grow; an Argon2 hash is longer still. If your password_hash column is VARCHAR(20) the hash is silently truncated and verify never matches. Fix: use VARCHAR(255) (or TEXT).
- Comparing secrets with == (timing attack) — if ($a == $hash) leaks information through how long the comparison takes and can be fooled by PHP's loose typing. Fix: never hand-compare hashes — use password_verify() for passwords and hash_equals() for other tokens; both run in constant time.
- No rehash check after upgrading cost/algorithm — you raise the bcrypt cost or move to Argon2, but old users keep their weak hashes forever. Fix: on each successful login, run password_needs_rehash() and re-store with password_hash() if it returns true.
- Session fixation — not regenerating the id on login — if you keep the same session id from before login, an attacker who planted that id rides into the account. Fix: call session_regenerate_id(true) immediately after a successful login (and on logout).
- "headers already sent" when starting a session or setting a cookie — session_start() , setcookie() , and header() must run before any output. A stray space or blank line before < ?php counts as output. Fix: call them at the very top, before echoing anything.
Pro Tips
- 💡 Let password_hash manage the salt. Don't generate or store salts yourself — they're already inside the returned hash, which is why password_verify needs nothing extra.
- 💡 One generic error message. Reply "invalid email or password" for every failure so attackers can't probe which emails are registered.
- 💡 Add rate limiting. Hashing is slow on purpose; still, lock or slow down repeated failed logins to blunt brute-force attempts.
- 💡 Always set login cookies httponly + secure + samesite . That blocks JavaScript theft (XSS) and limits cross-site sending (CSRF).
📋 Quick Reference — PHP Authentication
Function
Example
What It Does
password_hash
password_hash($pw, PASSWORD_DEFAULT)
Hash a password (salted, one-way)
password_verify
password_verify($pw, $hash)
Check a password against a hash
password_needs_rehash
password_needs_rehash($hash, PASSWORD_DEFAULT)
Should the stored hash be upgraded?
$pdo->prepare
$pdo->prepare("... WHERE email = ?")
Build a safe parameterised query
$stmt->execute
$stmt->execute([$email])
Bind values & run (no injection)
session_start
session_start();
Begin / resume the session
session_regenerate_id
session_regenerate_id(true)
New id on login (anti-fixation)
$_SESSION
$_SESSION['user_id'] = $id;
Store who is logged in
session_destroy
session_destroy();
Log the user out
Frequently Asked Questions
Mini-Challenge: A Tiny Auth Flow
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 the exact register-then-verify loop behind every real login screen.
🎉 Lesson Complete!
- ✅ Passwords are hashed with password_hash() (bcrypt/Argon2) and never stored as plain text, md5, or sha1
- ✅ Logins are checked with password_verify() — you compare hashes, you never decrypt
- ✅ Users are saved and looked up with PDO prepared statements , which stop SQL injection
- ✅ Login state lives in $_SESSION ; you session_regenerate_id() on login and session_destroy() to log out
- ✅ "Remember me" stores a hashed random token in a hardened cookie, and password_needs_rehash() keeps hashes current
- ✅ Protect pages by checking the session at the top and exit -ing guests out
- ✅ Next lesson: Advanced Security — CSRF tokens, XSS escaping, and hardening your app end to end
Practice quiz
Which function should you use to hash a password for storage?
- md5()
- sha1()
- password_hash()
- base64_encode()
Answer: password_hash(). password_hash() uses a slow, salted, password-specific algorithm (bcrypt or Argon2). md5/sha1 are fast, unsalted, and unsafe for passwords.
How do you check a typed password against a stored hash?
- password_verify($typed, $hash)
- Decrypt the hash and compare
- $typed === $hash
- md5($typed) == $hash
Answer: password_verify($typed, $hash). Hashing is one-way; you never decrypt. password_verify() re-hashes the typed password with the stored salt and compares in constant time.
Why does hashing the same password twice give two different hashes?
- The function is broken
- PHP caches the first result
- The cost factor changes randomly
- password_hash() adds a random salt each time
Answer: password_hash() adds a random salt each time. Each call adds a random salt, so identical passwords produce different hashes — defeating rainbow tables. The salt is stored inside the hash.
What does PASSWORD_DEFAULT currently use as its algorithm?
- md5
- bcrypt
- AES
- ROT13
Answer: bcrypt. PASSWORD_DEFAULT currently means bcrypt (hashes start with $2y$). It can upgrade automatically in future PHP versions.
Why use PDO prepared statements when saving a user?
- They keep user input out of the SQL, preventing SQL injection
- They run faster than plain queries always
- They hash the password automatically
- They compress the database
Answer: They keep user input out of the SQL, preventing SQL injection. Prepared statements use ? or :name placeholders so values are bound as data, never as SQL — defeating injection like '; DROP TABLE users; --.
Where does PHP track that a user is logged in?
- In a global $loggedIn variable
- In the URL query string
- In $_SESSION after session_start()
- In a cookie holding the plaintext password
Answer: In $_SESSION after session_start(). After session_start() you read/write $_SESSION; here the user's id is stored to remember who is logged in.
Why call session_regenerate_id(true) right after a successful login?
- To log the user out
- To prevent session fixation by issuing a brand-new session id
- To clear the database
- To speed up the session
Answer: To prevent session fixation by issuing a brand-new session id. It issues a new session id and deletes the old one, so any id an attacker planted (session fixation) becomes useless.
What is a timing attack, and what protects you?
- A DDoS on the login page — a firewall stops it
- A slow database query — an index fixes it
- An expired session — a longer TTL fixes it
- Measuring comparison time to leak matches — password_verify/hash_equals compare in constant time
Answer: Measuring comparison time to leak matches — password_verify/hash_equals compare in constant time. Constant-time comparisons (password_verify, hash_equals) don't leak how many characters matched; never compare secrets with ==.
For a 'remember me' cookie, what should you store in the database?
- The user's plaintext password
- A hash of the random token (treated like a password)
- The plain token exactly as sent to the browser
- The user's email in plain text only
Answer: A hash of the random token (treated like a password). Store a hash of the random token (never the password, never a plain id), and set the cookie httponly, secure, and samesite.
What does password_needs_rehash() tell you?
- Whether the password is correct
- Whether the user exists
- Whether the stored hash should be upgraded to a stronger cost/algorithm
- Whether the session expired
Answer: Whether the stored hash should be upgraded to a stronger cost/algorithm. On a successful login it reports if your cost/algorithm was strengthened, so you can transparently re-store a stronger hash while you have the plain password.
Continue this course
- Previous: Query Builders & ORM
- Next: Advanced Security