diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7016ace --- /dev/null +++ b/.env.example @@ -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 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..2482da3 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +- Default terminal is the Windows PowerShell, please use the PowerShell rules when using the terminal. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9bb370b --- /dev/null +++ b/README.md @@ -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 + 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. diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..f84490c --- /dev/null +++ b/app/__init__.py @@ -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 diff --git a/app/auth/__init__.py b/app/auth/__init__.py new file mode 100644 index 0000000..088b033 --- /dev/null +++ b/app/auth/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('auth', __name__) + +from app.auth import routes diff --git a/app/auth/forms.py b/app/auth/forms.py new file mode 100644 index 0000000..930af4f --- /dev/null +++ b/app/auth/forms.py @@ -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.') diff --git a/app/auth/routes.py b/app/auth/routes.py new file mode 100644 index 0000000..2c02104 --- /dev/null +++ b/app/auth/routes.py @@ -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')) diff --git a/app/main/__init__.py b/app/main/__init__.py new file mode 100644 index 0000000..3b580b0 --- /dev/null +++ b/app/main/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('main', __name__) + +from app.main import routes diff --git a/app/main/routes.py b/app/main/routes.py new file mode 100644 index 0000000..5b5e65b --- /dev/null +++ b/app/main/routes.py @@ -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) diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..0a497e0 --- /dev/null +++ b/app/models.py @@ -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'' + + 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") diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html new file mode 100644 index 0000000..a5ac148 --- /dev/null +++ b/app/templates/auth/login.html @@ -0,0 +1,94 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

+ Sign In +

