Difficulty: Advanced
Storing passwords securely is one of the most critical aspects of web application security. Never store plaintext passwords. Instead, store a cryptographic hash of the password. If your database is compromised, attackers cannot recover the original passwords from the hashes.
A hash function transforms input data into a fixed-size string. Good password hashing has these properties: it is deterministic (same input always produces same output), it is one-way (you cannot reverse the hash to get the password), small input changes produce completely different outputs, and it is computationally expensive (to slow down brute-force attacks).
Werkzeug (Flask's underlying library) provides generate_password_hash() and check_password_hash() functions. generate_password_hash uses the scrypt algorithm by default (or pbkdf2:sha256 in older versions) and automatically generates a random salt. The salt is stored as part of the hash string, so you do not need to manage it separately.
The salt is a random value added to the password before hashing. Without a salt, two users with the same password would have the same hash, and attackers could use precomputed rainbow tables. With a salt, each hash is unique even for identical passwords.
bcrypt is another popular password hashing algorithm. It is intentionally slow and includes a configurable work factor (cost). As computers get faster, you increase the work factor to keep hashing slow. Flask-Bcrypt provides bcrypt integration with Flask.
The verify flow is: user submits credentials, you find the user by username/email, retrieve the stored hash, and call check_password_hash(stored_hash, submitted_password). If it returns True, the password matches. Never compare password hashes directly; always use the timing-safe comparison provided by the library.
Password hashing should be performed in the User model for clean encapsulation. Define set_password and check_password methods on the model. This keeps the hashing logic centralized and makes it easy to change the algorithm later.
from werkzeug.security import generate_password_hash, check_password_hash
# Hash a password (automatically generates salt)
hash1 = generate_password_hash('mypassword')
hash2 = generate_password_hash('mypassword')
print(hash1) # scrypt:32768:8:1$salt$hash...
print(hash2) # Different hash! (different salt)
# Verify a password
print(check_password_hash(hash1, 'mypassword')) # True
print(check_password_hash(hash1, 'wrongpassword')) # False
# Specify method and salt length
hash3 = generate_password_hash('mypassword', method='pbkdf2:sha256', salt_length=16)
generate_password_hash creates a unique hash with a random salt each time. check_password_hash extracts the salt from the stored hash and verifies the password. Even the same password produces different hashes.
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app.extensions import db
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(256), nullable=False)
def set_password(self, password):
"""Hash and store the password."""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""Verify a password against the stored hash."""
return check_password_hash(self.password_hash, password)
# Usage:
user = User(username='alice', email='alice@example.com')
user.set_password('SecurePass123')
db.session.add(user)
db.session.commit()
# Later, during login:
if user.check_password('SecurePass123'):
login_user(user)
Encapsulate hashing in model methods. set_password hashes and stores. check_password verifies against the stored hash. The actual hash string is stored in the database, never the plaintext.
from flask import Flask, request, redirect, url_for, flash
from flask_login import login_user
@app.route('/register', methods=['POST'])
def register():
username = request.form['username']
email = request.form['email']
password = request.form['password']
# Check if user exists
if User.query.filter_by(email=email).first():
flash('Email already registered', 'error')
return redirect(url_for('register'))
# Create user with hashed password
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
login_user(user)
return redirect(url_for('dashboard'))
@app.route('/login', methods=['POST'])
def login():
email = request.form['email']
password = request.form['password']
user = User.query.filter_by(email=email).first()
# Check user exists AND password matches
if user is None or not user.check_password(password):
flash('Invalid email or password', 'error')
return redirect(url_for('login'))
login_user(user)
return redirect(url_for('dashboard'))
Registration hashes the password before storing. Login retrieves the user and verifies the password. The error message is intentionally vague ('Invalid email or password') to prevent user enumeration attacks.
from flask import Flask
from flask_bcrypt import Bcrypt
app = Flask(__name__)
bcrypt = Bcrypt(app)
# Hash a password
hashed = bcrypt.generate_password_hash('mypassword').decode('utf-8')
# Verify
print(bcrypt.check_password_hash(hashed, 'mypassword')) # True
print(bcrypt.check_password_hash(hashed, 'wrongpass')) # False
# In a model
class User(db.Model):
password_hash = db.Column(db.String(128))
def set_password(self, password):
self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
def check_password(self, password):
return bcrypt.check_password_hash(self.password_hash, password)
Flask-Bcrypt is an alternative to Werkzeug's hashing. Bcrypt includes a cost factor that makes it intentionally slow. The .decode('utf-8') converts bytes to string for database storage.
Hashing, bcrypt, Werkzeug Security, Salt, Password Verification