Flask-Security (Flask-Security-Too)
Authentication Library
Flask-Security (Flask-Security-Too)
Overview
Flask-Security is a Python library that quickly adds comprehensive security features to Flask applications. As of 2025, it continues active development as Flask-Security-Too, with version 5.6.2 being the latest release. It provides integrated functionality for authentication, authorization, role-based access control, token authentication, and two-factor authentication. Designed following OWASP best practices, it serves as a mature, production-ready library that acts as the "Swiss Army knife" for Flask developers' security requirements, handling all essential security needs for web applications.
Details
Flask-Security is maintained as part of the Pallets Community Ecosystem, receiving support from the Flask core development team. Built on top of Flask-Login, it may be overkill for simple authentication needs, but represents an ideal choice when comprehensive security features are required from the start.
Key Features
- Multiple Authentication Methods: Session, Basic HTTP, token authentication, WebAuthn
- Comprehensive User Management: User registration, login, password reset, account confirmation
- Advanced Authorization System: Role-based access control (RBAC), permission management
- Multi-Factor Authentication (MFA): Email, SMS, authenticator apps (TOTP), WebAuthn support
- Security Features: CSRF protection, password strength validation, account locking
- Social Authentication: OAuth2, OpenID Connect (authlib integration)
- API Support: RESTful API token authentication and JSON responses
Technical Characteristics
Flask-Security-Too provides a more opinionated, "batteries-included" solution by reducing reliance on abandoned projects and bundling support for common use cases. Version 5.x continues adding newer authentication/authorization standards, including Social Auth integration (using authlib) and other modern features.
Pros and Cons
Pros
- Comprehensive Functionality: Solves almost all authentication requirements with a single library
- Production Track Record: Proven in many production environments
- OWASP Compliance: Designed following security best practices
- Active Development: Continuously updated and improved as Flask-Security-Too
- Rich Customization: Fine-grained customization of templates, forms, and views
- Modern Standards Support: Latest authentication technologies like WebAuthn, OAuth2, OpenID Connect
- Detailed Documentation: Comprehensive, practical documentation with abundant examples
Cons
- High Learning Curve: Complex configuration due to rich feature set
- Over-Engineering: Excessive features for small-scale applications
- Many Dependencies: Requires numerous dependency libraries due to extensive functionality
- Customization Complexity: Requires understanding internal structure for custom requirements
- Performance Impact: Rich features may affect application startup time
Reference Pages
- Flask-Security-Too Official Documentation
- Flask-Security-Too PyPI
- Flask-Security-Too GitHub
- Flask-Security-Too Quickstart
Code Examples
Installation and Basic Setup
# Install dependencies
# pip install flask-security[common,mfa,fsqla]
import os
from flask import Flask, render_template_string
from flask_sqlalchemy import SQLAlchemy
from flask_security import Security, SQLAlchemyUserDatastore, \
UserMixin, RoleMixin, auth_required, hash_password
from flask_security.models import fsqla_v3 as fsqla
# Create Flask application
app = Flask(__name__)
app.config['DEBUG'] = True
# Security configuration
app.config['SECRET_KEY'] = os.environ.get("SECRET_KEY", 'your-secret-key')
app.config['SECURITY_PASSWORD_SALT'] = os.environ.get("SECURITY_PASSWORD_SALT", 'your-password-salt')
# Database configuration
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {"pool_pre_ping": True}
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# Cookie configuration
app.config["REMEMBER_COOKIE_SAMESITE"] = "strict"
app.config["SESSION_COOKIE_SAMESITE"] = "strict"
# Database initialization
db = SQLAlchemy(app)
fsqla.FsModels.set_db_info(db)
User Model and Role Setup
# Define Role model
class Role(db.Model, fsqla.FsRoleMixin):
pass
# Define User model
class User(db.Model, fsqla.FsUserMixin):
# Add custom fields
first_name = db.Column(db.String(255))
last_name = db.Column(db.String(255))
# Define custom payload
def get_security_payload(self):
rv = super().get_security_payload()
rv["first_name"] = self.first_name
rv["last_name"] = self.last_name
return rv
# Flask-Security setup
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)
# Database initialization
with app.app_context():
db.create_all()
# Create default roles
if not security.datastore.find_role("admin"):
security.datastore.create_role(
name="admin",
permissions={"admin-read", "admin-write", "user-read", "user-write"}
)
if not security.datastore.find_role("user"):
security.datastore.create_role(
name="user",
permissions={"user-read", "user-write"}
)
# Create default user
if not security.datastore.find_user(email="[email protected]"):
security.datastore.create_user(
email="[email protected]",
password=hash_password("admin123"),
roles=["admin"],
first_name="Admin",
last_name="User"
)
db.session.commit()
Authentication and Authorization Implementation
from flask_security import auth_required, roles_required, permissions_required
# Basic authentication required route
@app.route('/')
@auth_required()
def home():
return render_template_string("Hello {{ current_user.email }}!")
# Role-based authorization
@app.route('/admin')
@auth_required()
@roles_required('admin')
def admin_panel():
return render_template_string("Welcome to admin panel, {{ current_user.email }}!")
# Permission-based authorization
@app.route('/user-data')
@auth_required()
@permissions_required('user-read')
def user_data():
return render_template_string("User data for {{ current_user.email }}")
# Allow multiple authentication methods
@app.route('/api/data')
@auth_required("token", "session")
def api_data():
return {"message": "API data", "user": current_user.email}
# Custom authorization error handling
from flask import jsonify
import http
class MyForbiddenException(Exception):
def __init__(self, msg='Not permitted', status=http.HTTPStatus.FORBIDDEN):
self.info = {'status': status, 'msgs': [msg]}
@security.unauthz_handler
def my_unauthz_handler(func, params):
raise MyForbiddenException()
@app.errorhandler(MyForbiddenException)
def handle_forbidden(ex):
return jsonify(ex.info), ex.info['status']
Form-Based Authentication
# Flask-WTF integration and CSRF protection
import flask_wtf
from flask_wtf.csrf import CSRFProtect
# CSRF protection configuration
app.config["SECURITY_CSRF_COOKIE_NAME"] = "XSRF-TOKEN"
app.config["WTF_CSRF_TIME_LIMIT"] = None
csrf = CSRFProtect(app)
# Conditional CSRF protection (session authentication only)
app.config["WTF_CSRF_CHECK_DEFAULT"] = False
app.config["SECURITY_CSRF_PROTECT_MECHANISMS"] = ["session", "basic"]
# Custom login form
from flask_security.forms import LoginForm
from wtforms import StringField, validators
class ExtendedLoginForm(LoginForm):
username = StringField('Username', [validators.Optional()])
# Use custom form in security configuration
app.config['SECURITY_LOGIN_FORM'] = ExtendedLoginForm
# Check authentication status in templates
@app.route('/profile')
def profile():
return render_template_string("""
{% if _fs_is_user_authenticated %}
<h1>Profile for {{ current_user.email }}</h1>
<p>Role: {{ current_user.roles[0].name if current_user.roles else 'No role' }}</p>
{% else %}
<a href="{{ url_for('security.login') }}">Login</a>
{% endif %}
""")
API Token Authentication
from flask import request, jsonify
# Token authentication configuration
app.config['SECURITY_TOKEN_AUTHENTICATION_KEY'] = 'auth_token'
app.config['SECURITY_TOKEN_AUTHENTICATION_HEADER'] = 'X-Auth-Token'
app.config['SECURITY_TOKEN_MAX_AGE'] = 3600 # 1 hour
# Custom token expiry configuration
def custom_token_expiry(user):
import time
# Admin: 24 hours, regular user: 1 hour
if user.has_role('admin'):
return int(time.time()) + 86400
return int(time.time()) + 3600
app.config['SECURITY_TOKEN_EXPIRE_TIMESTAMP'] = custom_token_expiry
# API endpoints
@app.route('/api/login', methods=['POST'])
def api_login():
"""API login endpoint"""
data = request.get_json()
user = security.datastore.find_user(email=data.get('email'))
if user and user.verify_and_update_password(data.get('password')):
login_user(user)
token = user.get_auth_token()
return jsonify({
'token': token,
'user': user.get_security_payload()
})
return jsonify({'error': 'Invalid credentials'}), 401
@app.route('/api/protected')
@auth_required('token')
def protected_api():
"""API endpoint requiring token authentication"""
return jsonify({
'message': 'Protected data',
'user_id': current_user.id,
'permissions': [p for role in current_user.roles for p in role.permissions]
})
# Client-side token usage example (JavaScript)
def get_example_js():
return """
// Access API with token
function callProtectedAPI(token) {
return fetch('/api/protected', {
method: 'GET',
headers: {
'X-Auth-Token': token,
'Content-Type': 'application/json'
}
}).then(response => response.json());
}
// POST request with CSRF token
function postWithCSRF(data) {
return fetch('/api/data', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCookieValue('XSRF-TOKEN')
},
body: JSON.stringify(data)
});
}
"""
Testing and Security Configuration
import pytest
from flask import url_for
# Test configuration
@pytest.fixture
def app():
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
app.config['SECURITY_EMAIL_VALIDATOR_ARGS'] = {"check_deliverability": False}
with app.app_context():
db.create_all()
yield app
db.drop_all()
# Authentication tests
def test_login_required(client):
"""Test access to authentication-required page"""
response = client.get('/')
assert response.status_code == 302 # Redirect
assert '/login' in response.location
def test_valid_login(client):
"""Test valid login"""
response = client.post('/login', data={
'email': '[email protected]',
'password': 'admin123'
}, follow_redirects=True)
assert response.status_code == 200
def test_api_token_auth(client):
"""Test API token authentication"""
# Login and get token
login_response = client.post('/api/login', json={
'email': '[email protected]',
'password': 'admin123'
})
token = login_response.get_json()['token']
# Access API with token
api_response = client.get('/api/protected', headers={
'X-Auth-Token': token
})
assert api_response.status_code == 200
def test_role_based_access(client):
"""Test role-based access control"""
# Login as regular user
client.post('/login', data={
'email': '[email protected]',
'password': 'user123'
})
# Access admin page (expect 403 error)
response = client.get('/admin')
assert response.status_code == 403
# Enhanced security configuration
app.config.update({
# Password complexity
'SECURITY_PASSWORD_LENGTH_MIN': 8,
'SECURITY_PASSWORD_COMPLEXITY_CHECKER': 'zxcvbn',
# Account locking
'SECURITY_LOGIN_ERROR_VIEW': '/login-error',
'SECURITY_TRACKABLE': True,
# Email configuration
'SECURITY_EMAIL_VALIDATOR_ARGS': {'check_deliverability': True},
'SECURITY_CONFIRMABLE': True,
'SECURITY_RECOVERABLE': True,
# Session configuration
'PERMANENT_SESSION_LIFETIME': 3600, # 1 hour
'SESSION_COOKIE_SECURE': True, # HTTPS required
'SESSION_COOKIE_HTTPONLY': True,
# Other security settings
'SECURITY_FLASH_MESSAGES': True,
'SECURITY_REDIRECT_BEHAVIOR': 'spa', # SPA support
})
if __name__ == '__main__':
app.run(debug=True)