26 min read

How to store passwords securely in the Database?

How to store passwords securely in the Database?
Photo by Boitumelo / Unsplash

When building any application that requires user authentication, one of the most critical security decisions you'll make is how to store user passwords. Get this wrong, and you could expose your users to catastrophic security breaches. Get it right, and you'll have laid a solid foundation for your application's security.

In this comprehensive guide, we'll explore the evolution of password storage techniques, from the naive approaches that should never be used to the sophisticated methods employed by modern frameworks like Django. Whether you're a beginner just starting out or looking to deepen your understanding of password security, this article will take you through everything you need to know.

The Fundamental Problem: Why Password Storage is Tricky

Before diving into solutions, let's understand why storing passwords securely is such a challenge. The core issue is that passwords need to be verified (when users log in) but should never be retrievable by anyoneโ€”not even system administrators or developers.

This creates a unique requirement: we need a way to verify that a user has entered the correct password without actually storing the password itself. This is where the concept of one-way functions becomes crucial.

The Wrong Way: Plain Text Storage

Let's start with what you should absolutely never do: storing passwords in plain text.

CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    password VARCHAR(100)  -- NEVER DO THIS!
);

INSERT INTO users VALUES (1, 'john_doe', 'mypassword123');

This approach is a security disaster waiting to happen. If your database is compromised, every user's password is immediately exposed. Unfortunately, this still happens more often than you'd think, even in 2025.

Understanding Hashing: The First Line of Defense

What is Hashing?

Hashing is a mathematical process that takes an input (your password) and produces a fixed-length string of characters (the hash). The key properties of a good cryptographic hash function are:

  1. One-way function: It's computationally infeasible to reverse the process
  2. Deterministic: The same input always produces the same output
  3. Fixed output length: Regardless of input size, the output is always the same length
  4. Avalanche effect: A tiny change in input produces a dramatically different output

Let's see how this works with a simple example using SHA-256:

Input: "password123"
SHA-256 Hash: "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"

Input: "password124" (notice the tiny change)
SHA-256 Hash: "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"

Notice how completely different the hashes are, despite only changing one character.

Live Password Hashing Demo
๐Ÿ” Live Password Hashing Demo
SHA-256 Hash Output:
Type a password above to see its hash...

How Hashing Works for Password Storage

Instead of storing the actual password, we store its hash:

CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    password_hash VARCHAR(64)  -- Store the hash, not the password
);

-- When user registers with password "mypassword123"
INSERT INTO users VALUES (1, 'john_doe', 'ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f');

When the user tries to log in:

  1. Take the password they entered
  2. Hash it using the same algorithm
  3. Compare the resulting hash with the stored hash
  4. If they match, the password is correct

This approach means that even if your database is compromised, attackers don't have the actual passwordsโ€”they only have the hashes.

The Problem with Simple Hashing: Enter Rainbow Tables

While hashing seems like the perfect solution, it has a significant vulnerability: rainbow tables.

What are Rainbow Tables?

Rainbow tables are precomputed tables of common passwords and their corresponding hashes. Since hash functions are deterministic (the same input always produces the same output), attackers can create massive databases mapping common passwords to their hashes.

For example, a rainbow table might contain:

password123 โ†’ ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f
123456      โ†’ 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
qwerty      โ†’ 65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5

If an attacker gets your database and sees the hash ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f, they can quickly look it up in their rainbow table and discover that the original password was "password123".

The Scale of the Problem

Modern rainbow tables can contain billions of password-hash combinations for common algorithms like MD5, SHA-1, and even SHA-256. These tables can crack a significant percentage of passwords in seconds, making simple hashing inadequate for security.

Rainbow Table Attack Simulator
๐ŸŒˆ Rainbow Table Attack Simulator
๐Ÿ“‹ Precomputed Rainbow Table (Sample)
๐ŸŽฏ Rainbow Table Lookup
Click "Search Rainbow Table" to simulate an instant lookup attack
๐ŸŒ Brute Force Attack
Click "Start Brute Force" to simulate trying every combination
0.001s
Rainbow Table Lookup
2.3 hours
Brute Force Attack
โš ๏ธ Educational Demo: This simulates how rainbow tables work. Real rainbow tables contain millions of password-hash pairs and can crack common passwords in seconds.

The Solution: Adding Salt to Your Hashes

What is a Salt?

A salt is a random value that's added to your password before hashing. This simple addition completely defeats rainbow tables and significantly enhances security.

Here's how it works:

Password: "password123"
Salt: "8f2k9d7n"
Combined: "password1238f2k9d7n"
Hash: sha256("password1238f2k9d7n") = "a1b2c3d4e5f6..."

