diff --git a/.gitignore b/.gitignore index ba5702f..a38893e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ BUI* build.sh docker-compose.dist* GITEA.md +PROXY_AUTHELIA_SETUP.md \ No newline at end of file diff --git a/app.py b/app.py index 20ccf31..f3a4fe5 100644 --- a/app.py +++ b/app.py @@ -12,28 +12,28 @@ app = Flask(__name__) app.secret_key = os.getenv('SECRET_KEY', 'malias-default-secret-key-please-change') # Consistent secret key # Configure for reverse proxy (Authelia, Zoraxy, Nginx, etc.) -# This fixes HTTPS detection and redirects when behind a proxy +# Use higher number of proxies to accommodate both Zoraxy and Authelia app.wsgi_app = ProxyFix( app.wsgi_app, - x_for=1, # Trust X-Forwarded-For with 1 proxy - x_proto=1, # Trust X-Forwarded-Proto (http/https) - x_host=1, # Trust X-Forwarded-Host - x_prefix=1 # Trust X-Forwarded-Prefix + x_for=2, # Trust X-Forwarded-For with 2 proxies (Zoraxy + Authelia) + x_proto=2, # Trust X-Forwarded-Proto (http/https) + x_host=2, # Trust X-Forwarded-Host + x_prefix=2 # Trust X-Forwarded-Prefix ) # Initialize database on startup malias_w.init_database() -# Session configuration optimized for reverse proxy with Gunicorn +# Session configuration optimized for reverse proxy with Authelia app.config.update( PERMANENT_SESSION_LIFETIME=timedelta(hours=24), - SESSION_COOKIE_NAME='session', # Use standard name - SESSION_COOKIE_SECURE=False, # Backend is HTTP + SESSION_COOKIE_NAME='malias_session', # Unique name to avoid conflicts with Authelia + SESSION_COOKIE_SECURE=True, # Always use secure cookies with Authelia SESSION_COOKIE_HTTPONLY=True, - SESSION_COOKIE_SAMESITE='Lax', # Lax works better than None for HTTP backend + SESSION_COOKIE_SAMESITE='None', # Required for authentication proxies SESSION_COOKIE_PATH='/', SESSION_COOKIE_DOMAIN=None, # Let browser auto-set domain - SESSION_REFRESH_EACH_REQUEST=False, # Don't modify session unnecessarily + SESSION_REFRESH_EACH_REQUEST=True, # Keep session alive PREFERRED_URL_SCHEME='https', APPLICATION_ROOT='/', ) @@ -42,6 +42,22 @@ def login_required(f): """Decorator to require login for routes""" @wraps(f) def decorated_function(*args, **kwargs): + # Check for Authelia headers first + remote_user = request.headers.get('Remote-User') or request.headers.get('X-Remote-User') + remote_groups = request.headers.get('Remote-Groups') or request.headers.get('X-Remote-Groups') + + # If Authelia authenticated the user, trust it and bypass local auth + if remote_user: + if not session.get('logged_in'): + session.clear() + session.permanent = True + session['logged_in'] = True + session['authelia_user'] = remote_user + session['user_token'] = secrets.token_urlsafe(32) + session.modified = True + return f(*args, **kwargs) + + # Otherwise, fall back to local auth if not session.get('logged_in'): return jsonify({'status': 'error', 'message': 'Not authenticated', 'redirect': '/login'}), 401 return f(*args, **kwargs) @@ -50,6 +66,19 @@ def login_required(f): @app.route('/login', methods=['GET', 'POST']) def login(): """Login page""" + # Check for Authelia authentication + remote_user = request.headers.get('Remote-User') or request.headers.get('X-Remote-User') + + # If Authelia authenticated, redirect to index + if remote_user: + session.clear() + session.permanent = True + session['logged_in'] = True + session['authelia_user'] = remote_user + session['user_token'] = secrets.token_urlsafe(32) + session.modified = True + return redirect(url_for('index')) + if request.method == 'POST': password = request.json.get('password', '') if malias_w.verify_password(password): @@ -58,7 +87,10 @@ def login(): session['logged_in'] = True session['user_token'] = secrets.token_urlsafe(32) session.modified = True - return jsonify({'status': 'success', 'message': 'Login successful'}) + + response = jsonify({'status': 'success', 'message': 'Login successful'}) + # Ensure cookies are properly configured for proxied setup + return response else: return jsonify({'status': 'error', 'message': 'Invalid password'}) @@ -71,13 +103,37 @@ def login(): def logout(): """Logout""" session.pop('logged_in', None) + session.pop('authelia_user', None) + + # Determine if we need to redirect to Authelia logout + remote_user = request.headers.get('Remote-User') or request.headers.get('X-Remote-User') + + if remote_user: + # Redirect to Authelia logout if available + authelia_url = request.headers.get('X-Authelia-URL') or None + if authelia_url: + return redirect(f"{authelia_url}/logout") + return redirect(url_for('login')) @app.route('/') def index(): """Main page - requires login""" + # Check Authelia authentication + remote_user = request.headers.get('Remote-User') or request.headers.get('X-Remote-User') + + if remote_user and not session.get('logged_in'): + # Auto-login users authenticated by Authelia + session.clear() + session.permanent = True + session['logged_in'] = True + session['authelia_user'] = remote_user + session['user_token'] = secrets.token_urlsafe(32) + session.modified = True + if not session.get('logged_in'): return redirect(url_for('login')) + return render_template('index.html') @app.route('/list_aliases', methods=['POST']) @@ -294,6 +350,43 @@ def change_password_route(): except Exception as e: return jsonify({'status': 'error', 'message': str(e)}) +# Add a health check endpoint for proxies +@app.route('/health') +def health_check(): + """Health check endpoint for proxies""" + return jsonify({ + 'status': 'ok', + 'authenticated': session.get('logged_in', False), + 'authelia_user': session.get('authelia_user', None), + 'version': '1.0.0' + }) + +# Add a debugging endpoint (only active in debug mode) +@app.route('/debug') +def debug_info(): + """Show debug information about the current request""" + if not app.debug: + return jsonify({'status': 'error', 'message': 'Debug mode not enabled'}), 403 + + debug_data = { + 'headers': dict(request.headers), + 'cookies': dict(request.cookies), + 'session': dict(session) if session else {}, + 'remote_addr': request.remote_addr, + 'scheme': request.scheme, + 'host': request.host, + 'path': request.path, + 'is_secure': request.is_secure, + 'x_forwarded_for': request.headers.get('X-Forwarded-For'), + 'x_forwarded_proto': request.headers.get('X-Forwarded-Proto'), + 'x_forwarded_host': request.headers.get('X-Forwarded-Host'), + 'x_forwarded_prefix': request.headers.get('X-Forwarded-Prefix'), + 'remote_user': request.headers.get('Remote-User') or request.headers.get('X-Remote-User'), + 'remote_groups': request.headers.get('Remote-Groups') or request.headers.get('X-Remote-Groups'), + } + + return jsonify(debug_data) + if __name__ == '__main__': # Parse command-line arguments parser = argparse.ArgumentParser( @@ -373,4 +466,4 @@ Environment Variables (Docker): print('=' * 60) print() - app.run(debug=debug_mode, host=host, port=port) + app.run(debug=debug_mode, host=host, port=port) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c0839da..9a91e74 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,6 +40,7 @@ services: # Host binding (default: 0.0.0.0 for Docker) - FLASK_HOST=0.0.0.0 - # Secret key for sessions (generate unique key for production) - # Change this to a random string for better security - - SECRET_KEY=malias-production-secret-key-change-me \ No newline at end of file + # Secret key for sessions - CRITICAL for multi-worker Gunicorn! + # All workers must use the SAME secret key to decode session cookies + # Generate with: python3 -c "import secrets; print(secrets.token_hex(32))" + - SECRET_KEY=dca6920115b8bbc13c346c75d668a49849590c77d9baaf903296582f24ff816a \ No newline at end of file diff --git a/templates/index.html.bak b/templates/index.html.bak deleted file mode 100644 index c5819cd..0000000 --- a/templates/index.html.bak +++ /dev/null @@ -1,1519 +0,0 @@ - - -
- - - - -This action cannot be undone.
- -