How to Store Passwords Securely in Web Apps Using Bcrypt

How to Store Passwords Securely in Web Apps Using Bcrypt

Data breaches make headlines almost every week. The real damage is not the breach itself. It is what happens after, when attackers sit down with a stolen database and start cracking passwords. If those passwords were stored as plain text or hashed with MD5, millions of accounts can be compromised in hours. Bcrypt changes that math entirely, and understanding how it works is one of the most valuable things you can do for the people trusting your application.

Security Checklist

  1. Never store passwords as plain text or using MD5; use a purpose-built algorithm like bcrypt.
  2. Set a cost factor of 10 or higher to make brute-force attacks computationally expensive.
  3. Let bcrypt handle salting automatically, and store the full hash string in your database column.

Why Plain-Text and MD5 Storage Are Still Getting People Hacked

Plain-text storage is the worst possible option. Most developers know this. Yet legacy codebases still carry it. When a database leaks, every account is instantly compromised. There is no recovery path. Users whose passwords match across multiple sites lose access to everything at once.

MD5 seems like a step up, but it is not. MD5 was designed for speed. That speed is the problem. A modern GPU can compute billions of MD5 hashes per second. Attackers do not need to guess passwords directly. They precompute massive lists of hash outputs called rainbow tables, then look up your hash in seconds. MD5 was never designed with password storage in mind, and treating it as a security tool is a mistake that still costs real users real harm.

SHA-1 and SHA-256 are better for data integrity tasks, but they carry the same core flaw for password storage. They are fast. An attacker with decent hardware can work through hundreds of millions of SHA-256 guesses per second. Purpose-built password hashing algorithms flip this dynamic on purpose, and that is exactly the gap bcrypt was created to fill.

How Bcrypt Protects Passwords at a Fundamental Level

Bcrypt was published in 1999 by Niels Provos and David Mazieres. Its design goal was straightforward. Make password hashing slow enough that brute-force attacks become impractical, while keeping it fast enough that legitimate logins stay snappy. It achieves this through a tunable cost factor, and that tunability is what keeps bcrypt relevant even as hardware keeps getting faster.

Cost Factors and Why Slow Hashing Is a Feature

The cost factor is a number, typically between 10 and 14, that controls how many rounds of processing bcrypt runs. Each increment doubles the computational work. A cost factor of 10 means 2^10 rounds. A cost factor of 12 means 2^12 rounds. On modern hardware, a cost factor of 12 takes roughly 250 to 400 milliseconds per hash. That feels imperceptible to a user logging in. To an attacker trying millions of guesses, it is brutal.

The NIST digital identity guidelines specifically recommend using iterative, computation-heavy hashing functions when storing authentication credentials, because the goal is to increase the cost of offline dictionary attacks. Bcrypt was built for exactly this. A cost factor of 10 is a reasonable baseline today. If your server can handle it, 12 is better. The right number depends on your hardware and expected login volume. You want each hash to take at least 100 milliseconds on your production infrastructure.

Automatic Salting and What It Means for Your Database

One of bcrypt’s most practical features is built-in salting. A salt is a random value generated fresh for every password hash. It gets combined with the raw password before hashing. Even if two users choose the same password, their stored hashes look completely different. This kills precomputed rainbow table attacks outright.

You do not manage the salt yourself. Bcrypt generates it and embeds it directly into the output hash string. The stored value looks something like this:

$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW

That string contains the algorithm identifier, the cost factor, the salt, and the hash output all packed into one. When a user logs in later, your verification function reads the cost factor and salt directly from that stored string and recomputes the hash for comparison. You never need to store the salt as a separate field.

Testing Hash Outputs Before You Write a Single Line of Code

Before touching your codebase, it pays to understand what bcrypt actually produces at different cost settings. A Bcrypt hash generator lets you paste in a plain-text password, pick a cost factor, and see the resulting hash instantly. This is genuinely useful for building your mental model before implementation.

Try the same password at cost factor 10, then at 12. The hash strings look different each time because the salt regenerates on every run. That is expected behavior. Now try verifying: paste a hash and the original password into the verify field. It should confirm a match even though the hash looks brand new. This is exactly how bcrypt verification works in production code. The algorithm checks the inputs against the embedded salt and cost, not against a hardcoded comparison value.

