DEV Community

Cover image for Day 84 of #100DaysOfCode — Authentication in Flask
M Saad Ahmad
M Saad Ahmad

Posted on

Day 84 of #100DaysOfCode — Authentication in Flask

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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}>'
Enter fullscreen mode Exit fullscreen mode

UserMixin provides:

  • is_authenticated — returns True if the user is logged in
  • is_active — returns True if the account is active
  • is_anonymous — returns False for 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))
Enter fullscreen mode Exit fullscreen mode

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.')
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

@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')
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

Update the User model to add the relationship:

class User(UserMixin, db.Model):
    ...
    notes = db.relationship('Note', backref='author', lazy=True)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
@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])
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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)