Security
Password Security
Passwords are the most common attack vector. A single security mistake—storing passwords in plaintext, using weak hashing, or failing to rate-limit login attempts—can expose all your users' credentials. This guide covers what you need to know to handle passwords securely.
Hashing vs Encryption
The fundamental mistake is confusion between hashing and encryption. They are different operations with different purposes.
Encryption is reversible. If you encrypt a password, anyone with the decryption key can retrieve the original password. Storing encrypted passwords is worse than useless—it gives an attacker the illusion of security while actually storing the plaintext equivalent.
Hashing is one-way. You compute a hash of the password and store the hash. When the user logs in, you hash their input and compare it to the stored hash. If they match, the password is correct. If an attacker steals the hash database, they get hashes, not passwords—useless without reversing the hash, which should be computationally infeasible.
Hashing Algorithms: bcrypt, Argon2, scrypt
Not all hash functions are created equal. Some—like MD5 and SHA-1—are fast, which is exactly the problem. Fast hashing makes brute force attacks easy. Password hashing algorithms are intentionally slow to make brute force prohibitively expensive.
bcrypt
bcrypt is a battle-tested hashing algorithm designed for passwords. It automatically generates a salt, incorporates it into the hash, and includes the salt in the output. The algorithm has a "cost" parameter that determines how many iterations are performed. Higher cost = slower hashing = more expensive to brute force. bcrypt is slow by design—a single bcrypt operation takes around 100 milliseconds on modern hardware, making a billion-attempt brute force take months.
Argon2
Argon2 is the winner of the Password Hashing Competition (2015) and is considered the gold standard today. It's slower than bcrypt and more resistant to GPU and ASIC attacks because it uses significant memory during hashing. If your language/framework supports Argon2, prefer it over bcrypt.
scrypt
scrypt is another modern option that uses configurable memory and CPU cost parameters. It's in the middle ground between bcrypt and Argon2 in terms of adoption and performance.
| Algorithm | Speed | Resistance to GPU Attack | Adoption | Recommended |
|---|---|---|---|---|
| MD5 | Very fast | None (it's broken) | Legacy only | No |
| SHA-1/SHA-256 | Fast | Low | Common but inadequate | No |
| bcrypt | Slow | Moderate | Very widespread | Yes |
| scrypt | Very slow | High | Growing | Yes |
| Argon2 | Very slow | Highest | Growing | Best choice |
Salt and Rainbow Tables
A salt is random data mixed into the password before hashing. The same password with different salts produces different hashes. This prevents rainbow table attacks—a precomputed table mapping common passwords to their hashes.
Without a salt, an attacker can compute the hash of the top 1 million passwords once, then quickly look up whether users have any of those passwords. With a unique salt per user, the attacker must compute 1 million hashes per user, making large-scale precomputation useless. Modern password hashing algorithms (bcrypt, Argon2, scrypt) handle salt generation and inclusion automatically.
Password Strength and Usability
Requiring long, complex passwords with special characters seems secure, but it often backfires. Users can't remember them, so they write them down or reuse them. The modern consensus is simpler: require a minimum length (12-16 characters) and allow any characters. A 16-character passphrase is more likely to be unique and secure than a 10-character complex password.
The NIST guidelines recommend against password complexity rules and instead focus on preventing obviously weak passwords—those found in breach databases or that match common patterns.
Have I Been Pwned (HIBP) Integration
Troy Hunt's Have I Been Pwned service maintains a database of hundreds of millions of passwords from known breaches. You can integrate this into your registration and password change flows to prevent users from choosing passwords that are already compromised.
The HIBP API is clever: you hash the first 5 characters of the password with SHA-1 and query the API. The server returns all known password hashes starting with those 5 characters (thousands at a time), and you check locally whether your full hash is in the list. This reveals the result without sending the full password hash to a remote server.
Password Reset Flows
Password reset is a critical flow and a common attack vector. The user forgot their password, so you can't authenticate them with it. Instead, you must verify their identity another way.
Email-Based Password Reset
Send the user a time-limited token via email. Clicking the link in the email proves they control that email address, and they can set a new password. Tokens must be random and long (minimum 32 characters), time-limited (30 minutes is typical), and single-use. After the user resets their password, any old tokens must be invalidated.
Security Questions
Security questions are weak verification. Answers are often public (mother's maiden name is searchable), poorly chosen (answers people guess easily), or inconsistent (users forget their own answers). Avoid them if possible. If you must use them, combine with another factor like email verification.
Brute Force Protection
Even with proper hashing, an attacker with the password database can attempt to brute force it offline—trying billions of common passwords or combinations. The hash algorithm's slowness limits the attack speed, but it doesn't eliminate it.
For online attacks (where the attacker is sending login requests), you control the server and can defend by rate limiting. Track login attempts per username or IP address. After a threshold (e.g., 5 failed attempts), lock the account temporarily or require a CAPTCHA. After many failed attempts (e.g., 20), require email verification to unlock.
The key is making the attack costly. A locked account for 15 minutes limits an attacker to 4 guesses per hour per account—trying 1 million passwords would take years. Add a CAPTCHA after the lockout expires, and the attack becomes economically infeasible.
Beyond Passwords: Passwordless Alternatives
Passwords are inherently problematic. Users choose weak ones, reuse them across sites, and fall for phishing attacks. The industry is moving toward passwordless authentication:
Magic links: Send a time-limited link via email. Clicking it logs the user in. Works well for infrequent users but feels slow for daily-use apps.
Passkeys: Cryptographic credentials stored on the device. Phishing-resistant because the device never sends a secret—it only signs a challenge. Gradually replacing passwords as browsers and operating systems add support.
OAuth/OpenID Connect: Delegate authentication to Google, GitHub, or another provider. Users never share a password with your app; the provider verifies their identity.
Social Engineering and the Human Factor
The strongest password is useless if a user gives it to an attacker in response to a phishing email. Security training is important, but passwords are fundamentally vulnerable to social engineering because they're a secret—once shared, you can't tell if it was leaked by the user or stolen.
Passkeys and OAuth fix this: the credential is tied to the device, and the user doesn't share a secret. If an attacker convinces a user to log in to a fake website, the device simply refuses because the origin doesn't match the registered site. This is why the industry is moving away from passwords.
Password security is necessary for legacy reasons, but it's a losing battle. Implement it correctly (hashing, salt, rate limiting, HIBP checks) while planning a transition to passwordless auth. Users and your security team will thank you.