Authentication: Securing Your Application
Welcome to Authentication! Think of authentication as digital ID cards for your users. It verifies who they are and controls what they can access in your application.
Flask-Login Basics
Flask-Login manages user sessions and authentication:
pip install flask-login
from flask import Flask, render_template, redirect, url_for, flash, request
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login' # Redirect to login page if not authenticated
class User(UserMixin):
def __init__(self, id, username, email, password_hash):
self.id = id
self.username = username
self.email = email
self.password_hash = password_hash
# Mock user database (in real app, use SQLAlchemy)
users = {
1: User(1, 'admin', 'admin@example.com', generate_password_hash('password')),
2: User(2, 'user', 'user@example.com', generate_password_hash('password'))
}
@login_manager.user_loader
def load_user(user_id):
return users.get(int(user_id))
@app.route('/')
def home():
return render_template('home.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = next((u for u in users.values() if u.username == username), None)
if user and check_password_hash(user.password_hash, password):
login_user(user)
next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('dashboard'))
else:
flash('Invalid username or password', 'error')
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('home'))
@app.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html', user=current_user)
if __name__ == '__main__':
app.run(debug=True)
User Registration and Login
Complete authentication system with registration:
from flask import Flask, render_template, redirect, url_for, flash, request
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Email, EqualTo, Length
from werkzeug.security import generate_password_hash, check_password_hash
import re
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
class User(UserMixin):
def __init__(self, id, username, email, password_hash, is_active=True):
self.id = id
self.username = username
self.email = email
self.password_hash = password_hash
self.is_active = is_active
# Mock database
users = {}
user_id_counter = 1
@login_manager.user_loader
def load_user(user_id):
return users.get(int(user_id))
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=6)])
confirm_password = PasswordField('Confirm Password',
validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Sign Up')
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember = BooleanField('Remember Me')
submit = SubmitField('Login')
def validate_password_strength(password):
"""Validate password meets security requirements."""
if len(password) < 8:
return False, "Password must be at least 8 characters long"
if not re.search(r'[A-Z]', password):
return False, "Password must contain at least one uppercase letter"
if not re.search(r'[a-z]', password):
return False, "Password must contain at least one lowercase letter"
if not re.search(r'[0-9]', password):
return False, "Password must contain at least one number"
return True, "Password is strong"
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
form = RegistrationForm()
if form.validate_on_submit():
# Check if username or email already exists
if any(u.username == form.username.data for u in users.values()):
flash('Username already exists!', 'error')
return render_template('register.html', form=form)
if any(u.email == form.email.data for u in users.values()):
flash('Email already registered!', 'error')
return render_template('register.html', form=form)
# Validate password strength
is_valid, message = validate_password_strength(form.password.data)
if not is_valid:
flash(message, 'error')
return render_template('register.html', form=form)
# Create new user
global user_id_counter
user_id = user_id_counter
user_id_counter += 1
new_user = User(
id=user_id,
username=form.username.data,
email=form.email.data,
password_hash=generate_password_hash(form.password.data)
)
users[user_id] = new_user
flash('Registration successful! Please log in.', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
form = LoginForm()
if form.validate_on_submit():
user = next((u for u in users.values() if u.username == form.username.data), None)
if user and check_password_hash(user.password_hash, form.password.data):
login_user(user, remember=form.remember.data)
next_page = request.args.get('next')
flash('Login successful!', 'success')
return redirect(next_page) if next_page else redirect(url_for('dashboard'))
else:
flash('Invalid username or password', 'error')
return render_template('login.html', form=form)
@app.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('home'))
@app.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html')
if __name__ == '__main__':
app.run(debug=True)
Password Security
Implement secure password handling:
from werkzeug.security import generate_password_hash, check_password_hash
import secrets
import string
def generate_secure_password(length=12):
"""Generate a secure random password."""
characters = string.ascii_letters + string.digits + string.punctuation
return ''.join(secrets.choice(characters) for _ in range(length))
def hash_password(password):
"""Hash a password for storing."""
return generate_password_hash(password, method='pbkdf2:sha256', salt_length=16)
def verify_password(password_hash, password):
"""Verify a password against its hash."""
return check_password_hash(password_hash, password)
# Password reset functionality
def generate_reset_token():
"""Generate a secure token for password reset."""
return secrets.token_urlsafe(32)
# Store reset tokens with expiration
reset_tokens = {}
@app.route('/forgot_password', methods=['GET', 'POST'])
def forgot_password():
if request.method == 'POST':
email = request.form.get('email')
user = next((u for u in users.values() if u.email == email), None)
if user:
token = generate_reset_token()
reset_tokens[token] = {
'user_id': user.id,
'expires': datetime.utcnow() + timedelta(hours=1)
}
# Send email with reset link
send_reset_email(user.email, token)
flash('Password reset email sent!', 'info')
else:
flash('Email not found!', 'error')
return render_template('forgot_password.html')
@app.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
if token not in reset_tokens:
flash('Invalid or expired reset token!', 'error')
return redirect(url_for('forgot_password'))
token_data = reset_tokens[token]
if datetime.utcnow() > token_data['expires']:
del reset_tokens[token]
flash('Reset token has expired!', 'error')
return redirect(url_for('forgot_password'))
if request.method == 'POST':
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
if password != confirm_password:
flash('Passwords do not match!', 'error')
return render_template('reset_password.html', token=token)
is_valid, message = validate_password_strength(password)
if not is_valid:
flash(message, 'error')
return render_template('reset_password.html', token=token)
user = users[token_data['user_id']]
user.password_hash = hash_password(password)
del reset_tokens[token]
flash('Password reset successful! Please log in.', 'success')
return redirect(url_for('login'))
return render_template('reset_password.html', token=token)
Role-Based Access Control
Implement user roles and permissions:
from functools import wraps
from flask import abort
class User(UserMixin):
def __init__(self, id, username, email, password_hash, role='user'):
self.id = id
self.username = username
self.email = email
self.password_hash = password_hash
self.role = role # 'user', 'moderator', 'admin'
def has_role(self, role):
"""Check if user has a specific role."""
role_hierarchy = {'user': 1, 'moderator': 2, 'admin': 3}
return role_hierarchy.get(self.role, 0) >= role_hierarchy.get(role, 0)
def role_required(role):
"""Decorator to require a specific role."""
def decorator(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
if not current_user.has_role(role):
abort(403) # Forbidden
return f(*args, **kwargs)
return decorated_function
return decorator
@app.route('/admin')
@role_required('admin')
def admin_panel():
return render_template('admin.html')
@app.route('/moderate')
@role_required('moderator')
def moderation_panel():
return render_template('moderate.html')
@app.route('/user_profile')
@login_required
def user_profile():
return render_template('profile.html')
# Permission-based access
def permission_required(permission):
"""Decorator to require a specific permission."""
def decorator(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
if not current_user.has_permission(permission):
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
# Extend User class with permissions
class User(UserMixin):
def __init__(self, id, username, email, password_hash, role='user', permissions=None):
self.id = id
self.username = username
self.email = email
self.password_hash = password_hash
self.role = role
self.permissions = permissions or []
def has_permission(self, permission):
"""Check if user has a specific permission."""
return permission in self.permissions or self.has_role('admin')
def has_role(self, role):
role_hierarchy = {'user': 1, 'moderator': 2, 'admin': 3}
return role_hierarchy.get(self.role, 0) >= role_hierarchy.get(role, 0)
@app.route('/create_post')
@permission_required('create_post')
def create_post():
return render_template('create_post.html')
@app.route('/delete_post/<int:post_id>')
@permission_required('delete_post')
def delete_post(post_id):
# Delete post logic
return redirect(url_for('home'))
Session Management
Handle user sessions securely:
from flask import session
from datetime import datetime, timedelta
import secrets
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only
app.config['SESSION_COOKIE_HTTPONLY'] = True # Prevent XSS
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # CSRF protection
@app.before_request
def make_session_permanent():
"""Make session permanent for remember me functionality."""
session.permanent = True
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# ... login logic ...
if user and check_password_hash(user.password_hash, form.password.data):
login_user(user, remember=form.remember.data)
# Store additional session data
session['login_time'] = datetime.utcnow().isoformat()
session['ip_address'] = request.remote_addr
next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('dashboard'))
return render_template('login.html', form=form)
@app.route('/logout')
@login_required
def logout():
# Clear session data
session.clear()
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('home'))
# Session timeout
@app.before_request
def check_session_timeout():
if current_user.is_authenticated:
login_time = session.get('login_time')
if login_time:
login_datetime = datetime.fromisoformat(login_time)
if datetime.utcnow() - login_datetime > timedelta(hours=2):
logout_user()
session.clear()
flash('Session expired. Please log in again.', 'info')
return redirect(url_for('login'))
# Track user activity
@app.before_request
def update_last_activity():
if current_user.is_authenticated:
session['last_activity'] = datetime.utcnow().isoformat()
Security Best Practices
1. Password Policies
def validate_password_strength(password):
"""Comprehensive password validation."""
errors = []
if len(password) < 12:
errors.append("Password must be at least 12 characters long")
if not re.search(r'[A-Z]', password):
errors.append("Password must contain at least one uppercase letter")
if not re.search(r'[a-z]', password):
errors.append("Password must contain at least one lowercase letter")
if not re.search(r'[0-9]', password):
errors.append("Password must contain at least one number")
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
errors.append("Password must contain at least one special character")
# Check for common passwords
common_passwords = ['password', '123456', 'qwerty', 'admin']
if password.lower() in common_passwords:
errors.append("Password is too common")
# Check for sequential characters
if re.search(r'(012|123|234|345|456|567|678|789|890)', password):
errors.append("Password contains sequential numbers")
if re.search(r'(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)', password.lower()):
errors.append("Password contains sequential letters")
return len(errors) == 0, errors
# Password history (prevent reuse)
class User(UserMixin):
def __init__(self, id, username, email, password_hash, password_history=None):
self.id = id
self.username = username
self.email = email
self.password_hash = password_hash
self.password_history = password_history or []
def change_password(self, new_password):
"""Change password with history check."""
new_hash = hash_password(new_password)
# Check if password was used recently
if new_hash in self.password_history[-5:]: # Last 5 passwords
return False, "Cannot reuse recent passwords"
self.password_history.append(self.password_hash)
self.password_hash = new_hash
return True, "Password changed successfully"
2. Account Lockout
# Track failed login attempts
failed_attempts = {}
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
# Check if account is locked
if username in failed_attempts:
attempts, lockout_time = failed_attempts[username]
if attempts >= 5 and datetime.utcnow() < lockout_time:
flash('Account is temporarily locked due to too many failed attempts.', 'error')
return render_template('login.html', form=LoginForm())
# ... existing login logic ...
user = next((u for u in users.values() if u.username == username), None)
if user and check_password_hash(user.password_hash, form.password.data):
# Successful login - reset failed attempts
if username in failed_attempts:
del failed_attempts[username]
login_user(user, remember=form.remember.data)
flash('Login successful!', 'success')
return redirect(url_for('dashboard'))
else:
# Failed login - increment attempts
if username not in failed_attempts:
failed_attempts[username] = [0, None]
failed_attempts[username][0] += 1
if failed_attempts[username][0] >= 5:
# Lock account for 15 minutes
failed_attempts[username][1] = datetime.utcnow() + timedelta(minutes=15)
flash('Account locked due to too many failed attempts. Try again later.', 'error')
else:
remaining = 5 - failed_attempts[username][0]
flash(f'Invalid credentials. {remaining} attempts remaining.', 'error')
return render_template('login.html', form=LoginForm())
3. Two-Factor Authentication
import pyotp
import qrcode
import io
import base64
class User(UserMixin):
def __init__(self, id, username, email, password_hash, totp_secret=None):
self.id = id
self.username = username
self.email = email
self.password_hash = password_hash
self.totp_secret = totp_secret
def generate_totp_secret(self):
"""Generate a new TOTP secret."""
self.totp_secret = pyotp.random_base32()
return self.totp_secret
def verify_totp(self, token):
"""Verify a TOTP token."""
if not self.totp_secret:
return False
totp = pyotp.TOTP(self.totp_secret)
return totp.verify(token)
def generate_qr_code(secret, username):
"""Generate QR code for TOTP setup."""
totp = pyotp.TOTP(secret)
provisioning_uri = totp.provisioning_uri(username, issuer_name="Your App")
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(provisioning_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = io.BytesIO()
img.save(buffer, format="PNG")
buffer.seek(0)
return base64.b64encode(buffer.getvalue()).decode()
@app.route('/setup_2fa')
@login_required
def setup_2fa():
if current_user.totp_secret:
flash('2FA is already enabled.', 'info')
return redirect(url_for('dashboard'))
secret = current_user.generate_totp_secret()
qr_code = generate_qr_code(secret, current_user.username)
return render_template('setup_2fa.html', qr_code=qr_code, secret=secret)
@app.route('/verify_2fa', methods=['POST'])
@login_required
def verify_2fa():
token = request.form.get('token')
if current_user.verify_totp(token):
# Save the user with 2FA enabled
users[current_user.id] = current_user
flash('2FA setup successful!', 'success')
return redirect(url_for('dashboard'))
else:
flash('Invalid token. Please try again.', 'error')
return redirect(url_for('setup_2fa'))
# Modified login to include 2FA
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# ... password verification ...
if user and check_password_hash(user.password_hash, form.password.data):
if user.totp_secret:
# Store user temporarily and redirect to 2FA
session['pending_user_id'] = user.id
return redirect(url_for('verify_2fa_login'))
else:
login_user(user, remember=form.remember.data)
flash('Login successful!', 'success')
return redirect(url_for('dashboard'))
return render_template('login.html', form=form)
@app.route('/verify_2fa_login', methods=['GET', 'POST'])
def verify_2fa_login():
if 'pending_user_id' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
token = request.form.get('token')
user_id = session['pending_user_id']
user = users.get(user_id)
if user and user.verify_totp(token):
del session['pending_user_id']
login_user(user)
flash('Login successful!', 'success')
return redirect(url_for('dashboard'))
else:
flash('Invalid 2FA token.', 'error')
return render_template('verify_2fa_login.html')
Practical Examples
Example 1: Complete Blog with Authentication
from flask import Flask, render_template, redirect, url_for, flash, request
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
posts = db.relationship('Post', backref='author', lazy=True)
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)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@app.route('/')
def index():
posts = Post.query.order_by(Post.created_at.desc()).all()
return render_template('index.html', posts=posts)
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
if User.query.filter_by(username=username).first():
flash('Username already exists!', 'error')
return render_template('register.html')
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash('Registration successful!', 'success')
return redirect(url_for('login'))
return render_template('register.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
next_page = request.args.get('next')
flash('Login successful!', 'success')
return redirect(next_page) if next_page else redirect(url_for('index'))
else:
flash('Invalid username or password', 'error')
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('index'))
@app.route('/create_post', methods=['GET', 'POST'])
@login_required
def create_post():
if request.method == 'POST':
title = request.form.get('title')
content = request.form.get('content')
post = Post(title=title, content=content, author=current_user)
db.session.add(post)
db.session.commit()
flash('Post created successfully!', 'success')
return redirect(url_for('index'))
return render_template('create_post.html')
@app.route('/edit_post/<int:post_id>', methods=['GET', 'POST'])
@login_required
def edit_post(post_id):
post = Post.query.get_or_404(post_id)
if post.author != current_user and not current_user.is_admin:
abort(403)
if request.method == 'POST':
post.title = request.form.get('title')
post.content = request.form.get('content')
db.session.commit()
flash('Post updated successfully!', 'success')
return redirect(url_for('post', post_id=post.id))
return render_template('edit_post.html', post=post)
@app.route('/delete_post/<int:post_id>', methods=['POST'])
@login_required
def delete_post(post_id):
post = Post.query.get_or_404(post_id)
if post.author != current_user and not current_user.is_admin:
abort(403)
db.session.delete(post)
db.session.commit()
flash('Post deleted successfully!', 'success')
return redirect(url_for('index'))
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
Best Practices Summary
Security Checklist
- Use HTTPS in production
- Hash passwords with strong algorithms
- Implement password policies
- Use CSRF protection on forms
- Set secure session cookies
- Implement account lockout for failed attempts
- Add two-factor authentication
- Log security events
- Regular security audits
Authentication Flow
# 1. User Registration
@app.route('/register')
def register():
# Validate input
# Check for existing users
# Hash password
# Create user account
# Send confirmation email
# 2. User Login
@app.route('/login')
def login():
# Validate credentials
# Check account status
# Handle 2FA if enabled
# Set session
# Redirect to intended page
# 3. Protected Routes
@app.route('/protected')
@login_required
def protected():
# Check user permissions
# Serve content
# 4. Logout
@app.route('/logout')
@login_required
def logout():
# Clear session
# Redirect to home
Practice Exercises
Exercise 1: User Management System
Create a user management system with:
- User registration and login
- Profile management
- Password change functionality
- Account deactivation
- Admin panel for user management
- Email verification
Exercise 2: Secure File Upload
Build a file sharing application with:
- User authentication
- Secure file upload with validation
- File permissions (public/private)
- File download with access control
- Upload size limits and type restrictions
- Virus scanning integration
Exercise 3: API Authentication
Create a REST API with:
- JWT token authentication
- API key management
- Rate limiting
- Request logging
- CORS configuration
- API documentation
Exercise 4: Social Login
Implement social authentication with:
- Google OAuth login
- Facebook login integration
- User profile sync
- Account linking
- Social media sharing
Summary
Authentication secures your Flask applications:
Flask-Login Setup:
from flask_login import LoginManager, login_user, login_required, logout_user, current_user
login_manager = LoginManager()
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
User Model:
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=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)
Protected Routes:
@app.route('/protected')
@login_required
def protected():
return f'Hello, {current_user.username}!'
Security Features:
- Password hashing with Werkzeug
- Session management
- Role-based access control
- Two-factor authentication
- Account lockout protection
- CSRF protection
Congratulations! You’ve completed the Web Development module. You now know how to build secure, database-driven web applications with Flask! 🎉
Next Module: Data Science - analyzing data with pandas and matplotlib! 📊