User Authentication in Flask
Authentication is a critical aspect of most web applications, allowing users to create accounts, log in, and access protected resources. This guide covers implementing user authentication in Flask applications using Flask-Login and best security practices.
Getting Started with Flask-Login
Flask-Login is a popular extension that provides user session management for Flask. It handles logging in, logging out, and remembering users' sessions.
1. Installation
Install Flask-Login using pip:
pip install flask-login
2. Basic Setup
Configure Flask-Login in your application:
# In your app.py or __init__.py file
from flask import Flask
from flask_login import LoginManager
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key' # Required for session security
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login' # Specify the login view route
login_manager.login_message_category = 'info' # Optional: flash message category
3. User Model
Create a User model that implements the required Flask-Login methods:
# models.py
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from . import db # Assuming you're using a database (e.g., SQLAlchemy)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
email = db.Column(db.String(120), unique=True, index=True)
password_hash = db.Column(db.String(128))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
# Load user callback for Flask-Login
from . import login_manager
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
Implementing Authentication Routes
Now let's implement the routes for user registration, login, and logout.
1. Form Classes
Create forms for registration and login:
# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
from .models import User
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=20)])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired(), Length(min=8)])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('Username already taken. Please choose a different one.')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('Email already registered. Please use a different one.')
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Log In')
2. Authentication Routes
Create routes for registration, login, and logout:
# routes/auth.py
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.urls import url_parse
from . import db
from .models import User
from .forms import RegistrationForm, LoginForm
auth = Blueprint('auth', __name__)
@auth.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Your account has been created! You can now log in.', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', title='Register', form=form)
@auth.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid email or password', 'danger')
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('main.index')
return redirect(next_page)
return render_template('auth/login.html', title='Log In', form=form)
@auth.route('/logout')
def logout():
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('main.index'))
Creating Authentication Templates
Now let's create the HTML templates for registration and login forms.
1. Register Template
<!-- templates/auth/register.html -->
{% extends 'base.html' %}
{% block content %}
<div class="container mx-auto mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2 class="text-center">Register</h2>
</div>
<div class="card-body">
<form method="POST" action="">
{{ form.hidden_tag() }}
<div class="form-group mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control") }}
{% if form.username.errors %}
<div class="text-danger">
{% for error in form.username.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
<div class="text-danger">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% if form.password.errors %}
<div class="text-danger">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group mb-3">
{{ form.confirm_password.label(class="form-label") }}
{{ form.confirm_password(class="form-control") }}
{% if form.confirm_password.errors %}
<div class="text-danger">
{% for error in form.confirm_password.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group mb-3 text-center">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
<div class="card-footer text-center">
Already have an account? <a href="{{ url_for('auth.login') }}">Log In</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
2. Login Template
<!-- templates/auth/login.html -->
{% extends 'base.html' %}
{% block content %}
<div class="container mx-auto mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2 class="text-center">Log In</h2>
</div>
<div class="card-body">
<form method="POST" action="">
{{ form.hidden_tag() }}
<div class="form-group mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
<div class="text-danger">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% if form.password.errors %}
<div class="text-danger">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-check mb-3">
{{ form.remember_me(class="form-check-input") }}
{{ form.remember_me.label(class="form-check-label") }}
</div>
<div class="form-group mb-3 text-center">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
<div class="card-footer text-center">
Don't have an account? <a href="{{ url_for('auth.register') }}">Register</a>
<br>
<a href="{{ url_for('auth.reset_password_request') }}">Forgot your password?</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Protecting Routes
Use the @login_required
decorator to protect routes that should only be accessible to authenticated users.
# routes/protected.py
from flask import Blueprint, render_template
from flask_login import login_required, current_user
protected = Blueprint('protected', __name__)
@protected.route('/profile')
@login_required
def profile():
return render_template('profile.html', user=current_user)
@protected.route('/settings')
@login_required
def settings():
return render_template('settings.html', user=current_user)
When an unauthenticated user tries to access a protected route, they will be redirected to the login page specified in the login_view
parameter when setting up Flask-Login.
Password Reset Functionality
A complete authentication system should include a password reset feature. Here's how to implement it:
1. Form Classes
# forms.py
class ResetPasswordRequestForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
submit = SubmitField('Request Password Reset')
class ResetPasswordForm(FlaskForm):
password = PasswordField('New Password', validators=[DataRequired(), Length(min=8)])
confirm_password = PasswordField('Confirm New Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Reset Password')
2. Token Generation
Add methods to generate and verify reset tokens:
# models.py
from time import time
import jwt
from flask import current_app
class User(UserMixin, db.Model):
# ... existing code ...
def get_reset_password_token(self, expires_in=600):
return jwt.encode(
{'reset_password': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'],
algorithm='HS256'
)
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)['reset_password']
except:
return None
return User.query.get(id)
3. Email Function
Create a function to send password reset emails:
# email.py
from flask import render_template
from flask_mail import Message
from . import mail
def send_password_reset_email(user):
token = user.get_reset_password_token()
msg = Message('Password Reset Request',
sender='[email protected]',
recipients=[user.email])
msg.body = f'''To reset your password, visit the following link:
{url_for('auth.reset_password', token=token, _external=True)}
If you did not make this request, please ignore this email.
'''
mail.send(msg)
4. Reset Routes
# routes/auth.py
@auth.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = ResetPasswordRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user:
send_password_reset_email(user)
flash('Check your email for instructions to reset your password.', 'info')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password_request.html', title='Reset Password', form=form)
@auth.route('/reset_password/', methods=['GET', 'POST'])
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('main.index'))
user = User.verify_reset_password_token(token)
if not user:
flash('Invalid or expired token', 'warning')
return redirect(url_for('auth.reset_password_request'))
form = ResetPasswordForm()
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash('Your password has been reset.', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password.html', form=form)
Advanced Authentication Features
Feature | Description | Implementation |
---|---|---|
Two-Factor Authentication (2FA) | Add an extra layer of security using time-based one-time passwords (TOTP) | Use pyotp and Flask-TOTP to implement 2FA with authenticator apps |
OAuth Integration | Allow users to log in with social media accounts (Google, Facebook, etc.) | Use Flask-Dance or Authlib to implement OAuth authentication |
Rate Limiting | Prevent brute force attacks by limiting login attempts | Use Flask-Limiter to implement rate limiting on authentication routes |
Account Lockout | Lock accounts after multiple failed login attempts | Track failed attempts in the database and implement lockout logic |
Remember Me Functionality | Allow users to stay logged in across browser sessions | Use Flask-Login's remember functionality and secure cookies |
Role-Based Access Control | Define user roles with different permissions | Use Flask-Principal or implement custom role logic |
Security Best Practices
- Use HTTPS: Always serve your application over HTTPS in production
- Hash passwords: Never store plain-text passwords, use secure hashing algorithms like bcrypt
- Implement CSRF protection: Protect against cross-site request forgery attacks
- Set secure cookie flags: Use secure, HTTPOnly, and SameSite cookie attributes
- Implement account lockout: Lock accounts after multiple failed login attempts
- Use strong password policies: Require complex passwords with minimum length
- Implement logging: Log authentication events for security monitoring
- Use parameterized SQL queries: Prevent SQL injection attacks
- Sanitize user input: Prevent cross-site scripting (XSS) attacks
- Implement proper session management: Use secure session handling
- Validate email addresses: Implement email verification for new accounts
- Keep dependencies updated: Regularly update libraries to fix security vulnerabilities