Base scripts and templates added

This commit is contained in:
Hamit Şimşek
2025-05-30 00:07:07 +03:00
parent 73d497d4c3
commit ebd7dcc23b
28 changed files with 2579 additions and 0 deletions

43
.env.example Normal file
View File

@@ -0,0 +1,43 @@
# Flask 2FA Authentication Application Environment Configuration
# Copy this file to .env and modify the values as needed
# Flask Configuration
FLASK_CONFIG=development
FLASK_APP=run.py
FLASK_ENV=development
# Security Configuration (CRITICAL: Change these in production!)
SECRET_KEY=your-secret-key-change-this-in-production-use-secrets-manager-or-random-generator
# Database Configuration
DEV_DATABASE_URL=sqlite:///dev.db
DATABASE_URL=sqlite:///app.db
# Production Database Example (uncomment and modify for production)
# DATABASE_URL=postgresql://username:password@localhost/flask_2fa_prod
# Application Settings
DEBUG=True
# Security Headers (Production only)
# SESSION_COOKIE_SECURE=True
# SESSION_COOKIE_HTTPONLY=True
# SESSION_COOKIE_SAMESITE=Lax
# Logging Configuration
LOG_LEVEL=INFO
LOG_FILE=app.log
# SMTP Configuration (for future email features)
# MAIL_SERVER=smtp.gmail.com
# MAIL_PORT=587
# MAIL_USE_TLS=True
# MAIL_USERNAME=your-email@gmail.com
# MAIL_PASSWORD=your-app-password
# Development Notes:
# 1. Never commit the .env file to version control
# 2. Use strong, randomly generated SECRET_KEY in production
# 3. Use environment-specific database URLs
# 4. Enable HTTPS and secure cookies in production
# 5. Consider using external secret management services

1
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1 @@
- Default terminal is the Windows PowerShell, please use the PowerShell rules when using the terminal.

261
README.md Normal file
View File

