From d1d6885d24f51652584377c3078a7e7422419c8c Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Thu, 28 Aug 2025 10:57:33 -0400 Subject: [PATCH] Integrate CF Turnstile --- app.py | 34 +++++++++++++++- static/css/auth.css | 33 ++++++++++++++++ templates/base.html | 4 ++ templates/login.html | 92 ++++++++++++++++++++++++++++---------------- turnstile_utils.py | 65 +++++++++++++++++++++++++++++++ 5 files changed, 193 insertions(+), 35 deletions(-) create mode 100644 turnstile_utils.py diff --git a/app.py b/app.py index 34b76d8..b36392f 100644 --- a/app.py +++ b/app.py @@ -17,6 +17,7 @@ from payroll_excel_exporter import PayrollExcelExporter # Load environment variables in .env load_dotenv() +from turnstile_utils import turnstile_utils # Initialize Flask application app = Flask(__name__) @@ -1482,15 +1483,28 @@ def register(): @app.route('/login', methods=['GET', 'POST']) def login(): - """Enhanced user authentication with comprehensive logging""" + """Enhanced user authentication with Turnstile and comprehensive logging""" if request.method == 'POST': username = request.form.get('username', '').strip() password = request.form.get('password', '') + turnstile_response = request.form.get('cf-turnstile-response', '') if not username or not password: flash('Please enter both username and password.', 'error') return render_template('login.html') + # Verify Turnstile if enabled + if turnstile_utils.is_enabled(): + if not turnstile_utils.verify_turnstile(turnstile_response): + # Log failed Turnstile attempt + logger_handler.log_security_event( + event_type="turnstile_verification_failed", + description=f"Failed Turnstile verification for username: {username}", + severity="HIGH" + ) + flash('Please complete the security verification.', 'error') + return render_template('login.html') + try: # Find user (case-insensitive username) user = User.query.filter( @@ -1510,12 +1524,20 @@ def login(): user.last_login_date = datetime.utcnow() db.session.commit() - # Log successful login + # Log successful login with Turnstile info logger_handler.log_user_login( user_id=user.id, username=user.username, success=True ) + + # Log successful Turnstile verification + if turnstile_utils.is_enabled(): + logger_handler.log_security_event( + event_type="turnstile_verification_success", + description=f"Successful Turnstile verification for user: {user.username}", + severity="INFO" + ) flash(f'Welcome back, {user.full_name}!', 'success') print(f"User {user.username} logged in successfully") @@ -6266,6 +6288,14 @@ def inject_logging_status(): 'is_admin': has_admin_privileges(session.get('role', '')) } +@app.context_processor +def inject_turnstile(): + """Inject Turnstile settings into all templates""" + return { + 'turnstile_enabled': turnstile_utils.is_enabled(), + 'turnstile_site_key': turnstile_utils.get_site_key() + } + # Before request handler for request logging @app.before_request def log_request_info(): diff --git a/static/css/auth.css b/static/css/auth.css index b767ba4..6de5616 100644 --- a/static/css/auth.css +++ b/static/css/auth.css @@ -251,4 +251,37 @@ .auth-form .form-group input:focus { transform: none !important; } +} + +/* Turnstile Integration */ +.turnstile-container { + display: flex !important; + justify-content: center !important; + margin: 1.5rem 0 !important; +} + +.turnstile-container .cf-turnstile { + transform: scale(0.95) !important; + transform-origin: center !important; +} + +/* Responsive Turnstile */ +@media screen and (max-width: 480px) { + .turnstile-container .cf-turnstile { + transform: scale(0.85) !important; + } +} + +/* Form validation enhancement for Turnstile */ +.auth-form .form-group.turnstile-error { + border: 2px solid var(--error-color) !important; + border-radius: var(--radius-lg) !important; + padding: 1rem !important; + background-color: rgba(239, 68, 68, 0.05) !important; +} + +/* Dark theme support */ +.turnstile-container .cf-turnstile[data-theme="dark"] { + background-color: var(--gray-800) !important; + border-radius: var(--radius-lg) !important; } \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index cd93844..1d018f1 100644 --- a/templates/base.html +++ b/templates/base.html @@ -85,5 +85,9 @@ csrfToken: '{{ csrf_token() if csrf_token else '' }}' }; + {% if turnstile_enabled %} + + + {% endif %} diff --git a/templates/login.html b/templates/login.html index a663f44..77945fd 100644 --- a/templates/login.html +++ b/templates/login.html @@ -17,7 +17,7 @@

Sign in to your account

-
+
+ + {% if turnstile_enabled %} +
+
+
+
+ {% endif %} +
- - {% endblock %} {% block extra_scripts %} {% endblock %} \ No newline at end of file diff --git a/turnstile_utils.py b/turnstile_utils.py new file mode 100644 index 0000000..ea2fc0a --- /dev/null +++ b/turnstile_utils.py @@ -0,0 +1,65 @@ +import os +import requests +from flask import current_app, request as flask_request + +class TurnstileUtils: + """Cloudflare Turnstile utility class for verification""" + + def __init__(self): + self.site_key = os.environ.get('TURNSTILE_SITE_KEY', '') + self.secret_key = os.environ.get('TURNSTILE_SECRET_KEY', '') + self.enabled = os.environ.get('TURNSTILE_ENABLED', 'False').lower() == 'true' + self.verify_url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify' + + def is_enabled(self): + """Check if Turnstile is enabled and properly configured""" + return self.enabled and self.site_key and self.secret_key + + def get_site_key(self): + """Get the Turnstile site key for frontend usage""" + return self.site_key if self.is_enabled() else None + + def verify_turnstile(self, turnstile_response): + """Verify Turnstile response with Cloudflare""" + if not self.is_enabled(): + return True # Skip verification if disabled + + if not turnstile_response: + return False + + try: + # Get client IP for additional security + client_ip = self._get_client_ip() + + # Prepare verification request + payload = { + 'secret': self.secret_key, + 'response': turnstile_response, + 'remoteip': client_ip + } + + # Send verification request to Cloudflare + response = requests.post(self.verify_url, data=payload, timeout=10) + result = response.json() + + # Log verification attempt + current_app.logger.info(f"Turnstile verification: success={result.get('success', False)}, IP={client_ip}") + + return result.get('success', False) + + except Exception as e: + current_app.logger.error(f"Turnstile verification error: {e}") + return False # Fail secure + + def _get_client_ip(self): + """Get client IP address with proxy support""" + # Check for forwarded IP (behind proxy/load balancer) + if flask_request.headers.get('X-Forwarded-For'): + return flask_request.headers.get('X-Forwarded-For').split(',')[0].strip() + elif flask_request.headers.get('X-Real-IP'): + return flask_request.headers.get('X-Real-IP') + else: + return flask_request.environ.get('REMOTE_ADDR', 'unknown') + +# Initialize global instance +turnstile_utils = TurnstileUtils() \ No newline at end of file