Flask-Login
Authentication Library
Flask-Login
Overview
Flask-Login is a simple and lightweight user session management library for Flask applications. As of 2025, it continues to be actively maintained and widely used as a fundamental authentication feature in Flask applications. Characterized by a minimal design that focuses on user login, logout, and session memory features without providing complex authentication functions, it enables Flask beginners to easily build user authentication systems through intuitive APIs including UserMixin class, login_required decorator, and current_user proxy.
Details
Flask-Login follows a design philosophy focused on "how to do it" rather than "what to do." By delegating authentication logic (password verification, user registration, etc.) to developers and providing only session management functionality, it achieves high flexibility. It also supports advanced features such as session protection, remember me functionality, custom user loaders, and request loaders, enabling implementation according to security requirements. Due to its Flask-specific design, integration with the Flask ecosystem (WTForms, SQLAlchemy, etc.) is very smooth.
Key Features
- Lightweight Design: High-speed session management focused on essential minimum functionality
- Flask Integration: Perfect integration with Flask ecosystem and simple configuration
- Flexibility: High customizability through design that delegates authentication logic to developers
- Security: Standard equipped with session protection, CSRF countermeasures, and remember me functionality
- Testing Support: Automated testing support through FlaskLoginClient
- Production Track Record: Proven stability in many Flask applications
Advantages and Disadvantages
Advantages
- Established stable library as the standard authentication implementation for Flask applications
- Low learning cost and easy understanding through minimal API
- High degree of freedom in authentication logic, flexible response to business requirements
- Excellent compatibility with Flask ecosystem (WTForms, SQLAlchemy, etc.)
- Low overhead and good performance due to lightweight design
- Reliability and stable operation guarantee through years of track record
Disadvantages
- Additional implementation required for complex authentication requirements due to basic functionality only
- External authentication like OAuth, OpenID Connect requires separate implementation
- No support for advanced security features like multi-factor authentication (MFA)
- Limited scalability in large-scale applications
- Session-based constraints for scale-out
- Flask-only usage makes cross-framework migration difficult
Reference Pages
Usage Examples
Installation and Basic Configuration
# Install Flask-Login
pip install Flask-Login
# Install related packages
pip install Flask
pip install Flask-WTF # Form processing
pip install Flask-SQLAlchemy # Database
pip install Werkzeug # Password hashing
# app.py - Flask-Login Basic Configuration
import flask
import flask_login
from werkzeug.security import generate_password_hash, check_password_hash
# Flask application initialization
app = flask.Flask(__name__)
app.secret_key = 'your-secret-key-change-this' # Get from environment variables in production
# Flask-Login initialization
login_manager = flask_login.LoginManager()
login_manager.init_app(app)
# Settings for unauthenticated access to pages requiring login
login_manager.login_view = 'login'
login_manager.login_message = 'Login is required.'
login_manager.login_message_category = 'info'
# Session protection settings (strong: robust, basic: basic, None: disabled)
login_manager.session_protection = 'strong'
# Remember me function customization
login_manager.refresh_view = 'reauthenticate'
login_manager.needs_refresh_message = 'Re-authentication is required for security.'
login_manager.needs_refresh_message_category = 'info'
User Model and UserMixin
# models.py - User Model Definition
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
class User(UserMixin):
"""User model for Flask-Login"""
def __init__(self, user_id, email, password_hash, name=None, is_active=True):
self.id = user_id
self.email = email
self.password_hash = password_hash
self.name = name
self.active = is_active
self.created_at = datetime.utcnow()
self.last_login = None
def check_password(self, password):
"""Password verification"""
return check_password_hash(self.password_hash, password)
def set_password(self, password):
"""Password setting"""
self.password_hash = generate_password_hash(password)
def is_active(self):
"""Check if active user"""
return self.active
def is_authenticated(self):
"""Check if authenticated user"""
return True
def is_anonymous(self):
"""Check if anonymous user"""
return False
def get_id(self):
"""Return user ID as string"""
return str(self.id)
def update_last_login(self):
"""Update last login time"""
self.last_login = datetime.utcnow()
def __repr__(self):
return f'<User {self.email}>'
# Simple in-memory database (use SQLAlchemy etc. in actual development)
users_db = {
'1': User('1', '[email protected]', generate_password_hash('admin123'), 'Administrator'),
'2': User('2', '[email protected]', generate_password_hash('user123'), 'Regular User'),
'3': User('3', '[email protected]', generate_password_hash('demo123'), 'Demo User'),
}
def create_user(email, password, name):
"""Create new user"""
user_id = str(len(users_db) + 1)
password_hash = generate_password_hash(password)
user = User(user_id, email, password_hash, name)
users_db[user_id] = user
return user
def get_user_by_email(email):
"""Search user by email address"""
for user in users_db.values():
if user.email == email:
return user
return None
def authenticate_user(email, password):
"""User authentication"""
user = get_user_by_email(email)
if user and user.check_password(password):
user.update_last_login()
return user
return None
User Loader and Request Loader
# auth.py - Authentication Related Configuration
import base64
from flask import request, g
from flask_login import user_loaded_from_request
@login_manager.user_loader
def load_user(user_id):
"""Get user by user ID from session"""
return users_db.get(user_id)
@login_manager.request_loader
def load_user_from_request(request):
"""Get user from request (for API authentication)"""
# 1. Get API Key from URL parameters
api_key = request.args.get('api_key')
if api_key:
user = get_user_by_api_key(api_key)
if user:
return user
# 2. Get from Basic authentication header
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Basic '):
try:
# Parse Basic authentication
credentials = base64.b64decode(
auth_header.replace('Basic ', '', 1)
).decode('utf-8')
email, password = credentials.split(':', 1)
# User authentication
user = authenticate_user(email, password)
if user:
return user
except (ValueError, TypeError):
pass
# 3. Bearer token authentication (JWT token etc.)
if auth_header and auth_header.startswith('Bearer '):
token = auth_header.replace('Bearer ', '', 1)
user = get_user_by_token(token)
if user:
return user
return None
@user_loaded_from_request.connect
def user_loaded_from_request(app, user=None):
"""Signal when user is loaded from request"""
g.login_via_request = True
def get_user_by_api_key(api_key):
"""Search user by API Key (simple implementation)"""
# Use dedicated API Key table in actual implementation
api_keys = {
'admin-api-key-123': '1',
'user-api-key-456': '2'
}
user_id = api_keys.get(api_key)
return users_db.get(user_id) if user_id else None
def get_user_by_token(token):
"""Search user by Token (JWT etc.)"""
# Use JWT library to verify token in actual implementation
# Here is simple implementation
if token == 'valid-jwt-token':
return users_db.get('1')
return None
Authentication Views and Form Processing
# views.py - Authentication Related Views
from flask import render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.urls import url_parse
@app.route('/')
def index():
"""Home page"""
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Login page"""
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
remember = bool(request.form.get('remember'))
if not email or not password:
flash('Please enter email address and password.', 'error')
return render_template('login.html')
# User authentication
user = authenticate_user(email, password)
if user:
# Login successful
login_user(user, remember=remember)
flash(f'Welcome, {user.name}!', 'success')
# Process next parameter (redirect attack countermeasure)
next_page = request.args.get('next')
if next_page:
# Check safe URL (allow only within same domain)
parsed_url = url_parse(next_page)
if parsed_url.netloc == '' or parsed_url.netloc == request.host:
return redirect(next_page)
return redirect(url_for('dashboard'))
else:
flash('Email address or password is incorrect.', 'error')
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
"""Logout"""
user_name = current_user.name
logout_user()
flash(f'{user_name}, you have been logged out.', 'info')
return redirect(url_for('index'))
@app.route('/register', methods=['GET', 'POST'])
def register():
"""User registration"""
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
name = request.form.get('name')
# Validation
if not all([email, password, confirm_password, name]):
flash('Please fill in all fields.', 'error')
return render_template('register.html')
if password != confirm_password:
flash('Passwords do not match.', 'error')
return render_template('register.html')
if len(password) < 6:
flash('Password must be at least 6 characters.', 'error')
return render_template('register.html')
# Check existing user
if get_user_by_email(email):
flash('This email address is already registered.', 'error')
return render_template('register.html')
# Create user
user = create_user(email, password, name)
login_user(user)
flash(f'{user.name}, registration completed!', 'success')
return redirect(url_for('dashboard'))
return render_template('register.html')
@app.route('/dashboard')
@login_required
def dashboard():
"""Dashboard (login required)"""
return render_template('dashboard.html', user=current_user)
@app.route('/profile')
@login_required
def profile():
"""Profile page"""
return render_template('profile.html', user=current_user)
Advanced Authentication Features and Security
# advanced_auth.py - Advanced Authentication Features
from flask import session, g
from flask_login import fresh_login_required, confirm_login
from flask.sessions import SecureCookieSessionInterface
from datetime import datetime, timedelta
import secrets
# Custom session interface (for API)
class CustomSessionInterface(SecureCookieSessionInterface):
"""Prevent session creation during API requests"""
def save_session(self, *args, **kwargs):
if g.get('login_via_request'):
return
return super().save_session(*args, **kwargs)
app.session_interface = CustomSessionInterface()
@app.route('/admin')
@fresh_login_required
def admin():
"""Admin page (fresh login required)"""
return render_template('admin.html')
@app.route('/reauthenticate', methods=['GET', 'POST'])
@login_required
def reauthenticate():
"""Re-authentication page"""
if request.method == 'POST':
password = request.form.get('password')
if current_user.check_password(password):
confirm_login()
flash('Re-authentication completed.', 'success')
next_page = request.args.get('next', url_for('admin'))
return redirect(next_page)
else:
flash('Password is incorrect.', 'error')
return render_template('reauthenticate.html')
@login_manager.unauthorized_handler
def unauthorized():
"""Custom handling for unauthenticated users"""
if request.blueprint == 'api':
# Return JSON 401 for API endpoints
return jsonify({'error': 'Authentication required'}), 401
# Redirect to login page for web pages
flash('You need to log in to access this page.', 'warning')
return redirect(url_for('login', next=request.url))
@login_manager.needs_refresh_handler
def refresh():
"""Handling when re-authentication is required"""
return redirect(url_for('reauthenticate', next=request.url))
# Session management helper functions
def extend_session():
"""Extend session expiration"""
session.permanent = True
app.permanent_session_lifetime = timedelta(hours=1)
def create_csrf_token():
"""Generate CSRF token"""
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(16)
return session['csrf_token']
def validate_csrf_token(token):
"""Validate CSRF token"""
return session.get('csrf_token') == token
# API authentication decorator
from functools import wraps
def api_key_required(f):
"""API Key authentication decorator"""
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key') or request.args.get('api_key')
if not api_key:
return jsonify({'error': 'API key required'}), 401
user = get_user_by_api_key(api_key)
if not user:
return jsonify({'error': 'Invalid API key'}), 401
g.current_user = user
return f(*args, **kwargs)
return decorated_function
@app.route('/api/profile')
@api_key_required
def api_profile():
"""API: Profile retrieval"""
user = g.current_user
return jsonify({
'id': user.id,
'email': user.email,
'name': user.name,
'last_login': user.last_login.isoformat() if user.last_login else None
})
@app.route('/api/data')
@login_required # Session authentication
def api_data():
"""API: Data retrieval (session authentication)"""
return jsonify({
'message': 'Authenticated via session',
'user': current_user.email,
'data': ['item1', 'item2', 'item3']
})
Templates and Frontend Integration
<!-- templates/base.html - Base Template -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Flask-Login Demo{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<!-- Navigation Bar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">Flask App</a>
<div class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
<span class="navbar-text me-3">
Hello, {{ current_user.name }}
</span>
<a class="nav-link" href="{{ url_for('dashboard') }}">Dashboard</a>
<a class="nav-link" href="{{ url_for('profile') }}">Profile</a>
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
{% else %}
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
<a class="nav-link" href="{{ url_for('register') }}">Register</a>
{% endif %}
</div>
</div>
</nav>
<!-- Main Content -->
<div class="container mt-4">
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
<!-- templates/login.html - Login Page -->
{% extends "base.html" %}
{% block title %}Login - Flask-Login Demo{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">Login</h4>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember" name="remember">
<label class="form-check-label" for="remember">Remember login state</label>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
<div class="text-center mt-3">
<a href="{{ url_for('register') }}">Don't have an account? Register</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
<!-- templates/dashboard.html - Dashboard -->
{% extends "base.html" %}
{% block title %}Dashboard - Flask-Login Demo{% endblock %}
{% block content %}
<h1>Dashboard</h1>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5>User Information</h5>
</div>
<div class="card-body">
<table class="table">
<tr>
<th>User ID:</th>
<td>{{ user.id }}</td>
</tr>
<tr>
<th>Name:</th>
<td>{{ user.name }}</td>
</tr>
<tr>
<th>Email Address:</th>
<td>{{ user.email }}</td>
</tr>
<tr>
<th>Last Login:</th>
<td>{{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') if user.last_login else 'First login' }}</td>
</tr>
<tr>
<th>Created:</th>
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5>Quick Actions</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('profile') }}" class="btn btn-outline-primary">Edit Profile</a>
<a href="{{ url_for('admin') }}" class="btn btn-outline-warning">Admin Panel</a>
<a href="{{ url_for('logout') }}" class="btn btn-outline-danger">Logout</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Testing and Flask Test Client
# test_auth.py - Flask-Login Testing
import unittest
from flask_login import FlaskLoginClient
from app import app, users_db, create_user
class AuthTestCase(unittest.TestCase):
def setUp(self):
"""Setup before testing"""
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
app.test_client_class = FlaskLoginClient
self.client = app.test_client()
# Create test user
self.test_user = create_user('[email protected]', 'testpass', 'Test User')
def tearDown(self):
"""Cleanup after testing"""
# Delete test user
if self.test_user.id in users_db:
del users_db[self.test_user.id]
def test_login_logout(self):
"""Login/logout test"""
# Login test
response = self.client.post('/login', data={
'email': '[email protected]',
'password': 'testpass'
}, follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertIn('Dashboard', response.get_data(as_text=True))
# Logout test
response = self.client.get('/logout', follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertIn('logged out', response.get_data(as_text=True))
def test_login_required(self):
"""Login required page test"""
response = self.client.get('/dashboard')
self.assertEqual(response.status_code, 302) # Redirect
# Access after login
with self.client.session_transaction() as sess:
# Set user ID directly in session
sess['_user_id'] = self.test_user.id
sess['_fresh'] = True
response = self.client.get('/dashboard')
self.assertEqual(response.status_code, 200)
def test_api_authentication(self):
"""API authentication test"""
# API Key authentication
response = self.client.get('/api/profile', headers={
'X-API-Key': 'admin-api-key-123'
})
self.assertEqual(response.status_code, 200)
# Invalid API Key
response = self.client.get('/api/profile', headers={
'X-API-Key': 'invalid-key'
})
self.assertEqual(response.status_code, 401)
def test_with_logged_in_user(self):
"""Test with logged in user"""
with self.client.user(self.test_user):
# This request is executed in logged in state
response = self.client.get('/dashboard')
self.assertEqual(response.status_code, 200)
self.assertIn('Test User', response.get_data(as_text=True))
def test_remember_me(self):
"""Remember Me function test"""
response = self.client.post('/login', data={
'email': '[email protected]',
'password': 'testpass',
'remember': 'on'
})
# Check if remember_token cookie is set
cookies = {cookie.name: cookie.value for cookie in self.client.cookie_jar}
self.assertIn('remember_token', cookies)
def test_invalid_login(self):
"""Invalid login test"""
response = self.client.post('/login', data={
'email': '[email protected]',
'password': 'wrongpass'
})
self.assertEqual(response.status_code, 200)
self.assertIn('incorrect', response.get_data(as_text=True))
if __name__ == '__main__':
unittest.main()