In PHP, what do password_hash() and password_verify() do, and what exactly should you store in the database?
answer
- one-way hashing with a per-hash salt
- store the whole returned string, not the salt
- password_verify(plain, hash) returns bool
- PASSWORD_DEFAULT is currently bcrypt
- never compare hashes with ==
basics
~20 spassword_hash($plain, PASSWORD_DEFAULT) returns a self-describing string that embeds the algorithm, cost and a random salt; store that one string. password_verify($plain, $hash) rehashes the input the same way and returns a bool. Never generate the salt yourself or compare hashes manually.
solid answer
~50 s`password_hash($password, PASSWORD_DEFAULT)` runs a **slow, salted, one-way** hash and returns a single string like `$2y$12$....` that **embeds the algorithm identifier, the cost, and a randomly generated salt**. You store exactly that string — there is no separate salt column, and you must not supply your own salt (the old `salt` option was removed in PHP 8.0). To check a login, call `password_verify($submitted, $storedHash)`: it reads the parameters out of the stored string, hashes the submitted password the same way, and compares in constant time, returning a `bool`. `PASSWORD_DEFAULT` is **currently bcrypt** but may change to a stronger algorithm in a future PHP, so store the hash in a column wide enough for growth (`VARCHAR(255)`). Since PHP 8.0 `password_hash` throws a `ValueError` on an invalid algorithm rather than returning `false`. The whole point of the API is that you never handle the salt, the comparison, or the format yourself.
go deeper
Use password_hash to store and password_verify to check; store the whole returned string and never a separate salt.
Explain that the hash embeds algorithm, cost and salt, why each call differs, and that PASSWORD_DEFAULT is currently bcrypt but may change.
Reason about column width for algorithm agility, constant-time verification, and coexisting old and new hashes during upgrades.
Set the org standard for algorithm and parameters and weigh Argon2id vs bcrypt against your login-traffic budget.
## What the two functions do Passwords must be stored so that a database leak does not reveal them, which means a **slow, salted, one-way** transformation — not a fast digest and not encryption. PHP's password API does this for you: - **`password_hash(string $password, string|int|null $algo, array $options = [])`** returns a hash string. With `PASSWORD_DEFAULT` (currently **bcrypt**), the result looks like: `$2y$12$Rnd22CharSaltHere22CharsThenThe31CharDigestValue...` The `$`-delimited fields are the **algorithm** (`2y` = bcrypt), the **cost** (`12`), and then a Base64 blob holding the **salt** and the **digest**. A fresh random salt is generated for every call, so hashing the same password twice gives two different strings — and that is correct. - **`password_verify(string $password, string $hash)`** returns a `bool`. It parses the algorithm, cost and salt out of `$hash`, recomputes the digest from `$password`, and compares them. You pass the *plaintext* the user just typed and the *stored hash*; you never split them apart. ## What you store — and what you must not do | Do | Don't | |---|---| | Store the **entire** returned string in one column | Store the salt in a separate column | | Use `VARCHAR(255)` so a future, longer algorithm still fits | Size the column to exactly 60 chars | | Let PHP generate the salt | Pass your own salt (the option was removed in 8.0) | | Compare with `password_verify()` | Compare with `==`, `===`, `strcmp`, or `md5($x) == $stored` | The salt is embedded precisely so the database needs only one column and `password_verify` can find it. Because the salt is per-hash, two users with the same password get different stored strings, which defeats precomputed-table attacks. ## Why `password_verify`, not a manual comparison Three reasons interviewers probe: 1. **The salt lives in the hash.** You cannot recompute the digest yourself without first parsing the algorithm and salt out of the stored string — which is exactly what `password_verify` does. 2. **Constant time.** `password_verify` compares in constant time, so it does not leak information through timing. A hand-rolled `==` on hashes is both wrong (see the loose-comparison bypass) and timing-unsafe. 3. **Algorithm agility.** Because the stored string is self-describing, the same `password_verify` call keeps working after you upgrade the algorithm or cost for new hashes; old and new hashes coexist. ## Choosing the algorithm `PASSWORD_DEFAULT` follows PHP's recommendation and today resolves to `PASSWORD_BCRYPT`. You can pin `PASSWORD_BCRYPT` explicitly, or choose `PASSWORD_ARGON2ID` (memory-hard, no 72-byte input limit) if your build has it. The *theory* of which algorithm and parameters to pick, and why a fast hash like SHA-256 is unfit for passwords, belongs to the security concept homes; the PHP-specific facts are the three functions, the self-describing string, and the "store the whole thing, never the salt" rule. Note the failure behaviour changed in PHP 8.0: `password_hash` now **throws a `ValueError`** for an unknown algorithm instead of returning `false`, so old `if ($h === false)` checks are dead code on modern PHP.
- Why does hashing the same password twice give two different strings, and is that a bug?It is correct. `password_hash` generates a fresh random salt each call, and the salt is embedded in the output, so the two strings differ. `password_verify` still matches either against the original password because it reads each string's own salt. A per-hash salt is what stops one leaked rainbow table cracking every identical password at once.
- Do you need a separate salt column in the users table?No. The salt is part of the string `password_hash` returns, so you store just that one column (typically `VARCHAR(255)`). Adding your own salt column, or passing a salt to `password_hash`, is a mistake — the salt option was deprecated in 7.0 and removed in 8.0.
- Is `PASSWORD_ARGON2ID` better than the default?Argon2id is memory-hard and has no 72-byte input limit, which are real advantages, but 'better' depends on your build and CPU/memory budget — the algorithm comparison is a security-design question. In PHP terms, either is a valid `$algo`; use `PASSWORD_DEFAULT` unless you have a specific reason and the argon2 build support.
The stored string is like a sealed envelope that also prints its own combination on the outside: password_verify reads the combination (algorithm, cost, salt) off the envelope to re-check the contents, so you never keep the combination in a separate drawer.
saying these in an interview costs you the question
- you must generate and store your own salt
- compare stored hashes with == or ===
- the same password always hashes to the same string
- PASSWORD_DEFAULT is guaranteed to always be bcrypt
- password_hash returns false on failure so check === false
- store only the digest and keep the salt in another column