Get AI summaries of any video or article — Sign up free
Python Flask Tutorial: Full-Featured Web App Part 6 - User Authentication thumbnail

Python Flask Tutorial: Full-Featured Web App Part 6 - User Authentication

Corey Schafer·
5 min read

Based on Corey Schafer's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.

TL;DR

Install and initialize `flask-bcrypt`, then hash registration passwords with `generate_password_hash` and verify them with `check_password_hash` during login.

Briefing

User authentication is built end-to-end: passwords are securely hashed with bcrypt at registration, duplicate usernames/emails are blocked with custom WTForms validation, and Flask-Login manages sessions so users can log in, log out, and access protected pages like an account screen.

The workflow starts with password security. Instead of storing plain text passwords, the app installs `flask-bcrypt` and uses `generate_password_hash` to convert the submitted password into a bcrypt hash. Each run produces a different hash for the same input password, which prevents simple hash-table cracking if the database is leaked. Verification then relies on `check_password_hash`, comparing the stored hash against the password a user types during login.

Registration is updated so that a valid form submission creates a real database record. When the registration form validates, the entered password is hashed, a new `User` model instance is created with the submitted username and email, and the hashed password is stored instead of the raw password. The new user is added via `DB session dot add` and persisted with `DB session dot commit`. After successful signup, the app flashes a success message and redirects users to the login page.

A key robustness fix follows: the app prevents “ugly” SQLAlchemy integrity errors caused by duplicate usernames or emails. Rather than waiting for the database unique constraints to fail, custom validators are added to the registration form using WTForms’ `ValidationError`. The form now queries the `User` model to check whether the submitted username or email already exists; if so, it raises field-specific messages like “username is taken” and “email is taken,” giving clean feedback before any database commit.

Login is then implemented with `flask-login`. The app installs `flask-login`, initializes a `LoginManager`, and wires it into the user model with a `user_loader` callback that fetches users by ID. The `User` model inherits from `UserMixin` so Flask-Login can rely on required authentication attributes and methods. The login route switches from hard-coded credentials to database-backed checks: it filters by the submitted email, then validates the password using `bcrypt dot check_password_hash(user dot password, form dot password dot data)`. Successful authentication uses `login_user` (including “remember me” support) and redirects to the home page; failures flash “login unsuccessful.”

To improve navigation and access control, the app uses `current_user` to redirect already-authenticated users away from login/register pages. A logout route calls `logout_user`, and the layout template conditionally shows either login/register links or a logout link based on `current_user dot is authenticated`. Finally, an `account` route is protected with the `login_required` decorator. The login manager is configured with `login_manager dot login_view`, and the login route reads the `next` query parameter so that after authentication, users return to the page they originally tried to access—otherwise they land on the home page.

Cornell Notes

The authentication system adds secure registration and session-based login to a Flask app. Passwords are hashed with bcrypt during signup using `generate_password_hash`, and login verifies credentials with `check_password_hash` against the stored hash. Registration is hardened by adding custom WTForms validators that query the database to reject duplicate usernames and emails before SQLAlchemy integrity errors occur. Flask-Login then manages user sessions via a `LoginManager`, a `user_loader` function, and `UserMixin` on the user model. Protected routes use `login_required`, and the login flow respects the `next` query parameter so users return to the originally requested page after signing in.

Why does bcrypt produce a different hash every time for the same password, and how does the app still verify passwords correctly?

bcrypt’s hashing includes a random salt, so `generate_password_hash('testing')` yields a different stored value on each run even for the same input. That’s why the app cannot verify by re-hashing and comparing strings. Instead, it stores the bcrypt hash and uses `bcrypt.check_password_hash(stored_hash, candidate_password)` during login; the bcrypt library handles salt-aware comparison and returns `True` only when the candidate password matches the original.

What changes make registration actually create users in the database, and where does password hashing fit?

