Yesterday, I built a complete CRUD backend with HTML and API routes. Today, it was about authentication in Flask. In Django, this came completely built in. In Flask, you wire it together yourself using Flask-Login for session management and Werkzeug for password hashing. By the end of today, the app will have registration, login, logout, and protected routes.
What Flask-Login Does
Flask-Login manages the user session. It remembers who is logged in, provides the current_user object in views and templates, and handles the redirect to login for protected routes. It does not handle registration, password hashing, or database queries. Those are your responsibility.
pip install flask-login
Setup
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db = SQLAlchemy(app)
migrate = Migrate(app, db)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'warning'
login_manager.login_view = 'login' tells Flask-Login where to redirect unauthenticated users, the name of the login route function. In Django, this was LOGIN_URL in settings.py.
The User Model
Flask-Login requires the User model to implement four properties and methods. The easiest way is to inherit from UserMixin which provides default implementations for all of them:
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
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(256), nullable=False)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
UserMixin provides:
-
is_authenticated— returnsTrueif the user is logged in -
is_active— returnsTrueif the account is active -
is_anonymous— returnsFalsefor real users -
get_id()— returns the user's ID as a string
These are the same properties Django's User model has built in. UserMixin just gives them to you without having to write them manually.
The User Loader
Flask-Login needs to know how to load a user from the database given their ID. It uses this to reconstruct the user object from the session on each request:
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
This decorator registers the function. Flask-Login calls it automatically on every request where a user is logged in. In Django, this happened automatically through the session framework; Flask makes it explicit.
Registration
from flask_wtf import FlaskForm
from wtforms import StringField, EmailField, PasswordField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
class RegisterForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(),
Length(min=3, max=80)
])
email = EmailField('Email', validators=[
DataRequired(),
Email()
])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=8)
])
confirm_password = PasswordField('Confirm Password', validators=[
DataRequired(),
EqualTo('password', message='Passwords must match.')
])
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('Username already taken.')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('Email already registered.')
The validate_username and validate_email methods run automatically during form validation; they check uniqueness at the form level before anything hits the database constraints. Same pattern as Django's clean_fieldname().
from flask import render_template, redirect, url_for, flash
from flask_login import login_user
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('home'))
form = RegisterForm()
if form.validate_on_submit():
user = User(
username=form.username.data,
email=form.email.data
)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
login_user(user)
flash('Account created successfully.', 'success')
return redirect(url_for('home'))
return render_template('auth/register.html', form=form)
if current_user.is_authenticated at the top prevents logged-in users from seeing the registration page, redirects them home instead. Same guard, you'd put on a Django registration view.
login_user(user) tells Flask-Login to log the user in, and it creates the session. After this call, current_user in any view or template refers to this user.
Login
class LoginForm(FlaskForm):
email = EmailField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
remember = BooleanField('Remember me')
from flask_login import login_user, current_user
from flask import request
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('home'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and user.check_password(form.password.data):
login_user(user, remember=form.remember.data)
next_page = request.args.get('next')
flash('Logged in successfully.', 'success')
return redirect(next_page or url_for('home'))
flash('Invalid email or password.', 'danger')
return render_template('auth/login.html', form=form)
remember=form.remember.data. If the user checks "Remember me", Flask-Login sets a persistent cookie so the session survives after the browser closes. Django's login() function with remember works the same way.
next_page = request.args.get('next'). When Flask-Login redirects an unauthenticated user to login, it appends ?next=/original-url to the login URL. After successful login, redirect them back to where they were trying to go. This is the same ?next= handling you added to DevBoard's login template.
Logout
from flask_login import logout_user
@app.route('/logout')
def logout():
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('login'))
logout_user() clears the session. One line. In Django, this was from django.contrib.auth import logout; logout(request); same concept.
Protecting Routes
@login_required decorator
from flask_login import login_required
@app.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html')
@login_required is Flask-Login's equivalent of Django's @login_required. If an unauthenticated user hits this route, they get redirected to login_manager.login_view, the login page you set up earlier.
Checking in the view manually
@app.route('/settings')
@login_required
def settings():
if not current_user.is_active:
flash('Your account has been deactivated.', 'danger')
return redirect(url_for('home'))
return render_template('settings.html')
current_user in Templates
Flask-Login automatically makes current_user available in every Jinja2 template, no need to pass it manually:
{% if current_user.is_authenticated %}
<p>Welcome, {{ current_user.username }}</p>
<a href="{{ url_for('logout') }}">Logout</a>
{% else %}
<a href="{{ url_for('login') }}">Login</a>
<a href="{{ url_for('register') }}">Register</a>
{% endif %}
In Django,b this was request.user.is_authenticated, Flask-Login uses current_user.is_authenticated. Different name, same behavior.
Ownership Checks
Protecting a route is only half the job. You also need to make sure users can only access their own data:
@app.route('/notes/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def note_edit(id):
note = Note.query.get_or_404(id)
if note.author_id != current_user.id:
abort(403)
form = NoteForm(obj=note)
if form.validate_on_submit():
note.title = form.title.data
note.content = form.content.data
db.session.commit()
return redirect(url_for('note_detail', id=note.id))
return render_template('notes/form.html', form=form, note=note)
abort(403) returns a Forbidden response immediately. Same pattern as DevBoard's if post.author != request.user: return redirect(...), just using abort instead of redirect for cleaner semantics.
Linking Notes to Users
Update the Note model to belong to a user:
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
is_pinned = db.Column(db.Boolean, default=False)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
author = db.relationship('User', backref='notes')
Update the User model to add the relationship:
class User(UserMixin, db.Model):
...
notes = db.relationship('Note', backref='author', lazy=True)
Now when creating a note, attach the current user:
@app.route('/notes/new', methods=['GET', 'POST'])
@login_required
def note_create():
form = NoteForm()
if form.validate_on_submit():
note = Note(
title=form.title.data,
content=form.content.data,
is_pinned=form.is_pinned.data,
author_id=current_user.id
)
db.session.add(note)
db.session.commit()
return redirect(url_for('note_detail', id=note.id))
return render_template('notes/form.html', form=form)
And filter the list view to only show the current user's notes:
@app.route('/notes')
@login_required
def note_list():
notes = Note.query.filter_by(author_id=current_user.id)\
.order_by(Note.is_pinned.desc(), Note.created_at.desc())\
.all()
return render_template('notes/list.html', notes=notes)
Token Authentication for API Routes
Session-based auth works for browser users. API clients use token authentication instead. A simple approach without adding another library, generate a token on the user model:
import secrets
class User(UserMixin, db.Model):
...
api_token = db.Column(db.String(64), unique=True, nullable=True)
def generate_token(self):
self.api_token = secrets.token_hex(32)
db.session.commit()
return self.api_token
A decorator to protect API routes with the token:
from functools import wraps
from flask import request, jsonify
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({'error': 'Token required'}), 401
user = User.query.filter_by(api_token=token).first()
if not user:
return jsonify({'error': 'Invalid token'}), 401
return f(user, *args, **kwargs)
return decorated
@app.route('/api/notes', methods=['GET'])
@token_required
def api_note_list(current_user):
notes = Note.query.filter_by(author_id=current_user.id).all()
return jsonify([note.to_dict() for note in notes])
The token_required decorator extracts the token from the Authorization header, looks up the user, and passes them as the first argument to the view. Same concept as Django's TokenAuthentication in DRF, just written manually instead of configured.
An Endpoint to Get a Token
@app.route('/api/token', methods=['POST'])
def api_get_token():
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
user = User.query.filter_by(email=data.get('email')).first()
if not user or not user.check_password(data.get('password', '')):
return jsonify({'error': 'Invalid credentials'}), 401
token = user.generate_token()
return jsonify({'token': token})
POST your email and password, get a token back. Use that token in subsequent API requests. Same flow as DRF's obtain_auth_token endpoint.
Django vs Flask Authentication
| Django | Flask-Login |
|---|---|
Built-in User model |
Define your own model + UserMixin
|
create_user() |
Manual + set_password()
|
authenticate() |
Manual query + check_password()
|
login(request, user) |
login_user(user) |
logout(request) |
logout_user() |
@login_required |
@login_required |
request.user |
current_user |
request.user.is_authenticated |
current_user.is_authenticated |
LOGIN_URL in settings |
login_manager.login_view |
LOGIN_REDIRECT_URL |
next parameter handling |
| Token auth via DRF | Manual token_required decorator |
Flask requires more code, but nothing here is difficult; it's all explicit and readable. The upside is you understand exactly how authentication works in your app because you wrote it.
Wrapping Up
Authentication in Flask is more manual than in Django, but the pieces fit together cleanly: Flask-Login for session management, Werkzeug for password hashing, and a custom decorator for API token auth. The concepts are identical to Django; the execution is just more hands-on.
Top comments (0)