Need the #1 custom application developer in Brisbane?Click here →

Security

Password Security

10 min readLast reviewed: March 2026

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.

Never Store Plaintext Passwords
This should be obvious, but plaintext passwords in your database are an immediate, catastrophic vulnerability. If your database is breached, attackers have all passwords in cleartext and can try them against other services where users reuse passwords. Always hash.

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.

Comparison of password hashing algorithms. For new applications, use Argon2. For existing bcrypt deployments, stay the course—bcrypt is plenty secure.
AlgorithmSpeedResistance to GPU AttackAdoptionRecommended
MD5Very fastNone (it's broken)Legacy onlyNo
SHA-1/SHA-256FastLowCommon but inadequateNo
bcryptSlowModerateVery widespreadYes
scryptVery slowHighGrowingYes
Argon2Very slowHighestGrowingBest 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.

Unique Salt Per User
Every password must have a unique, randomly generated salt. Never reuse a salt across users, and never use a static application-wide salt. This is handled automatically by bcrypt and Argon2, so don't try to manage salt yourself.

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.

Implement HIBP Checks
Check new passwords against Have I Been Pwned during registration and password change. A prompt saying "Your password was found in a data breach—choose a different one" protects the user from their own poor choices without being annoying.

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.

Watch Out for Enumeration Attacks
Be careful when implementing rate limiting. If you show different error messages for "user not found" vs. "wrong password," attackers can enumerate valid usernames. Always show a generic message like "Invalid email or password" whether the user exists or not.

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.