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:
| Feature | Description |
|---|---|
| Salting | Bcrypt automatically generates a random salt for each password, ensuring that identical passwords produce different hashes |
| Cost factor | The algorithm includes a configurable work factor (number of hashing rounds) that controls how slow the hashing process is |
| One-way function | The 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.
.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)
| Rounds | Approximate Time | Recommendation |
|---|---|---|
| 10 | ~70ms | Minimum for production |
| 12 | ~250ms | Default: good balance |
| 14 | ~1s | High-security applications |
| 16 | ~4s | Very high security (may impact UX) |
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
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
| Practice | Description |
|---|---|
| Always hash passwords | Never store plaintext passwords |
Use check_password_hash() | Never compare hashes with == |
| Use HTTPS | Encrypt passwords in transit |
| Set appropriate cost factor | Balance security and performance |
| Return generic error messages | Say "Invalid username or password": never reveal which one was wrong |
| Add rate limiting | Prevent brute-force login attempts |
| Use a secret key | Set 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.