Authentication And Identity Systems
Password Login Systems
Storing Passwords
Never store a password, and never encrypt one — encryption is reversible, which is exactly what you do not want. Store a one-way hash produced by a slow, salted, purpose-built algorithm. PHP's password_hash() does this correctly with a single call, defaulting to bcrypt and supporting Argon2id:
<?php
declare(strict_types=1);
$hash = password_hash('correct horse battery staple', PASSWORD_DEFAULT);
var_dump(password_verify('correct horse battery staple', $hash)); // true
var_dump(password_verify('wrong guess', $hash)); // false
// The stored hash includes the algorithm, cost, and a random salt,
// so no separate salt column is needed and verification is self-describing.
Three properties matter. The hash is slow by design (a tunable cost factor), so an attacker who steals the database can try only thousands of guesses per second instead of billions. It is salted automatically and uniquely per password, so identical passwords produce different hashes and precomputed rainbow tables are useless. And it is self-describing: password_needs_rehash() lets you upgrade the cost or algorithm transparently as hardware improves, on the user's next successful login. Use password_verify() for the comparison — it is constant-time, so it does not leak information through how long a mismatch takes.
Password Policy That Helps
Modern guidance (NIST) inverted the old advice. Length is what matters — enforce a generous minimum (8 absolute, 12+ encouraged) and a high maximum (so passphrases and password managers work), and stop there. Do not impose composition rules ("one uppercase, one symbol"), do not force periodic rotation, and do not offer password hints or knowledge-based security questions — every one of those makes passwords weaker or more predictable in practice. The one screen worth adding is a check against known-breached passwords (via a k-anonymity API like Have I Been Pwned), because a password's presence in a breach corpus is the single best predictor that it will fall to credential stuffing.
Defending The Login Endpoint
A correct hash protects stolen data; it does nothing against someone guessing at the live login form. That defense is rate limiting and it is covered in depth by the rate limiting lesson, with one authentication-specific warning repeated here: prefer throttling and escalating friction over hard account lockouts, because an attacker who can lock any account by failing logins has turned your control into a denial-of-service weapon. Count failures per account and watch for the credential-stuffing signature of many accounts failing from a spread of addresses.
Two more front-door essentials: the login form is a state-changing POST and needs CSRF protection, and the whole exchange must be over HTTPS so the password is never on the wire in clear. On success, regenerate the session ID (covered in the token implementation lesson) to prevent session fixation.
The Reset Path Is Part Of The System
A password login system is not complete without its recovery flow, and that flow is a full authentication path — see the magic links lesson, whose single-use, expiring, unguessable-token mechanics are exactly what a password reset requires. Securing the login to bank-grade standards and the reset to "email a six-digit code that never expires" is the most common way password systems are actually broken.
What To Check
Before moving on, make sure you can:
- explain why passwords are hashed with a slow salted algorithm, never encrypted
- use
password_hash,password_verify, andpassword_needs_rehashcorrectly - state modern password policy: length over composition, no forced rotation, breach-check
- defend the login endpoint with throttling that cannot be weaponized
- name the login-form essentials: HTTPS, CSRF, session regeneration
- explain why the reset flow must be as strong as the login
What You Should Be Able To Do
After this lesson, you should be able to build a password login that stores credentials safely, resists both offline cracking and online guessing, avoids counterproductive policy, and secures its recovery path to the same standard as its front door.
Practice
Practice: Design A Password Login
Design the storage, verification, policy, and endpoint defenses for a PHP application's email/password login. Specify the hashing approach, the password rules you will and will not enforce, and every defense on the login endpoint itself.
Show solution
Verification: password_verify() only — constant-time, no hand-rolled comparison. When the account does not exist, verify against a fixed dummy hash anyway so failure timing does not reveal account existence.
Policy enforced: minimum length 12 (8 hard floor), maximum length high (say 128) so managers and passphrases work, and a breach-corpus check that warns or blocks on known-compromised passwords. Policy deliberately not enforced: no composition requirements, no forced rotation, no hints, no security questions.
Endpoint defenses: HTTPS only; CSRF token on the form; per-account failure throttling with escalating delays and CAPTCHA rather than hard lockout; a global alarm for the distributed-stuffing signature; identical response and timing for wrong-password and no-such-account; session ID regeneration on success.
Completeness: the reset flow is specified alongside, using single-use expiring tokens to the same standard — the design is not done until recovery is as strong as login.
Practice: Diagnose A Breach Blast Radius
A company is breached and its user table leaks. Passwords were stored as unsalted SHA-256. Within a day, attackers are logging into user accounts, and the same users report break-ins on other sites.
Task
Explain why unsalted SHA-256 failed so badly, why the damage spread to other sites, and what storage would have contained it.
Show solution
Two independent failures compounded.
SHA-256 is a fast hash — built for speed, the opposite of what password storage needs. Commodity hardware tries billions of SHA-256 guesses per second, so most passwords fall in minutes. Being unsalted makes it worse: identical passwords share a hash, so precomputed rainbow tables reverse common passwords instantly and cracking one "password123" cracks every account that used it. The combination means the leaked table was effectively plaintext within hours.
The spread to other sites is password reuse plus the enumerated real emails from the breach: attackers took the recovered email:password pairs and replayed them everywhere (credential stuffing). The breach's blast radius extended to every site where those users reused the password — which the original site cannot fix after the fact.
What would have contained it: password_hash() with bcrypt or Argon2id. Slow-by-design hashing drops the attacker from billions to thousands of guesses per second, turning a one-day mass compromise into a marginal, per-account effort that mostly does not pay off. Unique automatic salting kills the rainbow-table and shared-hash shortcuts. The reuse damage still exists in principle, but the breach corpus is no longer cheaply crackable, so most accounts — and their reused passwords elsewhere — stay protected.
The response also includes the compromise playbook's step: force a reset and treat every hash as exposed.
Practice: Define Password System Checks
Show solution
- A test asserts stored hashes use the current
PASSWORD_DEFAULTalgorithm and thatpassword_needs_rehashupgrade runs on login — catching a frozen cost factor as hardware moves. - A timing test asserts that wrong-password and no-such-account responses fall within the same latency band (dummy-hash verification present).
- A test confirms the login form rejects requests without a valid CSRF token and that session ID changes across a successful login.
- The rate-limit probe from the security track exercises the login past its threshold and confirms throttling (not lockout) and 429-style backoff.
- Policy tests: a 128-character passphrase is accepted, no composition rule is enforced, and a known-breached password is rejected or warned.
Human, periodically:
- Confirm the reset flow's controls still match the login's — the two drift apart as features are added, and the review is what catches a reset path that quietly became the weak link.
- Review authentication logs for the distributed-stuffing signature and confirm the global alarm still fires (planted-signal drill).