Why Salts are Effective

  1. Unique hashes: Even if two users have the same password, their hashes will be different because they have different salts
  2. Rainbow table defense: Attackers would need to create rainbow tables for every possible salt, which is computationally infeasible
  3. No additional complexity for users: The salt is generated automatically and stored alongside the hash

Salt Implementation

Here's how you might store salted hashes:

CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    password_hash VARCHAR(64),
    salt VARCHAR(32)
);

-- User registers with password "mypassword123"
-- Generate random salt: "8f2k9d7n"
-- Hash "mypassword1238f2k9d7n"
INSERT INTO users VALUES (1, 'john_doe', 'a1b2c3d4e5f6...', '8f2k9d7n');

For login verification:

  1. Retrieve the user's salt from the database
  2. Append the salt to the entered password
  3. Hash the combined string
  4. Compare with the stored hash
Salt Addition Demo
๐Ÿง‚ Salt Addition Demonstration
+
โŒ Without Salt (Vulnerable)
1. Input:
password123
SHA-256 Hash:
Loading...
โœ… With Salt (Secure)
1. Combine:
password123a7b9k2m8
SHA-256 Hash:
Loading...
๐ŸŽฏ Try Different Scenarios
๐Ÿšจ Why Salts Matter: Database Breach Simulation
Without salts, identical passwords create identical hashes, revealing patterns:

Salt Best Practices

  • Use cryptographically secure random number generators to create salts
  • Make salts at least 16 bytes long (128 bits)
  • Use a unique salt for every password, never reuse salts
  • Store the salt alongside the hashโ€”it's not secret information

Modern Approach: Multiple Rounds of Hashing

While salting solves the rainbow table problem, modern hardware can still compute hashes very quickly. A powerful computer can calculate millions or billions of hashes per second, making brute force attacks feasible.

The Concept of Key Stretching

The solution is key stretchingโ€”deliberately making the hashing process slower by performing multiple rounds of hashing. Instead of hashing once, we hash thousands or tens of thousands of times.

Round 1: hash(password + salt)
Round 2: hash(result_of_round_1)
Round 3: hash(result_of_round_2)
...
Round 10000: hash(result_of_round_9999)

This process:

  • Makes each password verification take longer (maybe 100-500 milliseconds)
  • Is barely noticeable to legitimate users
  • Makes brute force attacks exponentially more expensive for attackers

Adaptive Hashing Algorithms

Modern password hashing uses specialized algorithms designed for this purpose:

  • PBKDF2 (Password-Based Key Derivation Function 2)
  • bcrypt
  • scrypt
  • Argon2 (currently recommended as the best choice)

These algorithms are designed to be computationally expensive and have tunable parameters to control the computational cost.

Key Stretching Visualization
๐Ÿ”„ Key Stretching Visualization
Hashing Progress
Ready to start
Click "Start Key Stretching Process" to see the rounds of hashing in action
๐Ÿ‘ค Legitimate User
0.25s
Single login attempt
Barely noticeable delay
๐Ÿšจ Attacker
2.9 days
1 million password attempts
Computationally expensive
๐Ÿ’ก How it works: Each round takes the output of the previous round and hashes it again. This makes the process deliberately slow, increasing security against brute force attacks.

Visualization of Brute Force Attack with Salting

Brute Force Attack Timer
โฑ๏ธ Brute Force Attack Timer
Security Method Comparison
Security Method
Single Attempt
1K Attempts
1M Attempts
1B Attempts
๐ŸŽฏ Attack Time Visualization
๐Ÿ  Home Computer
Modern CPU
~100,000 hashes/sec
-
๐Ÿ’ป Server Farm
Multiple GPUs
~10,000,000 hashes/sec
-
๐Ÿญ Specialized Hardware
ASIC miners
~1,000,000,000 hashes/sec
-
โ˜๏ธ Cloud Botnet
Distributed attack
~100,000,000,000 hashes/sec
-
๐Ÿ’ก Hardware assumptions: Times calculated based on realistic attack scenarios. Modern GPUs can perform billions of simple hash operations per second, but key stretching significantly slows this down.

Django's Password Hashing: A Real-World Example

Let's examine how Django, one of the most popular web frameworks, implements secure password hashing.

Django's Default Configuration

Django uses PBKDF2 with SHA-256 by default, but it's designed to be flexible. Here's what a Django password hash looks like:

pbkdf2_sha256$320000$randomsalt$hashvalue

Breaking this down:

  • pbkdf2_sha256: The algorithm used
  • 320000: The number of iterations (rounds)
  • randomsalt: The randomly generated salt
  • hashvalue: The actual hash result

How Django Handles Password Hashing

When you create a user in Django:

from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password

