User passwords must never be stored as plaintext, or hashed with plain SHA-256, in the database. Werkzeug — the library that ships with Flask — already provides two functions, generate_password_hash() and check_password_hash(), that use the scrypt algorithm with a random salt — secure enough for most Flask projects without installing anything extra. This post shows you how to use them correctly in 5 minutes.
The Problem
Many beginner Flask tutorials still store passwords as plaintext, or “improve” things with hashlib.sha256(password.encode()).hexdigest(). Both are dangerous: a database leak exposes every password, and SHA-256 has no salt, so the same password always produces the same hash — an attacker can look it up in a rainbow table or try billions of common passwords in seconds. A weak password like “123456” is guessed instantly even after hashing.
# WRONG: storing plaintext in the databaseuser = User(username="linh", password="MatKhau123")db.session.add(user)# ALSO WRONG: SHA-256 without saltimport hashlibpw_hash = hashlib.sha256("MatKhau123".encode()).hexdigest()
The Solution
Werkzeug already ships with Flask, so you need to install nothing. generate_password_hash() uses scrypt by default with a random 16-byte salt; hashing the same password twice gives a different result each time, which makes rainbow tables useless. check_password_hash() compares with hmac.compare_digest — a constant-time comparison that resists timing attacks.
from werkzeug.security import generate_password_hash, check_password_hash# On registration: store only the hash, never the original passworduser = User( username="linh", password_hash=generate_password_hash("MatKhauManh!2026"),)db.session.add(user)db.session.commit()# On login: compare the entered password against the stored hashuser = User.query.filter_by(username="linh").first()if user and check_password_hash(user.password_hash, form.password.data): login_user(user)else: flash("Sai ten dang nhap hoac mat khau")
Scrypt is designed to be memory-hungry, so even an attacker with a GPU struggles to brute-force at scale; the random salt makes every hash unique. If your project needs a different algorithm, pass method="pbkdf2" — old hashes still verify, because the algorithm name and parameters live inside the hash string itself.
Apply It to Your Project
Rename the password column to password_hash (a String of at least 255 characters, since a scrypt hash is about 150 characters long), rewrite the registration and login routes to use the two functions above, then run the migration. For old users whose passwords are stored as plaintext or SHA-256, don’t try to “convert” the old hashes — that is impossible without the original password. The right approach is to make them reset their password at the next login, then store the new hash with Werkzeug.
Related Posts
- Flask: Managing Login with Flask-Login — login_required and remember me in 5 Minutes
- Flask: bảo mật API bằng JWT với flask-jwt-extended — access token + refresh token trong 5 phút
- Flask: Lock Down Session Cookies with a Strong SECRET_KEY, Secure, and SameSite in 5 Minutes
Frequently Asked Questions
Which hashing algorithm does Werkzeug use by default?
Scrypt with parameters n=32768, r=8, p=1 and a random 16-byte salt (since Werkzeug 3.x). You can switch to PBKDF2 with generate_password_hash(pw, method="pbkdf2").
Does the scrypt hash make login too slow?
Not noticeably for real users: each verification costs a few dozen milliseconds. That is small enough that nobody notices, but expensive enough to make bulk brute-forcing impractical.
Do old hashes still work after changing algorithms?
Yes. The hash string carries its own algorithm name and parameters, so check_password_hash() recognizes them and verifies correctly — no data migration needed.
Do I need to store the salt separately in the database?
No. The salt lives inside the hash string that generate_password_hash() returns, so a single password_hash column is enough.
Does check_password_hash() protect against timing attacks?
Yes. It compares with hmac.compare_digest, which runs in constant time no matter which character of the password is wrong, so an attacker cannot guess the password from response times.
References
- Werkzeug docs — Utilities: werkzeug.security — https://werkzeug.palletsprojects.com/en/stable/utils/#module-werkzeug.security
- Flask docs — Security Considerations — https://flask.palletsprojects.com/en/stable/security/
