Flask Password Hashing Techniques

Flask Password Hashing Techniques

When building a web application with Flask, securing user passwords is not just a good practice—it’s a necessity. Storing passwords in plain text is a critical security risk that can lead to devastating breaches. Instead, you should always hash passwords before storing them. In this article, we’ll explore why hashing matters, how to implement it in Flask, and the best practices to follow.

Why Hash Passwords?

Let’s start with the basics. Hashing is a one-way process that converts a password into a fixed-length string of characters. Unlike encryption, hashing is not reversible. This means that even if an attacker gains access to your database, they won’t be able to retrieve the original passwords. Instead, they’ll see only the hashed values.

But not all hashing methods are created equal. Simple algorithms like MD5 or SHA-1 are fast and were commonly used in the past, but they are vulnerable to brute-force and rainbow table attacks. Today, we use specialized password hashing functions that are deliberately slow and incorporate salt—a random value added to each password before hashing—to defend against these attacks.

In the Flask ecosystem, the most recommended library for password hashing is Werkzeug, which is included by default when you install Flask. It provides secure, up-to-date methods for hashing and verifying passwords.

Implementing Password Hashing with Werkzeug

Werkzeug offers a pair of functions that make password management straightforward: generate_password_hash for creating hashes and check_password_hash for verifying them. These functions use robust algorithms (by default, pbkdf2:sha256) and handle salting automatically.

Here’s a basic example of how to use them in your Flask application:

from werkzeug.security import generate_password_hash, check_password_hash

# Hashing a password
hashed_password = generate_password_hash('my_secure_password')
print(hashed_password)  # Looks like: pbkdf2:sha256:260000$N4v... (truncated)

# Verifying a password
is_correct = check_password_hash(hashed_password, 'my_secure_password')
print(is_correct)  # Output: True

is_incorrect = check_password_hash(hashed_password, 'wrong_password')
print(is_incorrect)  # Output: False

It’s that simple! You don’t need to worry about generating or managing salts—Werkzeug does it for you. The generate_password_hash function includes the salt and hashing parameters right in the output string, so check_password_hash can interpret it correctly later.

Now, let’s see how you might integrate this into a typical Flask user registration and login flow.

Integrating Hashing into a Flask Application

Imagine you have a User model and you’re using Flask-SQLAlchemy for database operations. When a user registers, you should hash their password before storing it. During login, you compare the provided password with the stored hash.

First, define your User model:

from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash

db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(120), nullable=False)

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

By adding set_password and check_password methods to your User class, you encapsulate the hashing logic neatly. Now, during registration:

@app.route('/register', methods=['POST'])
def register():
    username = request.form['username']
    password = request.form['password']
    user = User(username=username)
    user.set_password(password)
    db.session.add(user)
    db.session.commit()
    return 'User registered successfully!'

And for login:

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    user = User.query.filter_by(username=username).first()
    if user and user.check_password(password):
        return 'Login successful!'
    else:
        return 'Invalid credentials', 401

This approach ensures that plain text passwords are never stored in your database. Even you, as the developer, cannot see what the original passwords are—which is exactly how it should be.

Hashing Method Strengths Weaknesses
pbkdf2:sha256 Configurable iterations, widely supported Slower than some newer methods
bcrypt Adaptive hashing, very resistant to GPU attacks Slightly more complex setup
scrypt Memory-hard, good for limiting parallel attacks Less common in default setups

When using generate_password_hash, you can optionally specify the method and number of rounds (iterations) for the hashing algorithm. For example:

hashed = generate_password_hash('password', method='pbkdf2:sha256', salt_length=16)

However, in most cases, the defaults are secure enough. The important thing is to avoid outdated methods like md5 or plain hashing without salt.

  • Always use a dedicated password hashing function.
  • Never store passwords in plain text.
  • Rely on established libraries like Werkzeug rather than rolling your own.

Remember: the goal of hashing is to protect your users even if your database is compromised. Using weak methods undermines that protection entirely.

Advanced Considerations: Upgrading Hashing Methods