@@ -0,0 +1,261 @@
# Flask 2FA Authentication Application
A secure Flask web application implementing two-factor authentication (2FA) with industry best practices for web security.
## 🔒 Security Features
- **Two-Factor Authentication**: TOTP-based 2FA using PyOTP
- **Secure Password Storage**: Bcrypt hashing with automatic salt generation
- **CSRF Protection**: Automatic token validation on all forms
- **SQL Injection Prevention**: Parameterized queries with SQLAlchemy ORM
- **XSS Protection**: Automatic template escaping and CSP headers
- **Secure Session Management**: HTTPOnly, Secure, and SameSite cookie flags
- **Security Headers**: HSTS, X-Frame-Options, X-Content-Type-Options
- **Input Validation**: Server-side validation with WTForms
- **Secure Configuration**: Environment-based configuration management
## 🚀 Quick Start
### Prerequisites
- Python 3.9 or higher
- pip (Python package installer)
### Installation
1. **Clone the repository**
```bash
git clone <repository-url>
cd flask-2fa-auth
```
2. **Create and activate virtual environment**
```bash
# Windows
python -m venv venv
venv\Scripts\activate
# macOS/Linux
python3 -m venv venv
source venv/bin/activate
```
3. **Install dependencies**
```bash
pip install -r requirements.txt
```
4. **Set up environment variables**
```bash
# Copy the example environment file
copy .env.example .env
# Edit .env file with your configuration
# At minimum, change the SECRET_KEY for production
```
5. **Initialize the database**
```bash
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
```
6. **Run the application**
```bash
python run.py
```
The application will be available at `http://127.0.0.1:5000`
## 📱 2FA Setup Process
1. **Register**: Create a new account with username, email, and password
2. **Scan QR Code**: Use Google Authenticator, Authy, or similar app to scan the QR code
3. **Verify**: Enter the 6-digit code from your authenticator app
4. **Login**: Use your credentials + 2FA code for future logins
### Supported Authenticator Apps
- Google Authenticator
- Microsoft Authenticator
- Authy
- 1Password
- LastPass Authenticator
- Any TOTP-compatible app
## 🛠️ Project Structure
```
flask-2fa-auth/
├── app/
│ ├── __init__.py # Application factory
│ ├── models.py # User model with 2FA methods
│ ├── auth/ # Authentication blueprint
│ │ ├── __init__.py
│ │ ├── routes.py # Auth routes (register, login, verify)
│ │ └── forms.py # WTForms form classes
│ ├── main/ # Main application blueprint
│ │ ├── __init__.py
│ │ └── routes.py # Main routes (dashboard, profile)
│ └── templates/ # Jinja2 templates
│ ├── base.html # Base template with security headers
│ ├── index.html # Home page
│ ├── dashboard.html # User dashboard
│ ├── profile.html # User profile
│ └── auth/ # Authentication templates
│ ├── register.html
│ ├── login.html
│ ├── verify_otp.html
│ └── setup_2fa.html
├── config.py # Configuration classes
├── requirements.txt # Python dependencies
├── run.py # Application entry point
├── .env.example # Environment variables template
└── README.md # This file
```
## 🔧 Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `FLASK_CONFIG` | Configuration environment | `development` |
| `SECRET_KEY` | Flask secret key (CHANGE IN PRODUCTION!) | Auto-generated |
| `DATABASE_URL` | Database connection string | `sqlite:///app.db` |
| `DEBUG` | Enable debug mode | `True` |
### Configuration Classes
- **DevelopmentConfig**: Debug enabled, SQLite database
- **ProductionConfig**: Debug disabled, PostgreSQL recommended
- **TestingConfig**: In-memory database, CSRF disabled
## 🚀 Deployment
### Production Checklist
- [ ] Change `SECRET_KEY` to a cryptographically secure random value
- [ ] Set `FLASK_CONFIG=production`
- [ ] Use PostgreSQL or similar production database
- [ ] Enable HTTPS with valid SSL certificate
- [ ] Set secure environment variables
- [ ] Use a proper WSGI server (Gunicorn, uWSGI)
- [ ] Configure reverse proxy (Nginx, Apache)
- [ ] Set up monitoring and logging
- [ ] Regular security updates
### Example Production Deployment
```bash
# Install production dependencies
pip install gunicorn
# Set production environment
export FLASK_CONFIG=production
export SECRET_KEY="your-super-secure-random-key"
export DATABASE_URL="postgresql://user:pass@localhost/flask_2fa_prod"
# Run database migrations
flask db upgrade
# Start with Gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 run:app
```
## 🛡️ Security Considerations
### Authentication Security
- Passwords are hashed using bcrypt with automatic salt generation
- TOTP tokens expire every 30 seconds with built-in replay protection
- Failed login attempts are logged for security monitoring
- Session protection prevents session fixation attacks
### Web Security
- CSRF tokens protect against cross-site request forgery
- Security headers prevent clickjacking and XSS attacks
- Input validation prevents injection attacks
- Secure cookie settings protect session data
### Database Security
- Parameterized queries prevent SQL injection
- Connection pooling with proper timeouts
- Database credentials stored in environment variables
### Operational Security
- Security events logged for monitoring
- Environment-based configuration
- Separate development and production configurations
## 📚 API Reference
### User Model Methods
```python
user = User(username="john", email="john@example.com")
# Password management
user.set_password("secure_password")
user.check_password("password_to_verify")
# 2FA management
user.generate_totp_secret()
user.generate_totp_uri("MyApp")
user.verify_totp("123456")
user.generate_qr_code("MyApp")
user.enable_2fa()
user.disable_2fa()
```
### Routes
| Route | Method | Description |
|-------|--------|-------------|
| `/` | GET | Home page |
| `/auth/register` | GET, POST | User registration |
| `/auth/login` | GET, POST | User login (first factor) |
| `/auth/verify-otp` | GET, POST | 2FA verification |
| `/auth/setup-2fa` | GET | QR code for 2FA setup |
| `/auth/logout` | GET | User logout |
| `/dashboard` | GET | User dashboard (protected) |
| `/profile` | GET | User profile (protected) |
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
### Development Guidelines
- Follow PEP 8 style guidelines
- Add tests for new features
- Update documentation as needed
- Ensure security best practices are maintained
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🆘 Support
If you encounter any issues:
1. Check the [Issues](../../issues) page for existing solutions
2. Create a new issue with detailed information
3. Include error messages and steps to reproduce
## 🔗 Resources
- [Flask Documentation](https://flask.palletsprojects.com/)
- [Flask-Login Documentation](https://flask-login.readthedocs.io/)
- [PyOTP Documentation](https://pypi.org/project/pyotp/)
- [OWASP Security Guidelines](https://owasp.org/www-project-web-security-testing-guide/)
- [NIST 2FA Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html)
---
**⚠️ Security Notice**: This application implements security best practices, but security is an ongoing process. Always keep dependencies updated, monitor for vulnerabilities, and follow current security guidelines for production deployments.

69
app/__init__.py Normal file
View File

@@ -0,0 +1,69 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
from config import config
# Initialize extensions
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
csrf = CSRFProtect()
def create_app(config_name='default'):
"""
Application factory pattern for creating Flask app instances.
Security considerations:
- CSRF protection enabled globally
- Secure session configuration
- Login manager with proper security settings
"""
app = Flask(__name__)
app.config.from_object(config[config_name])
# Initialize extensions with app
db.init_app(app)
migrate.init_app(app, db)
csrf.init_app(app)
# Configure Flask-Login for security
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'info'
login_manager.session_protection = 'strong' # Enhanced session protection
@login_manager.user_loader
def load_user(user_id):
"""
User loader function for Flask-Login.
Uses parameterized query to prevent SQL injection.
"""
from app.models import User
return User.query.get(int(user_id))
# Register blueprints
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
from app.main import bp as main_bp
app.register_blueprint(main_bp)
# Security headers middleware
@app.after_request
def security_headers(response):
"""Add security headers to all responses."""
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
return response
return app
# Import models to ensure they are registered with SQLAlchemy
from app import models

5
app/auth/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('auth', __name__)
from app.auth import routes

79
app/auth/forms.py Normal file
View File

@@ -0,0 +1,79 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from app.models import User
class RegistrationForm(FlaskForm):
"""
User registration form with validation.
Security: CSRF protection enabled automatically by Flask-WTF.
"""
username = StringField('Username', validators=[
DataRequired(),
Length(min=3, max=20, message='Username must be between 3 and 20 characters.')
])
email = StringField('Email', validators=[
DataRequired(),
Email(message='Invalid email address.')
])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=8, message='Password must be at least 8 characters long.')
])
password2 = PasswordField('Confirm Password', validators=[
DataRequired(),
EqualTo('password', message='Passwords must match.')
])
submit = SubmitField('Register')
def validate_username(self, username):
"""
Custom validator to check username uniqueness.
Security: Uses parameterized query to prevent SQL injection.
"""
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('Username already exists. Please choose a different one.')
def validate_email(self, email):
"""
Custom validator to check email uniqueness.
Security: Uses parameterized query to prevent SQL injection.
"""
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('Email already registered. Please choose a different one.')
class LoginForm(FlaskForm):
"""
User login form.
Security: CSRF protection enabled automatically by Flask-WTF.
"""
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')
class TwoFactorForm(FlaskForm):
"""
Two-factor authentication verification form.
Security: CSRF protection enabled automatically by Flask-WTF.
"""
token = StringField('Authentication Code', validators=[
DataRequired(),
Length(min=6, max=6, message='Authentication code must be 6 digits.')
])
submit = SubmitField('Verify')
def validate_token(self, token):
"""Validate that token contains only digits."""
if not token.data.isdigit():
raise ValidationError('Authentication code must contain only digits.')

223
app/auth/routes.py Normal file
View File

@@ -0,0 +1,223 @@
from flask import render_template, redirect, url_for, flash, request, session
from flask_login import login_user, logout_user, current_user, login_required
from urllib.parse import urlparse
from app import db
from app.auth import bp
from app.auth.forms import RegistrationForm, LoginForm, TwoFactorForm
from app.models import User
import logging
# Configure logging for security events
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@bp.route('/register', methods=['GET', 'POST'])
def register():
"""
User registration endpoint with 2FA setup.
Security features:
- CSRF protection via Flask-WTF
- Password hashing via bcrypt
- Input validation and sanitization
- SQL injection prevention via parameterized queries
"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
try:
# Create new user with hashed password
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
# Generate TOTP secret for 2FA
user.generate_totp_secret()
# Save user to database
db.session.add(user)
db.session.commit()
# Log successful registration (without sensitive data)
logger.info(f'New user registered: {user.username}')
flash('Registration successful! Please scan the QR code with your authenticator app.', 'success')
# Store user ID in session for QR code display
session['temp_user_id'] = user.id
return redirect(url_for('auth.setup_2fa'))
except Exception as e:
# Rollback transaction on error
db.session.rollback()
logger.error(f'Registration error for user {form.username.data}: {str(e)}')
flash('Registration failed. Please try again.', 'error')
return render_template('auth/register.html', form=form, title='Register')
@bp.route('/setup-2fa')
def setup_2fa():
"""
Display QR code for 2FA setup after registration.
Security: Requires temp_user_id in session to prevent unauthorized access.
"""
user_id = session.get('temp_user_id')
if not user_id:
flash('Invalid session. Please register again.', 'error')
return redirect(url_for('auth.register'))
user = User.query.get(user_id)
if not user:
flash('User not found. Please register again.', 'error')
return redirect(url_for('auth.register'))
# Generate QR code for Google Authenticator
qr_code = user.generate_qr_code()
# Clear temp session data
session.pop('temp_user_id', None)
return render_template('auth/setup_2fa.html',
qr_code=qr_code,
username=user.username,
title='Setup Two-Factor Authentication')
@bp.route('/login', methods=['GET', 'POST'])
def login():
"""
User login endpoint with first-factor authentication.
Security features:
- CSRF protection via Flask-WTF
- Secure password verification
- Session protection
- Login attempt logging
"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
# Use parameterized query to prevent SQL injection
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
# Log failed login attempt
logger.warning(f'Failed login attempt for username: {form.username.data}')
flash('Invalid username or password', 'error')
return redirect(url_for('auth.login'))
# Store user ID in session for 2FA verification
session['temp_user_id'] = user.id
session['remember_me'] = form.remember_me.data
# Log successful first-factor authentication
logger.info(f'First-factor authentication successful for user: {user.username}')
# Redirect to 2FA verification
flash('Please enter your authentication code', 'info')
return redirect(url_for('auth.verify_otp'))
return render_template('auth/login.html', form=form, title='Sign In')
@bp.route('/verify-otp', methods=['GET', 'POST'])
def verify_otp():
"""
Second-factor authentication endpoint using TOTP.
Security features:
- CSRF protection via Flask-WTF
- Time-based token verification
- Session validation
- Replay attack protection (built into PyOTP)
"""
user_id = session.get('temp_user_id')
if not user_id:
flash('Session expired. Please log in again.', 'error')
return redirect(url_for('auth.login'))
user = User.query.get(user_id)
if not user:
flash('User not found. Please log in again.', 'error')
return redirect(url_for('auth.login'))
form = TwoFactorForm()
if form.validate_on_submit():
if user.verify_totp(form.token.data):
# Enable 2FA if this is the first successful verification
if not user.is_2fa_enabled:
user.enable_2fa()
# Update last login timestamp
user.last_login = db.func.current_timestamp()
db.session.commit()
# Clear temp session data
remember_me = session.pop('remember_me', False)
session.pop('temp_user_id', None)
# Complete login process
login_user(user, remember=remember_me)
# Log successful login
logger.info(f'Successful login for user: {user.username}')
flash('Login successful!', 'success')
# Redirect to originally requested page or dashboard
next_page = request.args.get('next')
if not next_page or urlparse(next_page).netloc != '':
next_page = url_for('main.index')
return redirect(next_page)
else:
# Log failed 2FA attempt
logger.warning(f'Failed 2FA verification for user: {user.username}')
flash('Invalid authentication code. Please try again.', 'error')
return render_template('auth/verify_otp.html', form=form, title='Two-Factor Authentication')
@bp.route('/logout')
@login_required
def logout():
"""
User logout endpoint.
Security: Properly clears session and logs security event.
"""
username = current_user.username if current_user.is_authenticated else 'Unknown'
logout_user()
# Clear any remaining session data
session.clear()
# Log logout event
logger.info(f'User logged out: {username}')
flash('You have been logged out successfully.', 'info')
return redirect(url_for('main.index'))
@bp.route('/disable-2fa', methods=['POST'])
@login_required
def disable_2fa():
"""
Disable two-factor authentication for the current user.
Security: Requires active login session and POST request.
"""
try:
current_user.disable_2fa()
logger.info(f'2FA disabled for user: {current_user.username}')
flash('Two-factor authentication has been disabled.', 'warning')
except Exception as e:
logger.error(f'Error disabling 2FA for user {current_user.username}: {str(e)}')
flash('Failed to disable two-factor authentication.', 'error')
return redirect(url_for('main.profile'))

5
app/main/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import routes

36
app/main/routes.py Normal file
View File

@@ -0,0 +1,36 @@
from flask import render_template
from flask_login import login_required, current_user
from app.main import bp
@bp.route('/')
@bp.route('/index')
def index():
"""
Home page - shows different content for authenticated vs anonymous users.
Security: No sensitive data exposed to anonymous users.
"""
return render_template('index.html', title='Home')
@bp.route('/dashboard')
@login_required
def dashboard():
"""
Protected dashboard for authenticated users.
Security: Requires valid login session with 2FA verification.
"""
return render_template('dashboard.html', title='Dashboard', user=current_user)
@bp.route('/profile')
@login_required
def profile():
"""
User profile page with 2FA management.
Security: Requires valid login session.
"""
return render_template('profile.html', title='Profile', user=current_user)

147
app/models.py Normal file
View File

@@ -0,0 +1,147 @@
import secrets
import pyotp
import qrcode
import io
import base64
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app import db
class User(UserMixin, db.Model):
"""
User model with two-factor authentication support.
Security features:
- Bcrypt password hashing
- TOTP secret generation and verification
- Secure random secret generation
"""
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(255), nullable=False)
totp_secret = db.Column(db.String(32), nullable=True)
is_2fa_enabled = db.Column(db.Boolean, default=False, nullable=False)
created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
last_login = db.Column(db.DateTime)
def __repr__(self):
return f'<User {self.username}>'
def set_password(self, password):
"""
Hash and set the user's password using bcrypt.
Security: Uses bcrypt with automatic salt generation
for resistance against rainbow table attacks.
"""
self.password_hash = generate_password_hash(password, method='pbkdf2:sha256')
def check_password(self, password):
"""
Verify the provided password against the stored hash.
Security: Uses constant-time comparison to prevent timing attacks.
"""
return check_password_hash(self.password_hash, password)
def generate_totp_secret(self):
"""
Generate a new TOTP secret for two-factor authentication.
Security: Uses cryptographically secure random generation.
"""
if not self.totp_secret:
self.totp_secret = pyotp.random_base32()
return self.totp_secret
def generate_totp_uri(self, issuer_name='Flask-2FA-App'):
"""
Generate a TOTP URI for QR code generation.
Args:
issuer_name: The name of the application (displayed in authenticator apps)
Returns:
str: TOTP URI compatible with authenticator apps like Google Authenticator
"""
if not self.totp_secret:
self.generate_totp_secret()
return pyotp.totp.TOTP(self.totp_secret).provisioning_uri(
name=self.username,
issuer_name=issuer_name
)
def verify_totp(self, token):
"""
Verify a TOTP token against the user's secret.
Args:
token: The 6-digit TOTP token from the authenticator app
Returns:
bool: True if the token is valid, False otherwise
Security: Uses time-window verification with built-in replay protection.
"""
if not self.totp_secret:
return False
totp = pyotp.TOTP(self.totp_secret)
# Verify token with a 1-period window (30 seconds before/after)
return totp.verify(token, valid_window=1)
def generate_qr_code(self, issuer_name='Flask-2FA-App'):
"""
Generate a QR code for the TOTP URI.
Returns:
str: Base64-encoded PNG image of the QR code
"""
uri = self.generate_totp_uri(issuer_name)
# Generate QR code
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(uri)
qr.make(fit=True)
# Create image
img = qr.make_image(fill_color="black", back_color="white")
# Convert to base64 for HTML embedding
img_buffer = io.BytesIO()
img.save(img_buffer, format='PNG')
img_buffer.seek(0)
return base64.b64encode(img_buffer.getvalue()).decode()
def enable_2fa(self):
"""Enable two-factor authentication for the user."""
if self.totp_secret:
self.is_2fa_enabled = True
db.session.commit()
def disable_2fa(self):
"""Disable two-factor authentication for the user."""
self.is_2fa_enabled = False
self.totp_secret = None
db.session.commit()
# Database event listeners for additional security
from sqlalchemy import event
@event.listens_for(User.password_hash, 'set')
def validate_password_hash(target, value, oldvalue, initiator):
"""Ensure password hash is never stored as plaintext."""
if value and not value.startswith('pbkdf2:sha256'):
raise ValueError("Password must be hashed before storage")

