Flask-Security (Flask-Security-Too)

authentication libraryPythonFlasksecurityuser managementauthorizationtoken authtwo-factor authentication

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

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)