+ +
+ Security Notice: This application uses two-factor authentication. + After entering your credentials, you'll need to provide a code from your authenticator app. +
+ +
+ {{ form.hidden_tag() }} + + +
+ {{ 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 %} +
+ {% for error in form.username.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ + +
+ {{ 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 %} +
+ {% for error in form.password.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ + +
+ {{ form.remember_me(class="form-check-input") }} + {{ form.remember_me.label(class="form-check-label") }} +
Keep me logged in on this device
+
+ + +
+ {{ form.submit(class="btn btn-primary btn-lg") }} +
+
+ +
+ +
+

Don't have an account? + Register here +

+
+
+
+
+ +
+
+
+
+
Login Process
+
+
+
    +
  1. Enter your username and password
  2. +
  3. If credentials are valid, you'll be redirected to 2FA verification
  4. +
  5. Open your authenticator app (Google Authenticator, Authy, etc.)
  6. +
  7. Enter the 6-digit code from your app
  8. +
  9. Access granted to your secure dashboard
  10. +
+
+ + Security Tip: Never share your authentication codes with anyone. + They expire every 30 seconds for your protection. +
+
+
+
+
+{% endblock %} diff --git a/app/templates/auth/register.html b/app/templates/auth/register.html new file mode 100644 index 0000000..dce971e --- /dev/null +++ b/app/templates/auth/register.html @@ -0,0 +1,108 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

+ Create Account +

+ +
+ Security Notice: Your password will be securely hashed using bcrypt. + Two-factor authentication will be automatically enabled for enhanced security. +
+ +
+ {{ form.hidden_tag() }} + + +
+ {{ form.username.label(class="form-label") }} + {{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }} + {% if form.username.errors %} +
+ {% for error in form.username.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
3-20 characters, alphanumeric only
+
+ + +
+ {{ form.email.label(class="form-label") }} + {{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }} + {% if form.email.errors %} +
+ {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ + +
+ {{ form.password.label(class="form-label") }} + {{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }} + {% if form.password.errors %} +
+ {% for error in form.password.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
Minimum 8 characters
+
+ + +
+ {{ form.password2.label(class="form-label") }} + {{ form.password2(class="form-control" + (" is-invalid" if form.password2.errors else "")) }} + {% if form.password2.errors %} +
+ {% for error in form.password2.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ + +
+ {{ form.submit(class="btn btn-primary btn-lg") }} +
+
+ +
+ +
+

Already have an account? + Sign in here +

+
+
+
+
+ +
+
+
+
+
Security Features
+
+
+
    +
  • Passwords are hashed using bcrypt with salt
  • +
  • CSRF protection on all forms
  • +
  • Two-factor authentication required
  • +
  • Secure session management
  • +
  • Input validation and sanitization
  • +
+
+
+
+
+{% endblock %} diff --git a/app/templates/auth/setup_2fa.html b/app/templates/auth/setup_2fa.html new file mode 100644 index 0000000..d126057 --- /dev/null +++ b/app/templates/auth/setup_2fa.html @@ -0,0 +1,135 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+

+ Registration Successful! +

+
+
+
+ + Welcome, {{ username }}! Your account has been created successfully. + To complete the setup, please scan the QR code below with your authenticator app. +
+ +

+ Setup Two-Factor Authentication +

+ +
+ QR Code for 2FA Setup + +

+ Scan this QR code with your authenticator app +

+
+ +
+
+
Setup Instructions:
+
    +
  1. Download an authenticator app: +
      +
    • Google Authenticator
    • +
    • Microsoft Authenticator
    • +
    • Authy
    • +
    • 1Password
    • +
    +
  2. +
  3. Open the app and tap "Add Account" or "+"
  4. +
  5. Choose "Scan QR Code"
  6. +
  7. Point your camera at the QR code above
  8. +
  9. Your account will be added automatically
  10. +
+
+
+
Security Benefits:
+
    +
  • Protects against password theft
  • +
  • Prevents unauthorized access
  • +
  • Works offline (no internet required)
  • +
  • Industry-standard TOTP protocol
  • +
  • Compatible with all major apps
  • +
+
+
+ +
+ + Important: Save your recovery codes or backup your authenticator app. + If you lose access to your authenticator, you may not be able to log in. +
+ + +
+
+
+
+ +
+
+
+
+
Frequently Asked Questions
+
+
+
+
+

+ +

+
+
+ 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. +
+
+
+ +
+

+ +

+
+
+ Google Authenticator is the most popular choice, but Microsoft Authenticator, Authy, + and 1Password also work excellently. Choose one that you're comfortable with. +
+
+
+ +
+

+ +

+
+
+ 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. +
+
+
+
+
+
+
+
+{% endblock %} diff --git a/app/templates/auth/verify_otp.html b/app/templates/auth/verify_otp.html new file mode 100644 index 0000000..845f880 --- /dev/null +++ b/app/templates/auth/verify_otp.html @@ -0,0 +1,104 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

+ Two-Factor Authentication +

+ +
+ + Security Step: Enter the 6-digit code from your authenticator app to complete login. +
+ +
+ {{ form.hidden_tag() }} + + +
+ {{ form.token.label(class="form-label") }} +
+ + + + {{ 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 %} +
+ {% for error in form.token.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+
+ Codes refresh every 30 seconds +
+
+ + +
+ {{ form.submit(class="btn btn-success btn-lg") }} +
+
+ +
+ + +
+
+
+ +
+
+
+
+
Trouble with 2FA?
+
+
+
Common Issues:
+
    +
  • Code not working? Make sure your device's time is synchronized
  • +
  • Lost your phone? Contact support for account recovery
  • +
  • App not installed? Download Google Authenticator or Authy
  • +
+ +
+ + Security Notice: Each code can only be used once and expires after 30 seconds. + This prevents replay attacks and ensures your account remains secure. +
+
+
+
+
+ + +{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..893b04d --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,138 @@ + + + + + + + + + + + + + {% if title %}{{ title }} - Flask 2FA App{% else %}Flask 2FA App{% endif %} + + + + + + + + + + + + + + +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + + {% block content %}{% endblock %} +
+ + +
+
+
+ + + This application implements security best practices including CSRF protection, + secure password hashing, two-factor authentication, and secure session management. + +
+

+ © 2025 Flask 2FA App. Built with security in mind. +

+
+
+ + + + + + {% block scripts %}{% endblock %} + + diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html new file mode 100644 index 0000000..048a45e --- /dev/null +++ b/app/templates/dashboard.html @@ -0,0 +1,169 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+

+ Welcome to Your Dashboard, {{ user.username }}! +

+

You have successfully logged in with two-factor authentication.

+
+
+
+
+ +
+
+
+
+ +
Account Information
+

View and manage your account settings and security preferences.

+ + Manage Profile + +
+
+
+ +
+
+
+ +
Security Status
+

+ {% if user.is_2fa_enabled %} + 2FA Enabled + {% else %} + 2FA Setup Required + {% endif %} +

+ + Security Settings + +
+
+
+ +
+
+
+ +
Last Login
+

+ {% if user.last_login %} + {{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') }} + {% else %} + First login - Welcome! + {% endif %} +

+ Keep track of your account activity +
+
+
+
+ +
+
+
+
+

Account Activity

+
+
+
+
+
Account Details
+ + + + + + + + + + + + + + + + + +
Username:{{ user.username }}
Email:{{ user.email }}
Account Created:{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}
2FA Status: + {% if user.is_2fa_enabled %} + + Enabled + + {% else %} + + Setup Required + + {% endif %} +
+
+ +
+
Security Recommendations
+
+
+
+ + Strong password in use +
+ +
+
+
+ + Two-factor authentication +
+ + {% if user.is_2fa_enabled %}✓{% else %}!{% endif %} + +
+
+
+ + Secure session active +
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
Security Tips
+
+
+
+
+
    +
  • Always log out when using shared computers
  • +
  • Keep your authenticator app backed up
  • +
  • Use unique passwords for all accounts
  • +
+
+
+
    +
  • Never share your 2FA codes with anyone
  • +
  • Check for suspicious account activity regularly
  • +
  • Update your password periodically
  • +
+
+
+
+
+
+
+{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..d043ca3 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,138 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

+ Flask 2FA Authentication +

+

+ Secure web application with two-factor authentication, built with security best practices. +

+ {% if not current_user.is_authenticated %} + + {% else %} + + {% endif %} +
+
+ +
+
+
+
+ +
Two-Factor Authentication
+

+ Enhanced security with TOTP-based 2FA using industry-standard protocols. + Compatible with Google Authenticator, Authy, and other popular apps. +

+
+
+
+ +
+
+
+ +
Secure by Design
+

+ Built with security best practices including CSRF protection, + bcrypt password hashing, secure sessions, and input validation. +

+
+
+
+ +
+
+
+ +
Modern Flask App
+

+ Implements Flask application factory pattern, blueprints, + SQLAlchemy ORM, and follows Flask security recommendations. +

+
+
+
+
+ +
+
+
+
+

Security Features

+
+
+
+
+
Authentication & Authorization
+
    +
  • Two-Factor Authentication: TOTP-based 2FA with PyOTP
  • +
  • Secure Password Storage: Bcrypt hashing with salt
  • +
  • Session Management: Flask-Login with secure settings
  • +
  • Login Protection: Strong session protection enabled
  • +
+ +
Data Protection
+
    +
  • CSRF Protection: Automatic token validation on forms
  • +
  • SQL Injection Prevention: Parameterized queries
  • +
  • Input Validation: Server-side validation with WTForms
  • +
  • XSS Prevention: Automatic template escaping
  • +
+
+ +
+
HTTP Security
+
    +
  • Security Headers: HSTS, X-Frame-Options, CSP
  • +
  • Secure Cookies: HTTPOnly, Secure, SameSite flags
  • +
  • Content Security Policy: Prevents code injection
  • +
  • HTTPS Enforcement: Production-ready configuration
  • +
+ +
Application Security
+
    +
  • Database Security: Connection pooling and timeouts
  • +
  • Error Handling: Secure error pages without information disclosure
  • +
  • Logging: Security events logged for monitoring
  • +
  • Environment Configuration: Separate configs for dev/prod
  • +
+
+
+
+
+
+
+ +{% if not current_user.is_authenticated %} +
+
+
+
+

Ready to Get Started?

+

+ Create your secure account in minutes and experience enterprise-grade security. +

+ + Create Account Now + +
+
+
+
+{% endif %} +{% endblock %} diff --git a/app/templates/profile.html b/app/templates/profile.html new file mode 100644 index 0000000..c70a97d --- /dev/null +++ b/app/templates/profile.html @@ -0,0 +1,220 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+

Profile Information

+
+
+
+
+
Account Details
+ + + + + + + + + + + + + + + + + + + + + +
Username:{{ user.username }}
Email:{{ user.email }}
Account ID:{{ user.id }}
Member Since:{{ user.created_at.strftime('%B %d, %Y') if user.created_at else 'N/A' }}
Last Login:{{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') if user.last_login else 'First login' }}
+
+ +
+
Quick Actions
+
+ + Dashboard + + + +
+
+
+
+
+
+ +
+
+
+

Security Settings

+
+
+
+
Two-Factor Authentication
+ {% if user.is_2fa_enabled %} +
+ + Enabled
+ Your account is protected with 2FA +
+ +
+ {{ csrf_token() }} + +
+ {% else %} +
+ + Setup Required
+ Complete 2FA setup for better security +
+ + + Setup 2FA + + {% endif %} +
+ +
+ +
Security Score
+
+
+
+
+ + {% if user.is_2fa_enabled %} + Excellent - All security features enabled + {% else %} + Good - Enable 2FA to reach 100% + {% endif %} + +
+
+
+
+ +
+
+
+
+

Account Activity

+
+
+
+
+
+
+ +
Account Age
+

+ {% if user.created_at %} + {{ ((user.created_at - user.created_at).days) if user.created_at else 0 }} days + {% else %} + New + {% endif %} +

+
+
+
+ +
+
+
+ +
Security Level
+

+ {% if user.is_2fa_enabled %} + High + {% else %} + Medium + {% endif %} +

+
+
+
+ +
+
+
+ +
Session Status
+

Active

+
+
+
+
+
+
+
+
+ +
+
+
+
+
Account Security Information
+
+
+
+
+
Authentication Methods
+
    +
  • + Password Authentication + Active +
  • +
  • + Two-Factor Authentication + + {% if user.is_2fa_enabled %}Active{% else %}Inactive{% endif %} + +
  • +
+
+ +
+
Security Features
+
    +
  • + CSRF Protection + Active +
  • +
  • + Secure Session + Active +
  • +
  • + Password Encryption + Bcrypt +
  • +
+
+
+ +
+ + Security Tip: 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. +
+
+
+
+
+{% endblock %} diff --git a/config.py b/config.py new file mode 100644 index 0000000..db5283d --- /dev/null +++ b/config.py @@ -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 +} diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..030bff5 --- /dev/null +++ b/init_db.py @@ -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() diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -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 diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -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() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -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"} diff --git a/migrations/versions/4ee6bbb732e4_initial_migration_with_user_model.py b/migrations/versions/4ee6bbb732e4_initial_migration_with_user_model.py new file mode 100644 index 0000000..a3c2fb0 --- /dev/null +++ b/migrations/versions/4ee6bbb732e4_initial_migration_with_user_model.py @@ -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 ### diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0e08652 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/run.py b/run.py new file mode 100644 index 0000000..3d45bd8 --- /dev/null +++ b/run.py @@ -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 + ) diff --git a/test_app.py b/test_app.py new file mode 100644 index 0000000..8e1ff5e --- /dev/null +++ b/test_app.py @@ -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()