mirror of
https://github.com/lightningcell/flask-2fa-auth.git
synced 2026-05-26 07:08:07 +00:00
Base scripts and templates added
This commit is contained in:
43
.env.example
Normal file
43
.env.example
Normal 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
1
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1 @@
|
||||
- Default terminal is the Windows PowerShell, please use the PowerShell rules when using the terminal.
|
||||
261
README.md
Normal file
261
README.md
Normal 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
69
app/__init__.py
Normal 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
5
app/auth/__init__.py
Normal 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
79
app/auth/forms.py
Normal 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
223
app/auth/routes.py
Normal 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
5
app/main/__init__.py
Normal 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
36
app/main/routes.py
Normal 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
147
app/models.py
Normal 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")
|
||||
94
app/templates/auth/login.html
Normal file
94
app/templates/auth/login.html
Normal 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 %}
|
||||
108
app/templates/auth/register.html
Normal file
108
app/templates/auth/register.html
Normal 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 %}
|
||||
135
app/templates/auth/setup_2fa.html
Normal file
135
app/templates/auth/setup_2fa.html
Normal 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 %}
|
||||
104
app/templates/auth/verify_otp.html
Normal file
104
app/templates/auth/verify_otp.html
Normal 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
138
app/templates/base.html
Normal 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>© 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>
|
||||
169
app/templates/dashboard.html
Normal file
169
app/templates/dashboard.html
Normal 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
138
app/templates/index.html
Normal 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
220
app/templates/profile.html
Normal 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
66
config.py
Normal 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
47
init_db.py
Normal 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
1
migrations/README
Normal file
@@ -0,0 +1 @@
|
||||
Single-database configuration for Flask.
|
||||
50
migrations/alembic.ini
Normal file
50
migrations/alembic.ini
Normal 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
113
migrations/env.py
Normal 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
24
migrations/script.py.mako
Normal 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"}
|
||||
@@ -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
11
requirements.txt
Normal 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
115
run.py
Normal 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
131
test_app.py
Normal 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()
|
||||
Reference in New Issue
Block a user