Authentication with Flask-Login
Flask has no login system of its own — no idea of "the current visitor," no session
bookkeeping for who's signed in, nothing. That's deliberate, in keeping with the
microframework philosophy from Why Flask?: authentication
is exactly the kind of feature you pick an extension for. Flask-Login is
the standard choice, and it's the piece that will let Bookshelf tell one visitor's shelves
and books apart from another's. This lesson covers the whole flow: wiring the extension in,
making the User model compatible with it, logging users in and out, protecting
views, and reading the logged-in user anywhere in the app.
Installing and Initializing Flask-Login
Install it into Bookshelf's virtual environment the same way every other extension in this track has been installed:
pip install flask-login
Flask-Login is initialized the same way Flask-SQLAlchemy was in
Project Structure & App Factory —
create an unbound LoginManager at module level, then call
init_app() on it inside the app factory once the real app instance exists:
# extensions.py
from flask_login import LoginManager
login_manager = LoginManager()
# app.py
from flask import Flask
from extensions import db, login_manager
def create_app():
app = Flask(__name__)
app.config.from_object('config.Config')
db.init_app(app)
login_manager.init_app(app)
return app
Making User Compatible: UserMixin
Flask-Login needs any user object it works with to expose a small, specific set of
properties and methods — is_authenticated, is_active,
is_anonymous, and get_id(). Rather than writing those by hand on
Bookshelf's User model, you inherit them from UserMixin, which
supplies sensible defaults for all four:
# models.py
from flask_login import UserMixin
from 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(255), nullable=False)
UserMixin's default get_id() returns str(self.id),
which is exactly right for Bookshelf's User.id primary key — no override
needed. is_authenticated and is_active default to
True for any instance that exists, which is the correct behavior unless a
project later adds something like an account-suspension flag.
The Required user_loader Callback
Flask-Login stores only a user's id in the session — not the whole
User object — and needs to be told how to turn that stored id back into a
real User on every request. You provide that logic once, as a function
decorated with @login_manager.user_loader:
# models.py
from extensions import login_manager
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
Without registering this callback, Flask-Login has no way to reconstruct
current_user on subsequent requests, and every attempt to use it will fail.
This is genuinely required — every Flask-Login setup needs exactly one user_loader.
Logging a User In: login_user()
A login view looks up the submitted username, verifies the password, and — on success —
calls login_user() to tell Flask-Login "this visitor is now authenticated as
this user." The password-checking details are covered fully in the next lesson,
Registration & Password Hashing; here's
the shape of the view with that check included:
# auth/views.py
from flask import request, redirect, url_for, flash
from flask_login import login_user
from werkzeug.security import check_password_hash
from models import User
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
user = User.query.filter_by(username=request.form['username']).first()
if user and check_password_hash(user.password_hash, request.form['password']):
login_user(user)
return redirect(url_for('book_list'))
flash('Invalid username or password.')
return render_template('auth/login.html')
login_user(user) writes the user's id into the session (see
Sessions & Cookies), so Flask-Login's
user_loader can restore current_user on every following request,
until they log out or the session expires.
Logging Out: logout_user()
Ending a session is a single call — logout_user() clears the recorded user id
from the session:
# auth/views.py
from flask_login import logout_user, login_required
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('login'))
Protecting Views: @login_required
Most of Bookshelf only makes sense for a logged-in visitor — adding a book, editing a
shelf, rating something you've finished. Decorating a view with
@login_required blocks anonymous visitors from reaching it entirely:
# books/views.py
from flask_login import login_required, current_user
@app.route('/books/add', methods=['GET', 'POST'])
@login_required
def add_book():
# current_user is guaranteed to be a real, logged-in User here
if request.method == 'POST':
book = Book(
title=request.form['title'],
author=request.form['author'],
user_id=current_user.id,
)
db.session.add(book)
db.session.commit()
return redirect(url_for('book_list'))
return render_template('books/add.html')
An anonymous visitor who requests /books/add never reaches the function body
at all — Flask-Login intercepts the request and redirects them before your code runs.
Where Unauthenticated Visitors Land: login_view and next
@login_required needs to know where to send a visitor who isn't logged in.
You configure that once, right where login_manager is initialized:
# app.py
login_manager.init_app(app)
login_manager.login_view = 'login' # the endpoint name of the login view
With that set, an anonymous visitor hitting /books/add is redirected to
/login automatically — and Flask-Login appends the page they were originally
headed for as a next query parameter, e.g. /login?next=/books/add.
The login view reads it back after a successful login, so the visitor lands exactly where
they intended instead of always bouncing to a generic homepage:
# auth/views.py
from flask import request, redirect, url_for
from werkzeug.urls import url_has_allowed_host_and_scheme
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# ...username/password check and login_user(user) as shown above...
next_page = request.args.get('next')
return redirect(next_page or url_for('book_list'))
return render_template('auth/login.html')
next points somewhere inside Bookshelf itself before redirecting to it (as the werkzeug.urls import above hints at) — otherwise a crafted link could redirect a freshly logged-in visitor to an external, malicious site. We'll return to this kind of caution in Security Best Practices.Reading the Logged-In User: current_user
current_user is a context-local proxy, exactly like current_app
from Application Context & Globals —
it always resolves to whichever user Flask-Login has restored for the current request (via
user_loader), or to an anonymous placeholder if nobody's logged in. It's
available in views and, just as usefully, directly inside Jinja2 templates with no extra
work — Flask-Login injects it into every template's context automatically. A nav bar that
switches between "Log In" and "Log Out" is the classic example:
{% if current_user.is_authenticated %}
<span>Signed in as {{ current_user.username }}</span>
<a href="{{ url_for('logout') }}">Log Out</a>
{% else %}
<a href="{{ url_for('login') }}">Log In</a>
{% endif %}
current_user.is_authenticated comes straight from UserMixin, and
current_user.username reads the real column off the underlying
User row — no separate lookup required, in the view or the template.