Base scripts and templates added

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

69
app/__init__.py Normal file
View File

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

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

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

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

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

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

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

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

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

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

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

147
app/models.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

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

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