Skip to main content

How to Hash Passwords With Bcrypt in Flask in Python

Password hashing is a critical security practice that converts plaintext passwords into irreversible, encrypted strings before storing them. If a database is ever compromised, attackers only see hashed values, i.e. not the actual passwords. Bcrypt is one of the most widely recommended hashing algorithms because it is intentionally slow, includes a built-in salt, and allows you to increase the computational cost over time as hardware gets faster.

In this guide, you will learn how to implement password hashing in a Flask application using Flask-Bcrypt, covering everything from installation to a complete user registration and login example.

How Bcrypt Works

Bcrypt is based on the Blowfish cipher and incorporates three key security features:

FeatureDescription
SaltingBcrypt automatically generates a random salt for each password, ensuring that identical passwords produce different hashes
Cost factorThe algorithm includes a configurable work factor (number of hashing rounds) that controls how slow the hashing process is
One-way functionThe hash cannot be reversed to recover the original password: verification is done by hashing the input again and comparing

Setting Up Flask-Bcrypt

Step 1: Install Flask-Bcrypt

pip install flask flask-bcrypt

Step 2: Initialize Bcrypt in Your Flask App

from flask import Flask
from flask_bcrypt import Bcrypt

app = Flask(__name__)
bcrypt = Bcrypt(app)

The Bcrypt object wraps your Flask app and provides methods for hashing and verifying passwords.

Hashing a Password

Use generate_password_hash() to hash a plaintext password:

from flask import Flask
from flask_bcrypt import Bcrypt

app = Flask(__name__)
bcrypt = Bcrypt(app)

password = 'my_secure_password'
hashed = bcrypt.generate_password_hash(password).decode('utf-8')

print(f"Original: {password}")
print(f"Hashed: {hashed}")

Output:

Original:  my_secure_password
Hashed: $2b$12$vBaedEhJCOZFIMmei9LWRuRf3Wb72RD.1yoIuDR4jqqExZCU6YlHC

The hash includes the algorithm identifier ($2b$), the cost factor ($12$), the salt, and the hashed password: all in a single string.

Why .decode('utf-8')?

generate_password_hash() returns a bytes object. Decoding it to a UTF-8 string makes it easier to store in databases and compare later:

from flask import Flask
from flask_bcrypt import Bcrypt

app = Flask(__name__)
bcrypt = Bcrypt(app)

# Returns bytes
hashed_bytes = bcrypt.generate_password_hash('password')
print(type(hashed_bytes)) # <class 'bytes'>

# Decode to string for storage
hashed_string = hashed_bytes.decode('utf-8')
print(type(hashed_string)) # <class 'str'>

Output:

<class 'bytes'>
<class 'str'>

Verifying a Password

Use check_password_hash() to verify a password against its stored hash:

from flask import Flask
from flask_bcrypt import Bcrypt

app = Flask(__name__)
bcrypt = Bcrypt(app)

hashed = bcrypt.generate_password_hash('my_secure_password')

is_valid = bcrypt.check_password_hash(hashed, 'my_secure_password')
print(f"Password matches: {is_valid}") # True

is_wrong = bcrypt.check_password_hash(hashed, 'wrong_password')
print(f"Wrong password matches: {is_wrong}") # False

Output:

Password matches: True
Wrong password matches: False

The function internally extracts the salt from the stored hash, applies it to the input password, hashes the result, and compares the two hashes.

Basic Flask Example

Here is a minimal Flask app that demonstrates hashing and verification:

from flask import Flask
from flask_bcrypt import Bcrypt

app = Flask(__name__)
bcrypt = Bcrypt(app)


@app.route('/')
def index():
password = 'Password@123'

# Hash the password
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')

# Verify the password
is_valid = bcrypt.check_password_hash(hashed_password, password)
is_invalid = bcrypt.check_password_hash(hashed_password, 'wrong_pass')

return (
f"<h3>Password Hashing Demo</h3>"
f"<p><strong>Original:</strong> {password}</p>"
f"<p><strong>Hashed:</strong> {hashed_password}</p>"
f"<p><strong>Correct password check:</strong> {is_valid}</p>"
f"<p><strong>Wrong password check:</strong> {is_invalid}</p>"
)


if __name__ == '__main__':
app.run(debug=True)

Output in browser:

Password Hashing Demo
Original: Password@123
Hashed: $2b$12$Na1e8y0Ws.ldqQwTzw3IxOMaUEWDnTzezUuStssY0huTZEIA8sDAG
Correct password check: True
Wrong password check: False

