Simple security for Flask apps
Flask-Security provides WTForms-based form classes for handling common security-related user interactions including login, registration, password reset, and account confirmation. These forms include built-in validation, CSRF protection, and customization options.
Forms for user authentication and login.
class LoginForm:
"""Default login form with email, password, and remember fields"""
# Fields
email: StringField # User email address
password: PasswordField # User password
remember: BooleanField # Remember me checkbox
next: HiddenField # Redirect URL after login
submit: SubmitField # Submit button
def validate(self):
"""
Custom validation logic for login form.
Returns:
bool: True if form data is valid
"""Forms for user account creation and registration.
class ConfirmRegisterForm:
"""Base registration form for confirmable registration"""
# Fields
email: StringField # User email (with unique validation)
password: PasswordField # User password (with length validation)
submit: SubmitField # Submit button
def to_dict(self):
"""
Convert form data to dictionary.
Returns:
dict: Form field values
"""
class RegisterForm:
"""Default registration form extending ConfirmRegisterForm"""
# Inherits email, password, submit from ConfirmRegisterForm
password_confirm: PasswordField # Password confirmation field
next: HiddenField # Redirect URL after registrationForms for password reset and password change operations.
class ForgotPasswordForm:
"""Form for requesting password reset"""
# Fields
email: StringField # Email address (with existing user validation)
submit: SubmitField # Submit button
def validate(self):
"""
Validate email and ensure user doesn't require confirmation.
Returns:
bool: True if form data is valid
"""
class ResetPasswordForm:
"""Form for resetting password with token"""
# Fields
password: PasswordField # New password (with length validation)
password_confirm: PasswordField # Password confirmation
submit: SubmitField # Submit button
class ChangePasswordForm:
"""Form for changing existing password"""
# Fields
password: PasswordField # Current password
new_password: PasswordField # New password
new_password_confirm: PasswordField # New password confirmation
submit: SubmitField # Submit buttonForms for passwordless login and confirmation management.
class PasswordlessLoginForm:
"""Form for passwordless login via email"""
# Fields
email: StringField # Email address (with existing user validation)
submit: SubmitField # Submit button
def validate(self):
"""
Validate email and ensure user is active.
Returns:
bool: True if form data is valid
"""
class SendConfirmationForm:
"""Form for requesting email confirmation"""
# Fields
email: StringField # Email address
submit: SubmitField # Submit buttonBase mixins that provide common form functionality.
class EmailFormMixin:
"""Mixin providing email field"""
email: StringField
class PasswordFormMixin:
"""Mixin providing password field with validation"""
password: PasswordField
class NewPasswordFormMixin:
"""Mixin providing new password field with validation"""
password: PasswordField
class PasswordConfirmFormMixin:
"""Mixin providing password confirmation field"""
password_confirm: PasswordField
class NextFormMixin:
"""Mixin providing next URL hidden field"""
next: HiddenField
class RegisterFormMixin:
"""Mixin providing registration-specific functionality"""
passCustom validators used by Flask-Security forms.
def unique_user_email(form, field):
"""
Validator ensuring email address is unique.
Args:
form: Form instance
field: Email field
Raises:
ValidationError: If email already exists
"""
def valid_user_email(form, field):
"""
Validator ensuring email belongs to existing user.
Args:
form: Form instance
field: Email field
Raises:
ValidationError: If user doesn't exist
"""
def get_form_field_label(key):
"""
Get localized form field label.
Args:
key (str): Field label key
Returns:
str: Localized label text
"""from flask import render_template, request, redirect
from flask_security import Security, login_user
from flask_security.forms import LoginForm
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = user_datastore.find_user(email=form.email.data)
if user and verify_password(form.password.data, user.password):
login_user(user, remember=form.remember.data)
return redirect(form.next.data or '/')
return render_template('login.html', form=form)from flask_security.forms import LoginForm, RegisterForm
from wtforms import StringField, TextAreaField
from wtforms.validators import DataRequired
class CustomLoginForm(LoginForm):
username = StringField('Username or Email', validators=[DataRequired()])
def validate(self):
# Custom validation logic
if not super().validate():
return False
# Allow login with username or email
user = (user_datastore.find_user(email=self.username.data) or
user_datastore.find_user(username=self.username.data))
if not user:
self.username.errors.append('Invalid username or email')
return False
return True
class ExtendedRegisterForm(RegisterForm):
first_name = StringField('First Name', validators=[DataRequired()])
last_name = StringField('Last Name', validators=[DataRequired()])
bio = TextAreaField('Bio')
def validate(self):
if not super().validate():
return False
# Custom validation for names
if len(self.first_name.data) < 2:
self.first_name.errors.append('First name too short')
return False
return True
# Register custom forms with Flask-Security
security = Security(app, user_datastore,
login_form=CustomLoginForm,
register_form=ExtendedRegisterForm)<!-- login.html template -->
<form method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<div class="form-group">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% for error in form.password.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<div class="form-check">
{{ form.remember(class="form-check-input") }}
{{ form.remember.label(class="form-check-label") }}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>from flask_security.forms import RegisterForm
from flask_security import hash_password
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm()
if form.validate_on_submit():
# Create user with form data
user = user_datastore.create_user(
email=form.email.data,
password=hash_password(form.password.data)
)
user_datastore.commit()
# Send confirmation email if enabled
if app.config.get('SECURITY_CONFIRMABLE'):
send_confirmation_instructions(user)
message = 'Check your email to confirm your account'
else:
login_user(user)
message = 'Registration successful'
flash(message)
return redirect('/')
return render_template('register.html', form=form)from wtforms.validators import ValidationError
from flask_security.forms import RegisterForm
def validate_strong_password(form, field):
"""Custom validator for strong passwords"""
password = field.data
if len(password) < 8:
raise ValidationError('Password must be at least 8 characters')
if not any(c.isupper() for c in password):
raise ValidationError('Password must contain an uppercase letter')
if not any(c.islower() for c in password):
raise ValidationError('Password must contain a lowercase letter')
if not any(c.isdigit() for c in password):
raise ValidationError('Password must contain a number')
class StrongPasswordRegisterForm(RegisterForm):
password = PasswordField('Password', validators=[
DataRequired(),
validate_strong_password
])from flask import jsonify
from flask_security.forms import LoginForm
@app.route('/api/login', methods=['POST'])
def api_login():
form = LoginForm()
if form.validate_on_submit():
user = user_datastore.find_user(email=form.email.data)
if user and verify_password(form.password.data, user.password):
login_user(user, remember=form.remember.data)
return jsonify({
'success': True,
'user': user.get_security_payload(),
'auth_token': user.get_auth_token()
})
# Return validation errors
return jsonify({
'success': False,
'errors': form.errors
}), 400from flask import session
from flask_security.forms import RegisterForm
class Step1RegisterForm(Form):
email = StringField('Email', validators=[DataRequired(), Email()])
submit = SubmitField('Next')
class Step2RegisterForm(Form):
password = PasswordField('Password', validators=[DataRequired()])
password_confirm = PasswordField('Confirm Password')
submit = SubmitField('Complete Registration')
@app.route('/register/step1', methods=['GET', 'POST'])
def register_step1():
form = Step1RegisterForm()
if form.validate_on_submit():
session['registration_email'] = form.email.data
return redirect('/register/step2')
return render_template('register_step1.html', form=form)
@app.route('/register/step2', methods=['GET', 'POST'])
def register_step2():
if 'registration_email' not in session:
return redirect('/register/step1')
form = Step2RegisterForm()
if form.validate_on_submit():
user = user_datastore.create_user(
email=session.pop('registration_email'),
password=hash_password(form.password.data)
)
user_datastore.commit()
return redirect('/login')
return render_template('register_step2.html', form=form)tessl i tessl/pypi-flask-security@3.0.0