Spending ten minutes with a hash generator before writing implementation code saves hours of confusion later. You will understand what your hashing library is actually returning, and you will recognize immediately when something looks wrong in your output.

Implementing Bcrypt in Your Web App

The implementation process follows the same pattern in most server-side environments. Here is the general flow:

  1. Install your bcrypt library. In Node.js, that is the bcrypt or bcryptjs package. In Python, it is the bcrypt library. In PHP, the password_hash() function uses bcrypt natively. In Go, it lives in golang.org/x/crypto/bcrypt.
  2. Define your cost factor as a constant. Put it in your configuration file, not scattered inline across your codebase. This makes future increases a single-line change without a code audit.
  3. Hash on registration. When a user creates an account, pass their plain-text password to your hashing function along with the cost factor. Store the returned string in your database. Never retain the plain-text after this step.
  4. Verify on login. When the user authenticates, pass their submitted password and the stored hash to your compare function. It returns true or false. You never decrypt anything. Bcrypt is one-way by design.
  5. Rehash when the cost factor changes. At login time, check whether the stored hash was generated with an older cost. If so, rehash with the current cost and update the stored value. The user experiences nothing different.

That last step gets skipped more often than it should, and the consequences accumulate quietly. Hardware gets faster every year. A cost factor that was appropriately slow in 2019 may be too fast in 2026. Building a rehash check into your login flow means stored hashes stay hardened against current hardware without forcing a mass password reset.

Password Column Design and Database Access Controls

The bcrypt hash string is always 60 characters long for the $2b$ variant. Your password column should be a fixed CHAR(60) or VARCHAR(72) field. Getting the column size wrong causes silent truncation bugs that produce maddening login failures, so pin the column size correctly from day one.

Beyond column sizing, the relationship between hashed passwords and database permissions deserves deliberate attention. The application user account that connects to your database should have read access to the password column only when it needs to verify credentials. Write access should be scoped as tightly as your architecture allows. Keeping your authentication queries in a dedicated module with its own permission boundary, separate from your broader application logic, limits the blast radius if another part of the app is compromised.

Audit logging for authentication attempts adds another layer of defense. Log the timestamp, the user identifier, the source IP, and whether the attempt succeeded. Do not log the password or the hash. The goal is to detect brute-force attempts against your login endpoint, not to duplicate sensitive credential data in a second location where it can be exposed again.

Database encryption at rest complements bcrypt but does not replace it. If your disk is encrypted and an attacker steals a backup, they cannot read the raw files. But if they gain SQL access to a running instance, encryption at rest offers nothing. Bcrypt protects you in that second scenario, because the hashes are computationally useless without cracking each one individually at the cost you configured.

Keeping Your Users Protected Long After Launch

Bcrypt is not a ship-and-forget decision. Keep your bcrypt library updated as part of your regular dependency maintenance cycle. Review your cost factor every year or two against current hardware benchmarks. The target is to keep login hashing time in the 100 to 500 millisecond range on your production servers. If it drops below that threshold, raise the cost factor and confirm your rehash-on-login logic is in place.

Rate limiting your login endpoint is a required companion to bcrypt. Even a high cost factor can be worn down if an attacker can send unlimited requests in parallel. Implement exponential backoff after repeated failed attempts and block IPs that exceed a threshold in a short window. Bcrypt slows down the cracking of stolen hashes. Rate limiting stops online guessing against your live system. Both protections are needed, and neither covers what the other does.

Transport security matters throughout this chain. Bcrypt cannot help if a password travels from browser to server in plain text. HTTPS is non-negotiable for any page handling authentication. Mixed-content warnings and misconfigured redirects still catch real applications off guard in production, so verify your TLS configuration is enforced end to end, not just on the login page itself.

Password storage is one layer of a larger system. Done right, it makes your users significantly harder to compromise even when everything else around it goes wrong. That is the promise bcrypt was built to deliver. With the right cost factor, proper salting, and a database access model that limits exposure, you give your users a meaningful defense they will never see but will genuinely rely on.

Post Comment