# Creating a user
user = User.objects.create_user(
    username='john_doe',
    password='mypassword123'  # Django automatically hashes this
)

# The actual stored value looks like:
# pbkdf2_sha256$320000$8f2k9d7nxmvp$a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0

Django's Password Verification Process

When a user logs in, Django:

  1. Retrieves the stored password hash
  2. Extracts the algorithm, iteration count, and salt
  3. Takes the entered password and applies the same process
  4. Compares the results
from django.contrib.auth.hashers import check_password

# During login
is_valid = check_password('mypassword123', stored_hash)
# Returns True if password matches, False otherwise

Django's Security Features

Django includes several advanced security features:

  1. Algorithm agility: Easy to upgrade to new algorithms
  2. Automatic iteration upgrades: Increases iterations over time as hardware improves
  3. Multiple algorithm support: Can verify old passwords while using new algorithms for new passwords
  4. Timing attack protection: Uses constant-time comparison functions

Customizing Django's Password Hashing

You can configure Django to use different algorithms or adjust parameters:

# settings.py
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]

The first hasher in the list is used for new passwords, while the others are used for verification of existing passwords.

Django Password Format Breakdown
๐Ÿ Django Password Format Breakdown
Django Password Hash:
Click "Generate Django Hash" to see the result
๐Ÿ”„ Django Hashing Process
๐Ÿ”ง Django Supported Algorithms
๐Ÿ›ก๏ธ Security Features:
  • Algorithm Agility: Easy to upgrade to new algorithms without breaking existing passwords
  • Automatic Iteration Upgrades: Django can automatically increase iterations for better security
  • Salt Generation: Unique cryptographically secure salt for each password
  • Constant-Time Comparison: Prevents timing attacks during verification
# Django password hashing example from django.contrib.auth.hashers import make_password, check_password # Creating a hash password_hash = make_password('mypassword123') print(password_hash) # Verifying a password is_valid = check_password('mypassword123', password_hash) print(is_valid) # True

Best Practices for Secure Password Storage

1. Never Roll Your Own Crypto

Use established libraries and frameworks that implement these techniques correctly. Popular options include:

  • Django (Python)
  • Laravel (PHP)
  • Spring Security (Java)
  • bcrypt libraries (available in most languages)

2. Keep Your Hashing Up to Date

Security requirements evolve over time. What's secure today might not be secure in five years. Regularly review and update your password hashing strategy.

3. Monitor and Adjust Iteration Counts

As hardware becomes more powerful, increase the number of iterations in your key stretching. A good rule of thumb is to aim for 100-500 milliseconds of computation time.

4. Implement Additional Security Measures

Password hashing is just one part of a comprehensive security strategy:

  • Enforce strong password policies
  • Implement rate limiting for login attempts
  • Use multi-factor authentication where possible
  • Monitor for suspicious login patterns
  • Regularly audit your security practices

5. Prepare for Breaches

Assume that your database will eventually be compromised and plan accordingly:

  • Use the strongest password hashing available
  • Have an incident response plan
  • Know how to quickly notify users and force password resets
  • Consider implementing breach detection systems

The Future of Password Security

The landscape of password security continues to evolve. Emerging trends include:

  • Passwordless authentication using biometrics or hardware tokens
  • Zero-knowledge proof systems that can verify passwords without storing any password-related information
  • Quantum-resistant algorithms to prepare for the advent of quantum computing

However, passwords aren't going away anytime soon, making secure storage techniques more important than ever.

Conclusion

Storing passwords securely is a complex topic that has evolved significantly over the years. From the early days of plain text storage to modern adaptive hashing algorithms, each advancement has been driven by the need to stay ahead of increasingly sophisticated attacks.

The key takeaways are:

  1. Never store passwords in plain textโ€”this is inexcusable in modern applications
  2. Simple hashing is not enoughโ€”rainbow tables make it vulnerable
  3. Always use saltsโ€”they're essential for preventing rainbow table attacks
  4. Implement key stretchingโ€”make your hashing computationally expensive
  5. Use established librariesโ€”don't implement these techniques yourself
  6. Stay currentโ€”security requirements evolve over time

By following these principles and using frameworks like Django that implement these techniques correctly, you can ensure that your users' passwords remain secure even in the face of database breaches. Remember, security is not a one-time implementation but an ongoing commitment to protecting your users' data.

The techniques we've discussedโ€”from basic hashing to salted, multi-round hashing as implemented in modern frameworksโ€”represent the current best practices in password security. By understanding these concepts and implementing them correctly, you're taking a crucial step toward building secure, trustworthy applications.

Subscribe to my newsletter.

Become a subscriber receive the latest updates in your inbox.