Fixed login with only ip:port access
This commit is contained in:
314
app.py
314
app.py
@@ -18,82 +18,61 @@ logging.basicConfig(
|
||||
)
|
||||
logger = logging.getLogger('mailcow-alias-manager')
|
||||
|
||||
# Environment variable to control proxy mode
|
||||
ENABLE_PROXY = os.getenv('ENABLE_PROXY', 'false').lower() in ('true', '1', 'yes')
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.getenv('SECRET_KEY', 'malias-default-secret-key-please-change') # Consistent secret key
|
||||
|
||||
# Configure for reverse proxy - modified to handle multiple proxy layers including Authelia
|
||||
# Increasing the number of proxies to handle Zoraxy->Authelia->App chain
|
||||
app.wsgi_app = ProxyFix(
|
||||
app.wsgi_app,
|
||||
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
|
||||
)
|
||||
# Configure app differently based on proxy mode
|
||||
if ENABLE_PROXY:
|
||||
# Configure for reverse proxy - modified to handle multiple proxy layers including Authelia
|
||||
# Increasing the number of proxies to handle Zoraxy->Authelia->App chain
|
||||
app.wsgi_app = ProxyFix(
|
||||
app.wsgi_app,
|
||||
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
|
||||
)
|
||||
|
||||
# Session configuration optimized for reverse proxy with Authelia
|
||||
app.config.update(
|
||||
PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
|
||||
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='None', # Required for authentication proxies
|
||||
SESSION_COOKIE_PATH='/',
|
||||
SESSION_COOKIE_DOMAIN=None, # Let browser auto-set domain
|
||||
SESSION_REFRESH_EACH_REQUEST=True, # Keep session alive
|
||||
PREFERRED_URL_SCHEME='https'
|
||||
)
|
||||
|
||||
logger.info("Running in PROXY mode (with Authelia integration)")
|
||||
else:
|
||||
# Direct mode - simpler configuration for IP:port access
|
||||
app.config.update(
|
||||
PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
|
||||
SESSION_COOKIE_NAME='malias_session',
|
||||
SESSION_COOKIE_SECURE=False, # Allow non-HTTPS cookies for direct IP access
|
||||
SESSION_COOKIE_HTTPONLY=True,
|
||||
SESSION_COOKIE_SAMESITE='Lax', # More compatible SameSite setting
|
||||
SESSION_COOKIE_PATH='/',
|
||||
SESSION_REFRESH_EACH_REQUEST=True
|
||||
)
|
||||
|
||||
logger.info("Running in DIRECT ACCESS mode (no reverse proxy integration)")
|
||||
|
||||
# Initialize database on startup
|
||||
malias_w.init_database()
|
||||
|
||||
# Session configuration optimized for reverse proxy with Authelia
|
||||
app.config.update(
|
||||
PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
|
||||
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='None', # Required for authentication proxies
|
||||
SESSION_COOKIE_PATH='/',
|
||||
SESSION_COOKIE_DOMAIN=None, # Let browser auto-set domain
|
||||
SESSION_REFRESH_EACH_REQUEST=True, # Keep session alive
|
||||
PREFERRED_URL_SCHEME='https'
|
||||
)
|
||||
|
||||
# Set this to True to use the special cookie fix for Zoraxy
|
||||
ZORAXY_COOKIE_FIX = True
|
||||
|
||||
def fix_cookie_for_zoraxy(response):
|
||||
"""Special handler for Zoraxy cookie issues with SameSite=None"""
|
||||
if not ZORAXY_COOKIE_FIX:
|
||||
return response
|
||||
|
||||
# Get all cookies from the response
|
||||
cookies = response.headers.getlist('Set-Cookie')
|
||||
if not cookies:
|
||||
return response
|
||||
|
||||
# Clear existing cookies
|
||||
del response.headers['Set-Cookie']
|
||||
|
||||
# Fix each cookie and add it back
|
||||
for cookie in cookies:
|
||||
if 'malias_session' in cookie and 'SameSite' not in cookie:
|
||||
# Add SameSite=None and Secure attributes
|
||||
if 'HttpOnly' in cookie:
|
||||
cookie = cookie.replace('HttpOnly', 'HttpOnly; SameSite=None; Secure')
|
||||
else:
|
||||
cookie += '; SameSite=None; Secure'
|
||||
response.headers.add('Set-Cookie', cookie)
|
||||
|
||||
return response
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
"""Process the response before it's sent"""
|
||||
# Apply special cookie fix for Zoraxy
|
||||
response = fix_cookie_for_zoraxy(response)
|
||||
|
||||
# Set CORS headers to allow Zoraxy and Authelia to work together
|
||||
response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
|
||||
response.headers['Access-Control-Allow-Credentials'] = 'true'
|
||||
|
||||
# Cache control
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '0'
|
||||
|
||||
return response
|
||||
|
||||
def get_authelia_user():
|
||||
"""Helper to get authenticated user from Authelia headers"""
|
||||
# Only check for Authelia if proxy mode is enabled
|
||||
if not ENABLE_PROXY:
|
||||
return None
|
||||
|
||||
# Check several possible header variations
|
||||
auth_headers = [
|
||||
'Remote-User',
|
||||
@@ -131,26 +110,27 @@ def login_required(f):
|
||||
"""Decorator to require login for routes"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# Check for Authelia authentication
|
||||
authelia_user = get_authelia_user()
|
||||
|
||||
# If Authelia authenticated the user, update local session
|
||||
if authelia_user:
|
||||
# Log all headers for debugging
|
||||
if app.debug:
|
||||
logger.info(f"Headers for authenticated request: {dict(request.headers)}")
|
||||
|
||||
if not session.get('logged_in') or session.get('authelia_user') != authelia_user:
|
||||
logger.info(f"Auto-login via Authelia for user: {authelia_user}")
|
||||
session.clear()
|
||||
session.permanent = True
|
||||
session['logged_in'] = True
|
||||
session['authelia_user'] = authelia_user
|
||||
session['user_token'] = secrets.token_urlsafe(32)
|
||||
session['auth_method'] = 'authelia'
|
||||
session.modified = True
|
||||
|
||||
return f(*args, **kwargs)
|
||||
# Check for Authelia authentication when in proxy mode
|
||||
if ENABLE_PROXY:
|
||||
authelia_user = get_authelia_user()
|
||||
|
||||
# If Authelia authenticated the user, update local session
|
||||
if authelia_user:
|
||||
# Log all headers for debugging
|
||||
if app.debug:
|
||||
logger.info(f"Headers for authenticated request: {dict(request.headers)}")
|
||||
|
||||
if not session.get('logged_in') or session.get('authelia_user') != authelia_user:
|
||||
logger.info(f"Auto-login via Authelia for user: {authelia_user}")
|
||||
session.clear()
|
||||
session.permanent = True
|
||||
session['logged_in'] = True
|
||||
session['authelia_user'] = authelia_user
|
||||
session['user_token'] = secrets.token_urlsafe(32)
|
||||
session['auth_method'] = 'authelia'
|
||||
session.modified = True
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
# Regular session check
|
||||
if not session.get('logged_in'):
|
||||
@@ -165,15 +145,17 @@ def login_required(f):
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""Login page"""
|
||||
# First, try Authelia authentication
|
||||
authelia_user = get_authelia_user()
|
||||
# Only check for Authelia if in proxy mode
|
||||
authelia_user = None
|
||||
if ENABLE_PROXY:
|
||||
authelia_user = get_authelia_user()
|
||||
|
||||
# Debug logging for all requests
|
||||
if app.debug:
|
||||
logger.info(f"Login route: method={request.method}, headers={dict(request.headers)}")
|
||||
|
||||
# If Authelia authenticated, login and redirect to index
|
||||
if authelia_user:
|
||||
# If Authelia authenticated (and we're in proxy mode), login and redirect to index
|
||||
if ENABLE_PROXY and authelia_user:
|
||||
logger.info(f"Login via Authelia for user: {authelia_user}")
|
||||
session.clear()
|
||||
session.permanent = True
|
||||
@@ -183,19 +165,7 @@ def login():
|
||||
session['auth_method'] = 'authelia'
|
||||
session.modified = True
|
||||
|
||||
# Set a cookie manually to ensure it's properly formatted for Zoraxy
|
||||
response = redirect(url_for('index'))
|
||||
# Set cookie parameters to work with Zoraxy/Authelia
|
||||
response.set_cookie(
|
||||
key=app.config['SESSION_COOKIE_NAME'],
|
||||
value=secrets.token_urlsafe(32), # Generate a new token instead of using session.sid
|
||||
max_age=int(app.config['PERMANENT_SESSION_LIFETIME'].total_seconds()),
|
||||
path=app.config['SESSION_COOKIE_PATH'],
|
||||
secure=app.config['SESSION_COOKIE_SECURE'],
|
||||
httponly=app.config['SESSION_COOKIE_HTTPONLY'],
|
||||
samesite='None'
|
||||
)
|
||||
return response
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Handle form submission for local authentication
|
||||
if request.method == 'POST':
|
||||
@@ -212,23 +182,7 @@ def login():
|
||||
session.modified = True
|
||||
|
||||
# Return JSON response
|
||||
response = jsonify({'status': 'success', 'message': 'Login successful'})
|
||||
|
||||
# Manually set cookie with correct parameters for Zoraxy
|
||||
if ZORAXY_COOKIE_FIX:
|
||||
session_token = secrets.token_urlsafe(32) # Generate a new token
|
||||
response.set_cookie(
|
||||
app.config['SESSION_COOKIE_NAME'],
|
||||
session_token,
|
||||
max_age=int(app.config['PERMANENT_SESSION_LIFETIME'].total_seconds()),
|
||||
secure=app.config['SESSION_COOKIE_SECURE'],
|
||||
httponly=app.config['SESSION_COOKIE_HTTPONLY'],
|
||||
samesite='None',
|
||||
path=app.config['SESSION_COOKIE_PATH']
|
||||
)
|
||||
logger.info(f"Set fixed cookie for Zoraxy: {app.config['SESSION_COOKIE_NAME']}")
|
||||
|
||||
return response
|
||||
return jsonify({'status': 'success', 'message': 'Login successful'})
|
||||
else:
|
||||
logger.warning("Login failed: Invalid password")
|
||||
return jsonify({'status': 'error', 'message': 'Invalid password'})
|
||||
@@ -258,8 +212,8 @@ def logout():
|
||||
# Clear local session
|
||||
session.clear()
|
||||
|
||||
# If user was authenticated via Authelia, try to redirect to Authelia logout
|
||||
if auth_method == 'authelia' or authelia_user:
|
||||
# If user was authenticated via Authelia (and we're in proxy mode), try to redirect to Authelia logout
|
||||
if ENABLE_PROXY and (auth_method == 'authelia' or authelia_user):
|
||||
# Look for Authelia URL in headers
|
||||
authelia_url = request.headers.get('X-Authelia-URL')
|
||||
|
||||
@@ -284,47 +238,25 @@ def logout():
|
||||
return redirect(common_authelia_urls[0])
|
||||
|
||||
# Default case: redirect to login page
|
||||
response = redirect(url_for('login'))
|
||||
|
||||
# Clear cookie by setting expired date
|
||||
response.set_cookie(app.config['SESSION_COOKIE_NAME'], '', expires=0)
|
||||
|
||||
return response
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Main page - requires login"""
|
||||
# Try to auto-login with Authelia
|
||||
authelia_user = get_authelia_user()
|
||||
|
||||
if authelia_user and not session.get('logged_in'):
|
||||
# Auto-login for users authenticated by Authelia
|
||||
logger.info(f"Auto-login via Authelia for user: {authelia_user}")
|
||||
session.clear()
|
||||
session.permanent = True
|
||||
session['logged_in'] = True
|
||||
session['authelia_user'] = authelia_user
|
||||
session['user_token'] = secrets.token_urlsafe(32)
|
||||
session['auth_method'] = 'authelia'
|
||||
session.modified = True
|
||||
# Try to auto-login with Authelia (only in proxy mode)
|
||||
if ENABLE_PROXY:
|
||||
authelia_user = get_authelia_user()
|
||||
|
||||
# Set cookie manually with correct parameters
|
||||
response = make_response(render_template('index.html'))
|
||||
|
||||
if ZORAXY_COOKIE_FIX:
|
||||
session_token = secrets.token_urlsafe(32) # Generate a new token
|
||||
response.set_cookie(
|
||||
app.config['SESSION_COOKIE_NAME'],
|
||||
session_token,
|
||||
max_age=int(app.config['PERMANENT_SESSION_LIFETIME'].total_seconds()),
|
||||
secure=app.config['SESSION_COOKIE_SECURE'],
|
||||
httponly=app.config['SESSION_COOKIE_HTTPONLY'],
|
||||
samesite='None',
|
||||
path=app.config['SESSION_COOKIE_PATH']
|
||||
)
|
||||
logger.info(f"Set fixed cookie for Zoraxy on index: {app.config['SESSION_COOKIE_NAME']}")
|
||||
|
||||
return response
|
||||
if authelia_user and not session.get('logged_in'):
|
||||
# Auto-login for users authenticated by Authelia
|
||||
logger.info(f"Auto-login via Authelia for user: {authelia_user}")
|
||||
session.clear()
|
||||
session.permanent = True
|
||||
session['logged_in'] = True
|
||||
session['authelia_user'] = authelia_user
|
||||
session['user_token'] = secrets.token_urlsafe(32)
|
||||
session['auth_method'] = 'authelia'
|
||||
session.modified = True
|
||||
|
||||
# Check if logged in
|
||||
if not session.get('logged_in'):
|
||||
@@ -569,10 +501,11 @@ def health_check():
|
||||
'authenticated': session.get('logged_in', False),
|
||||
'authelia_user': session.get('authelia_user', None),
|
||||
'auth_method': session.get('auth_method'),
|
||||
'version': '1.0.2',
|
||||
'version': '1.0.3',
|
||||
'cookies': {k: '***' for k in request.cookies.keys()}, # Only show cookie names
|
||||
'authelia_headers_present': get_authelia_user() is not None,
|
||||
'zoraxy_headers_present': 'X-Forwarded-Server' in request.headers
|
||||
'proxy_mode_enabled': ENABLE_PROXY,
|
||||
'zoraxy_detected': 'X-Forwarded-Server' in request.headers
|
||||
})
|
||||
|
||||
# Add a debugging endpoint
|
||||
@@ -589,11 +522,12 @@ def debug_info():
|
||||
'host': request.host,
|
||||
'path': request.path,
|
||||
'is_secure': request.is_secure,
|
||||
'proxy_mode_enabled': ENABLE_PROXY,
|
||||
'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': get_authelia_user(),
|
||||
'remote_user': get_authelia_user()
|
||||
}
|
||||
|
||||
# Additional server information
|
||||
@@ -607,8 +541,8 @@ def debug_info():
|
||||
},
|
||||
'app_config': {
|
||||
'DEBUG': app.debug,
|
||||
'PREFERRED_URL_SCHEME': app.config['PREFERRED_URL_SCHEME'],
|
||||
'ZORAXY_COOKIE_FIX': ZORAXY_COOKIE_FIX
|
||||
'PREFERRED_URL_SCHEME': app.config.get('PREFERRED_URL_SCHEME', 'http'),
|
||||
'ENABLE_PROXY': ENABLE_PROXY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,50 +552,6 @@ def debug_info():
|
||||
response.headers['Expires'] = '0'
|
||||
return response
|
||||
|
||||
# Dedicated endpoint for header diagnostics
|
||||
@app.route('/headers')
|
||||
def show_headers():
|
||||
"""Show all request headers - useful for debugging proxies"""
|
||||
# Log headers to help diagnose issues with Zoraxy/Authelia
|
||||
logger.info(f"Headers endpoint: All headers received: {dict(request.headers)}")
|
||||
return jsonify({
|
||||
'headers': dict(request.headers),
|
||||
'remote_addr': request.remote_addr,
|
||||
'authelia_user': get_authelia_user()
|
||||
})
|
||||
|
||||
# Self-test endpoint for cookies
|
||||
@app.route('/cookie-test')
|
||||
def cookie_test():
|
||||
"""Test cookie handling"""
|
||||
# Clear any existing cookie
|
||||
resp = make_response(jsonify({'status': 'ok', 'message': 'Cookie set test'}))
|
||||
|
||||
# Set a test cookie with the same attributes as session
|
||||
resp.set_cookie(
|
||||
'malias_test_cookie',
|
||||
'test-value',
|
||||
max_age=3600,
|
||||
secure=app.config['SESSION_COOKIE_SECURE'],
|
||||
httponly=app.config['SESSION_COOKIE_HTTPONLY'],
|
||||
samesite='None',
|
||||
path=app.config['SESSION_COOKIE_PATH']
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
# Endpoint to check if test cookie is set
|
||||
@app.route('/cookie-check')
|
||||
def cookie_check():
|
||||
"""Check if test cookie was properly set"""
|
||||
test_cookie = request.cookies.get('malias_test_cookie')
|
||||
return jsonify({
|
||||
'test_cookie_present': test_cookie is not None,
|
||||
'test_cookie_value': test_cookie if test_cookie else None,
|
||||
'all_cookies': {k: '***' for k in request.cookies.keys()}
|
||||
})
|
||||
|
||||
# New endpoint to test Zoraxy auth configuration
|
||||
@app.route('/authelia-test')
|
||||
def authelia_test():
|
||||
"""Test if Authelia headers are correctly passed through Zoraxy"""
|
||||
@@ -672,6 +562,7 @@ def authelia_test():
|
||||
auth_related_headers = [
|
||||
'Remote-User', 'X-Remote-User', 'Remote-Groups', 'X-Remote-Groups',
|
||||
'Remote-Name', 'X-Remote-Name', 'Remote-Email', 'X-Remote-Email',
|
||||
'X-Authelia-Username', 'X-Authelia-Groups', 'X-Authelia-DisplayName', 'X-Authelia-Email',
|
||||
'X-Authelia-URL', 'X-Original-URL', 'X-Forwarded-Proto'
|
||||
]
|
||||
|
||||
@@ -688,6 +579,7 @@ def authelia_test():
|
||||
auth_cookies[cookie_name] = '***' # Hide actual value
|
||||
|
||||
return jsonify({
|
||||
'proxy_mode_enabled': ENABLE_PROXY,
|
||||
'request_host': request.host,
|
||||
'authelia_user_detected': get_authelia_user() is not None,
|
||||
'authelia_user': get_authelia_user(),
|
||||
@@ -696,7 +588,7 @@ def authelia_test():
|
||||
'all_headers_count': len(all_headers),
|
||||
'zoraxy_detected': any('zoraxy' in h.lower() for h in all_headers.keys()) or 'X-Forwarded-Server' in all_headers,
|
||||
'host_header': request.headers.get('Host'),
|
||||
'referer': request.headers.get('Referer'),
|
||||
'referer': request.headers.get('Referer')
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -717,6 +609,7 @@ Environment Variables (Docker):
|
||||
FLASK_PORT Port to run on (default: 5172)
|
||||
FLASK_HOST Host to bind to (default: 0.0.0.0)
|
||||
FLASK_ENV Set to 'production' or 'development'
|
||||
ENABLE_PROXY Set to 'true' to enable proxy/Authelia integration (default: false)
|
||||
'''
|
||||
)
|
||||
|
||||
@@ -772,6 +665,7 @@ Environment Variables (Docker):
|
||||
print(f' Host: {host}')
|
||||
print(f' Port: {port}')
|
||||
print(f' URL: http://{host}:{port}')
|
||||
print(f' Proxy: {"ENABLED" if ENABLE_PROXY else "DISABLED"}')
|
||||
print('=' * 60)
|
||||
if debug_mode:
|
||||
print(' ⚠️ WARNING: Debug mode is enabled!')
|
||||
@@ -781,6 +675,6 @@ Environment Variables (Docker):
|
||||
print('=' * 60)
|
||||
print()
|
||||
|
||||
logger.info(f"Starting Mailcow Alias Manager - Debug: {debug_mode}, Host: {host}, Port: {port}")
|
||||
logger.info(f"Starting Mailcow Alias Manager - Debug: {debug_mode}, Host: {host}, Port: {port}, Proxy: {ENABLE_PROXY}")
|
||||
|
||||
app.run(debug=debug_mode, host=host, port=port)
|
||||
Reference in New Issue
Block a user