Files
ProxMenux/AppImage/scripts/auth_manager.py
2025-11-07 20:05:29 +01:00

282 lines
7.8 KiB
Python

"""
Authentication Manager Module
Handles all authentication-related operations including:
- Loading/saving auth configuration
- Password hashing and verification
- JWT token generation and validation
- Auth status checking
"""
import os
import json
import hashlib
from datetime import datetime, timedelta
from pathlib import Path
try:
import jwt
JWT_AVAILABLE = True
except ImportError:
JWT_AVAILABLE = False
print("Warning: PyJWT not available. Authentication features will be limited.")
# Configuration
CONFIG_DIR = Path.home() / ".config" / "proxmenux-monitor"
AUTH_CONFIG_FILE = CONFIG_DIR / "auth.json"
JWT_SECRET = "proxmenux-monitor-secret-key-change-in-production"
JWT_ALGORITHM = "HS256"
TOKEN_EXPIRATION_HOURS = 24
def ensure_config_dir():
"""Ensure the configuration directory exists"""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
def load_auth_config():
"""
Load authentication configuration from file
Returns dict with structure:
{
"enabled": bool,
"username": str,
"password_hash": str,
"declined": bool, # True if user explicitly declined auth
"configured": bool # True if auth has been set up (enabled or declined)
}
"""
if not AUTH_CONFIG_FILE.exists():
return {
"enabled": False,
"username": None,
"password_hash": None,
"declined": False,
"configured": False
}
try:
with open(AUTH_CONFIG_FILE, 'r') as f:
config = json.load(f)
# Ensure all required fields exist
config.setdefault("declined", False)
config.setdefault("configured", config.get("enabled", False) or config.get("declined", False))
return config
except Exception as e:
print(f"Error loading auth config: {e}")
return {
"enabled": False,
"username": None,
"password_hash": None,
"declined": False,
"configured": False
}
def save_auth_config(config):
"""Save authentication configuration to file"""
ensure_config_dir()
try:
with open(AUTH_CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
return True
except Exception as e:
print(f"Error saving auth config: {e}")
return False
def hash_password(password):
"""Hash a password using SHA-256"""
return hashlib.sha256(password.encode()).hexdigest()
def verify_password(password, password_hash):
"""Verify a password against its hash"""
return hash_password(password) == password_hash
def generate_token(username):
"""Generate a JWT token for the given username"""
if not JWT_AVAILABLE:
return None
payload = {
'username': username,
'exp': datetime.utcnow() + timedelta(hours=TOKEN_EXPIRATION_HOURS),
'iat': datetime.utcnow()
}
try:
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
return token
except Exception as e:
print(f"Error generating token: {e}")
return None
def verify_token(token):
"""
Verify a JWT token
Returns username if valid, None otherwise
"""
if not JWT_AVAILABLE or not token:
return None
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
return payload.get('username')
except jwt.ExpiredSignatureError:
print("Token has expired")
return None
except jwt.InvalidTokenError as e:
print(f"Invalid token: {e}")
return None
def get_auth_status():
"""
Get current authentication status
Returns dict with:
{
"auth_enabled": bool,
"auth_configured": bool,
"declined": bool,
"username": str or None,
"authenticated": bool
}
"""
config = load_auth_config()
return {
"auth_enabled": config.get("enabled", False),
"auth_configured": config.get("configured", False), # Frontend expects this field name
"declined": config.get("declined", False),
"username": config.get("username") if config.get("enabled") else None,
"authenticated": False # Will be set to True by the route handler if token is valid
}
def setup_auth(username, password):
"""
Set up authentication with username and password
Returns (success: bool, message: str)
"""
if not username or not password:
return False, "Username and password are required"
if len(password) < 6:
return False, "Password must be at least 6 characters"
config = {
"enabled": True,
"username": username,
"password_hash": hash_password(password),
"declined": False,
"configured": True
}
if save_auth_config(config):
return True, "Authentication configured successfully"
else:
return False, "Failed to save authentication configuration"
def decline_auth():
"""
Mark authentication as declined by user
Returns (success: bool, message: str)
"""
config = load_auth_config()
config["enabled"] = False
config["declined"] = True
config["configured"] = True
config["username"] = None
config["password_hash"] = None
if save_auth_config(config):
return True, "Authentication declined"
else:
return False, "Failed to save configuration"
def disable_auth():
"""
Disable authentication (different from decline - can be re-enabled)
Returns (success: bool, message: str)
"""
config = load_auth_config()
config["enabled"] = False
config["username"] = None
config["password_hash"] = None
config["declined"] = False
config["configured"] = False
if save_auth_config(config):
return True, "Authentication disabled"
else:
return False, "Failed to save configuration"
def enable_auth():
"""
Enable authentication (must already be configured)
Returns (success: bool, message: str)
"""
config = load_auth_config()
if not config.get("username") or not config.get("password_hash"):
return False, "Authentication not configured. Please set up username and password first."
config["enabled"] = True
config["declined"] = False
if save_auth_config(config):
return True, "Authentication enabled"
else:
return False, "Failed to save configuration"
def change_password(old_password, new_password):
"""
Change the authentication password
Returns (success: bool, message: str)
"""
config = load_auth_config()
if not config.get("enabled"):
return False, "Authentication is not enabled"
if not verify_password(old_password, config.get("password_hash", "")):
return False, "Current password is incorrect"
if len(new_password) < 6:
return False, "New password must be at least 6 characters"
config["password_hash"] = hash_password(new_password)
if save_auth_config(config):
return True, "Password changed successfully"
else:
return False, "Failed to save new password"
def authenticate(username, password):
"""
Authenticate a user with username and password
Returns (success: bool, token: str or None, message: str)
"""
config = load_auth_config()
if not config.get("enabled"):
return False, None, "Authentication is not enabled"
if username != config.get("username"):
return False, None, "Invalid username or password"
if not verify_password(password, config.get("password_hash", "")):
return False, None, "Invalid username or password"
token = generate_token(username)
if token:
return True, token, "Authentication successful"
else:
return False, None, "Failed to generate authentication token"