View File

@@ -0,0 +1,94 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="auth-form">
<h2 class="text-center mb-4">
<i class="bi bi-box-arrow-in-right"></i> Sign In
</h2>
<div class="security-notice">
<strong>Security Notice:</strong> This application uses two-factor authentication.
After entering your credentials, you'll need to provide a code from your authenticator app.
</div>
<form method="POST" novalidate>
{{ form.hidden_tag() }}
<!-- Username field -->
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else ""),
placeholder="Enter your username") }}
{% if form.username.errors %}
<div class="invalid-feedback">
{% for error in form.username.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<!-- Password field -->
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else ""),
placeholder="Enter your password") }}
{% if form.password.errors %}
<div class="invalid-feedback">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<!-- Remember me checkbox -->
<div class="mb-3 form-check">
{{ form.remember_me(class="form-check-input") }}
{{ form.remember_me.label(class="form-check-label") }}
<div class="form-text">Keep me logged in on this device</div>
</div>
<!-- Submit button -->
<div class="d-grid">
{{ form.submit(class="btn btn-primary btn-lg") }}
</div>
</form>
<hr class="my-4">
<div class="text-center">
<p>Don't have an account?
<a href="{{ url_for('auth.register') }}" class="text-decoration-none">Register here</a>
</p>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-shield-lock"></i> Login Process</h5>
</div>
<div class="card-body">
<ol>
<li>Enter your username and password</li>
<li>If credentials are valid, you'll be redirected to 2FA verification</li>
<li>Open your authenticator app (Google Authenticator, Authy, etc.)</li>
<li>Enter the 6-digit code from your app</li>
<li>Access granted to your secure dashboard</li>
</ol>
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle"></i>
<strong>Security Tip:</strong> Never share your authentication codes with anyone.
They expire every 30 seconds for your protection.
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,108 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="auth-form">
<h2 class="text-center mb-4">
<i class="bi bi-person-plus"></i> Create Account
</h2>
<div class="security-notice">
<strong>Security Notice:</strong> Your password will be securely hashed using bcrypt.
Two-factor authentication will be automatically enabled for enhanced security.
</div>
<form method="POST" novalidate>
{{ form.hidden_tag() }}
<!-- Username field -->
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }}
{% if form.username.errors %}
<div class="invalid-feedback">
{% for error in form.username.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">3-20 characters, alphanumeric only</div>
</div>
<!-- Email field -->
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }}
{% if form.email.errors %}
<div class="invalid-feedback">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<!-- Password field -->
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }}
{% if form.password.errors %}
<div class="invalid-feedback">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">Minimum 8 characters</div>
</div>
<!-- Confirm password field -->
<div class="mb-3">
{{ form.password2.label(class="form-label") }}
{{ form.password2(class="form-control" + (" is-invalid" if form.password2.errors else "")) }}
{% if form.password2.errors %}
<div class="invalid-feedback">
{% for error in form.password2.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<!-- Submit button -->
<div class="d-grid">
{{ form.submit(class="btn btn-primary btn-lg") }}
</div>
</form>
<hr class="my-4">
<div class="text-center">
<p>Already have an account?
<a href="{{ url_for('auth.login') }}" class="text-decoration-none">Sign in here</a>
</p>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-info-circle"></i> Security Features</h5>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li><i class="bi bi-check-circle text-success"></i> Passwords are hashed using bcrypt with salt</li>
<li><i class="bi bi-check-circle text-success"></i> CSRF protection on all forms</li>
<li><i class="bi bi-check-circle text-success"></i> Two-factor authentication required</li>
<li><i class="bi bi-check-circle text-success"></i> Secure session management</li>
<li><i class="bi bi-check-circle text-success"></i> Input validation and sanitization</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,135 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-success text-white">
<h3 class="mb-0">
<i class="bi bi-check-circle"></i> Registration Successful!
</h3>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>Welcome, {{ username }}!</strong> Your account has been created successfully.
To complete the setup, please scan the QR code below with your authenticator app.
</div>
<h4 class="text-center mb-4">
<i class="bi bi-qr-code"></i> Setup Two-Factor Authentication
</h4>
<div class="qr-code-container">
<img src="data:image/png;base64,{{ qr_code }}"
alt="QR Code for 2FA Setup"
class="img-fluid mb-3"
style="max-width: 300px;">
<p class="text-muted">
Scan this QR code with your authenticator app
</p>
</div>
<div class="row mt-4">
<div class="col-md-6">
<h5><i class="bi bi-list-ol"></i> Setup Instructions:</h5>
<ol>
<li>Download an authenticator app:
<ul class="mt-2">
<li>Google Authenticator</li>
<li>Microsoft Authenticator</li>
<li>Authy</li>
<li>1Password</li>
</ul>
</li>
<li>Open the app and tap "Add Account" or "+"</li>
<li>Choose "Scan QR Code"</li>
<li>Point your camera at the QR code above</li>
<li>Your account will be added automatically</li>
</ol>
</div>
<div class="col-md-6">
<h5><i class="bi bi-shield-check"></i> Security Benefits:</h5>
<ul>
<li>Protects against password theft</li>
<li>Prevents unauthorized access</li>
<li>Works offline (no internet required)</li>
<li>Industry-standard TOTP protocol</li>
<li>Compatible with all major apps</li>
</ul>
</div>
</div>
<div class="alert alert-warning mt-4">
<i class="bi bi-exclamation-triangle"></i>
<strong>Important:</strong> Save your recovery codes or backup your authenticator app.
If you lose access to your authenticator, you may not be able to log in.
</div>
<div class="text-center mt-4">
<a href="{{ url_for('auth.login') }}" class="btn btn-primary btn-lg">
<i class="bi bi-box-arrow-in-right"></i> Continue to Login
</a>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-question-circle"></i> Frequently Asked Questions</h5>
</div>
<div class="card-body">
<div class="accordion" id="faqAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne">
What if I can't scan the QR code?
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
<div class="accordion-body">
You can manually enter the secret key in your authenticator app. Look for an option like
"Enter code manually" or "Manual entry" in your app.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo">
Which authenticator app should I use?
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
<div class="accordion-body">
Google Authenticator is the most popular choice, but Microsoft Authenticator, Authy,
and 1Password also work excellently. Choose one that you're comfortable with.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingThree">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree">
Is this secure?
</button>
</h2>
<div id="collapseThree" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
<div class="accordion-body">
Yes! This uses the industry-standard TOTP (Time-based One-Time Password) protocol,
which is used by major services like Google, Microsoft, and GitHub. The codes change
every 30 seconds and can't be reused.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,104 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="auth-form">
<h2 class="text-center mb-4">
<i class="bi bi-shield-check"></i> Two-Factor Authentication
</h2>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>Security Step:</strong> Enter the 6-digit code from your authenticator app to complete login.
</div>
<form method="POST" novalidate>
{{ form.hidden_tag() }}
<!-- Token field -->
<div class="mb-3">
{{ form.token.label(class="form-label") }}
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-key"></i>
</span>
{{ form.token(class="form-control form-control-lg text-center" + (" is-invalid" if form.token.errors else ""),
placeholder="000000", maxlength="6", style="letter-spacing: 0.5em;") }}
{% if form.token.errors %}
<div class="invalid-feedback">
{% for error in form.token.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-text">
<i class="bi bi-clock"></i> Codes refresh every 30 seconds
</div>
</div>
<!-- Submit button -->
<div class="d-grid">
{{ form.submit(class="btn btn-success btn-lg") }}
</div>
</form>
<hr class="my-4">
<div class="text-center">
<a href="{{ url_for('auth.login') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Login
</a>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-question-circle"></i> Trouble with 2FA?</h5>
</div>
<div class="card-body">
<h6>Common Issues:</h6>
<ul>
<li><strong>Code not working?</strong> Make sure your device's time is synchronized</li>
<li><strong>Lost your phone?</strong> Contact support for account recovery</li>
<li><strong>App not installed?</strong> Download Google Authenticator or Authy</li>
</ul>
<div class="alert alert-warning mt-3">
<i class="bi bi-exclamation-triangle"></i>
<strong>Security Notice:</strong> Each code can only be used once and expires after 30 seconds.
This prevents replay attacks and ensures your account remains secure.
</div>
</div>
</div>
</div>
</div>
<script>
// Auto-focus the token input and auto-submit when 6 digits are entered
document.addEventListener('DOMContentLoaded', function() {
const tokenInput = document.getElementById('token');
if (tokenInput) {
tokenInput.focus();
tokenInput.addEventListener('input', function(e) {
// Only allow digits
this.value = this.value.replace(/\D/g, '');
// Auto-submit when 6 digits are entered
if (this.value.length === 6) {
// Small delay to show the complete code
setTimeout(() => {
this.form.submit();
}, 200);
}
});
}
});
</script>
{% endblock %}

138
app/templates/base.html Normal file
View File

@@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- Security headers implemented via meta tags -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; script-src 'self' https://cdn.jsdelivr.net; font-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:;">
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="X-Frame-Options" content="DENY">
<title>
{% if title %}{{ title }} - Flask 2FA App{% else %}Flask 2FA App{% endif %}
</title>
<!-- Bootstrap CSS for styling -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons - Multiple CDN options for reliability -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" rel="stylesheet" media="print" onload="this.media='all'">
<style>
.security-notice {
background-color: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 10px;
margin: 10px 0;
}
.qr-code-container {
text-align: center;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.auth-form {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
</style>
</head>
<body class="bg-light">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('main.index') }}">
<i class="bi bi-shield-lock"></i> Flask 2FA App
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.index') }}">Home</a>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.dashboard') }}">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.profile') }}">Profile</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
{% if current_user.is_authenticated %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> {{ current_user.username }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('main.profile') }}">Profile</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">Register</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Main content -->
<main class="container mt-4">
<!-- Flash messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Page content -->
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="mt-5 py-4 bg-dark text-white">
<div class="container text-center">
<div class="security-notice text-light">
<small>
<i class="bi bi-shield-check"></i>
This application implements security best practices including CSRF protection,
secure password hashing, two-factor authentication, and secure session management.
</small>
</div>
<p class="mb-0">
<small>&copy; 2025 Flask 2FA App. Built with security in mind.</small>
</p>
</div>
</footer>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Additional security: CSP nonce for inline scripts if needed -->
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,169 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="card bg-success text-white mb-4">
<div class="card-body">
<h2 class="mb-0">
<i class="bi bi-speedometer2"></i> Welcome to Your Dashboard, {{ user.username }}!
</h2>
<p class="mb-0">You have successfully logged in with two-factor authentication.</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-body text-center">
<i class="bi bi-person-circle display-4 text-primary mb-3"></i>
<h5 class="card-title">Account Information</h5>
<p class="card-text">View and manage your account settings and security preferences.</p>
<a href="{{ url_for('main.profile') }}" class="btn btn-primary">
<i class="bi bi-gear"></i> Manage Profile
</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body text-center">
<i class="bi bi-shield-check display-4 text-success mb-3"></i>
<h5 class="card-title">Security Status</h5>
<p class="card-text">
{% if user.is_2fa_enabled %}
<span class="badge bg-success">2FA Enabled</span>
{% else %}
<span class="badge bg-warning">2FA Setup Required</span>
{% endif %}
</p>
<a href="{{ url_for('main.profile') }}" class="btn btn-outline-success">
<i class="bi bi-shield-lock"></i> Security Settings
</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body text-center">
<i class="bi bi-clock-history display-4 text-info mb-3"></i>
<h5 class="card-title">Last Login</h5>
<p class="card-text">
{% if user.last_login %}
{{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') }}
{% else %}
First login - Welcome!
{% endif %}
</p>
<small class="text-muted">Keep track of your account activity</small>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h4><i class="bi bi-activity"></i> Account Activity</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Account Details</h6>
<table class="table table-borderless">
<tr>
<td><strong>Username:</strong></td>
<td>{{ user.username }}</td>
</tr>
<tr>
<td><strong>Email:</strong></td>
<td>{{ user.email }}</td>
</tr>
<tr>
<td><strong>Account Created:</strong></td>
<td>{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}</td>
</tr>
<tr>
<td><strong>2FA Status:</strong></td>
<td>
{% if user.is_2fa_enabled %}
<span class="badge bg-success">
<i class="bi bi-check-circle"></i> Enabled
</span>
{% else %}
<span class="badge bg-warning">
<i class="bi bi-exclamation-triangle"></i> Setup Required
</span>
{% endif %}
</td>
</tr>
</table>
</div>
<div class="col-md-6">
<h6>Security Recommendations</h6>
<div class="list-group">
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-check-circle text-success"></i>
Strong password in use
</div>
<span class="badge bg-success rounded-pill"></span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-{% if user.is_2fa_enabled %}check-circle text-success{% else %}exclamation-triangle text-warning{% endif %}"></i>
Two-factor authentication
</div>
<span class="badge bg-{% if user.is_2fa_enabled %}success{% else %}warning{% endif %} rounded-pill">
{% if user.is_2fa_enabled %}✓{% else %}!{% endif %}
</span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-check-circle text-success"></i>
Secure session active
</div>
<span class="badge bg-success rounded-pill"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card border-info">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-lightbulb"></i> Security Tips</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<ul class="list-unstyled">
<li><i class="bi bi-arrow-right text-primary"></i> Always log out when using shared computers</li>
<li><i class="bi bi-arrow-right text-primary"></i> Keep your authenticator app backed up</li>
<li><i class="bi bi-arrow-right text-primary"></i> Use unique passwords for all accounts</li>
</ul>
</div>
<div class="col-md-6">
<ul class="list-unstyled">
<li><i class="bi bi-arrow-right text-primary"></i> Never share your 2FA codes with anyone</li>
<li><i class="bi bi-arrow-right text-primary"></i> Check for suspicious account activity regularly</li>
<li><i class="bi bi-arrow-right text-primary"></i> Update your password periodically</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

138
app/templates/index.html Normal file
View File

@@ -0,0 +1,138 @@
{% extends "base.html" %}
{% block content %}
<div class="jumbotron bg-primary text-white rounded p-5 mb-4">
<div class="container text-center">
<h1 class="display-4">
<i class="bi bi-shield-lock"></i> Flask 2FA Authentication
</h1>
<p class="lead">
Secure web application with two-factor authentication, built with security best practices.
</p>
{% if not current_user.is_authenticated %}
<div class="mt-4">
<a href="{{ url_for('auth.register') }}" class="btn btn-light btn-lg me-3">
<i class="bi bi-person-plus"></i> Get Started
</a>
<a href="{{ url_for('auth.login') }}" class="btn btn-outline-light btn-lg">
<i class="bi bi-box-arrow-in-right"></i> Sign In
</a>
</div>
{% else %}
<div class="mt-4">
<a href="{{ url_for('main.dashboard') }}" class="btn btn-light btn-lg">
<i class="bi bi-speedometer2"></i> Go to Dashboard
</a>
</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="card h-100">
<div class="card-body text-center">
<i class="bi bi-shield-check display-4 text-success mb-3"></i>
<h5 class="card-title">Two-Factor Authentication</h5>
<p class="card-text">
Enhanced security with TOTP-based 2FA using industry-standard protocols.
Compatible with Google Authenticator, Authy, and other popular apps.
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body text-center">
<i class="bi bi-lock display-4 text-primary mb-3"></i>
<h5 class="card-title">Secure by Design</h5>
<p class="card-text">
Built with security best practices including CSRF protection,
bcrypt password hashing, secure sessions, and input validation.
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body text-center">
<i class="bi bi-code-slash display-4 text-info mb-3"></i>
<h5 class="card-title">Modern Flask App</h5>
<p class="card-text">
Implements Flask application factory pattern, blueprints,
SQLAlchemy ORM, and follows Flask security recommendations.
</p>
</div>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h3><i class="bi bi-info-circle"></i> Security Features</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h5>Authentication & Authorization</h5>
<ul>
<li><strong>Two-Factor Authentication:</strong> TOTP-based 2FA with PyOTP</li>
<li><strong>Secure Password Storage:</strong> Bcrypt hashing with salt</li>
<li><strong>Session Management:</strong> Flask-Login with secure settings</li>
<li><strong>Login Protection:</strong> Strong session protection enabled</li>
</ul>
<h5 class="mt-4">Data Protection</h5>
<ul>
<li><strong>CSRF Protection:</strong> Automatic token validation on forms</li>
<li><strong>SQL Injection Prevention:</strong> Parameterized queries</li>
<li><strong>Input Validation:</strong> Server-side validation with WTForms</li>
<li><strong>XSS Prevention:</strong> Automatic template escaping</li>
</ul>
</div>
<div class="col-md-6">
<h5>HTTP Security</h5>
<ul>
<li><strong>Security Headers:</strong> HSTS, X-Frame-Options, CSP</li>
<li><strong>Secure Cookies:</strong> HTTPOnly, Secure, SameSite flags</li>
<li><strong>Content Security Policy:</strong> Prevents code injection</li>
<li><strong>HTTPS Enforcement:</strong> Production-ready configuration</li>
</ul>
<h5 class="mt-4">Application Security</h5>
<ul>
<li><strong>Database Security:</strong> Connection pooling and timeouts</li>
<li><strong>Error Handling:</strong> Secure error pages without information disclosure</li>
<li><strong>Logging:</strong> Security events logged for monitoring</li>
<li><strong>Environment Configuration:</strong> Separate configs for dev/prod</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{% if not current_user.is_authenticated %}
<div class="row mt-4">
<div class="col-md-12">
<div class="card bg-light">
<div class="card-body text-center">
<h4><i class="bi bi-rocket"></i> Ready to Get Started?</h4>
<p class="lead">
Create your secure account in minutes and experience enterprise-grade security.
</p>
<a href="{{ url_for('auth.register') }}" class="btn btn-primary btn-lg">
<i class="bi bi-person-plus"></i> Create Account Now
</a>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

220
app/templates/profile.html Normal file
View File

@@ -0,0 +1,220 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3><i class="bi bi-person-circle"></i> Profile Information</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h5>Account Details</h5>
<table class="table table-borderless">
<tr>
<td><strong>Username:</strong></td>
<td>{{ user.username }}</td>
</tr>
<tr>
<td><strong>Email:</strong></td>
<td>{{ user.email }}</td>
</tr>
<tr>
<td><strong>Account ID:</strong></td>
<td>{{ user.id }}</td>
</tr>
<tr>
<td><strong>Member Since:</strong></td>
<td>{{ user.created_at.strftime('%B %d, %Y') if user.created_at else 'N/A' }}</td>
</tr>
<tr>
<td><strong>Last Login:</strong></td>
<td>{{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') if user.last_login else 'First login' }}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<h5>Quick Actions</h5>
<div class="d-grid gap-2">
<a href="{{ url_for('main.dashboard') }}" class="btn btn-primary">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
<button type="button" class="btn btn-outline-secondary" disabled>
<i class="bi bi-pencil"></i> Edit Profile (Coming Soon)
</button>
<button type="button" class="btn btn-outline-secondary" disabled>
<i class="bi bi-key"></i> Change Password (Coming Soon)
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h4><i class="bi bi-shield-lock"></i> Security Settings</h4>
</div>
<div class="card-body">
<div class="security-status mb-3">
<h6>Two-Factor Authentication</h6>
{% if user.is_2fa_enabled %}
<div class="alert alert-success">
<i class="bi bi-check-circle"></i>
<strong>Enabled</strong><br>
Your account is protected with 2FA
</div>
<form method="POST" action="{{ url_for('auth.disable_2fa') }}"
onsubmit="return confirm('Are you sure you want to disable two-factor authentication? This will make your account less secure.');">
{{ csrf_token() }}
<button type="submit" class="btn btn-outline-warning btn-sm">
<i class="bi bi-shield-x"></i> Disable 2FA
</button>
</form>
{% else %}
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i>
<strong>Setup Required</strong><br>
Complete 2FA setup for better security
</div>
<a href="{{ url_for('auth.register') }}" class="btn btn-success btn-sm">
<i class="bi bi-shield-check"></i> Setup 2FA
</a>
{% endif %}
</div>
<hr>
<h6>Security Score</h6>
<div class="progress mb-2">
<div class="progress-bar {% if user.is_2fa_enabled %}bg-success{% else %}bg-warning{% endif %}"
style="width: {% if user.is_2fa_enabled %}100{% else %}60{% endif %}%">
</div>
</div>
<small class="text-muted">
{% if user.is_2fa_enabled %}
Excellent - All security features enabled
{% else %}
Good - Enable 2FA to reach 100%
{% endif %}
</small>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h4><i class="bi bi-graph-up"></i> Account Activity</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="card bg-light">
<div class="card-body text-center">
<i class="bi bi-calendar-check display-6 text-primary"></i>
<h6 class="mt-2">Account Age</h6>
<h4>
{% if user.created_at %}
{{ ((user.created_at - user.created_at).days) if user.created_at else 0 }} days
{% else %}
New
{% endif %}
</h4>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-light">
<div class="card-body text-center">
<i class="bi bi-shield-check display-6 text-success"></i>
<h6 class="mt-2">Security Level</h6>
<h4>
{% if user.is_2fa_enabled %}
High
{% else %}
Medium
{% endif %}
</h4>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-light">
<div class="card-body text-center">
<i class="bi bi-clock-history display-6 text-info"></i>
<h6 class="mt-2">Session Status</h6>
<h4>Active</h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card border-info">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Account Security Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Authentication Methods</h6>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
Password Authentication
<span class="badge bg-success rounded-pill">Active</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Two-Factor Authentication
<span class="badge bg-{% if user.is_2fa_enabled %}success{% else %}secondary{% endif %} rounded-pill">
{% if user.is_2fa_enabled %}Active{% else %}Inactive{% endif %}
</span>
</li>
</ul>
</div>
<div class="col-md-6">
<h6>Security Features</h6>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
CSRF Protection
<span class="badge bg-success rounded-pill">Active</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Secure Session
<span class="badge bg-success rounded-pill">Active</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Password Encryption
<span class="badge bg-success rounded-pill">Bcrypt</span>
</li>
</ul>
</div>
</div>
<div class="alert alert-info mt-3 mb-0">
<i class="bi bi-lightbulb"></i>
<strong>Security Tip:</strong> Your account benefits from multiple layers of security including
encrypted passwords, CSRF protection, secure sessions, and optional two-factor authentication.
All these features work together to keep your account safe from unauthorized access.
</div>
</div>
</div>
</div>
</div>
{% endblock %}

66
config.py Normal file
View File

@@ -0,0 +1,66 @@
import os
from datetime import timedelta
class Config:
"""Base configuration class with security best practices."""
# Security: Use environment variables for sensitive data
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-key-change-in-production-use-secrets-manager'
# CSRF Protection - enabled by default with Flask-WTF
WTF_CSRF_ENABLED = True
WTF_CSRF_TIME_LIMIT = 3600 # 1 hour CSRF token timeout
# Database configuration with connection pooling
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_pre_ping': True, # Verify connections before use
'pool_recycle': 300, # Recycle connections every 5 minutes
}
# Session security settings
PERMANENT_SESSION_LIFETIME = timedelta(hours=1)
SESSION_COOKIE_SECURE = True # HTTPS only cookies
SESSION_COOKIE_HTTPONLY = True # Prevent XSS attacks
SESSION_COOKIE_SAMESITE = 'Lax' # CSRF protection
# Additional security headers
SEND_FILE_MAX_AGE_DEFAULT = timedelta(hours=1)
class DevelopmentConfig(Config):
"""Development configuration with debug enabled."""
DEBUG = True
# Allow HTTP cookies in development
SESSION_COOKIE_SECURE = False
# Development database
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 'sqlite:///dev.db'
class ProductionConfig(Config):
"""Production configuration with enhanced security."""
DEBUG = False
# Production should use PostgreSQL or similar
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'postgresql://user:pass@localhost/flask_2fa'
# Stricter session settings for production
PERMANENT_SESSION_LIFETIME = timedelta(minutes=30)
class TestingConfig(Config):
"""Testing configuration."""
TESTING = True
WTF_CSRF_ENABLED = False # Disable CSRF for testing
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig
}

47
init_db.py Normal file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
Database initialization script for Flask 2FA Authentication Application
This script creates the database tables and sets up the initial schema.
"""
import os
import sys
# Add the project root to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app import create_app, db
from app.models import User
def init_database():
"""Initialize the database with tables."""
app = create_app('development')
with app.app_context():
print("Creating database tables...")
# Create all tables
db.create_all()
print("Database tables created successfully!")
# Show created tables
print("\nCreated tables:")
print("- User table with columns:")
print(" - id (Primary Key)")
print(" - username (Unique)")
print(" - email (Unique)")
print(" - password_hash")
print(" - totp_secret")
print(" - is_2fa_enabled")
print(" - created_at")
print(" - last_login")
print("\nDatabase initialization complete!")
print("You can now run the application with: python run.py")
if __name__ == '__main__':
init_database()

1
migrations/README Normal file
View File

@@ -0,0 +1 @@
Single-database configuration for Flask.

50
migrations/alembic.ini Normal file
View File

@@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

113
migrations/env.py Normal file
View File

@@ -0,0 +1,113 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
# this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
conf_args = current_app.extensions['migrate'].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
connectable = get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,46 @@
"""Initial migration with User model
Revision ID: 4ee6bbb732e4
Revises:
Create Date: 2025-05-29 23:49:19.086429
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4ee6bbb732e4'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=80), nullable=False),
sa.Column('email', sa.String(length=120), nullable=False),
sa.Column('password_hash', sa.String(length=255), nullable=False),
sa.Column('totp_secret', sa.String(length=32), nullable=True),
sa.Column('is_2fa_enabled', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('last_login', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True)
batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_user_username'))
batch_op.drop_index(batch_op.f('ix_user_email'))
op.drop_table('user')
# ### end Alembic commands ###

11
requirements.txt Normal file
View File

@@ -0,0 +1,11 @@
Flask>=2.3.0
Flask-Login>=0.6.3
Flask-WTF>=1.2.1
Flask-SQLAlchemy>=3.1.1
Flask-Migrate>=4.0.5
SQLAlchemy>=2.0.0
PyOTP>=2.9.0
bcrypt>=4.1.2
qrcode[pil]>=7.4.2
python-dotenv>=1.0.0
WTForms>=3.1.0

115
run.py Normal file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
Flask 2FA Authentication Application
This is the main entry point for the Flask application with two-factor authentication.
The application implements security best practices including:
- CSRF protection
- Secure password hashing with bcrypt
- Two-factor authentication using TOTP
- Secure session management
- SQL injection prevention
- XSS protection
Security considerations:
- Never run in debug mode in production
- Use environment variables for sensitive configuration
- Ensure HTTPS is enabled in production
- Regularly update dependencies for security patches
"""
import os
from app import create_app, db
from app.models import User
from flask_migrate import upgrade
def deploy():
"""
Deployment function for production environments.
This function:
1. Creates the application instance
2. Runs database migrations
3. Sets up initial data if needed
"""
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
with app.app_context():
# Create database tables
db.create_all()
# Run database migrations
upgrade()
# Create application instance
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
@app.shell_context_processor
def make_shell_context():
"""
Shell context processor for Flask shell command.
Provides convenient access to common objects when using 'flask shell'.
"""
return {
'db': db,
'User': User
}
@app.cli.command()
def deploy_cmd():
"""Flask CLI command for deployment."""
deploy()
@app.cli.command()
def create_admin():
"""
Create an admin user for testing purposes.
Security note: This is for development/testing only.
In production, admin users should be created through secure processes.
"""
username = input("Admin username: ")
email = input("Admin email: ")
password = input("Admin password: ")
if User.query.filter_by(username=username).first():
print(f"User {username} already exists!")
return
admin_user = User(username=username, email=email)
admin_user.set_password(password)
admin_user.generate_totp_secret()
db.session.add(admin_user)
db.session.commit()
print(f"Admin user {username} created successfully!")
print("Please complete 2FA setup by registering through the web interface.")
if __name__ == '__main__':
# Security warning for development
if app.config.get('DEBUG'):
print("\n" + "="*60)
print("WARNING: Running in DEBUG mode!")
print("This should NEVER be used in production.")
print("Set FLASK_CONFIG=production for production deployment.")
print("="*60 + "\n")
# Get port from environment or default to 5000
port = int(os.environ.get('PORT', 5000))
# Run the application
# Note: For production, use a proper WSGI server like Gunicorn
app.run(
host='127.0.0.1', # Change to '0.0.0.0' if you need external access
port=port,
debug=app.config.get('DEBUG', False),
threaded=True # Enable threading for better performance
)

131
test_app.py Normal file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
Test script for Flask 2FA Authentication Application
This script performs basic functionality tests to ensure the application
is working correctly.
"""
import os
import sys
import requests
import time
# Add the project root to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app import create_app, db
from app.models import User
def test_database():
"""Test database connectivity and User model."""
print("Testing database connectivity...")
app = create_app('development')
with app.app_context():
try:
# Test database connection
db.create_all()
# Test User model
test_user = User(username='testuser', email='test@example.com')
test_user.set_password('testpassword123')
test_user.generate_totp_secret()
print("✓ Database connection successful")
print("✓ User model working correctly")
print("✓ Password hashing working")
print("✓ TOTP secret generation working")
# Test TOTP URI generation
uri = test_user.generate_totp_uri()
print(f"✓ TOTP URI generated: {uri[:50]}...")
# Test QR code generation
qr_code = test_user.generate_qr_code()
print(f"✓ QR code generated ({len(qr_code)} characters)")
return True
except Exception as e:
print(f"✗ Database test failed: {e}")
return False
def test_application_startup():
"""Test if the application starts without errors."""
print("\nTesting application startup...")
try:
app = create_app('development')
with app.test_client() as client:
# Test home page
response = client.get('/')
if response.status_code == 200:
print("✓ Home page loads successfully")
else:
print(f"✗ Home page failed: {response.status_code}")
return False
# Test registration page
response = client.get('/auth/register')
if response.status_code == 200:
print("✓ Registration page loads successfully")
else:
print(f"✗ Registration page failed: {response.status_code}")
return False
# Test login page
response = client.get('/auth/login')
if response.status_code == 200:
print("✓ Login page loads successfully")
else:
print(f"✗ Login page failed: {response.status_code}")
return False
return True
except Exception as e:
print(f"✗ Application startup test failed: {e}")
return False
def run_all_tests():
"""Run all tests."""
print("=" * 60)
print("Flask 2FA Authentication Application - Test Suite")
print("=" * 60)
tests_passed = 0
total_tests = 2
# Test database
if test_database():
tests_passed += 1
# Test application startup
if test_application_startup():
tests_passed += 1
# Results
print("\n" + "=" * 60)
print("TEST RESULTS")
print("=" * 60)
print(f"Tests passed: {tests_passed}/{total_tests}")
if tests_passed == total_tests:
print("✓ All tests passed! The application is ready to use.")
print("\nTo start the application:")
print(" python run.py")
print("\nThen open your browser to: http://127.0.0.1:5000")
else:
print("✗ Some tests failed. Please check the errors above.")
print("=" * 60)
if __name__ == '__main__':
run_all_tests()