mirror of
https://github.com/lightningcell/flask-2fa-auth.git
synced 2026-05-26 07:08:07 +00:00
Base scripts and templates added
This commit is contained in:
69
app/__init__.py
Normal file
69
app/__init__.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_login import LoginManager
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from config import config
|
||||
|
||||
# Initialize extensions
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
login_manager = LoginManager()
|
||||
csrf = CSRFProtect()
|
||||
|
||||
|
||||
def create_app(config_name='default'):
|
||||
"""
|
||||
Application factory pattern for creating Flask app instances.
|
||||
|
||||
Security considerations:
|
||||
- CSRF protection enabled globally
|
||||
- Secure session configuration
|
||||
- Login manager with proper security settings
|
||||
"""
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config[config_name])
|
||||
|
||||
# Initialize extensions with app
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
csrf.init_app(app)
|
||||
|
||||
# Configure Flask-Login for security
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = 'auth.login'
|
||||
login_manager.login_message = 'Please log in to access this page.'
|
||||
login_manager.login_message_category = 'info'
|
||||
login_manager.session_protection = 'strong' # Enhanced session protection
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
"""
|
||||
User loader function for Flask-Login.
|
||||
Uses parameterized query to prevent SQL injection.
|
||||
"""
|
||||
from app.models import User
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
# Register blueprints
|
||||
from app.auth import bp as auth_bp
|
||||
app.register_blueprint(auth_bp, url_prefix='/auth')
|
||||
|
||||
from app.main import bp as main_bp
|
||||
app.register_blueprint(main_bp)
|
||||
|
||||
# Security headers middleware
|
||||
@app.after_request
|
||||
def security_headers(response):
|
||||
"""Add security headers to all responses."""
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['X-Frame-Options'] = 'DENY'
|
||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
||||
return response
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# Import models to ensure they are registered with SQLAlchemy
|
||||
from app import models
|
||||
5
app/auth/__init__.py
Normal file
5
app/auth/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('auth', __name__)
|
||||
|
||||
from app.auth import routes
|
||||
79
app/auth/forms.py
Normal file
79
app/auth/forms.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, SubmitField, BooleanField
|
||||
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
|
||||
from app.models import User
|
||||
|
||||
|
||||
class RegistrationForm(FlaskForm):
|
||||
"""
|
||||
User registration form with validation.
|
||||
|
||||
Security: CSRF protection enabled automatically by Flask-WTF.
|
||||
"""
|
||||
username = StringField('Username', validators=[
|
||||
DataRequired(),
|
||||
Length(min=3, max=20, message='Username must be between 3 and 20 characters.')
|
||||
])
|
||||
email = StringField('Email', validators=[
|
||||
DataRequired(),
|
||||
Email(message='Invalid email address.')
|
||||
])
|
||||
password = PasswordField('Password', validators=[
|
||||
DataRequired(),
|
||||
Length(min=8, message='Password must be at least 8 characters long.')
|
||||
])
|
||||
password2 = PasswordField('Confirm Password', validators=[
|
||||
DataRequired(),
|
||||
EqualTo('password', message='Passwords must match.')
|
||||
])
|
||||
submit = SubmitField('Register')
|
||||
|
||||
def validate_username(self, username):
|
||||
"""
|
||||
Custom validator to check username uniqueness.
|
||||
|
||||
Security: Uses parameterized query to prevent SQL injection.
|
||||
"""
|
||||
user = User.query.filter_by(username=username.data).first()
|
||||
if user:
|
||||
raise ValidationError('Username already exists. Please choose a different one.')
|
||||
|
||||
def validate_email(self, email):
|
||||
"""
|
||||
Custom validator to check email uniqueness.
|
||||
|
||||
Security: Uses parameterized query to prevent SQL injection.
|
||||
"""
|
||||
user = User.query.filter_by(email=email.data).first()
|
||||
if user:
|
||||
raise ValidationError('Email already registered. Please choose a different one.')
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
"""
|
||||
User login form.
|
||||
|
||||
Security: CSRF protection enabled automatically by Flask-WTF.
|
||||
"""
|
||||
username = StringField('Username', validators=[DataRequired()])
|
||||
password = PasswordField('Password', validators=[DataRequired()])
|
||||
remember_me = BooleanField('Remember Me')
|
||||
submit = SubmitField('Sign In')
|
||||
|
||||
|
||||
class TwoFactorForm(FlaskForm):
|
||||
"""
|
||||
Two-factor authentication verification form.
|
||||
|
||||
Security: CSRF protection enabled automatically by Flask-WTF.
|
||||
"""
|
||||
token = StringField('Authentication Code', validators=[
|
||||
DataRequired(),
|
||||
Length(min=6, max=6, message='Authentication code must be 6 digits.')
|
||||
])
|
||||
submit = SubmitField('Verify')
|
||||
|
||||
def validate_token(self, token):
|
||||
"""Validate that token contains only digits."""
|
||||
if not token.data.isdigit():
|
||||
raise ValidationError('Authentication code must contain only digits.')
|
||||
223
app/auth/routes.py
Normal file
223
app/auth/routes.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from flask import render_template, redirect, url_for, flash, request, session
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
from urllib.parse import urlparse
|
||||
from app import db
|
||||
from app.auth import bp
|
||||
from app.auth.forms import RegistrationForm, LoginForm, TwoFactorForm
|
||||
from app.models import User
|
||||
import logging
|
||||
|
||||
# Configure logging for security events
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@bp.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
"""
|
||||
User registration endpoint with 2FA setup.
|
||||
|
||||
Security features:
|
||||
- CSRF protection via Flask-WTF
|
||||
- Password hashing via bcrypt
|
||||
- Input validation and sanitization
|
||||
- SQL injection prevention via parameterized queries
|
||||
"""
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
form = RegistrationForm()
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
# Create new user with hashed password
|
||||
user = User(username=form.username.data, email=form.email.data)
|
||||
user.set_password(form.password.data)
|
||||
|
||||
# Generate TOTP secret for 2FA
|
||||
user.generate_totp_secret()
|
||||
|
||||
# Save user to database
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
# Log successful registration (without sensitive data)
|
||||
logger.info(f'New user registered: {user.username}')
|
||||
|
||||
flash('Registration successful! Please scan the QR code with your authenticator app.', 'success')
|
||||
|
||||
# Store user ID in session for QR code display
|
||||
session['temp_user_id'] = user.id
|
||||
|
||||
return redirect(url_for('auth.setup_2fa'))
|
||||
|
||||
except Exception as e:
|
||||
# Rollback transaction on error
|
||||
db.session.rollback()
|
||||
logger.error(f'Registration error for user {form.username.data}: {str(e)}')
|
||||
flash('Registration failed. Please try again.', 'error')
|
||||
|
||||
return render_template('auth/register.html', form=form, title='Register')
|
||||
|
||||
|
||||
@bp.route('/setup-2fa')
|
||||
def setup_2fa():
|
||||
"""
|
||||
Display QR code for 2FA setup after registration.
|
||||
|
||||
Security: Requires temp_user_id in session to prevent unauthorized access.
|
||||
"""
|
||||
user_id = session.get('temp_user_id')
|
||||
if not user_id:
|
||||
flash('Invalid session. Please register again.', 'error')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
user = User.query.get(user_id)
|
||||
if not user:
|
||||
flash('User not found. Please register again.', 'error')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
# Generate QR code for Google Authenticator
|
||||
qr_code = user.generate_qr_code()
|
||||
|
||||
# Clear temp session data
|
||||
session.pop('temp_user_id', None)
|
||||
|
||||
return render_template('auth/setup_2fa.html',
|
||||
qr_code=qr_code,
|
||||
username=user.username,
|
||||
title='Setup Two-Factor Authentication')
|
||||
|
||||
|
||||
@bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""
|
||||
User login endpoint with first-factor authentication.
|
||||
|
||||
Security features:
|
||||
- CSRF protection via Flask-WTF
|
||||
- Secure password verification
|
||||
- Session protection
|
||||
- Login attempt logging
|
||||
"""
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
# Use parameterized query to prevent SQL injection
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
# Log failed login attempt
|
||||
logger.warning(f'Failed login attempt for username: {form.username.data}')
|
||||
flash('Invalid username or password', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# Store user ID in session for 2FA verification
|
||||
session['temp_user_id'] = user.id
|
||||
session['remember_me'] = form.remember_me.data
|
||||
|
||||
# Log successful first-factor authentication
|
||||
logger.info(f'First-factor authentication successful for user: {user.username}')
|
||||
|
||||
# Redirect to 2FA verification
|
||||
flash('Please enter your authentication code', 'info')
|
||||
return redirect(url_for('auth.verify_otp'))
|
||||
|
||||
return render_template('auth/login.html', form=form, title='Sign In')
|
||||
|
||||
|
||||
@bp.route('/verify-otp', methods=['GET', 'POST'])
|
||||
def verify_otp():
|
||||
"""
|
||||
Second-factor authentication endpoint using TOTP.
|
||||
|
||||
Security features:
|
||||
- CSRF protection via Flask-WTF
|
||||
- Time-based token verification
|
||||
- Session validation
|
||||
- Replay attack protection (built into PyOTP)
|
||||
"""
|
||||
user_id = session.get('temp_user_id')
|
||||
if not user_id:
|
||||
flash('Session expired. Please log in again.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
user = User.query.get(user_id)
|
||||
if not user:
|
||||
flash('User not found. Please log in again.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
form = TwoFactorForm()
|
||||
if form.validate_on_submit():
|
||||
if user.verify_totp(form.token.data):
|
||||
# Enable 2FA if this is the first successful verification
|
||||
if not user.is_2fa_enabled:
|
||||
user.enable_2fa()
|
||||
|
||||
# Update last login timestamp
|
||||
user.last_login = db.func.current_timestamp()
|
||||
db.session.commit()
|
||||
|
||||
# Clear temp session data
|
||||
remember_me = session.pop('remember_me', False)
|
||||
session.pop('temp_user_id', None)
|
||||
|
||||
# Complete login process
|
||||
login_user(user, remember=remember_me)
|
||||
|
||||
# Log successful login
|
||||
logger.info(f'Successful login for user: {user.username}')
|
||||
|
||||
flash('Login successful!', 'success')
|
||||
# Redirect to originally requested page or dashboard
|
||||
next_page = request.args.get('next')
|
||||
if not next_page or urlparse(next_page).netloc != '':
|
||||
next_page = url_for('main.index')
|
||||
return redirect(next_page)
|
||||
else:
|
||||
# Log failed 2FA attempt
|
||||
logger.warning(f'Failed 2FA verification for user: {user.username}')
|
||||
flash('Invalid authentication code. Please try again.', 'error')
|
||||
|
||||
return render_template('auth/verify_otp.html', form=form, title='Two-Factor Authentication')
|
||||
|
||||
|
||||
@bp.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
"""
|
||||
User logout endpoint.
|
||||
|
||||
Security: Properly clears session and logs security event.
|
||||
"""
|
||||
username = current_user.username if current_user.is_authenticated else 'Unknown'
|
||||
logout_user()
|
||||
|
||||
# Clear any remaining session data
|
||||
session.clear()
|
||||
|
||||
# Log logout event
|
||||
logger.info(f'User logged out: {username}')
|
||||
|
||||
flash('You have been logged out successfully.', 'info')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
|
||||
@bp.route('/disable-2fa', methods=['POST'])
|
||||
@login_required
|
||||
def disable_2fa():
|
||||
"""
|
||||
Disable two-factor authentication for the current user.
|
||||
|
||||
Security: Requires active login session and POST request.
|
||||
"""
|
||||
try:
|
||||
current_user.disable_2fa()
|
||||
logger.info(f'2FA disabled for user: {current_user.username}')
|
||||
flash('Two-factor authentication has been disabled.', 'warning')
|
||||
except Exception as e:
|
||||
logger.error(f'Error disabling 2FA for user {current_user.username}: {str(e)}')
|
||||
flash('Failed to disable two-factor authentication.', 'error')
|
||||
|
||||
return redirect(url_for('main.profile'))
|
||||
5
app/main/__init__.py
Normal file
5
app/main/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('main', __name__)
|
||||
|
||||
from app.main import routes
|
||||
36
app/main/routes.py
Normal file
36
app/main/routes.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from flask import render_template
|
||||
from flask_login import login_required, current_user
|
||||
from app.main import bp
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
@bp.route('/index')
|
||||
def index():
|
||||
"""
|
||||
Home page - shows different content for authenticated vs anonymous users.
|
||||
|
||||
Security: No sensitive data exposed to anonymous users.
|
||||
"""
|
||||
return render_template('index.html', title='Home')
|
||||
|
||||
|
||||
@bp.route('/dashboard')
|
||||
@login_required
|
||||
def dashboard():
|
||||
"""
|
||||
Protected dashboard for authenticated users.
|
||||
|
||||
Security: Requires valid login session with 2FA verification.
|
||||
"""
|
||||
return render_template('dashboard.html', title='Dashboard', user=current_user)
|
||||
|
||||
|
||||
@bp.route('/profile')
|
||||
@login_required
|
||||
def profile():
|
||||
"""
|
||||
User profile page with 2FA management.
|
||||
|
||||
Security: Requires valid login session.
|
||||
"""
|
||||
return render_template('profile.html', title='Profile', user=current_user)
|
||||
147
app/models.py
Normal file
147
app/models.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import secrets
|
||||
import pyotp
|
||||
import qrcode
|
||||
import io
|
||||
import base64
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import UserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from app import db
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
"""
|
||||
User model with two-factor authentication support.
|
||||
|
||||
Security features:
|
||||
- Bcrypt password hashing
|
||||
- TOTP secret generation and verification
|
||||
- Secure random secret generation
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
|
||||
password_hash = db.Column(db.String(255), nullable=False)
|
||||
totp_secret = db.Column(db.String(32), nullable=True)
|
||||
is_2fa_enabled = db.Column(db.Boolean, default=False, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
|
||||
last_login = db.Column(db.DateTime)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
def set_password(self, password):
|
||||
"""
|
||||
Hash and set the user's password using bcrypt.
|
||||
|
||||
Security: Uses bcrypt with automatic salt generation
|
||||
for resistance against rainbow table attacks.
|
||||
"""
|
||||
self.password_hash = generate_password_hash(password, method='pbkdf2:sha256')
|
||||
|
||||
def check_password(self, password):
|
||||
"""
|
||||
Verify the provided password against the stored hash.
|
||||
|
||||
Security: Uses constant-time comparison to prevent timing attacks.
|
||||
"""
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def generate_totp_secret(self):
|
||||
"""
|
||||
Generate a new TOTP secret for two-factor authentication.
|
||||
|
||||
Security: Uses cryptographically secure random generation.
|
||||
"""
|
||||
if not self.totp_secret:
|
||||
self.totp_secret = pyotp.random_base32()
|
||||
return self.totp_secret
|
||||
|
||||
def generate_totp_uri(self, issuer_name='Flask-2FA-App'):
|
||||
"""
|
||||
Generate a TOTP URI for QR code generation.
|
||||
|
||||
Args:
|
||||
issuer_name: The name of the application (displayed in authenticator apps)
|
||||
|
||||
Returns:
|
||||
str: TOTP URI compatible with authenticator apps like Google Authenticator
|
||||
"""
|
||||
if not self.totp_secret:
|
||||
self.generate_totp_secret()
|
||||
|
||||
return pyotp.totp.TOTP(self.totp_secret).provisioning_uri(
|
||||
name=self.username,
|
||||
issuer_name=issuer_name
|
||||
)
|
||||
|
||||
def verify_totp(self, token):
|
||||
"""
|
||||
Verify a TOTP token against the user's secret.
|
||||
|
||||
Args:
|
||||
token: The 6-digit TOTP token from the authenticator app
|
||||
|
||||
Returns:
|
||||
bool: True if the token is valid, False otherwise
|
||||
|
||||
Security: Uses time-window verification with built-in replay protection.
|
||||
"""
|
||||
if not self.totp_secret:
|
||||
return False
|
||||
|
||||
totp = pyotp.TOTP(self.totp_secret)
|
||||
# Verify token with a 1-period window (30 seconds before/after)
|
||||
return totp.verify(token, valid_window=1)
|
||||
|
||||
def generate_qr_code(self, issuer_name='Flask-2FA-App'):
|
||||
"""
|
||||
Generate a QR code for the TOTP URI.
|
||||
|
||||
Returns:
|
||||
str: Base64-encoded PNG image of the QR code
|
||||
"""
|
||||
uri = self.generate_totp_uri(issuer_name)
|
||||
|
||||
# Generate QR code
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=10,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data(uri)
|
||||
qr.make(fit=True)
|
||||
|
||||
# Create image
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
# Convert to base64 for HTML embedding
|
||||
img_buffer = io.BytesIO()
|
||||
img.save(img_buffer, format='PNG')
|
||||
img_buffer.seek(0)
|
||||
|
||||
return base64.b64encode(img_buffer.getvalue()).decode()
|
||||
|
||||
def enable_2fa(self):
|
||||
"""Enable two-factor authentication for the user."""
|
||||
if self.totp_secret:
|
||||
self.is_2fa_enabled = True
|
||||
db.session.commit()
|
||||
|
||||
def disable_2fa(self):
|
||||
"""Disable two-factor authentication for the user."""
|
||||
self.is_2fa_enabled = False
|
||||
self.totp_secret = None
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# Database event listeners for additional security
|
||||
from sqlalchemy import event
|
||||
|
||||
@event.listens_for(User.password_hash, 'set')
|
||||
def validate_password_hash(target, value, oldvalue, initiator):
|
||||
"""Ensure password hash is never stored as plaintext."""
|
||||
if value and not value.startswith('pbkdf2:sha256'):
|
||||
raise ValueError("Password must be hashed before storage")
|
||||
94
app/templates/auth/login.html
Normal file
94
app/templates/auth/login.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="auth-form">
|
||||
<h2 class="text-center mb-4">
|
||||
<i class="bi bi-box-arrow-in-right"></i> Sign In
|
||||
</h2>
|
||||
|
||||
<div class="security-notice">
|
||||
<strong>Security Notice:</strong> This application uses two-factor authentication.
|
||||
After entering your credentials, you'll need to provide a code from your authenticator app.
|
||||
</div>
|
||||
|
||||
<form method="POST" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Username field -->
|
||||
<div class="mb-3">
|
||||
{{ form.username.label(class="form-label") }}
|
||||
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else ""),
|
||||
placeholder="Enter your username") }}
|
||||
{% if form.username.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.username.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Password field -->
|
||||
<div class="mb-3">
|
||||
{{ form.password.label(class="form-label") }}
|
||||
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else ""),
|
||||
placeholder="Enter your password") }}
|
||||
{% if form.password.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.password.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Remember me checkbox -->
|
||||
<div class="mb-3 form-check">
|
||||
{{ form.remember_me(class="form-check-input") }}
|
||||
{{ form.remember_me.label(class="form-check-label") }}
|
||||
<div class="form-text">Keep me logged in on this device</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit button -->
|
||||
<div class="d-grid">
|
||||
{{ form.submit(class="btn btn-primary btn-lg") }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="text-center">
|
||||
<p>Don't have an account?
|
||||
<a href="{{ url_for('auth.register') }}" class="text-decoration-none">Register here</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="bi bi-shield-lock"></i> Login Process</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ol>
|
||||
<li>Enter your username and password</li>
|
||||
<li>If credentials are valid, you'll be redirected to 2FA verification</li>
|
||||
<li>Open your authenticator app (Google Authenticator, Authy, etc.)</li>
|
||||
<li>Enter the 6-digit code from your app</li>
|
||||
<li>Access granted to your secure dashboard</li>
|
||||
</ol>
|
||||
<div class="alert alert-info mt-3">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Security Tip:</strong> Never share your authentication codes with anyone.
|
||||
They expire every 30 seconds for your protection.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
108
app/templates/auth/register.html
Normal file
108
app/templates/auth/register.html
Normal file
@@ -0,0 +1,108 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="auth-form">
|
||||
<h2 class="text-center mb-4">
|
||||
<i class="bi bi-person-plus"></i> Create Account
|
||||
</h2>
|
||||
|
||||
<div class="security-notice">
|
||||
<strong>Security Notice:</strong> Your password will be securely hashed using bcrypt.
|
||||
Two-factor authentication will be automatically enabled for enhanced security.
|
||||
</div>
|
||||
|
||||
<form method="POST" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Username field -->
|
||||
<div class="mb-3">
|
||||
{{ form.username.label(class="form-label") }}
|
||||
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }}
|
||||
{% if form.username.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.username.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">3-20 characters, alphanumeric only</div>
|
||||
</div>
|
||||
|
||||
<!-- Email field -->
|
||||
<div class="mb-3">
|
||||
{{ form.email.label(class="form-label") }}
|
||||
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }}
|
||||
{% if form.email.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.email.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Password field -->
|
||||
<div class="mb-3">
|
||||
{{ form.password.label(class="form-label") }}
|
||||
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }}
|
||||
{% if form.password.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.password.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Minimum 8 characters</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm password field -->
|
||||
<div class="mb-3">
|
||||
{{ form.password2.label(class="form-label") }}
|
||||
{{ form.password2(class="form-control" + (" is-invalid" if form.password2.errors else "")) }}
|
||||
{% if form.password2.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.password2.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Submit button -->
|
||||
<div class="d-grid">
|
||||
{{ form.submit(class="btn btn-primary btn-lg") }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="text-center">
|
||||
<p>Already have an account?
|
||||
<a href="{{ url_for('auth.login') }}" class="text-decoration-none">Sign in here</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="bi bi-info-circle"></i> Security Features</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="bi bi-check-circle text-success"></i> Passwords are hashed using bcrypt with salt</li>
|
||||
<li><i class="bi bi-check-circle text-success"></i> CSRF protection on all forms</li>
|
||||
<li><i class="bi bi-check-circle text-success"></i> Two-factor authentication required</li>
|
||||
<li><i class="bi bi-check-circle text-success"></i> Secure session management</li>
|
||||
<li><i class="bi bi-check-circle text-success"></i> Input validation and sanitization</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
135
app/templates/auth/setup_2fa.html
Normal file
135
app/templates/auth/setup_2fa.html
Normal file
@@ -0,0 +1,135 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h3 class="mb-0">
|
||||
<i class="bi bi-check-circle"></i> Registration Successful!
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Welcome, {{ username }}!</strong> Your account has been created successfully.
|
||||
To complete the setup, please scan the QR code below with your authenticator app.
|
||||
</div>
|
||||
|
||||
<h4 class="text-center mb-4">
|
||||
<i class="bi bi-qr-code"></i> Setup Two-Factor Authentication
|
||||
</h4>
|
||||
|
||||
<div class="qr-code-container">
|
||||
<img src="data:image/png;base64,{{ qr_code }}"
|
||||
alt="QR Code for 2FA Setup"
|
||||
class="img-fluid mb-3"
|
||||
style="max-width: 300px;">
|
||||
|
||||
<p class="text-muted">
|
||||
Scan this QR code with your authenticator app
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6">
|
||||
<h5><i class="bi bi-list-ol"></i> Setup Instructions:</h5>
|
||||
<ol>
|
||||
<li>Download an authenticator app:
|
||||
<ul class="mt-2">
|
||||
<li>Google Authenticator</li>
|
||||
<li>Microsoft Authenticator</li>
|
||||
<li>Authy</li>
|
||||
<li>1Password</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Open the app and tap "Add Account" or "+"</li>
|
||||
<li>Choose "Scan QR Code"</li>
|
||||
<li>Point your camera at the QR code above</li>
|
||||
<li>Your account will be added automatically</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5><i class="bi bi-shield-check"></i> Security Benefits:</h5>
|
||||
<ul>
|
||||
<li>Protects against password theft</li>
|
||||
<li>Prevents unauthorized access</li>
|
||||
<li>Works offline (no internet required)</li>
|
||||
<li>Industry-standard TOTP protocol</li>
|
||||
<li>Compatible with all major apps</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning mt-4">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>Important:</strong> Save your recovery codes or backup your authenticator app.
|
||||
If you lose access to your authenticator, you may not be able to log in.
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="{{ url_for('auth.login') }}" class="btn btn-primary btn-lg">
|
||||
<i class="bi bi-box-arrow-in-right"></i> Continue to Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="bi bi-question-circle"></i> Frequently Asked Questions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="accordion" id="faqAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingOne">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne">
|
||||
What if I can't scan the QR code?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseOne" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
||||
<div class="accordion-body">
|
||||
You can manually enter the secret key in your authenticator app. Look for an option like
|
||||
"Enter code manually" or "Manual entry" in your app.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingTwo">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo">
|
||||
Which authenticator app should I use?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseTwo" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
||||
<div class="accordion-body">
|
||||
Google Authenticator is the most popular choice, but Microsoft Authenticator, Authy,
|
||||
and 1Password also work excellently. Choose one that you're comfortable with.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingThree">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree">
|
||||
Is this secure?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseThree" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
||||
<div class="accordion-body">
|
||||
Yes! This uses the industry-standard TOTP (Time-based One-Time Password) protocol,
|
||||
which is used by major services like Google, Microsoft, and GitHub. The codes change
|
||||
every 30 seconds and can't be reused.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
104
app/templates/auth/verify_otp.html
Normal file
104
app/templates/auth/verify_otp.html
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="auth-form">
|
||||
<h2 class="text-center mb-4">
|
||||
<i class="bi bi-shield-check"></i> Two-Factor Authentication
|
||||
</h2>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Security Step:</strong> Enter the 6-digit code from your authenticator app to complete login.
|
||||
</div>
|
||||
|
||||
<form method="POST" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Token field -->
|
||||
<div class="mb-3">
|
||||
{{ form.token.label(class="form-label") }}
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-key"></i>
|
||||
</span>
|
||||
{{ form.token(class="form-control form-control-lg text-center" + (" is-invalid" if form.token.errors else ""),
|
||||
placeholder="000000", maxlength="6", style="letter-spacing: 0.5em;") }}
|
||||
{% if form.token.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in form.token.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<i class="bi bi-clock"></i> Codes refresh every 30 seconds
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit button -->
|
||||
<div class="d-grid">
|
||||
{{ form.submit(class="btn btn-success btn-lg") }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('auth.login') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="bi bi-question-circle"></i> Trouble with 2FA?</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>Common Issues:</h6>
|
||||
<ul>
|
||||
<li><strong>Code not working?</strong> Make sure your device's time is synchronized</li>
|
||||
<li><strong>Lost your phone?</strong> Contact support for account recovery</li>
|
||||
<li><strong>App not installed?</strong> Download Google Authenticator or Authy</li>
|
||||
</ul>
|
||||
|
||||
<div class="alert alert-warning mt-3">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>Security Notice:</strong> Each code can only be used once and expires after 30 seconds.
|
||||
This prevents replay attacks and ensures your account remains secure.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto-focus the token input and auto-submit when 6 digits are entered
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tokenInput = document.getElementById('token');
|
||||
if (tokenInput) {
|
||||
tokenInput.focus();
|
||||
|
||||
tokenInput.addEventListener('input', function(e) {
|
||||
// Only allow digits
|
||||
this.value = this.value.replace(/\D/g, '');
|
||||
|
||||
// Auto-submit when 6 digits are entered
|
||||
if (this.value.length === 6) {
|
||||
// Small delay to show the complete code
|
||||
setTimeout(() => {
|
||||
this.form.submit();
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
138
app/templates/base.html
Normal file
138
app/templates/base.html
Normal file
@@ -0,0 +1,138 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<!-- Security headers implemented via meta tags -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; script-src 'self' https://cdn.jsdelivr.net; font-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="X-Frame-Options" content="DENY">
|
||||
|
||||
<title>
|
||||
{% if title %}{{ title }} - Flask 2FA App{% else %}Flask 2FA App{% endif %}
|
||||
</title>
|
||||
<!-- Bootstrap CSS for styling -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons - Multiple CDN options for reliability -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" rel="stylesheet" media="print" onload="this.media='all'">
|
||||
|
||||
<style>
|
||||
.security-notice {
|
||||
background-color: #e3f2fd;
|
||||
border-left: 4px solid #2196f3;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.qr-code-container {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ url_for('main.index') }}">
|
||||
<i class="bi bi-shield-lock"></i> Flask 2FA App
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.index') }}">Home</a>
|
||||
</li>
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.dashboard') }}">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.profile') }}">Profile</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle"></i> {{ current_user.username }}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{{ url_for('main.profile') }}">Profile</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.register') }}">Register</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="container mt-4">
|
||||
<!-- Flash messages -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Page content -->
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-5 py-4 bg-dark text-white">
|
||||
<div class="container text-center">
|
||||
<div class="security-notice text-light">
|
||||
<small>
|
||||
<i class="bi bi-shield-check"></i>
|
||||
This application implements security best practices including CSRF protection,
|
||||
secure password hashing, two-factor authentication, and secure session management.
|
||||
</small>
|
||||
</div>
|
||||
<p class="mb-0">
|
||||
<small>© 2025 Flask 2FA App. Built with security in mind.</small>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Additional security: CSP nonce for inline scripts if needed -->
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
169
app/templates/dashboard.html
Normal file
169
app/templates/dashboard.html
Normal file
@@ -0,0 +1,169 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card bg-success text-white mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="mb-0">
|
||||
<i class="bi bi-speedometer2"></i> Welcome to Your Dashboard, {{ user.username }}!
|
||||
</h2>
|
||||
<p class="mb-0">You have successfully logged in with two-factor authentication.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-person-circle display-4 text-primary mb-3"></i>
|
||||
<h5 class="card-title">Account Information</h5>
|
||||
<p class="card-text">View and manage your account settings and security preferences.</p>
|
||||
<a href="{{ url_for('main.profile') }}" class="btn btn-primary">
|
||||
<i class="bi bi-gear"></i> Manage Profile
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-shield-check display-4 text-success mb-3"></i>
|
||||
<h5 class="card-title">Security Status</h5>
|
||||
<p class="card-text">
|
||||
{% if user.is_2fa_enabled %}
|
||||
<span class="badge bg-success">2FA Enabled</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">2FA Setup Required</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<a href="{{ url_for('main.profile') }}" class="btn btn-outline-success">
|
||||
<i class="bi bi-shield-lock"></i> Security Settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-clock-history display-4 text-info mb-3"></i>
|
||||
<h5 class="card-title">Last Login</h5>
|
||||
<p class="card-text">
|
||||
{% if user.last_login %}
|
||||
{{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||
{% else %}
|
||||
First login - Welcome!
|
||||
{% endif %}
|
||||
</p>
|
||||
<small class="text-muted">Keep track of your account activity</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4><i class="bi bi-activity"></i> Account Activity</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>Account Details</h6>
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td><strong>Username:</strong></td>
|
||||
<td>{{ user.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Email:</strong></td>
|
||||
<td>{{ user.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Account Created:</strong></td>
|
||||
<td>{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>2FA Status:</strong></td>
|
||||
<td>
|
||||
{% if user.is_2fa_enabled %}
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle"></i> Enabled
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i> Setup Required
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h6>Security Recommendations</h6>
|
||||
<div class="list-group">
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="bi bi-check-circle text-success"></i>
|
||||
Strong password in use
|
||||
</div>
|
||||
<span class="badge bg-success rounded-pill">✓</span>
|
||||
</div>
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="bi bi-{% if user.is_2fa_enabled %}check-circle text-success{% else %}exclamation-triangle text-warning{% endif %}"></i>
|
||||
Two-factor authentication
|
||||
</div>
|
||||
<span class="badge bg-{% if user.is_2fa_enabled %}success{% else %}warning{% endif %} rounded-pill">
|
||||
{% if user.is_2fa_enabled %}✓{% else %}!{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="bi bi-check-circle text-success"></i>
|
||||
Secure session active
|
||||
</div>
|
||||
<span class="badge bg-success rounded-pill">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card border-info">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-lightbulb"></i> Security Tips</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="bi bi-arrow-right text-primary"></i> Always log out when using shared computers</li>
|
||||
<li><i class="bi bi-arrow-right text-primary"></i> Keep your authenticator app backed up</li>
|
||||
<li><i class="bi bi-arrow-right text-primary"></i> Use unique passwords for all accounts</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="bi bi-arrow-right text-primary"></i> Never share your 2FA codes with anyone</li>
|
||||
<li><i class="bi bi-arrow-right text-primary"></i> Check for suspicious account activity regularly</li>
|
||||
<li><i class="bi bi-arrow-right text-primary"></i> Update your password periodically</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
138
app/templates/index.html
Normal file
138
app/templates/index.html
Normal file
@@ -0,0 +1,138 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron bg-primary text-white rounded p-5 mb-4">
|
||||
<div class="container text-center">
|
||||
<h1 class="display-4">
|
||||
<i class="bi bi-shield-lock"></i> Flask 2FA Authentication
|
||||
</h1>
|
||||
<p class="lead">
|
||||
Secure web application with two-factor authentication, built with security best practices.
|
||||
</p>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('auth.register') }}" class="btn btn-light btn-lg me-3">
|
||||
<i class="bi bi-person-plus"></i> Get Started
|
||||
</a>
|
||||
<a href="{{ url_for('auth.login') }}" class="btn btn-outline-light btn-lg">
|
||||
<i class="bi bi-box-arrow-in-right"></i> Sign In
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('main.dashboard') }}" class="btn btn-light btn-lg">
|
||||
<i class="bi bi-speedometer2"></i> Go to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-shield-check display-4 text-success mb-3"></i>
|
||||
<h5 class="card-title">Two-Factor Authentication</h5>
|
||||
<p class="card-text">
|
||||
Enhanced security with TOTP-based 2FA using industry-standard protocols.
|
||||
Compatible with Google Authenticator, Authy, and other popular apps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-lock display-4 text-primary mb-3"></i>
|
||||
<h5 class="card-title">Secure by Design</h5>
|
||||
<p class="card-text">
|
||||
Built with security best practices including CSRF protection,
|
||||
bcrypt password hashing, secure sessions, and input validation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-code-slash display-4 text-info mb-3"></i>
|
||||
<h5 class="card-title">Modern Flask App</h5>
|
||||
<p class="card-text">
|
||||
Implements Flask application factory pattern, blueprints,
|
||||
SQLAlchemy ORM, and follows Flask security recommendations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="bi bi-info-circle"></i> Security Features</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Authentication & Authorization</h5>
|
||||
<ul>
|
||||
<li><strong>Two-Factor Authentication:</strong> TOTP-based 2FA with PyOTP</li>
|
||||
<li><strong>Secure Password Storage:</strong> Bcrypt hashing with salt</li>
|
||||
<li><strong>Session Management:</strong> Flask-Login with secure settings</li>
|
||||
<li><strong>Login Protection:</strong> Strong session protection enabled</li>
|
||||
</ul>
|
||||
|
||||
<h5 class="mt-4">Data Protection</h5>
|
||||
<ul>
|
||||
<li><strong>CSRF Protection:</strong> Automatic token validation on forms</li>
|
||||
<li><strong>SQL Injection Prevention:</strong> Parameterized queries</li>
|
||||
<li><strong>Input Validation:</strong> Server-side validation with WTForms</li>
|
||||
<li><strong>XSS Prevention:</strong> Automatic template escaping</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>HTTP Security</h5>
|
||||
<ul>
|
||||
<li><strong>Security Headers:</strong> HSTS, X-Frame-Options, CSP</li>
|
||||
<li><strong>Secure Cookies:</strong> HTTPOnly, Secure, SameSite flags</li>
|
||||
<li><strong>Content Security Policy:</strong> Prevents code injection</li>
|
||||
<li><strong>HTTPS Enforcement:</strong> Production-ready configuration</li>
|
||||
</ul>
|
||||
|
||||
<h5 class="mt-4">Application Security</h5>
|
||||
<ul>
|
||||
<li><strong>Database Security:</strong> Connection pooling and timeouts</li>
|
||||
<li><strong>Error Handling:</strong> Secure error pages without information disclosure</li>
|
||||
<li><strong>Logging:</strong> Security events logged for monitoring</li>
|
||||
<li><strong>Environment Configuration:</strong> Separate configs for dev/prod</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not current_user.is_authenticated %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<h4><i class="bi bi-rocket"></i> Ready to Get Started?</h4>
|
||||
<p class="lead">
|
||||
Create your secure account in minutes and experience enterprise-grade security.
|
||||
</p>
|
||||
<a href="{{ url_for('auth.register') }}" class="btn btn-primary btn-lg">
|
||||
<i class="bi bi-person-plus"></i> Create Account Now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
220
app/templates/profile.html
Normal file
220
app/templates/profile.html
Normal file
@@ -0,0 +1,220 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="bi bi-person-circle"></i> Profile Information</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Account Details</h5>
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td><strong>Username:</strong></td>
|
||||
<td>{{ user.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Email:</strong></td>
|
||||
<td>{{ user.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Account ID:</strong></td>
|
||||
<td>{{ user.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Member Since:</strong></td>
|
||||
<td>{{ user.created_at.strftime('%B %d, %Y') if user.created_at else 'N/A' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Last Login:</strong></td>
|
||||
<td>{{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') if user.last_login else 'First login' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>Quick Actions</h5>
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{{ url_for('main.dashboard') }}" class="btn btn-primary">
|
||||
<i class="bi bi-speedometer2"></i> Dashboard
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary" disabled>
|
||||
<i class="bi bi-pencil"></i> Edit Profile (Coming Soon)
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" disabled>
|
||||
<i class="bi bi-key"></i> Change Password (Coming Soon)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4><i class="bi bi-shield-lock"></i> Security Settings</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="security-status mb-3">
|
||||
<h6>Two-Factor Authentication</h6>
|
||||
{% if user.is_2fa_enabled %}
|
||||
<div class="alert alert-success">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
<strong>Enabled</strong><br>
|
||||
Your account is protected with 2FA
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('auth.disable_2fa') }}"
|
||||
onsubmit="return confirm('Are you sure you want to disable two-factor authentication? This will make your account less secure.');">
|
||||
{{ csrf_token() }}
|
||||
<button type="submit" class="btn btn-outline-warning btn-sm">
|
||||
<i class="bi bi-shield-x"></i> Disable 2FA
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>Setup Required</strong><br>
|
||||
Complete 2FA setup for better security
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('auth.register') }}" class="btn btn-success btn-sm">
|
||||
<i class="bi bi-shield-check"></i> Setup 2FA
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h6>Security Score</h6>
|
||||
<div class="progress mb-2">
|
||||
<div class="progress-bar {% if user.is_2fa_enabled %}bg-success{% else %}bg-warning{% endif %}"
|
||||
style="width: {% if user.is_2fa_enabled %}100{% else %}60{% endif %}%">
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
{% if user.is_2fa_enabled %}
|
||||
Excellent - All security features enabled
|
||||
{% else %}
|
||||
Good - Enable 2FA to reach 100%
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4><i class="bi bi-graph-up"></i> Account Activity</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-calendar-check display-6 text-primary"></i>
|
||||
<h6 class="mt-2">Account Age</h6>
|
||||
<h4>
|
||||
{% if user.created_at %}
|
||||
{{ ((user.created_at - user.created_at).days) if user.created_at else 0 }} days
|
||||
{% else %}
|
||||
New
|
||||
{% endif %}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-shield-check display-6 text-success"></i>
|
||||
<h6 class="mt-2">Security Level</h6>
|
||||
<h4>
|
||||
{% if user.is_2fa_enabled %}
|
||||
High
|
||||
{% else %}
|
||||
Medium
|
||||
{% endif %}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-clock-history display-6 text-info"></i>
|
||||
<h6 class="mt-2">Session Status</h6>
|
||||
<h4>Active</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card border-info">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Account Security Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>Authentication Methods</h6>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Password Authentication
|
||||
<span class="badge bg-success rounded-pill">Active</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Two-Factor Authentication
|
||||
<span class="badge bg-{% if user.is_2fa_enabled %}success{% else %}secondary{% endif %} rounded-pill">
|
||||
{% if user.is_2fa_enabled %}Active{% else %}Inactive{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h6>Security Features</h6>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
CSRF Protection
|
||||
<span class="badge bg-success rounded-pill">Active</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Secure Session
|
||||
<span class="badge bg-success rounded-pill">Active</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Password Encryption
|
||||
<span class="badge bg-success rounded-pill">Bcrypt</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-3 mb-0">
|
||||
<i class="bi bi-lightbulb"></i>
|
||||
<strong>Security Tip:</strong> Your account benefits from multiple layers of security including
|
||||
encrypted passwords, CSRF protection, secure sessions, and optional two-factor authentication.
|
||||
All these features work together to keep your account safe from unauthorized access.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user