On a valid registration submission, the route hashes the submitted password from `form.password.data` using `bcrypt.generate_password_hash(...)` (decoded to a string if needed). It then creates a `User` instance with `username=form.username.data`, `email=form.email.data`, and `password=hashed_password`. Finally, it persists the user with `DB session.add(user)` and `DB session.commit()`, then flashes a success message and redirects to the login route.

How does the app avoid SQLAlchemy unique-constraint errors when someone tries to reuse an existing username or email?

Instead of relying on database constraints to fail at commit time, the registration form adds custom WTForms validators using `validate_<fieldname>(self, field)`. Each validator queries the `User` model (e.g., `User.query.filter_by(username=username.data).first()`). If a record exists, it raises `ValidationError` with a clear message like “username is taken, please choose a different one.” The same pattern is applied to email, producing field-level errors before any database insert occurs.

What setup does Flask-Login require to reload a user from a session, and how is it implemented here?

Flask-Login needs a `user_loader` callback registered on the `LoginManager`. The app defines a function (e.g., `load_user(user_id)`) decorated with `@login_manager.user_loader`. That function returns `User.query.get(int(user_id))`. The user model also inherits from `UserMixin`, which supplies required properties/methods like `is_authenticated`, `is_active`, `is_anonymous`, and `get_id` so session management works correctly.

How does the login route validate credentials using the database and bcrypt?

The login route first queries for a user by the submitted email: `user = User.query.filter_by(email=form.email.data).first()`. It then checks both existence and password validity in one conditional: `if user and bcrypt.check_password_hash(user.password, form.password.data):`. If valid, it logs the user in with `login_user(user, remember=form.remember.data)` and redirects to the home page; otherwise it flashes “login unsuccessful. Please check email and password.”

How does the app ensure users return to the page they originally requested after being forced to log in?

When a protected route triggers `login_required`, Flask-Login redirects to the login page with a `next` query parameter. The login route imports `request` and reads `next_page = request.args.get('next')`. After successful login, it redirects to `next_page` if it exists; otherwise it redirects to the home page. This prevents the awkward extra step of manually navigating back to the protected page.

Review Questions

  1. What is the difference between `generate_password_hash` and `check_password_hash`, and why can’t password verification be done by hashing again and comparing strings?
  2. How do custom WTForms validators prevent duplicate username/email issues before SQLAlchemy raises an integrity error?
  3. Describe the role of `login_required` and the `next` query parameter in the protected-route login flow.

Key Points

  1. 1

    Install and initialize `flask-bcrypt`, then hash registration passwords with `generate_password_hash` and verify them with `check_password_hash` during login.

  2. 2

    Store bcrypt hashes in the database instead of plain text passwords to reduce risk if the database is compromised.

  3. 3

    Create users on successful registration by hashing the submitted password, instantiating the `User` model, adding it to the DB session, and committing.

  4. 4

    Prevent duplicate usernames and emails by adding WTForms custom validators that query the `User` model and raise `ValidationError` with user-friendly messages.

  5. 5

    Use Flask-Login (`LoginManager`, `user_loader`, and `UserMixin`) to manage sessions and reload users by ID.

  6. 6

    Protect routes with `login_required` and configure `login_manager.login_view` so unauthenticated users are redirected to the login page.

  7. 7

    Improve post-login UX by reading the `next` query parameter in the login route and redirecting users back to their originally requested page.

Highlights

bcrypt’s salt means the same password never produces the same hash twice, so verification must use `check_password_hash` rather than string comparison.
Custom WTForms validators catch duplicate usernames/emails before database commits, replacing a broken signup experience with clear field errors.
Flask-Login session management hinges on a `user_loader` function that fetches users by ID and a `UserMixin`-based user model.
Protected routes redirect unauthenticated users to login, and the `next` parameter ensures they return to the intended page after authentication.

Topics

  • Password Hashing
  • WTForms Validation
  • Flask-Login Sessions
  • User Registration
  • Protected Routes

Mentioned

  • DB
  • WT
  • WTForms
  • SQLAlchemy