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_authenticated→True(the user passed login)is_active→True(account not disabled)is_anonymous→False(not an anonymous user)get_id()→ returnsstr(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.
See Also
- Python Django Admin Get an intuitive feel for Django Admin so Python behavior stops feeling unpredictable.
- Python Django Basics Get an intuitive feel for Django Basics so Python behavior stops feeling unpredictable.
- Python Django Celery Integration Why your Django app needs a helper to handle slow jobs in the background.
- Python Django Channels Websockets How Django can send real-time updates to your browser without you refreshing the page.
- Python Django Custom Management Commands How to teach Django new tricks by creating your own command-line shortcuts.