Complete Example: User Registration and Login

Here is a more realistic example with user registration and login routes:

from flask import Flask, request, jsonify
from flask_bcrypt import Bcrypt

app = Flask(__name__)
bcrypt = Bcrypt(app)

# In-memory user store (use a database in production)
users = {}


@app.route('/register', methods=['POST'])
def register():
data = request.get_json()
username = data.get('username')
password = data.get('password')

if not username or not password:
return jsonify({'error': 'Username and password are required'}), 400

if username in users:
return jsonify({'error': 'User already exists'}), 409

# Hash the password before storing
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
users[username] = hashed_password

return jsonify({'message': f'User {username} registered successfully'}), 201


@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')

if username not in users:
return jsonify({'error': 'Invalid username or password'}), 401

# Verify the password against the stored hash
if bcrypt.check_password_hash(users[username], password):
return jsonify({'message': f'Welcome back, {username}!'}), 200
else:
return jsonify({'error': 'Invalid username or password'}), 401


if __name__ == '__main__':
app.run(debug=True)

Testing With curl

Register a user:

curl -X POST http://localhost:5000/register \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "secure_pass_123"}'

Response:

{"message": "User alice registered successfully"}

Login with correct password:

curl -X POST http://localhost:5000/login \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "secure_pass_123"}'

Response:

{"message": "Welcome back, alice!"}

Login with wrong password:

curl -X POST http://localhost:5000/login \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "wrong_password"}'

Response:

{"error": "Invalid username or password"}

Configuring the Cost Factor (Rounds)

The cost factor controls how many hashing rounds Bcrypt performs. Higher values make hashing slower (more secure) but increase server load:

app = Flask(__name__)
app.config['BCRYPT_LOG_ROUNDS'] = 14 # Default is 12

bcrypt = Bcrypt(app)
RoundsApproximate TimeRecommendation
10~70msMinimum for production
12~250msDefault: good balance
14~1sHigh-security applications
16~4sVery high security (may impact UX)
tip

Start with the default (12 rounds) and increase as your server hardware allows. The goal is to make each hash take 250ms–1s, slow enough to deter brute-force attacks but fast enough for a good user experience.

Common Mistakes and Best Practices

Mistake 1: Storing Plaintext Passwords

# ❌ NEVER store passwords as plaintext
users[username] = password

# ✅ Always hash before storing
users[username] = bcrypt.generate_password_hash(password).decode('utf-8')

Mistake 2: Comparing Hashes With ==

# ❌ Direct string comparison is vulnerable to timing attacks
if stored_hash == bcrypt.generate_password_hash(password).decode('utf-8'):
# This also fails because bcrypt generates different salts each time

# ✅ Always use check_password_hash
if bcrypt.check_password_hash(stored_hash, password):
# Correct: uses constant-time comparison

Mistake 3: Using the Same Hash to Compare

Each call to generate_password_hash() produces a different hash for the same password (due to random salting):

hash1 = bcrypt.generate_password_hash('password').decode('utf-8')
hash2 = bcrypt.generate_password_hash('password').decode('utf-8')

print(hash1 == hash2) # False: different salts!

# ✅ Use check_password_hash instead
print(bcrypt.check_password_hash(hash1, 'password')) # True
print(bcrypt.check_password_hash(hash2, 'password')) # True
warning

Never try to verify passwords by generating a new hash and comparing strings. Always use check_password_hash(), which extracts the salt from the stored hash and performs a constant-time comparison to prevent timing attacks.

Security Best Practices

PracticeDescription
Always hash passwordsNever store plaintext passwords
Use check_password_hash()Never compare hashes with ==
Use HTTPSEncrypt passwords in transit
Set appropriate cost factorBalance security and performance
Return generic error messagesSay "Invalid username or password": never reveal which one was wrong
Add rate limitingPrevent brute-force login attempts
Use a secret keySet app.secret_key for session management

Conclusion

Implementing password hashing with Bcrypt in Flask is straightforward with the Flask-Bcrypt extension. Use generate_password_hash() to hash passwords before storing them and check_password_hash() to verify passwords during login.

Bcrypt's built-in salting ensures that identical passwords produce different hashes, and its configurable cost factor allows you to increase security as hardware improves.

By following the best practices outlined in this guide, never storing plaintext, using constant-time comparison, and returning generic error messages, you can build a secure authentication system that protects your users' credentials.