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:

FunctionWhat 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)
Note: 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.

Coming up next: being logged in answers "who are you?" — Authorization & Custom Decorators tackles the separate question of "what are you allowed to do?", starting with making sure one user can't edit another user's books.