Flask-Login Authentication — Core Concepts

What Flask-Login handles

Flask-Login manages one specific problem: tracking which user is logged in across requests. It doesn’t handle password storage, registration forms, or OAuth flows. Those are separate concerns. Flask-Login’s job is session management and access control.

It provides:

  • Storing the logged-in user’s ID in the session
  • Loading the user object from the ID on each request
  • Protecting views that require authentication
  • “Remember me” functionality
  • Login/logout mechanics

The user loader

Flask-Login needs one callback from you: a function that takes a user ID and returns the user object:

from flask_login import LoginManager

login_manager = LoginManager()

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

This runs on every request for authenticated users. Flask-Login stores only the user ID in the session (not the entire user object), then calls your loader to reconstruct the user. This keeps sessions small and ensures you always work with fresh database data.

The UserMixin

Your User model needs four properties and methods for Flask-Login. The UserMixin provides defaults:

from flask_login import UserMixin

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(120), unique=True)
    password_hash = db.Column(db.String(256))

UserMixin provides:

  • is_authenticatedTrue (the user passed login)
  • is_activeTrue (account not disabled)
  • is_anonymousFalse (not an anonymous user)
  • get_id() → returns str(self.id)

Override is_active to handle banned or unverified accounts — Flask-Login checks this property and rejects inactive users.

Login and logout

from flask_login import login_user, logout_user
from werkzeug.security import check_password_hash

@app.route('/login', methods=['POST'])
def login():
    user = User.query.filter_by(email=request.form['email']).first()
    if user and check_password_hash(user.password_hash, request.form['password']):
        login_user(user, remember=request.form.get('remember'))
        next_page = request.args.get('next')
        return redirect(next_page or url_for('dashboard'))
    flash('Invalid email or password')
    return redirect(url_for('login'))

@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index'))

login_user() stores the user’s ID in Flask’s session. logout_user() clears it. The remember parameter creates a long-lived cookie so users stay logged in after closing the browser.

Protecting routes

The login_required decorator redirects unauthenticated users to the login page:

from flask_login import login_required, current_user

@app.route('/dashboard')
@login_required
def dashboard():
    return render_template('dashboard.html', user=current_user)

When an unauthenticated user hits /dashboard, Flask-Login redirects to the login view (configured via login_manager.login_view = 'login'). After login, the next parameter redirects them back to where they were trying to go.

current_user is a proxy object that points to the loaded user for the current request. In templates, it’s available automatically:

{% if current_user.is_authenticated %}
    Welcome, {{ current_user.email }}!
{% else %}
    <a href="{{ url_for('login') }}">Log in</a>
{% endif %}

How the session works

Flask-Login stores minimal data in the session cookie:

session['_user_id'] = '42'          # The user's ID
session['_fresh'] = True             # Was this a direct login (not "remember me")?
session['_id'] = 'abc123...'         # Session identifier for invalidation

The _fresh flag distinguishes between users who just entered their password versus users restored from a “remember me” cookie. Sensitive operations (password change, payment) should require a fresh login:

from flask_login import fresh_login_required

@app.route('/change-password', methods=['POST'])
@fresh_login_required  # Requires recent password entry, not just "remember me"
def change_password():
    # ...

Common misconception

“Flask-Login handles password hashing.” It doesn’t. Flask-Login never sees passwords. Use werkzeug.security.generate_password_hash and check_password_hash (or bcrypt or argon2) for password storage. Flask-Login starts its work after you’ve verified the password — it just manages the session.

The anonymous user

When no one is logged in, current_user returns an AnonymousUserMixin instance where is_authenticated is False. This means templates and views can always reference current_user without checking if it’s None first.

You can customize the anonymous user class:

class AnonymousUser(AnonymousUserMixin):
    def can(self, permission):
        return False

login_manager.anonymous_user = AnonymousUser

One thing to remember: Flask-Login is deliberately narrow — it manages session lifecycle and access control, nothing else. Password hashing, registration, OAuth, and email verification are separate libraries. This narrowness is a feature: it does one thing reliably and composes cleanly with other tools.

pythonflaskauthenticationsecurity

See Also