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.

Important
Authentication and security are complex topics. This guide covers the basics, but for production applications, consider using established libraries and frameworks, and always follow the latest security best 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
Security Warning
Authentication is a complex topic with significant security implications. For production applications, consider using well-established libraries and frameworks that have been thoroughly tested and reviewed.