Registration & Password Hashing
The previous lesson showed a login view calling check_password_hash() without
explaining where the hash it's checking against actually comes from. This lesson fills that
gap from the other end: building the registration view that creates a new Bookshelf
User in the first place, and — critically — never once touching a plain-text
password along the way.
Why a Plain-Text Password Is a Serious Risk
It would be technically simple to save whatever a visitor types into a "password" field
directly into the User.password_hash column. It would also be a genuinely
dangerous mistake, for a reason that has nothing to do with theory: people reuse passwords
across sites constantly. If Bookshelf stored passwords in plain text and its database was
ever exposed — through a misconfigured backup, an SQL injection bug, a compromised admin
account, or simply an employee with more access than they should have — every single
stored password would be instantly readable, in full, by whoever got access. Not just
"readable enough to guess" — readable exactly as the user typed it, and usable to log into
their email, their bank, or any other account where they reused it.
The fix is to never store the password at all. Instead, you store a one-way hash of it — a scrambled value that's computationally straightforward to produce from the original password, but effectively impossible to reverse back into the original. Verifying a login doesn't require reversing anything: you hash the submitted password the same way and compare the two hashes.
werkzeug.security: Hashing Without Extra Installs
Flask installs Werkzeug as a core dependency, and Werkzeug ships a small, ready-to-use security module — so there's no separate package to add for basic password hashing. Two functions cover the entire flow:
| Function | What it does |
|---|---|
generate_password_hash(password) | Turns a plain-text password into a salted hash string, safe to store in the database |
check_password_hash(hash, password) | Re-hashes a submitted password and compares it against a stored hash, returning True/False |
Both are imported straight from Werkzeug:
from werkzeug.security import generate_password_hash, check_password_hash
The Registration View
Bookshelf's registration view uses a RegistrationForm built with Flask-WTF,
the same way every form in this track has been built since
Forms with Flask-WTF. On a valid submission, the
password is hashed before the new User row is ever created:
# auth/views.py
from flask import render_template, redirect, url_for
from werkzeug.security import generate_password_hash
from flask_login import login_user
from extensions import db
from models import User
from .forms import RegistrationForm
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
new_user = User(
username=form.username.data,
email=form.email.data,
password_hash=generate_password_hash(form.password.data),
)
db.session.add(new_user)
db.session.commit()
login_user(new_user)
return redirect(url_for('book_list'))
return render_template('auth/register.html', form=form)
Notice form.password.data is used only as the input to
generate_password_hash() — it is never assigned to any column and never
saved anywhere. What lands in password_hash is a long, salted string that
reveals nothing about the original password.
Verifying a Password at Login
This completes the login view sketched in
Authentication with Flask-Login:
check_password_hash() takes the stored hash and the
freshly submitted plain-text password, hashes the second one internally using the
same salt recorded in the first, and reports whether they match:
# auth/views.py
from werkzeug.security import check_password_hash
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and check_password_hash(user.password_hash, form.password.data):
login_user(user)
return redirect(url_for('book_list'))
flash('Invalid username or password.')
return render_template('auth/login.html', form=form)
check_password_hash() never needs to "un-hash" anything — it re-derives a hash from the submitted password and compares strings. There is no operation that turns a hash back into the original password; that's the entire point of one-way hashing.Rejecting a Username or Email That's Already Taken
Registration also needs to stop two accounts from claiming the same username or email
before the database's own uniqueness constraint gets a chance to reject the insert with a
raw error. WTForms supports custom validation by defining a method named
validate_<fieldname> directly on the form class — Flask-WTF calls it
automatically as part of validate_on_submit():
# auth/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, ValidationError
from models import User
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
def validate_username(self, field):
existing_user = User.query.filter_by(username=field.data).first()
if existing_user:
raise ValidationError('That username is already taken.')
Since Flask-WTF matches validate_username to the username field
by name, this check runs as part of ordinary form validation — no extra wiring in the view.
A matching validate_email method, following the identical pattern, covers
duplicate emails.
Signing the New User In Immediately
Bookshelf's registration view already does this — look back at the login_user(new_user)
call right after db.session.commit() in the registration view above. Once a
new account is safely created, there's no reason to make someone type their brand-new
credentials a second time on a separate login page; calling login_user()
immediately gives them a signed-in session in the same request that created their account.
An Alternative for Production: Flask-Bcrypt
Werkzeug's default hashing (currently based on the scrypt or
pbkdf2 algorithms depending on version) is secure and entirely sufficient for
Bookshelf and most projects getting started. Some production applications instead choose
bcrypt — a hashing algorithm purpose-built for passwords, deliberately slow
to compute so that brute-forcing a large stolen database of hashes takes meaningfully
longer. The Flask-Bcrypt extension wraps it in a similarly small API
(bcrypt.generate_password_hash() / bcrypt.check_password_hash()).
You don't need it to follow the rest of this track — this is conceptual awareness for when
a project's security requirements call for it.