Over time, hashing standards evolve. What was secure five years ago might be vulnerable today. It’s a good practice to periodically review and potentially upgrade your password hashing method. However, since hashing is one-way, you can’t simply “rehash” existing passwords. Instead, you need to wait until users log in again, then upgrade their hash.

Here’s a strategy you might use:

  1. Add a column to your User model to indicate the hashing method used (e.g., hashing_method).
  2. When a user logs in, check if their password was hashed with an old method.
  3. If it was, rehash the provided password using the new method and update the database.

For example:

class User(db.Model):
    # ... previous fields ...
    hashing_method = db.Column(db.String(50), default='pbkdf2:sha256')

    def check_password(self, password):
        if check_password_hash(self.password_hash, password):
            # Check if we need to upgrade the hashing method
            if self.hashing_method != 'pbkdf2:sha256:260000':
                self.set_password(password)  # Rehash with current method
                self.hashing_method = 'pbkdf2:sha256:260000'
                db.session.commit()
            return True
        return False

This way, you gradually migrate all user passwords to the latest standard without any downtime or forced resets.

Using bcrypt with Flask

While Werkzeug’s default hashing is secure, some developers prefer bcrypt for its adaptive nature. Bcrypt is designed to become slower over time as hardware improves, making it resistant to brute-force attacks. To use bcrypt in Flask, you can install the flask-bcrypt extension:

pip install flask-bcrypt

Here’s how to integrate it:

from flask_bcrypt import Bcrypt

bcrypt = Bcrypt(app)

# Hashing a password
hashed = bcrypt.generate_password_hash('password').decode('utf-8')

# Checking a password
is_valid = bcrypt.check_password_hash(hashed, 'password')

The principle is the same as with Werkzeug, but the underlying algorithm is bcrypt. Both are excellent choices; the decision often comes down to personal or team preference.

Feature Werkzeug Bcrypt
Default Algorithm pbkdf2:sha256 bcrypt
Ease of Use Very easy, built-in Requires extra package
Resistance to GPU Attacks Good Excellent
  • Werkzeug is included with Flask—no extra installation needed.
  • Bcrypt is often praised for its resilience against hardware-accelerated attacks.
  • Both are vastly superior to naive hashing or encryption.

Regardless of which you choose, the critical point is that you are hashing passwords correctly. Avoid the temptation to cut corners.

Common Mistakes to Avoid

Even with the right tools, it’s possible to make mistakes in implementation. Here are a few pitfalls to watch out for:

  • Using weak passwords: Hashing can’t protect against easily guessable passwords like "123456" or "password". Consider implementing password strength requirements.
  • Not using HTTPS: Transmitting passwords over unencrypted connections exposes them to interception. Always use HTTPS in production.
  • Logging passwords: Accidentally logging passwords in plain text (e.g., in error logs) is a common oversight. Be cautious with what you log.
  • Reusing salts: While Werkzeug and bcrypt handle salting for you, if you were to implement hashing manually, reusing salts would be a severe flaw.

Let’s look at an example of what not to do:

# ❌ DANGER: UNSECURE HASHING WITHOUT SALT
import hashlib

def bad_hash(password):
    return hashlib.sha256(password.encode()).hexdigest()

# This is vulnerable to rainbow table attacks!

Never do this. Always use a library that handles salting and uses a time-tested algorithm.

Testing Your Implementation

It’s essential to test your password hashing to ensure it works as expected. Here’s a simple test case using pytest:

def test_user_password_hashing():
    user = User(username='testuser')
    user.set_password('testpass')
    assert user.check_password('testpass') is True
    assert user.check_password('wrongpass') is False
    assert user.password_hash != 'testpass'  # Ensure it's hashed

Testing helps catch regressions and ensures your authentication flow remains reliable.

Final Thoughts

Password hashing is a non-negotiable aspect of modern web development. With Flask and Werkzeug, you have everything you need to implement it securely and efficiently. Remember:

  • Always hash passwords before storing them.
  • Use built-in libraries like Werkzeug or well-maintained extensions like Flask-Bcrypt.
  • Stay informed about best practices and be prepared to upgrade your hashing method over time.

Your users trust you with their data. By taking password security seriously, you’re not just protecting their accounts—you’re upholding that trust.

Now go forth and build securely!