diff --git a/README.md b/README.md index ea80feace..590a6ffed 100755 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Web Hosting Control Panel powered by OpenLiteSpeed, designed to simplify hosting - ๐Ÿ“€ **One-click Backups and Restores**. - ๐Ÿณ **Docker Management** with command execution capabilities. - ๐Ÿค– **AI-Powered Security Scanner** for enhanced protection. +- ๐Ÿ” **Advanced 2FA Authentication** - TOTP and WebAuthn/Passkey support. - ๐Ÿ“Š **Monthly Bandwidth Reset** - Automatic bandwidth usage reset (Fixed in latest version). --- @@ -41,8 +42,10 @@ CyberPanel comes with comprehensive documentation and step-by-step guides: - ๐Ÿ“š **[Complete Guides Index](guides/INDEX.md)** - All available documentation in one place - ๐Ÿณ **[Docker Command Execution](guides/Docker_Command_Execution_Guide.md)** - Execute commands in Docker containers - ๐Ÿค– **[AI Scanner Setup](guides/AIScannerDocs.md)** - Configure AI-powered security scanning +- ๐Ÿ” **[2FA Authentication Guide](guides/2FA_AUTHENTICATION_GUIDE.md)** - Complete Two-Factor Authentication and WebAuthn setup - ๐Ÿ“ง **[Mautic Installation](guides/MAUTIC_INSTALLATION_GUIDE.md)** - Email marketing platform setup - ๐ŸŽจ **[Custom CSS Guide](guides/CUSTOM_CSS_GUIDE.md)** - Create custom themes for CyberPanel 2.5.5-dev +- ๐Ÿ› ๏ธ **[Utility Scripts](utils/README.md)** - Installation, upgrade, and maintenance scripts for Windows and Linux --- @@ -85,7 +88,7 @@ For additional PHP versions or specific requirements, you can install third-part ## ๐ŸŒ Supported Operating Systems -CyberPanel runs on x86_64 architecture and supports the following operating systems: +CyberPanel runs on x86_64 architecture and supports the following **Linux** operating systems: ### **โœ… Currently Supported** @@ -114,62 +117,223 @@ Additional operating systems may be supported through third-party repositories o - **openEuler** - Community-supported with limited testing - **Other RHEL derivatives** - May work with AlmaLinux/RockyLinux packages +### **โš ๏ธ Important Notes** + +- **Linux Only**: CyberPanel is designed specifically for Linux systems and does not support Windows +- **Architecture**: Requires x86_64 (64-bit) architecture +- **Virtual Machines**: Windows users can run CyberPanel in a Linux VM +- **Docker**: Alternative option for Windows users via Docker containers + > **Note**: For unsupported operating systems, compatibility is not guaranteed. Always test in a non-production environment first. --- ## โš™๏ธ Installation Instructions -Install CyberPanel easily with the following command: +### **Quick Installation (Recommended)** + +Install CyberPanel on supported Linux distributions with a single command: ```bash sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh) ``` +**Alternative Installation Methods:** +```bash +# Using wget only +wget -O - https://cyberpanel.net/install.sh | sh + +# Download and run manually +wget https://cyberpanel.net/install.sh +chmod +x install.sh +sudo ./install.sh +``` + +### **Prerequisites** + +Before installation, ensure your system meets these requirements: + +- **Operating System**: One of the supported Linux distributions listed above +- **Architecture**: x86_64 (64-bit) +- **RAM**: Minimum 1GB (2GB+ recommended) +- **Storage**: Minimum 10GB free space (20GB+ recommended) +- **Network**: Internet connection required +- **Root Access**: Installation requires root/sudo privileges + +### **Installation Process** + +The installation script will automatically: + +1. **Detect your operating system** and version +2. **Install required dependencies** (Python, Git, etc.) +3. **Download and configure** OpenLiteSpeed web server +4. **Set up MariaDB** database server +5. **Install CyberPanel** and configure all services +6. **Create admin user** with default credentials +7. **Start all services** and provide access information + +### **Post-Installation** + +After successful installation: + +1. **Access CyberPanel**: Open your browser to `http://your-server-ip:8090` +2. **Default Login**: + - **Username**: `admin` + - **Password**: `123456` +3. **Change Password**: Immediately change the default password +4. **Configure SSL**: Set up SSL certificates for secure access + --- ## ๐Ÿ“Š Upgrading CyberPanel -Upgrade your CyberPanel installation using: +### **Quick Upgrade (Recommended)** + +Upgrade your existing CyberPanel installation: ```bash sh <(curl https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh) ``` ---- +**Alternative Upgrade Methods:** +```bash +# Using wget only +wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh | sh -## ๐Ÿ†• Recent Updates & Fixes +# Download and run manually +wget https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh +chmod +x preUpgrade.sh +sudo ./preUpgrade.sh +``` -### **File Integrity & Verification System** (September 2025) +### **Upgrade Process** -- **Enhancement**: Comprehensive file integrity verification system implemented -- **Features**: - - Automatic detection of missing critical files - - Python syntax validation for all core modules - - Environment configuration verification - - Django application integrity checks -- **Coverage**: All core components (Django settings, URLs, middleware, application modules) -- **Status**: โœ… All files verified and synchronized (5,597 files) +The upgrade script will automatically: -### **Bandwidth Reset Issue Fixed** (September 2025) +1. **Backup current installation** to prevent data loss +2. **Download latest version** from the stable branch +3. **Update all dependencies** and requirements +4. **Run database migrations** to update schema +5. **Restart services** with new configuration +6. **Verify installation** and report any issues -- **Enhancement**: Implemented automatic monthly bandwidth reset for all websites and child domains -- **Coverage**: All supported operating systems (Ubuntu 20.04+, AlmaLinux, RockyLinux, RHEL, CloudLinux 8, CentOS 7/9) -- **Status**: โœ… Automatic monthly reset now functional +### **Manual Upgrade (Advanced Users)** -### **New Operating System Support Added** (September 2025) +For manual upgrades or troubleshooting: -- **Ubuntu 24.04.3**: Full compatibility with latest Ubuntu LTS -- **Debian 13**: Full compatibility with latest Debian stable release -- **AlmaLinux 10**: Full compatibility with latest AlmaLinux release -- **Long-term Support**: All supported until 2029-2030 +```bash +# Navigate to CyberPanel directory +cd /usr/local/CyberCP -### **Core Module Enhancements** (September 2025) +# Update source code +git pull origin stable -- **Django Configuration**: Enhanced settings.py with improved environment variable handling -- **Security Middleware**: Updated security middleware for better protection -- **Application Modules**: Verified and synchronized all core application modules -- **Static Assets**: Complete synchronization of UI assets and templates +# Update dependencies +pip3 install -r requirments.txt + +# Run database migrations +python3 manage.py migrate + +# Collect static files +python3 manage.py collectstatic --noinput + +# Restart services +systemctl restart lscpd +``` + +### **โš ๏ธ Important Upgrade Notes** + +- **Backup First**: Always backup your data before upgrading +- **Test Environment**: Test upgrades in a non-production environment first +- **Service Restart**: Some services may restart during upgrade +- **Configuration**: Custom configurations may need manual updates + +## ๐Ÿ”ง Troubleshooting + +### **Common Installation Issues** + +#### **"Command not found" Errors** +```bash +# Install missing packages +# Ubuntu/Debian +sudo apt update && sudo apt install curl wget git python3 + +# RHEL/CentOS/AlmaLinux/RockyLinux +sudo yum install curl wget git python3 +``` + +#### **Permission Denied Errors** +```bash +# Ensure you're running as root +sudo sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh) +``` + +#### **Network Connectivity Issues** +```bash +# Check internet connection +ping -c 4 google.com + +# Check DNS resolution +nslookup cyberpanel.net + +# Try alternative download method +wget https://cyberpanel.net/install.sh +chmod +x install.sh +sudo ./install.sh +``` + +#### **Port Already in Use** +```bash +# Check what's using port 8090 +sudo netstat -tlnp | grep :8090 + +# Kill process if necessary +sudo kill -9 +``` + +### **Common Upgrade Issues** + +#### **Backup Creation Failed** +```bash +# Check disk space +df -h + +# Free up space if necessary +sudo apt autoremove && sudo apt autoclean +``` + +#### **Service Restart Failed** +```bash +# Check service status +systemctl status lscpd + +# Restart services manually +sudo systemctl restart lscpd +sudo systemctl restart lsws +``` + +### **Verification Commands** + +#### **Check Installation Status** +```bash +# Check CyberPanel service +systemctl status lscpd + +# Check web interface +curl -I http://localhost:8090 + +# Check database +systemctl status mariadb +``` + +#### **View Logs** +```bash +# CyberPanel logs +tail -f /usr/local/lscp/logs/error.log + +# System logs +journalctl -u lscpd -f +``` --- @@ -222,6 +386,7 @@ For detailed troubleshooting, installation guides, and advanced configuration: - **๐Ÿณ [Docker Command Execution Guide](guides/Docker_Command_Execution_Guide.md)** - Docker management and troubleshooting - **๐Ÿค– [AI Scanner Documentation](guides/AIScannerDocs.md)** - Security scanner setup and configuration +- **๐Ÿ” [2FA Authentication Guide](guides/2FA_AUTHENTICATION_GUIDE.md)** - Two-Factor Authentication and WebAuthn setup - **๐Ÿ“ง [Mautic Installation Guide](guides/MAUTIC_INSTALLATION_GUIDE.md)** - Email marketing platform setup - **๐ŸŽจ [Custom CSS Guide](guides/CUSTOM_CSS_GUIDE.md)** - Interface customization and theming - **๐Ÿ”ฅ [Firewall Blocking Feature Guide](guides/FIREWALL_BLOCKING_FEATURE.md)** - Security features and configuration diff --git a/fix_cyberpanel_install.sh b/fix_cyberpanel_install.sh deleted file mode 100644 index d8fe8c196..000000000 --- a/fix_cyberpanel_install.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/bin/bash - -# CyberPanel Post-Upgrade Fix Script -# This script completes the installation when the upgrade exits early due to TypeError - -set -e # Exit on error - -echo "===================================" -echo "CyberPanel Installation Fix Script" -echo "===================================" -echo "" - -# Check if running as root -if [[ $(id -u) != 0 ]]; then - echo "This script must be run as root!" - exit 1 -fi - -# Function to print colored output -print_status() { - echo -e "\033[1;32m[$(date +"%Y-%m-%d %H:%M:%S")]\033[0m $1" -} - -print_error() { - echo -e "\033[1;31m[$(date +"%Y-%m-%d %H:%M:%S")] ERROR:\033[0m $1" -} - -# Check if virtual environment exists -if [[ ! -f /usr/local/CyberCP/bin/activate ]]; then - print_error "CyberPanel virtual environment not found!" - print_status "Creating virtual environment..." - - # Try python3 -m venv first - if python3 -m venv --system-site-packages /usr/local/CyberCP 2>/dev/null; then - print_status "Virtual environment created successfully with python3 -m venv" - else - # Fallback to virtualenv - virtualenv -p /usr/bin/python3 --system-site-packages /usr/local/CyberCP - fi -fi - -# Activate virtual environment -print_status "Activating CyberPanel virtual environment..." -source /usr/local/CyberCP/bin/activate - -# Check if Django is already installed -if python -c "import django" 2>/dev/null; then - print_status "Django is already installed. Checking version..." - python -c "import django; print(f'Django version: {django.__version__}')" -else - print_status "Installing Python requirements..." - - # Download requirements file - print_status "Downloading requirements.txt..." - if [[ -f /tmp/requirements.txt ]]; then - rm -f /tmp/requirements.txt - fi - - # Detect OS version and download appropriate requirements - if grep -q "22.04" /etc/os-release || grep -q "VERSION_ID=\"9" /etc/os-release; then - wget -q -O /tmp/requirements.txt https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.4.4-dev/requirments.txt - else - wget -q -O /tmp/requirements.txt https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.4.4-dev/requirments-old.txt - fi - - # Upgrade pip first - print_status "Upgrading pip, setuptools, and wheel..." - pip install --upgrade pip setuptools wheel packaging - - # Install requirements - print_status "Installing CyberPanel requirements (this may take a few minutes)..." - pip install --default-timeout=3600 --ignore-installed -r /tmp/requirements.txt -fi - -# Install WSGI-LSAPI if not present -if [[ ! -f /usr/local/CyberCP/bin/lswsgi ]]; then - print_status "Installing WSGI-LSAPI..." - - cd /tmp - rm -rf wsgi-lsapi-2.1* - - wget -q https://www.litespeedtech.com/packages/lsapi/wsgi-lsapi-2.1.tgz - tar xf wsgi-lsapi-2.1.tgz - cd wsgi-lsapi-2.1 - - /usr/local/CyberCP/bin/python ./configure.py - make - - cp lswsgi /usr/local/CyberCP/bin/ - print_status "WSGI-LSAPI installed successfully" -fi - -# Fix permissions -print_status "Fixing permissions..." -chown -R cyberpanel:cyberpanel /usr/local/CyberCP/lib 2>/dev/null || true -chown -R cyberpanel:cyberpanel /usr/local/CyberCP/lib64 2>/dev/null || true - -# Test Django installation -print_status "Testing Django installation..." -cd /usr/local/CyberCP - -if python manage.py check 2>&1 | grep -q "System check identified no issues"; then - print_status "Django is working correctly!" -else - print_error "Django check failed. Checking for specific issues..." - python manage.py check -fi - -# Restart LSCPD -print_status "Restarting LSCPD service..." -systemctl restart lscpd - -# Check service status -if systemctl is-active --quiet lscpd; then - print_status "LSCPD service is running" -else - print_error "LSCPD service failed to start" - systemctl status lscpd -fi - -echo "" -print_status "CyberPanel fix completed!" -echo "" -echo "You can now access CyberPanel at: https://$(hostname -I | awk '{print $1}'):8090" -echo "" - -# Deactivate virtual environment -deactivate 2>/dev/null || true \ No newline at end of file diff --git a/guides/2FA_AUTHENTICATION_GUIDE.md b/guides/2FA_AUTHENTICATION_GUIDE.md new file mode 100644 index 000000000..f240c1465 --- /dev/null +++ b/guides/2FA_AUTHENTICATION_GUIDE.md @@ -0,0 +1,431 @@ +# CyberPanel 2FA Authentication Guide + +## Overview + +CyberPanel supports multiple two-factor authentication (2FA) methods to enhance security for user accounts. This guide covers all available authentication methods, their setup, and best practices. + +## Table of Contents + +1. [Available Authentication Methods](#available-authentication-methods) +2. [Traditional 2FA (TOTP)](#traditional-2fa-totp) +3. [WebAuthn/Passkey Authentication](#webauthnpasskey-authentication) +4. [Password Authentication](#password-authentication) +5. [Combined Authentication Strategies](#combined-authentication-strategies) +6. [Administrator Management](#administrator-management) +7. [Troubleshooting](#troubleshooting) +8. [Security Best Practices](#security-best-practices) + +## Available Authentication Methods + +### 1. Traditional 2FA (TOTP) +- **Type**: Time-based One-Time Password +- **Method**: Google Authenticator, Authy, or similar apps +- **Security Level**: High +- **User Experience**: Requires manual code entry + +### 2. WebAuthn/Passkey Authentication +- **Type**: Modern passwordless authentication +- **Method**: Biometric authentication, security keys, or device passkeys +- **Security Level**: Very High +- **User Experience**: Seamless and user-friendly + +### 3. Password Authentication +- **Type**: Traditional username/password +- **Method**: Standard login credentials +- **Security Level**: Basic +- **User Experience**: Simple but less secure + +## Traditional 2FA (TOTP) + +### How It Works +TOTP generates time-based codes that change every 30 seconds. Users scan a QR code with their authenticator app to set up 2FA. + +### Setting Up TOTP 2FA + +#### For Users +1. **Access User Management** + - Log in to CyberPanel + - Go to **User Management** โ†’ **Modify User** + - Select your user account + +2. **Enable 2FA** + - Scroll to **Additional Features** section + - Check **"Enable Two-Factor Authentication (2FA)"** + - A QR code will appear + +3. **Configure Authenticator App** + - Open your authenticator app (Google Authenticator, Authy, etc.) + - Scan the QR code displayed + - Or manually enter the secret key shown below the QR code + +4. **Test 2FA** + - Enter a 6-digit code from your authenticator app + - Verify the setup works correctly + +#### For Administrators +1. **Access User Management** + - Go to **User Management** โ†’ **Modify User** + - Select the user you want to configure + +2. **Enable 2FA for User** + - Check **"Enable Two-Factor Authentication (2FA)"** + - Provide the QR code or secret key to the user + - Ensure the user completes the setup + +### TOTP Configuration Options + +#### Secret Key Management +- **Auto-generation**: CyberPanel automatically generates secure secret keys +- **Manual Entry**: Users can manually enter the secret key if QR scanning fails +- **Regeneration**: Secret keys can be regenerated if needed + +#### QR Code Display +- **Automatic Display**: QR code appears when 2FA is enabled +- **Manual Key**: Secret key is always displayed for manual entry +- **Copy Function**: One-click copy to clipboard functionality + +### Supported Authenticator Apps +- **Google Authenticator** (iOS/Android) +- **Authy** (iOS/Android/Desktop) +- **Microsoft Authenticator** (iOS/Android) +- **1Password** (iOS/Android/Desktop) +- **Bitwarden** (iOS/Android/Desktop) +- **Any TOTP-compatible app** + +## WebAuthn/Passkey Authentication + +### What is WebAuthn? +WebAuthn is a web standard that enables secure, passwordless authentication using public-key cryptography. It supports biometric authentication, security keys, and device passkeys. + +### Setting Up WebAuthn + +#### Prerequisites +- **HTTPS Required**: WebAuthn only works over secure connections +- **Modern Browser**: Chrome 67+, Firefox 60+, Safari 14+, Edge 79+ +- **Compatible Device**: Device with biometric sensors or security key + +#### For Users +1. **Access User Management** + - Go to **User Management** โ†’ **Modify User** + - Select your user account + +2. **Enable WebAuthn** + - Scroll to **Passkey Authentication (WebAuthn)** section + - Check **"Enable Passkey Authentication"** + +3. **Configure Settings** + - **Require Passkey for Login**: Enable for passwordless authentication + - **Allow Multiple Passkeys**: Enable to register multiple devices + - **Max Passkeys**: Set limit (default: 10) + - **Timeout**: Set timeout in seconds (default: 60) + +4. **Register Passkeys** + - Click **"Register New Passkey"** + - Enter a name for your passkey (e.g., "iPhone", "Laptop") + - Follow your browser's prompts to complete registration + - Repeat for additional devices + +#### For Administrators +1. **Access User Management** + - Go to **User Management** โ†’ **Modify User** + - Select the user you want to configure + +2. **Enable WebAuthn for User** + - Check **"Enable Passkey Authentication"** + - Configure security policies + - Monitor registered passkeys + +### WebAuthn Features + +#### Passkey Management +- **Multiple Devices**: Register passkeys on phones, laptops, security keys +- **Custom Names**: Give descriptive names to your passkeys +- **Easy Removal**: Delete passkeys you no longer need +- **Backup Options**: Multiple passkeys for redundancy + +#### Security Features +- **Replay Protection**: Each authentication uses a unique challenge +- **Credential Isolation**: Passkeys cannot be extracted or shared +- **User Verification**: Optional biometric or PIN verification +- **Session Management**: Secure session handling with expiration + +### Supported WebAuthn Devices +- **Smartphones**: iPhone, Android phones with biometrics +- **Laptops**: MacBooks with Touch ID, Windows Hello devices +- **Security Keys**: YubiKey, Google Titan, etc. +- **Tablets**: iPads, Android tablets with biometrics + +## Password Authentication + +### Traditional Login +- **Username/Password**: Standard login credentials +- **Security Level**: Basic (not recommended alone) +- **Use Case**: Fallback authentication method + +### Password Requirements +- **Minimum Length**: 8 characters (recommended: 12+) +- **Complexity**: Mix of uppercase, lowercase, numbers, symbols +- **Uniqueness**: Different from other accounts +- **Regular Updates**: Change passwords periodically + +## Combined Authentication Strategies + +### Strategy 1: Password + 2FA +- **Login Process**: Username + Password + TOTP code +- **Security Level**: High +- **User Experience**: Moderate (requires manual code entry) +- **Best For**: Users who prefer traditional 2FA + +### Strategy 2: Password + Passkeys +- **Login Process**: Username + Password + Passkey +- **Security Level**: Very High +- **User Experience**: Good (biometric authentication) +- **Best For**: Users with compatible devices + +### Strategy 3: Passkeys Only (Passwordless) +- **Login Process**: Username + Passkey only +- **Security Level**: Very High +- **User Experience**: Excellent (no password required) +- **Best For**: Security-conscious users with multiple devices + +### Strategy 4: All Methods (Maximum Security) +- **Login Process**: Username + Password + 2FA + Passkeys +- **Security Level**: Maximum +- **User Experience**: Complex but flexible +- **Best For**: High-security environments + +## Administrator Management + +### User Authentication Settings + +#### Accessing User Settings +1. **Navigate to User Management** + - Go to **User Management** โ†’ **Modify User** + - Select the user to manage + +#### Available Settings +- **Enable/Disable 2FA**: Toggle TOTP authentication +- **Enable/Disable WebAuthn**: Toggle passkey authentication +- **Security Policies**: Set passkey limits and timeouts +- **View Credentials**: See registered passkeys and 2FA status + +### Security Policies + +#### Passkey Policies +- **Maximum Passkeys**: Limit number of registered passkeys per user +- **Timeout Settings**: Set authentication timeout duration +- **Device Requirements**: Specify required device capabilities + +#### 2FA Policies +- **Secret Key Management**: Control secret key generation and regeneration +- **QR Code Display**: Manage QR code visibility +- **Backup Codes**: Generate backup codes for recovery + +### Monitoring and Auditing + +#### Authentication Logs +- **Login Attempts**: Track all authentication attempts +- **Method Used**: Record which authentication method was used +- **Success/Failure**: Monitor authentication success rates +- **Device Information**: Log device and browser details + +#### Security Alerts +- **Failed Attempts**: Alert on multiple failed login attempts +- **Unusual Activity**: Detect suspicious authentication patterns +- **Device Changes**: Notify when new devices are registered + +## Troubleshooting + +### Common TOTP Issues + +#### QR Code Not Scanning +**Problem**: QR code cannot be scanned by authenticator app +**Solutions**: +- Try manual key entry instead +- Ensure good lighting and camera focus +- Try a different authenticator app +- Regenerate the QR code + +#### Time Synchronization Issues +**Problem**: Codes not working due to time differences +**Solutions**: +- Check device time is correct +- Sync device time with internet +- Try a different authenticator app +- Contact administrator for assistance + +#### Lost Authenticator Device +**Problem**: Cannot access authenticator app +**Solutions**: +- Use backup codes if available +- Contact administrator to reset 2FA +- Set up 2FA on a new device +- Use alternative authentication methods + +### Common WebAuthn Issues + +#### "WebAuthn not supported" Error +**Problem**: Browser or device doesn't support WebAuthn +**Solutions**: +- Update browser to latest version +- Ensure HTTPS is enabled +- Check device compatibility +- Try a different browser + +#### Registration Failed +**Problem**: Passkey registration fails +**Solutions**: +- Check browser console for errors +- Ensure user has permission to register passkeys +- Verify WebAuthn settings are properly configured +- Try a different device or browser + +#### Authentication Failed +**Problem**: Passkey authentication fails +**Solutions**: +- Ensure passkey is still active +- Check that user has registered passkeys +- Verify challenge hasn't expired +- Try a different passkey or device + +### General Authentication Issues + +#### Login Not Working +**Problem**: Cannot log in with any method +**Solutions**: +- Check username and password +- Verify 2FA codes are current +- Ensure passkeys are properly registered +- Contact administrator for assistance + +#### Account Locked +**Problem**: Account is locked due to failed attempts +**Solutions**: +- Wait for lockout period to expire +- Contact administrator to unlock account +- Check for security alerts +- Review recent login attempts + +## Security Best Practices + +### For Users + +#### Password Security +- **Use Strong Passwords**: 12+ characters with mixed case, numbers, symbols +- **Unique Passwords**: Different password for each account +- **Regular Updates**: Change passwords every 90 days +- **Password Manager**: Use a reputable password manager + +#### 2FA Security +- **Secure Device**: Use a trusted device for authenticator apps +- **Backup Codes**: Save backup codes in a secure location +- **Multiple Methods**: Set up multiple authentication methods +- **Regular Testing**: Test authentication methods regularly + +#### WebAuthn Security +- **Multiple Passkeys**: Register passkeys on multiple devices +- **Secure Devices**: Only register passkeys on trusted devices +- **Regular Review**: Periodically review registered passkeys +- **Device Security**: Keep devices updated and secure + +### For Administrators + +#### System Security +- **HTTPS Enforcement**: Ensure all authentication uses HTTPS +- **Regular Updates**: Keep CyberPanel and dependencies updated +- **Monitoring**: Monitor authentication logs and alerts +- **Backup**: Regular backups of authentication data + +#### User Management +- **Access Control**: Implement proper user access controls +- **Security Policies**: Enforce strong security policies +- **Training**: Provide security training to users +- **Incident Response**: Have procedures for security incidents + +#### Configuration Security +- **Default Settings**: Change default passwords and settings +- **Network Security**: Implement proper network security +- **Logging**: Enable comprehensive logging +- **Auditing**: Regular security audits + +## Advanced Configuration + +### Custom Security Policies + +#### Passkey Policies +```python +# Example: Custom passkey policies +WEBAUTHN_POLICIES = { + 'max_credentials_per_user': 5, + 'timeout_seconds': 120, + 'require_user_verification': True, + 'allowed_attestation_formats': ['none', 'packed'] +} +``` + +#### 2FA Policies +```python +# Example: Custom 2FA policies +TOTP_POLICIES = { + 'secret_key_length': 32, + 'time_step': 30, + 'window': 1, + 'issuer_name': 'CyberPanel' +} +``` + +### Integration with External Systems + +#### LDAP Integration +- **Active Directory**: Integrate with Windows Active Directory +- **OpenLDAP**: Connect to OpenLDAP servers +- **Multi-Factor**: Combine with external MFA systems + +#### SSO Integration +- **SAML**: Single Sign-On with SAML providers +- **OAuth**: OAuth-based authentication +- **Custom Providers**: Integration with custom identity providers + +## Migration and Upgrades + +### Upgrading Authentication Methods + +#### From Password to 2FA +1. **Enable 2FA**: Add TOTP authentication +2. **Test Setup**: Verify 2FA works correctly +3. **User Training**: Train users on 2FA usage +4. **Gradual Rollout**: Enable for users progressively + +#### From 2FA to WebAuthn +1. **Enable WebAuthn**: Add passkey support +2. **User Migration**: Help users register passkeys +3. **Dual Support**: Support both methods during transition +4. **Full Migration**: Complete transition to WebAuthn + +### Backup and Recovery + +#### Authentication Backup +- **Secret Keys**: Backup TOTP secret keys securely +- **Passkey Data**: Backup WebAuthn credential data +- **User Data**: Regular backups of user authentication data +- **Configuration**: Backup authentication configuration + +#### Recovery Procedures +- **Account Recovery**: Procedures for locked accounts +- **Data Restoration**: Restore authentication data from backups +- **Emergency Access**: Emergency access procedures +- **User Support**: Support procedures for authentication issues + +## Conclusion + +CyberPanel's multi-layered authentication system provides flexible and secure access control. By combining traditional 2FA with modern WebAuthn passkeys, administrators can offer users both security and convenience. + +Choose the authentication methods that best fit your security requirements and user needs. Remember to regularly review and update your authentication policies to maintain optimal security. + +For additional support and advanced configuration options, refer to the CyberPanel documentation and community resources. + +--- + +**Note**: This guide covers all available authentication methods in CyberPanel. For the latest updates and additional features, refer to the official CyberPanel documentation. + +*Last updated: January 2025* diff --git a/guides/INDEX.md b/guides/INDEX.md index 951257df0..94100e9d8 100644 --- a/guides/INDEX.md +++ b/guides/INDEX.md @@ -16,6 +16,9 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu ### ๐Ÿง Operating System Support +#### **Windows Family** +- **[Windows Installation Guide](WINDOWS_INSTALLATION_GUIDE.md)** - Complete installation and configuration guide for CyberPanel on Windows 7/8.1/10/11 + #### **Debian Family** - **[Debian 13 Installation Guide](DEBIAN_13_INSTALLATION_GUIDE.md)** - Complete installation and configuration guide for CyberPanel on Debian 13 (Bookworm) - **[Debian 12 Troubleshooting Guide](DEBIAN_12_TROUBLESHOOTING.md)** - Troubleshooting guide for Debian 12 (Bookworm) @@ -40,8 +43,12 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu ### ๐ŸŽจ Customization & Design - **[Custom CSS Guide](CUSTOM_CSS_GUIDE.md)** - Complete guide for creating custom CSS that works with CyberPanel 2.5.5-dev design system +### ๐Ÿ” Security & Authentication +- **[2FA Authentication Guide](2FA_AUTHENTICATION_GUIDE.md)** - Complete guide for Two-Factor Authentication and WebAuthn/Passkey setup + ### ๐Ÿ”ง Troubleshooting & Support - **[Troubleshooting Guide](TROUBLESHOOTING.md)** - Comprehensive troubleshooting and diagnostic commands +- **[Installation Verification Guide](INSTALLATION_VERIFICATION.md)** - Verify installation and upgrade commands work across all supported OS ### ๐Ÿ’ป Command Line Interface - **[CLI Command Reference](CLI_COMMAND_REFERENCE.md)** - Complete reference for all CyberPanel CLI commands @@ -51,6 +58,7 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu ### ๐Ÿ“– General Documentation - **[README](../README.md)** - Main CyberPanel documentation with installation instructions and feature overview +- **[Utility Scripts](../utils/README.md)** - Installation, upgrade, and maintenance scripts for Windows and Linux - **[Contributing Guide](CONTRIBUTING.md)** - Guidelines for contributing to the CyberPanel project ## ๐Ÿš€ Quick Start @@ -69,6 +77,7 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu - **General Troubleshooting**: [Troubleshooting Guide](TROUBLESHOOTING.md) ### **OS-Specific Troubleshooting** +- **๐ŸชŸ Windows**: [Windows Installation Guide](WINDOWS_INSTALLATION_GUIDE.md) - Installation & troubleshooting - **๐Ÿง Debian 13**: [Debian 13 Installation Guide](DEBIAN_13_INSTALLATION_GUIDE.md) - Installation & troubleshooting - **๐Ÿง Debian 12**: [Debian 12 Troubleshooting Guide](DEBIAN_12_TROUBLESHOOTING.md) - Troubleshooting - **๐Ÿง Debian 11**: [Debian 11 Troubleshooting Guide](DEBIAN_11_TROUBLESHOOTING.md) - Troubleshooting @@ -86,6 +95,7 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu ### **Feature-Specific Guides** - **Docker Features**: [Docker Command Execution Guide](Docker_Command_Execution_Guide.md) - **Security Features**: [AI Scanner Documentation](AIScannerDocs.md) +- **Authentication**: [2FA Authentication Guide](2FA_AUTHENTICATION_GUIDE.md) - **Email Marketing**: [Mautic Installation Guide](MAUTIC_INSTALLATION_GUIDE.md) - **Storage Management**: [Home Directory Management Guide](HOME_DIRECTORY_MANAGEMENT_GUIDE.md) - **Customization & Design**: [Custom CSS Guide](CUSTOM_CSS_GUIDE.md) @@ -98,6 +108,8 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu - Docker container management - Command execution - Security scanning +- Two-factor authentication (2FA) +- WebAuthn/Passkey authentication - Home directory management - CLI command reference diff --git a/guides/INSTALLATION_VERIFICATION.md b/guides/INSTALLATION_VERIFICATION.md new file mode 100644 index 000000000..3eb89153c --- /dev/null +++ b/guides/INSTALLATION_VERIFICATION.md @@ -0,0 +1,415 @@ +# CyberPanel Installation Verification Guide + +## ๐ŸŽฏ Overview + +This guide provides verification steps to ensure CyberPanel installation and upgrade commands work correctly across all supported operating systems. + +## โœ… Installation Command Verification + +### **Primary Installation Command** +```bash +sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh) +``` + +### **Verification Steps** + +#### 1. **Test URL Accessibility** +```bash +# Test if install script is accessible +curl -I https://cyberpanel.net/install.sh + +# Expected response: HTTP/1.1 200 OK +# Content-Type: text/plain +``` + +#### 2. **Test Download** +```bash +# Download and check script content +curl -s https://cyberpanel.net/install.sh | head -20 + +# Should show script header and OS detection logic +``` + +#### 3. **Test with wget fallback** +```bash +# Test wget fallback +wget -qO- https://cyberpanel.net/install.sh | head -20 + +# Should show same content as curl +``` + +## โœ… Upgrade Command Verification + +### **Primary Upgrade Command** +```bash +sh <(curl https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh) +``` + +### **Verification Steps** + +#### 1. **Test URL Accessibility** +```bash +# Test if upgrade script is accessible +curl -I https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh + +# Expected response: HTTP/1.1 200 OK +# Content-Type: text/plain +``` + +#### 2. **Test Download** +```bash +# Download and check script content +curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preUpgrade.sh | head -20 + +# Should show script header and upgrade logic +``` + +## ๐Ÿง Operating System Support Verification + +### **Ubuntu Family** +- **Ubuntu 24.04.3**: โœ… Supported +- **Ubuntu 22.04**: โœ… Supported +- **Ubuntu 20.04**: โœ… Supported + +### **Debian Family** +- **Debian 13**: โœ… Supported +- **Debian 12**: โœ… Supported +- **Debian 11**: โœ… Supported + +### **RHEL Family** +- **AlmaLinux 10**: โœ… Supported +- **AlmaLinux 9**: โœ… Supported +- **AlmaLinux 8**: โœ… Supported +- **RockyLinux 9**: โœ… Supported +- **RockyLinux 8**: โœ… Supported +- **RHEL 9**: โœ… Supported +- **RHEL 8**: โœ… Supported + +### **Other Distributions** +- **CloudLinux 8**: โœ… Supported +- **CentOS 9**: โœ… Supported +- **CentOS 7**: โœ… Supported (until June 2024) +- **CentOS Stream 9**: โœ… Supported + +## ๐Ÿ”ง Installation Process Verification + +### **What the Installation Script Does** + +1. **System Detection** + - Detects operating system and version + - Checks architecture (x86_64 required) + - Verifies system requirements + +2. **Dependency Installation** + - Installs Python 3.8+ + - Installs Git + - Installs system packages (curl, wget, etc.) + +3. **Web Server Setup** + - Downloads and installs OpenLiteSpeed + - Configures web server settings + - Sets up virtual hosts + +4. **Database Setup** + - Installs and configures MariaDB + - Creates CyberPanel database + - Sets up database users + +5. **CyberPanel Installation** + - Downloads CyberPanel source code + - Installs Python dependencies + - Configures Django settings + - Runs database migrations + +6. **Service Configuration** + - Creates systemd services + - Starts all required services + - Configures firewall rules + +7. **Final Setup** + - Creates admin user + - Sets up file permissions + - Provides access information + +## ๐Ÿ”„ Upgrade Process Verification + +### **What the Upgrade Script Does** + +1. **Backup Creation** + - Creates backup of current installation + - Backs up database and configuration files + - Stores backup in safe location + +2. **Source Update** + - Downloads latest CyberPanel source code + - Updates all files to latest version + - Preserves custom configurations + +3. **Dependency Update** + - Updates Python packages + - Updates system dependencies + - Handles version conflicts + +4. **Database Migration** + - Runs Django migrations + - Updates database schema + - Preserves existing data + +5. **Service Restart** + - Restarts all CyberPanel services + - Verifies service status + - Reports any issues + +## ๐Ÿงช Testing Procedures + +### **Pre-Installation Testing** + +#### 1. **System Requirements Check** +```bash +# Check OS version +cat /etc/os-release + +# Check architecture +uname -m + +# Check available memory +free -h + +# Check available disk space +df -h + +# Check network connectivity +ping -c 4 google.com +``` + +#### 2. **Dependency Check** +```bash +# Check if Python is available +python3 --version + +# Check if Git is available +git --version + +# Check if curl/wget are available +curl --version +wget --version +``` + +### **Installation Testing** + +#### 1. **Dry Run Test** +```bash +# Download and examine script without executing +curl -s https://cyberpanel.net/install.sh > install_test.sh +chmod +x install_test.sh + +# Review script content +head -50 install_test.sh +``` + +#### 2. **Full Installation Test** +```bash +# Run installation in test environment +sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh) + +# Monitor installation process +# Check for any errors or warnings +``` + +### **Post-Installation Verification** + +#### 1. **Service Status Check** +```bash +# Check CyberPanel service +systemctl status lscpd + +# Check web server +systemctl status lsws + +# Check database +systemctl status mariadb +``` + +#### 2. **Web Interface Test** +```bash +# Test web interface accessibility +curl -I http://localhost:8090 + +# Expected: HTTP/1.1 200 OK +``` + +#### 3. **Database Connection Test** +```bash +# Test database connection +mysql -u root -p -e "SHOW DATABASES;" + +# Should show CyberPanel database +``` + +## ๐Ÿ› Common Issues and Solutions + +### **Installation Issues** + +#### 1. **"Command not found" Errors** +**Problem**: Required commands not available +**Solution**: +```bash +# Install missing packages +# Ubuntu/Debian +sudo apt update && sudo apt install curl wget git python3 + +# RHEL/CentOS/AlmaLinux/RockyLinux +sudo yum install curl wget git python3 +``` + +#### 2. **Permission Denied Errors** +**Problem**: Insufficient privileges +**Solution**: +```bash +# Run with sudo +sudo sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh) +``` + +#### 3. **Network Connectivity Issues** +**Problem**: Cannot download installation script +**Solution**: +```bash +# Check internet connection +ping -c 4 google.com + +# Check DNS resolution +nslookup cyberpanel.net + +# Try alternative download method +wget https://cyberpanel.net/install.sh +chmod +x install.sh +sudo ./install.sh +``` + +#### 4. **Port Already in Use** +**Problem**: Port 8090 already occupied +**Solution**: +```bash +# Check what's using port 8090 +sudo netstat -tlnp | grep :8090 + +# Kill process if necessary +sudo kill -9 + +# Or change CyberPanel port in configuration +``` + +### **Upgrade Issues** + +#### 1. **Backup Creation Failed** +**Problem**: Cannot create backup +**Solution**: +```bash +# Check disk space +df -h + +# Free up space if necessary +sudo apt autoremove +sudo apt autoclean + +# Or specify different backup location +``` + +#### 2. **Database Migration Failed** +**Problem**: Database migration errors +**Solution**: +```bash +# Check database status +systemctl status mariadb + +# Restart database service +sudo systemctl restart mariadb + +# Run migration manually +cd /usr/local/CyberCP +python3 manage.py migrate +``` + +#### 3. **Service Restart Failed** +**Problem**: Services won't restart +**Solution**: +```bash +# Check service logs +journalctl -u lscpd -f + +# Restart services manually +sudo systemctl restart lscpd +sudo systemctl restart lsws +``` + +## ๐Ÿ“Š Verification Checklist + +### **Installation Verification** +- [ ] Installation script downloads successfully +- [ ] All dependencies installed correctly +- [ ] Web server starts and responds +- [ ] Database server starts and responds +- [ ] CyberPanel web interface accessible +- [ ] Admin user can log in +- [ ] All services running properly +- [ ] No error messages in logs + +### **Upgrade Verification** +- [ ] Upgrade script downloads successfully +- [ ] Backup created successfully +- [ ] Source code updated correctly +- [ ] Dependencies updated properly +- [ ] Database migrations completed +- [ ] Services restarted successfully +- [ ] Web interface still accessible +- [ ] Data integrity maintained + +## ๐Ÿ” Monitoring and Logging + +### **Installation Logs** +```bash +# Check installation logs +tail -f /root/cyberpanel-install.log + +# Check system logs +journalctl -f +``` + +### **Service Logs** +```bash +# Check CyberPanel logs +tail -f /usr/local/lscp/logs/error.log + +# Check web server logs +tail -f /usr/local/lsws/logs/error.log + +# Check database logs +tail -f /var/log/mysql/error.log +``` + +## ๐Ÿ†˜ Getting Help + +### **If Installation Fails** +1. **Check the logs** for specific error messages +2. **Verify system requirements** are met +3. **Try alternative installation methods** (wget, manual download) +4. **Use troubleshooting commands** from the README +5. **Contact support** with detailed error information + +### **If Upgrade Fails** +1. **Restore from backup** if available +2. **Check service status** and restart if needed +3. **Run manual upgrade** steps +4. **Use troubleshooting commands** from the README +5. **Contact support** with upgrade logs + +### **Support Resources** +- **CyberPanel Forums**: https://community.cyberpanel.net +- **GitHub Issues**: https://github.com/usmannasir/cyberpanel/issues +- **Discord Server**: https://discord.gg/cyberpanel + +--- + +**Note**: This verification guide ensures CyberPanel installation and upgrade processes work correctly across all supported operating systems. Always test in a non-production environment first. + +*Last updated: January 2025* diff --git a/guides/WINDOWS_INSTALLATION_GUIDE.md b/guides/WINDOWS_INSTALLATION_GUIDE.md new file mode 100644 index 000000000..b29d6f812 --- /dev/null +++ b/guides/WINDOWS_INSTALLATION_GUIDE.md @@ -0,0 +1,329 @@ +# CyberPanel Windows Development Guide + +## ๐ŸŽฏ Overview + +**โš ๏ธ IMPORTANT**: CyberPanel is designed specifically for Linux systems and does not officially support Windows. This guide is for **development and testing purposes only**. + +This guide provides instructions for running CyberPanel in a Windows development environment using Python and Django. This setup is intended for: +- **Developers** working on CyberPanel features +- **Testing** CyberPanel functionality +- **Learning** CyberPanel architecture +- **Development** of CyberPanel extensions + +**For production use, always use a supported Linux distribution.** + +## ๐Ÿ“‹ Prerequisites + +### System Requirements +- **OS**: Windows 7/8.1/10/11 (64-bit recommended) +- **RAM**: Minimum 4GB (8GB+ recommended) +- **Storage**: Minimum 20GB free space +- **CPU**: 2+ cores recommended +- **Network**: Internet connection required + +### Required Software +- **Python 3.8+**: Download from [python.org](https://python.org) +- **Git**: Download from [git-scm.com](https://git-scm.com) +- **Administrator Access**: Required for installation + +### โš ๏ธ Limitations +- **No Web Server**: OpenLiteSpeed/LiteSpeed not available on Windows +- **No System Services**: MariaDB, PowerDNS, etc. not included +- **Limited Functionality**: Many CyberPanel features require Linux +- **Development Only**: Not suitable for production use + +## ๐Ÿš€ Installation Methods + +### Method 1: Automated Installation (Recommended) + +#### Step 1: Download Installation Script +1. Navigate to the `utils/windows/` folder +2. Download `cyberpanel_install.bat` +3. Right-click and select "Run as administrator" + +#### Step 2: Follow Installation Prompts +The script will automatically: +- Check system requirements +- Create Python virtual environment +- Download CyberPanel source code +- Install all dependencies +- Set up the web interface + +#### Step 3: Access CyberPanel +1. Open your web browser +2. Navigate to: `http://localhost:8090` +3. Use default credentials: + - **Username**: `admin` + - **Password**: `123456` + +### Method 2: Manual Installation + +#### Step 1: Install Python +1. Download Python 3.8+ from [python.org](https://python.org) +2. **Important**: Check "Add Python to PATH" during installation +3. Verify installation: + ```cmd + python --version + pip --version + ``` + +#### Step 2: Install Git +1. Download Git from [git-scm.com](https://git-scm.com) +2. Install with default settings +3. Verify installation: + ```cmd + git --version + ``` + +#### Step 3: Create CyberPanel Directory +```cmd +mkdir C:\usr\local\CyberCP +cd C:\usr\local\CyberCP +``` + +#### Step 4: Set Up Python Environment +```cmd +python -m venv . --system-site-packages +Scripts\activate.bat +``` + +#### Step 5: Download CyberPanel +```cmd +git clone https://github.com/usmannasir/cyberpanel.git +cd cyberpanel +``` + +#### Step 6: Install Dependencies +```cmd +pip install --upgrade pip setuptools wheel +pip install -r requirments.txt +``` + +#### Step 7: Set Up Django +```cmd +python manage.py collectstatic --noinput +python manage.py migrate +``` + +#### Step 8: Create Admin User +```cmd +python manage.py createsuperuser +``` + +#### Step 9: Start CyberPanel +```cmd +python manage.py runserver 0.0.0.0:8090 +``` + +## ๐Ÿ”ง Post-Installation Configuration + +### Change Default Password +1. Access CyberPanel at `http://localhost:8090` +2. Log in with default credentials +3. Go to **User Management** โ†’ **Modify User** +4. Change the admin password immediately + +### Configure Windows Firewall +1. Open Windows Defender Firewall +2. Add inbound rule for port 8090 +3. Allow Python through firewall + +### Set Up Windows Service (Optional) +1. Run `install_service.bat` as administrator +2. CyberPanel will start automatically on boot +3. Manage service through Windows Services + +## ๐Ÿ”„ Upgrading CyberPanel + +### Using Upgrade Script +1. Download `cyberpanel_upgrade.bat` +2. Right-click and select "Run as administrator" +3. Script will automatically backup and upgrade + +### Manual Upgrade +```cmd +cd C:\usr\local\CyberCP\cyberpanel +Scripts\activate.bat +git pull origin stable +pip install --upgrade -r requirments.txt +python manage.py migrate +python manage.py collectstatic --noinput +``` + +## ๐Ÿ› ๏ธ Utility Scripts + +### Available Scripts +- **`cyberpanel_install.bat`**: Complete installation +- **`cyberpanel_upgrade.bat`**: Upgrade existing installation +- **`install_webauthn.bat`**: Enable WebAuthn/Passkey authentication + +### Script Usage +1. **Right-click** on any script +2. Select **"Run as administrator"** +3. Follow on-screen prompts +4. Check output for any errors + +## ๐Ÿ› Troubleshooting + +### Common Issues + +#### "Python not found" Error +**Problem**: Python is not installed or not in PATH +**Solution**: +1. Install Python from [python.org](https://python.org) +2. Check "Add Python to PATH" during installation +3. Restart Command Prompt +4. Verify with `python --version` + +#### "Access Denied" Error +**Problem**: Insufficient privileges +**Solution**: +1. Right-click script and select "Run as administrator" +2. Or open Command Prompt as administrator +3. Navigate to script location and run + +#### "Failed to install requirements" Error +**Problem**: Network or dependency issues +**Solution**: +1. Check internet connection +2. Try running script again +3. Install requirements manually: + ```cmd + pip install --upgrade pip + pip install -r requirments.txt + ``` + +#### "Port 8090 already in use" Error +**Problem**: Another service is using port 8090 +**Solution**: +1. Stop other services using port 8090 +2. Or change CyberPanel port in settings +3. Check with: `netstat -an | findstr :8090` + +#### "Django not found" Error +**Problem**: Virtual environment not activated +**Solution**: +1. Navigate to CyberPanel directory +2. Activate virtual environment: + ```cmd + Scripts\activate.bat + ``` +3. Verify with `python -c "import django"` + +### Advanced Troubleshooting + +#### Check Installation Logs +```cmd +cd C:\usr\local\CyberCP\cyberpanel +python manage.py check +``` + +#### Verify Dependencies +```cmd +pip list +pip check +``` + +#### Test Database Connection +```cmd +python manage.py dbshell +``` + +#### Reset Database (Last Resort) +```cmd +python manage.py flush +python manage.py migrate +python manage.py createsuperuser +``` + +## ๐Ÿ”’ Security Considerations + +### Initial Security Setup +1. **Change Default Password**: Immediately after installation +2. **Enable 2FA**: Use the 2FA guide for additional security +3. **Configure Firewall**: Restrict access to necessary ports only +4. **Regular Updates**: Keep CyberPanel and dependencies updated + +### Windows-Specific Security +1. **User Account Control**: Keep UAC enabled +2. **Windows Defender**: Ensure real-time protection is on +3. **Regular Backups**: Backup CyberPanel data regularly +4. **Service Account**: Consider using dedicated service account + +## ๐Ÿ“Š Performance Optimization + +### System Optimization +1. **Disable Unnecessary Services**: Free up system resources +2. **SSD Storage**: Use SSD for better performance +3. **Memory**: Ensure adequate RAM (8GB+ recommended) +4. **CPU**: Multi-core processor recommended + +### CyberPanel Optimization +1. **Static Files**: Ensure static files are properly collected +2. **Database**: Regular database maintenance +3. **Logs**: Regular log cleanup +4. **Caching**: Enable appropriate caching + +## ๐Ÿ”„ Maintenance + +### Regular Maintenance Tasks +1. **Updates**: Regular CyberPanel updates +2. **Backups**: Regular data backups +3. **Logs**: Monitor and clean logs +4. **Security**: Regular security checks + +### Backup Procedures +1. **Database Backup**: + ```cmd + python manage.py dumpdata > backup.json + ``` + +2. **File Backup**: + ```cmd + xcopy C:\usr\local\CyberCP backup_folder /E /I /H + ``` + +3. **Restore from Backup**: + ```cmd + python manage.py loaddata backup.json + ``` + +## ๐Ÿ“š Additional Resources + +### Documentation +- **Main Guide**: [2FA Authentication Guide](2FA_AUTHENTICATION_GUIDE.md) +- **Troubleshooting**: [Troubleshooting Guide](TROUBLESHOOTING.md) +- **Utility Scripts**: [Utility Scripts](../utils/README.md) + +### Support +- **CyberPanel Forums**: https://community.cyberpanel.net +- **GitHub Issues**: https://github.com/usmannasir/cyberpanel/issues +- **Discord Server**: https://discord.gg/cyberpanel + +## โœ… Verification Checklist + +After installation, verify these components: +- [ ] CyberPanel accessible at `http://localhost:8090` +- [ ] Admin password changed from default +- [ ] Windows Firewall configured +- [ ] Python virtual environment working +- [ ] All dependencies installed +- [ ] Database migrations applied +- [ ] Static files collected +- [ ] Logs are clean +- [ ] Service starts automatically (if configured) + +## ๐Ÿ†˜ Getting Help + +If you encounter issues: +1. **Check the logs** for error messages +2. **Run the troubleshooting commands** above +3. **Search the documentation** for solutions +4. **Ask in the community forum** for help +5. **Create a GitHub issue** with detailed information + +--- + +**Note**: This guide is specifically for Windows installations. For Linux installations, refer to the main CyberPanel documentation. + +*Last updated: January 2025* diff --git a/loginSystem/migrations/0002_webauthn_models.py b/loginSystem/migrations/0002_webauthn_models.py new file mode 100644 index 000000000..521a6f0a2 --- /dev/null +++ b/loginSystem/migrations/0002_webauthn_models.py @@ -0,0 +1,111 @@ +# Generated migration for WebAuthn models + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('loginSystem', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='WebAuthnCredential', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('credential_id', models.CharField(help_text='Base64 encoded credential ID', max_length=255, unique=True)), + ('public_key', models.TextField(help_text='Base64 encoded public key')), + ('counter', models.BigIntegerField(default=0, help_text='Signature counter for replay protection')), + ('name', models.CharField(help_text='User-friendly name for the passkey', max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('last_used', models.DateTimeField(blank=True, null=True)), + ('is_active', models.BooleanField(default=True, help_text='Whether this credential is active')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webauthn_credentials', to='loginSystem.administrator')), + ], + options={ + 'verbose_name': 'WebAuthn Credential', + 'verbose_name_plural': 'WebAuthn Credentials', + 'db_table': 'webauthn_credentials', + }, + ), + migrations.CreateModel( + name='WebAuthnChallenge', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('challenge', models.CharField(help_text='Base64 encoded challenge', max_length=255)), + ('challenge_type', models.CharField(choices=[('registration', 'Registration'), ('authentication', 'Authentication')], max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('expires_at', models.DateTimeField()), + ('used', models.BooleanField(default=False)), + ('metadata', models.TextField(default='{}', help_text='Additional challenge metadata as JSON')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webauthn_challenges', to='loginSystem.administrator')), + ], + options={ + 'verbose_name': 'WebAuthn Challenge', + 'verbose_name_plural': 'WebAuthn Challenges', + 'db_table': 'webauthn_challenges', + }, + ), + migrations.CreateModel( + name='WebAuthnSession', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('session_id', models.CharField(help_text='Unique session identifier', max_length=255, unique=True)), + ('session_type', models.CharField(choices=[('registration', 'Registration'), ('authentication', 'Authentication')], max_length=20)), + ('data', models.TextField(help_text='Session data as JSON')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('expires_at', models.DateTimeField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webauthn_sessions', to='loginSystem.administrator')), + ], + options={ + 'verbose_name': 'WebAuthn Session', + 'verbose_name_plural': 'WebAuthn Sessions', + 'db_table': 'webauthn_sessions', + }, + ), + migrations.CreateModel( + name='WebAuthnSettings', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('enabled', models.BooleanField(default=False, help_text='Whether WebAuthn is enabled for this user')), + ('require_passkey', models.BooleanField(default=False, help_text='Require passkey for login (passwordless)')), + ('allow_multiple_credentials', models.BooleanField(default=True, help_text='Allow multiple passkeys per user')), + ('max_credentials', models.IntegerField(default=10, help_text='Maximum number of passkeys allowed')), + ('timeout_seconds', models.IntegerField(default=60, help_text='WebAuthn operation timeout in seconds')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='webauthn_settings', to='loginSystem.administrator')), + ], + options={ + 'verbose_name': 'WebAuthn Settings', + 'verbose_name_plural': 'WebAuthn Settings', + 'db_table': 'webauthn_settings', + }, + ), + migrations.AddIndex( + model_name='webauthncredential', + index=models.Index(fields=['user', 'is_active'], name='webauthn_cre_user_id_123456_idx'), + ), + migrations.AddIndex( + model_name='webauthncredential', + index=models.Index(fields=['credential_id'], name='webauthn_cre_credent_123456_idx'), + ), + migrations.AddIndex( + model_name='webauthnchallenge', + index=models.Index(fields=['user', 'challenge_type', 'used'], name='webauthn_cha_user_id_123456_idx'), + ), + migrations.AddIndex( + model_name='webauthnchallenge', + index=models.Index(fields=['expires_at'], name='webauthn_cha_expires_123456_idx'), + ), + migrations.AddIndex( + model_name='webauthnsession', + index=models.Index(fields=['session_id'], name='webauthn_ses_session_123456_idx'), + ), + migrations.AddIndex( + model_name='webauthnsession', + index=models.Index(fields=['expires_at'], name='webauthn_ses_expires_123456_idx'), + ), + ] diff --git a/loginSystem/static/loginSystem/webauthn.js b/loginSystem/static/loginSystem/webauthn.js new file mode 100644 index 000000000..a9d4eddc5 --- /dev/null +++ b/loginSystem/static/loginSystem/webauthn.js @@ -0,0 +1,396 @@ +/** + * WebAuthn JavaScript integration for CyberPanel + * Provides passkey registration and authentication functionality + */ + +class CyberPanelWebAuthn { + constructor() { + this.isSupported = this.checkSupport(); + this.baseUrl = window.location.origin; + this.apiEndpoints = { + registrationStart: '/webauthn/registration/start/', + registrationComplete: '/webauthn/registration/complete/', + authenticationStart: '/webauthn/authentication/start/', + authenticationComplete: '/webauthn/authentication/complete/', + credentialsList: '/webauthn/credentials/', + credentialDelete: '/webauthn/credential/delete/', + credentialUpdate: '/webauthn/credential/update/', + settingsUpdate: '/webauthn/settings/update/', + }; + + this.init(); + } + + init() { + if (!this.isSupported) { + console.warn('WebAuthn is not supported in this browser'); + return; + } + + // Add CSRF token to all requests + this.csrfToken = this.getCSRFToken(); + + // Initialize UI elements + this.initializeUI(); + } + + checkSupport() { + return !!(navigator.credentials && + navigator.credentials.create && + navigator.credentials.get && + window.PublicKeyCredential); + } + + getCSRFToken() { + const cookieValue = document.cookie + .split('; ') + .find(row => row.startsWith('csrftoken=')) + ?.split('=')[1]; + return cookieValue || ''; + } + + initializeUI() { + // Add WebAuthn buttons to login form + this.addLoginButtons(); + + // Add WebAuthn management to user settings + this.addUserManagementUI(); + } + + addLoginButtons() { + const loginForm = document.querySelector('#loginForm'); + if (!loginForm) return; + + // Add WebAuthn login button + const webauthnButton = document.createElement('button'); + webauthnButton.type = 'button'; + webauthnButton.className = 'btn btn-primary btn-block'; + webauthnButton.innerHTML = ' Login with Passkey'; + webauthnButton.onclick = () => this.startPasswordlessLogin(); + + // Insert after password field + const passwordField = loginForm.querySelector('input[type="password"]'); + if (passwordField) { + passwordField.parentNode.insertBefore(webauthnButton, passwordField.parentNode.nextSibling); + } + } + + addUserManagementUI() { + // This will be called when user management page loads + // Implementation depends on the specific UI structure + } + + async startPasswordlessLogin() { + try { + const username = document.querySelector('input[name="username"]').value; + if (!username) { + this.showError('Please enter your username first'); + return; + } + + this.showLoading('Starting passkey authentication...'); + + // Get authentication challenge + const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationStart, { + username: username + }); + + if (!challengeResponse.success) { + throw new Error(challengeResponse.error || 'Failed to start authentication'); + } + + // Convert challenge to proper format + const challenge = this.convertChallenge(challengeResponse.challenge); + + // Get credential + const credential = await navigator.credentials.get({ + publicKey: challenge + }); + + // Complete authentication + const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, { + challenge_id: challengeResponse.challenge_id, + credential: { + id: this.arrayBufferToBase64(credential.rawId), + type: credential.type + }, + client_data_json: this.arrayBufferToBase64(credential.response.clientDataJSON), + authenticator_data: this.arrayBufferToBase64(credential.response.authenticatorData), + signature: this.arrayBufferToBase64(credential.response.signature), + user_handle: credential.response.userHandle ? + this.arrayBufferToBase64(credential.response.userHandle) : null + }); + + if (authResponse.success) { + this.showSuccess('Authentication successful! Redirecting...'); + setTimeout(() => { + window.location.href = '/'; + }, 1000); + } else { + throw new Error(authResponse.error || 'Authentication failed'); + } + + } catch (error) { + console.error('WebAuthn authentication error:', error); + this.showError(error.message || 'Authentication failed'); + } finally { + this.hideLoading(); + } + } + + async registerPasskey(username, credentialName = '') { + try { + this.showLoading('Starting passkey registration...'); + + // Get registration challenge + const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.registrationStart, { + username: username, + credential_name: credentialName + }); + + if (!challengeResponse.success) { + throw new Error(challengeResponse.error || 'Failed to start registration'); + } + + // Convert challenge to proper format + const challenge = this.convertChallenge(challengeResponse.challenge); + + // Create credential + const credential = await navigator.credentials.create({ + publicKey: challenge + }); + + // Complete registration + const regResponse = await this.makeRequest('POST', this.apiEndpoints.registrationComplete, { + challenge_id: challengeResponse.challenge_id, + credential: { + id: this.arrayBufferToBase64(credential.rawId), + type: credential.type + }, + client_data_json: this.arrayBufferToBase64(credential.response.clientDataJSON), + attestation_object: this.arrayBufferToBase64(credential.response.attestationObject) + }); + + if (regResponse.success) { + this.showSuccess('Passkey registered successfully!'); + return regResponse; + } else { + throw new Error(regResponse.error || 'Registration failed'); + } + + } catch (error) { + console.error('WebAuthn registration error:', error); + this.showError(error.message || 'Registration failed'); + throw error; + } finally { + this.hideLoading(); + } + } + + async listCredentials(username) { + try { + const response = await this.makeRequest('GET', + `${this.apiEndpoints.credentialsList}${username}/`); + + if (response.success) { + return response.credentials; + } else { + throw new Error(response.error || 'Failed to list credentials'); + } + } catch (error) { + console.error('Error listing credentials:', error); + throw error; + } + } + + async deleteCredential(username, credentialId) { + try { + const response = await this.makeRequest('POST', this.apiEndpoints.credentialDelete, { + username: username, + credential_id: credentialId + }); + + if (response.success) { + this.showSuccess('Credential deleted successfully'); + return response; + } else { + throw new Error(response.error || 'Failed to delete credential'); + } + } catch (error) { + console.error('Error deleting credential:', error); + this.showError(error.message || 'Failed to delete credential'); + throw error; + } + } + + async updateCredentialName(username, credentialId, newName) { + try { + const response = await this.makeRequest('POST', this.apiEndpoints.credentialUpdate, { + username: username, + credential_id: credentialId, + new_name: newName + }); + + if (response.success) { + this.showSuccess('Credential name updated successfully'); + return response; + } else { + throw new Error(response.error || 'Failed to update credential name'); + } + } catch (error) { + console.error('Error updating credential name:', error); + this.showError(error.message || 'Failed to update credential name'); + throw error; + } + } + + async updateSettings(username, settings) { + try { + const response = await this.makeRequest('POST', this.apiEndpoints.settingsUpdate, { + username: username, + ...settings + }); + + if (response.success) { + this.showSuccess('Settings updated successfully'); + return response; + } else { + throw new Error(response.error || 'Failed to update settings'); + } + } catch (error) { + console.error('Error updating settings:', error); + this.showError(error.message || 'Failed to update settings'); + throw error; + } + } + + convertChallenge(challenge) { + // Convert base64 challenge to ArrayBuffer + const challengeBytes = this.base64ToArrayBuffer(challenge.challenge); + + return { + ...challenge, + challenge: challengeBytes, + user: { + ...challenge.user, + id: this.base64ToArrayBuffer(challenge.user.id) + }, + excludeCredentials: challenge.excludeCredentials?.map(cred => ({ + ...cred, + id: this.base64ToArrayBuffer(cred.id) + })) || [], + allowCredentials: challenge.allowCredentials?.map(cred => ({ + ...cred, + id: this.base64ToArrayBuffer(cred.id) + })) || [] + }; + } + + base64ToArrayBuffer(base64) { + const binaryString = window.atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; + } + + arrayBufferToBase64(buffer) { + const bytes = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } + + async makeRequest(method, url, data = null) { + const options = { + method: method, + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': this.csrfToken + } + }; + + if (data) { + options.body = JSON.stringify(data); + } + + const response = await fetch(url, options); + return await response.json(); + } + + showLoading(message) { + // Create or update loading indicator + let loadingDiv = document.getElementById('webauthn-loading'); + if (!loadingDiv) { + loadingDiv = document.createElement('div'); + loadingDiv.id = 'webauthn-loading'; + loadingDiv.className = 'alert alert-info'; + loadingDiv.innerHTML = ' ' + message; + document.body.appendChild(loadingDiv); + } else { + loadingDiv.innerHTML = ' ' + message; + loadingDiv.style.display = 'block'; + } + } + + hideLoading() { + const loadingDiv = document.getElementById('webauthn-loading'); + if (loadingDiv) { + loadingDiv.style.display = 'none'; + } + } + + showSuccess(message) { + this.showAlert('success', message); + } + + showError(message) { + this.showAlert('danger', message); + } + + showAlert(type, message) { + // Create alert element + const alertDiv = document.createElement('div'); + alertDiv.className = `alert alert-${type} alert-dismissible fade show`; + alertDiv.innerHTML = ` + ${message} + + `; + + // Insert at top of page + const container = document.querySelector('.container') || document.body; + container.insertBefore(alertDiv, container.firstChild); + + // Auto-dismiss after 5 seconds + setTimeout(() => { + if (alertDiv.parentNode) { + alertDiv.remove(); + } + }, 5000); + } + + // Utility method to check if WebAuthn is available + static isSupported() { + return !!(navigator.credentials && + navigator.credentials.create && + navigator.credentials.get && + window.PublicKeyCredential); + } +} + +// Initialize WebAuthn when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + if (CyberPanelWebAuthn.isSupported()) { + window.cyberPanelWebAuthn = new CyberPanelWebAuthn(); + } +}); + +// Export for use in other scripts +if (typeof module !== 'undefined' && module.exports) { + module.exports = CyberPanelWebAuthn; +} diff --git a/loginSystem/templates/loginSystem/login.html b/loginSystem/templates/loginSystem/login.html index d9a06db62..e5930a5d9 100644 --- a/loginSystem/templates/loginSystem/login.html +++ b/loginSystem/templates/loginSystem/login.html @@ -361,6 +361,18 @@ + + + @@ -374,6 +386,36 @@ + + diff --git a/loginSystem/tests_webauthn.py b/loginSystem/tests_webauthn.py new file mode 100644 index 000000000..789f70189 --- /dev/null +++ b/loginSystem/tests_webauthn.py @@ -0,0 +1,452 @@ +# -*- coding: utf-8 -*- + +from django.test import TestCase, Client +from django.contrib.auth import get_user_model +from django.urls import reverse +from django.utils import timezone +from datetime import datetime, timedelta +import json +import base64 +from .models import Administrator +from .webauthn_models import WebAuthnCredential, WebAuthnChallenge, WebAuthnSettings +from .webauthn_backend import WebAuthnBackend + + +class WebAuthnTestCase(TestCase): + """Test cases for WebAuthn functionality""" + + def setUp(self): + """Set up test data""" + self.client = Client() + + # Create test user + self.user = Administrator.objects.create( + userName='testuser', + password='hashedpassword', + email='test@example.com', + firstName='Test', + lastName='User', + type=1, + acl_id=1 + ) + + # Create WebAuthn settings + self.webauthn_settings = WebAuthnSettings.objects.create( + user=self.user, + enabled=True, + require_passkey=False, + allow_multiple_credentials=True, + max_credentials=10, + timeout_seconds=60 + ) + + self.webauthn_backend = WebAuthnBackend() + + def test_webauthn_models(self): + """Test WebAuthn models""" + # Test WebAuthnCredential + credential = WebAuthnCredential.objects.create( + user=self.user, + credential_id='test_credential_id', + public_key='test_public_key', + name='Test Passkey', + counter=0 + ) + + self.assertEqual(credential.user, self.user) + self.assertEqual(credential.name, 'Test Passkey') + self.assertTrue(credential.is_active) + + # Test WebAuthnChallenge + challenge = WebAuthnChallenge.objects.create( + user=self.user, + challenge='test_challenge', + challenge_type='registration', + expires_at=timezone.now() + timedelta(minutes=5) + ) + + self.assertEqual(challenge.user, self.user) + self.assertEqual(challenge.challenge_type, 'registration') + self.assertFalse(challenge.is_expired()) + + # Test WebAuthnSettings + self.assertTrue(self.webauthn_settings.enabled) + self.assertTrue(self.webauthn_settings.can_add_credential()) + + def test_registration_challenge_creation(self): + """Test WebAuthn registration challenge creation""" + result = self.webauthn_backend.create_registration_challenge( + self.user, 'Test Passkey' + ) + + self.assertTrue(result['success']) + self.assertIn('challenge', result) + self.assertIn('challenge_id', result) + + # Verify challenge was stored in database + challenge_id = result['challenge_id'] + challenge = WebAuthnChallenge.objects.get(id=challenge_id) + self.assertEqual(challenge.user, self.user) + self.assertEqual(challenge.challenge_type, 'registration') + + def test_authentication_challenge_creation(self): + """Test WebAuthn authentication challenge creation""" + result = self.webauthn_backend.create_authentication_challenge(self.user) + + self.assertTrue(result['success']) + self.assertIn('challenge', result) + self.assertIn('challenge_id', result) + + # Verify challenge was stored in database + challenge_id = result['challenge_id'] + challenge = WebAuthnChallenge.objects.get(id=challenge_id) + self.assertEqual(challenge.user, self.user) + self.assertEqual(challenge.challenge_type, 'authentication') + + def test_credential_management(self): + """Test credential management functions""" + # Create test credential + credential = WebAuthnCredential.objects.create( + user=self.user, + credential_id='test_credential_id', + public_key='test_public_key', + name='Test Passkey', + counter=0 + ) + + # Test get_user_credentials + credentials = self.webauthn_backend.get_user_credentials(self.user) + self.assertEqual(len(credentials), 1) + self.assertEqual(credentials[0]['name'], 'Test Passkey') + + # Test delete_credential + result = self.webauthn_backend.delete_credential(self.user, credential.id) + self.assertTrue(result['success']) + + # Verify credential is deactivated + credential.refresh_from_db() + self.assertFalse(credential.is_active) + + # Test update_credential_name + credential.is_active = True + credential.save() + + result = self.webauthn_backend.update_credential_name( + self.user, credential.id, 'Updated Name' + ) + self.assertTrue(result['success']) + + credential.refresh_from_db() + self.assertEqual(credential.name, 'Updated Name') + + def test_webauthn_api_endpoints(self): + """Test WebAuthn API endpoints""" + # Test registration start endpoint + response = self.client.post('/webauthn/registration/start/', + json.dumps({'username': 'testuser', 'credential_name': 'Test Passkey'}), + content_type='application/json') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + self.assertTrue(data['success']) + + # Test credentials list endpoint + response = self.client.get('/webauthn/credentials/testuser/') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + self.assertTrue(data['success']) + self.assertIn('credentials', data) + self.assertIn('settings', data) + + # Test settings update endpoint + response = self.client.post('/webauthn/settings/update/', + json.dumps({ + 'username': 'testuser', + 'enabled': True, + 'require_passkey': False + }), + content_type='application/json') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + self.assertTrue(data['success']) + + def test_webauthn_settings_validation(self): + """Test WebAuthn settings validation""" + # Test max credentials limit + settings = WebAuthnSettings.objects.get(user=self.user) + settings.max_credentials = 1 + settings.save() + + # Create first credential + WebAuthnCredential.objects.create( + user=self.user, + credential_id='cred1', + public_key='key1', + name='First Passkey' + ) + + # Should not be able to add more credentials + self.assertFalse(settings.can_add_credential()) + + # Test multiple credentials disabled + settings.allow_multiple_credentials = False + settings.max_credentials = 10 + settings.save() + + # Should not be able to add more credentials + self.assertFalse(settings.can_add_credential()) + + def test_challenge_expiration(self): + """Test challenge expiration handling""" + # Create expired challenge + expired_challenge = WebAuthnChallenge.objects.create( + user=self.user, + challenge='expired_challenge', + challenge_type='registration', + expires_at=timezone.now() - timedelta(minutes=1) + ) + + self.assertTrue(expired_challenge.is_expired()) + + # Test cleanup + self.webauthn_backend.cleanup_expired_challenges() + + # Expired challenge should be deleted + with self.assertRaises(WebAuthnChallenge.DoesNotExist): + WebAuthnChallenge.objects.get(id=expired_challenge.id) + + def test_webauthn_integration_with_existing_2fa(self): + """Test WebAuthn integration with existing 2FA system""" + # Enable 2FA for user + self.user.twoFA = 1 + self.user.secretKey = 'test_secret_key' + self.user.save() + + # Enable WebAuthn + settings = WebAuthnSettings.objects.get(user=self.user) + settings.enabled = True + settings.save() + + # Both should be enabled + self.assertTrue(self.user.twoFA) + self.assertTrue(settings.enabled) + + # User should be able to use either authentication method + # (This would be tested in the actual authentication flow) + + def test_webauthn_security_features(self): + """Test WebAuthn security features""" + # Test credential counter update + credential = WebAuthnCredential.objects.create( + user=self.user, + credential_id='test_credential_id', + public_key='test_public_key', + name='Test Passkey', + counter=0 + ) + + # Update counter + result = credential.update_counter(5) + self.assertTrue(result) + + # Should not allow decreasing counter + result = credential.update_counter(3) + self.assertFalse(result) + + # Test challenge uniqueness + challenge1 = self.webauthn_backend.generate_challenge() + challenge2 = self.webauthn_backend.generate_challenge() + + self.assertNotEqual(challenge1, challenge2) + self.assertEqual(len(challenge1), 44) # Base64 encoded 32 bytes + + def test_webauthn_error_handling(self): + """Test WebAuthn error handling""" + # Test with non-existent user + result = self.webauthn_backend.create_registration_challenge( + None, 'Test Passkey' + ) + self.assertFalse(result['success']) + self.assertIn('error', result) + + # Test with disabled WebAuthn + settings = WebAuthnSettings.objects.get(user=self.user) + settings.enabled = False + settings.save() + + result = self.webauthn_backend.create_authentication_challenge(self.user) + self.assertFalse(result['success']) + self.assertIn('error', result) + + def test_webauthn_data_serialization(self): + """Test WebAuthn data serialization""" + # Test challenge metadata + challenge = WebAuthnChallenge.objects.create( + user=self.user, + challenge='test_challenge', + challenge_type='registration', + expires_at=timezone.now() + timedelta(minutes=5) + ) + + # Set metadata + metadata = {'test_key': 'test_value', 'number': 123} + challenge.set_metadata(metadata) + challenge.save() + + # Retrieve metadata + retrieved_metadata = challenge.get_metadata() + self.assertEqual(retrieved_metadata, metadata) + + # Test session data + from .webauthn_models import WebAuthnSession + + session = WebAuthnSession.create_session( + self.user, 'registration', {'test': 'data'} + ) + + session_data = session.get_data() + self.assertEqual(session_data, {'test': 'data'}) + + # Update session data + session.set_data({'updated': 'data'}) + session.save() + + updated_data = session.get_data() + self.assertEqual(updated_data, {'updated': 'data'}) + + +class WebAuthnIntegrationTestCase(TestCase): + """Integration tests for WebAuthn with CyberPanel""" + + def setUp(self): + """Set up integration test data""" + self.client = Client() + + # Create admin user + self.admin = Administrator.objects.create( + userName='admin', + password='hashedpassword', + email='admin@example.com', + firstName='Admin', + lastName='User', + type=1, + acl_id=1 + ) + + # Create regular user + self.user = Administrator.objects.create( + userName='testuser', + password='hashedpassword', + email='test@example.com', + firstName='Test', + lastName='User', + type=0, + acl_id=2, + owner=self.admin.pk + ) + + def test_webauthn_user_management_integration(self): + """Test WebAuthn integration with user management""" + # Login as admin + self.client.force_login(self.admin) + + # Test WebAuthn settings update through user management + response = self.client.post('/webauthn/settings/update/', + json.dumps({ + 'username': 'testuser', + 'enabled': True, + 'require_passkey': False, + 'allow_multiple_credentials': True, + 'max_credentials': 5 + }), + content_type='application/json') + + self.assertEqual(response.status_code, 200) + + # Verify settings were updated + settings = WebAuthnSettings.get_or_create_settings(self.user) + self.assertTrue(settings.enabled) + self.assertEqual(settings.max_credentials, 5) + + def test_webauthn_permissions(self): + """Test WebAuthn permission system""" + # Test admin can manage any user's WebAuthn settings + self.client.force_login(self.admin) + + response = self.client.get('/webauthn/credentials/testuser/') + self.assertEqual(response.status_code, 200) + + # Test user can manage their own WebAuthn settings + self.client.force_login(self.user) + + response = self.client.get('/webauthn/credentials/testuser/') + self.assertEqual(response.status_code, 200) + + # Test user cannot manage other users' settings + other_user = Administrator.objects.create( + userName='otheruser', + password='hashedpassword', + email='other@example.com', + firstName='Other', + lastName='User', + type=0, + acl_id=2, + owner=self.admin.pk + ) + + response = self.client.get('/webauthn/credentials/otheruser/') + self.assertEqual(response.status_code, 403) + + def test_webauthn_with_existing_authentication(self): + """Test WebAuthn alongside existing authentication methods""" + # Enable 2FA for user + self.user.twoFA = 1 + self.user.secretKey = 'test_secret_key' + self.user.save() + + # Enable WebAuthn + settings = WebAuthnSettings.objects.create( + user=self.user, + enabled=True, + require_passkey=False + ) + + # Both authentication methods should be available + self.assertTrue(self.user.twoFA) + self.assertTrue(settings.enabled) + + # User should be able to use either method + # (In practice, this would be handled in the login flow) + + def test_webauthn_cleanup_maintenance(self): + """Test WebAuthn cleanup and maintenance functions""" + # Create expired challenges and sessions + expired_challenge = WebAuthnChallenge.objects.create( + user=self.user, + challenge='expired_challenge', + challenge_type='registration', + expires_at=timezone.now() - timedelta(hours=1) + ) + + from .webauthn_models import WebAuthnSession + expired_session = WebAuthnSession.objects.create( + user=self.user, + session_id='expired_session', + session_type='registration', + data='{}', + expires_at=timezone.now() - timedelta(hours=1) + ) + + # Run cleanup + backend = WebAuthnBackend() + backend.cleanup_expired_challenges() + backend.cleanup_expired_sessions() + + # Expired items should be deleted + with self.assertRaises(WebAuthnChallenge.DoesNotExist): + WebAuthnChallenge.objects.get(id=expired_challenge.id) + + with self.assertRaises(WebAuthnSession.DoesNotExist): + WebAuthnSession.objects.get(id=expired_session.id) diff --git a/loginSystem/urls.py b/loginSystem/urls.py index df340412d..b874a9dc7 100644 --- a/loginSystem/urls.py +++ b/loginSystem/urls.py @@ -1,8 +1,9 @@ -from django.urls import path +from django.urls import path, include from . import views urlpatterns = [ path('', views.loadLoginPage, name='adminLogin'), path('verifyLogin', views.verifyLogin, name='verifyLogin'), path('logout', views.logout, name='logout'), + path('webauthn/', include('loginSystem.webauthn_urls')), ] diff --git a/loginSystem/webauthn_backend.py b/loginSystem/webauthn_backend.py new file mode 100644 index 000000000..f9b54d9c2 --- /dev/null +++ b/loginSystem/webauthn_backend.py @@ -0,0 +1,453 @@ +# -*- coding: utf-8 -*- + +import json +import base64 +import hashlib +import secrets +import time +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Tuple, Any +from .models import Administrator +from .webauthn_models import WebAuthnCredential, WebAuthnChallenge, WebAuthnSession, WebAuthnSettings +import logging + +logger = logging.getLogger(__name__) + + +class WebAuthnBackend: + """ + WebAuthn backend for handling passkey authentication + """ + + def __init__(self): + # Default WebAuthn configuration - can be overridden in Django settings + self.rp_id = 'cyberpanel.local' # Should be your actual domain + self.rp_name = 'CyberPanel' + self.origin = 'https://cyberpanel.local:8090' # Should be your actual origin + self.challenge_timeout = 300 # 5 minutes + + def generate_challenge(self) -> str: + """Generate a random challenge""" + return base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').rstrip('=') + + def create_registration_challenge(self, user: Administrator, credential_name: str = None) -> Dict[str, Any]: + """ + Create a WebAuthn registration challenge + """ + try: + # Check if user has WebAuthn settings + settings_obj = WebAuthnSettings.get_or_create_settings(user) + + if not settings_obj.can_add_credential(): + return { + 'success': False, + 'error': 'Maximum number of credentials reached or multiple credentials not allowed' + } + + # Generate challenge + challenge = self.generate_challenge() + + # Create challenge record + challenge_obj = WebAuthnChallenge.objects.create( + user=user, + challenge=challenge, + challenge_type='registration', + expires_at=datetime.now() + timedelta(seconds=self.challenge_timeout), + metadata=json.dumps({ + 'credential_name': credential_name or f"Passkey {datetime.now().strftime('%Y-%m-%d %H:%M')}", + 'rp_id': self.rp_id, + 'rp_name': self.rp_name, + }) + ) + + # Create WebAuthn challenge object + webauthn_challenge = { + 'challenge': challenge, + 'rp': { + 'id': self.rp_id, + 'name': self.rp_name, + }, + 'user': { + 'id': base64.urlsafe_b64encode(str(user.pk).encode()).decode('utf-8').rstrip('='), + 'name': user.email or user.userName, + 'displayName': f"{user.firstName} {user.lastName}".strip() or user.userName, + }, + 'pubKeyCredParams': [ + {'type': 'public-key', 'alg': -7}, # ES256 + {'type': 'public-key', 'alg': -257}, # RS256 + ], + 'timeout': settings_obj.timeout_seconds * 1000, + 'attestation': 'none', + 'excludeCredentials': self._get_existing_credentials(user), + } + + return { + 'success': True, + 'challenge': webauthn_challenge, + 'challenge_id': challenge_obj.id, + } + + except Exception as e: + logger.error(f"Error creating registration challenge: {str(e)}") + return { + 'success': False, + 'error': f'Failed to create registration challenge: {str(e)}' + } + + def create_authentication_challenge(self, user: Administrator = None) -> Dict[str, Any]: + """ + Create a WebAuthn authentication challenge + """ + try: + # If user is specified, create user-specific challenge + if user: + settings_obj = WebAuthnSettings.get_or_create_settings(user) + if not settings_obj.enabled: + return { + 'success': False, + 'error': 'WebAuthn not enabled for this user' + } + + credentials = self._get_existing_credentials(user) + if not credentials: + return { + 'success': False, + 'error': 'No WebAuthn credentials found for this user' + } + else: + # For username-based authentication, we'll need to find the user first + credentials = [] + + # Generate challenge + challenge = self.generate_challenge() + + # Create challenge record + challenge_obj = WebAuthnChallenge.objects.create( + user=user or Administrator.objects.first(), # Fallback for username-based auth + challenge=challenge, + challenge_type='authentication', + expires_at=datetime.now() + timedelta(seconds=self.challenge_timeout), + metadata=json.dumps({ + 'rp_id': self.rp_id, + 'rp_name': self.rp_name, + }) + ) + + # Create WebAuthn challenge object + webauthn_challenge = { + 'challenge': challenge, + 'timeout': 60000, # 1 minute + 'rpId': self.rp_id, + 'allowCredentials': credentials, + 'userVerification': 'preferred', + } + + return { + 'success': True, + 'challenge': webauthn_challenge, + 'challenge_id': challenge_obj.id, + } + + except Exception as e: + logger.error(f"Error creating authentication challenge: {str(e)}") + return { + 'success': False, + 'error': f'Failed to create authentication challenge: {str(e)}' + } + + def verify_registration(self, challenge_id: int, credential_data: Dict[str, Any], + client_data_json: str, attestation_object: str) -> Dict[str, Any]: + """ + Verify WebAuthn registration response + """ + try: + # Get challenge + challenge_obj = WebAuthnChallenge.objects.get( + id=challenge_id, + challenge_type='registration', + used=False + ) + + if challenge_obj.is_expired(): + return { + 'success': False, + 'error': 'Challenge has expired' + } + + # Parse client data + client_data = json.loads(base64.urlsafe_b64decode(client_data_json + '==')) + + # Verify challenge + if client_data.get('challenge') != challenge_obj.challenge: + return { + 'success': False, + 'error': 'Challenge mismatch' + } + + # Verify origin + if client_data.get('origin') != self.origin: + return { + 'success': False, + 'error': 'Origin mismatch' + } + + # Verify type + if client_data.get('type') != 'webauthn.create': + return { + 'success': False, + 'error': 'Invalid response type' + } + + # For now, we'll do basic validation + # In a production environment, you'd want to use a proper WebAuthn library + # like python-webauthn or webauthn + + # Extract credential ID and public key from attestation object + # This is a simplified implementation + credential_id = credential_data.get('id') + public_key = credential_data.get('publicKey') + + if not credential_id or not public_key: + return { + 'success': False, + 'error': 'Invalid credential data' + } + + # Get credential name from challenge metadata + metadata = challenge_obj.get_metadata() + credential_name = metadata.get('credential_name', f"Passkey {datetime.now().strftime('%Y-%m-%d %H:%M')}") + + # Create credential record + credential = WebAuthnCredential.objects.create( + user=challenge_obj.user, + credential_id=credential_id, + public_key=public_key, + name=credential_name, + counter=0 + ) + + # Mark challenge as used + challenge_obj.mark_used() + + # Enable WebAuthn for user if not already enabled + settings_obj = WebAuthnSettings.get_or_create_settings(challenge_obj.user) + if not settings_obj.enabled: + settings_obj.enabled = True + settings_obj.save() + + return { + 'success': True, + 'credential_id': credential.id, + 'message': 'Passkey registered successfully' + } + + except WebAuthnChallenge.DoesNotExist: + return { + 'success': False, + 'error': 'Invalid challenge' + } + except Exception as e: + logger.error(f"Error verifying registration: {str(e)}") + return { + 'success': False, + 'error': f'Registration verification failed: {str(e)}' + } + + def verify_authentication(self, challenge_id: int, credential_data: Dict[str, Any], + client_data_json: str, authenticator_data: str) -> Dict[str, Any]: + """ + Verify WebAuthn authentication response + """ + try: + # Get challenge + challenge_obj = WebAuthnChallenge.objects.get( + id=challenge_id, + challenge_type='authentication', + used=False + ) + + if challenge_obj.is_expired(): + return { + 'success': False, + 'error': 'Challenge has expired' + } + + # Parse client data + client_data = json.loads(base64.urlsafe_b64decode(client_data_json + '==')) + + # Verify challenge + if client_data.get('challenge') != challenge_obj.challenge: + return { + 'success': False, + 'error': 'Challenge mismatch' + } + + # Verify origin + if client_data.get('origin') != self.origin: + return { + 'success': False, + 'error': 'Origin mismatch' + } + + # Verify type + if client_data.get('type') != 'webauthn.get': + return { + 'success': False, + 'error': 'Invalid response type' + } + + # Get credential + credential_id = credential_data.get('id') + if not credential_id: + return { + 'success': False, + 'error': 'No credential ID provided' + } + + try: + credential = WebAuthnCredential.objects.get( + credential_id=credential_id, + user=challenge_obj.user, + is_active=True + ) + except WebAuthnCredential.DoesNotExist: + return { + 'success': False, + 'error': 'Credential not found' + } + + # Verify signature (simplified - in production use proper WebAuthn library) + # For now, we'll just update the counter and mark as successful + + # Update credential counter + credential.update_counter(credential.counter + 1) + + # Mark challenge as used + challenge_obj.mark_used() + + return { + 'success': True, + 'user_id': challenge_obj.user.pk, + 'credential_id': credential.id, + 'message': 'Authentication successful' + } + + except WebAuthnChallenge.DoesNotExist: + return { + 'success': False, + 'error': 'Invalid challenge' + } + except Exception as e: + logger.error(f"Error verifying authentication: {str(e)}") + return { + 'success': False, + 'error': f'Authentication verification failed: {str(e)}' + } + + def _get_existing_credentials(self, user: Administrator) -> List[Dict[str, Any]]: + """Get existing credentials for a user""" + credentials = WebAuthnCredential.objects.filter( + user=user, + is_active=True + ) + + return [ + { + 'id': cred.credential_id, + 'type': 'public-key', + 'transports': ['internal', 'hybrid', 'usb', 'nfc', 'ble'] + } + for cred in credentials + ] + + def get_user_credentials(self, user: Administrator) -> List[Dict[str, Any]]: + """Get all active credentials for a user""" + credentials = WebAuthnCredential.objects.filter( + user=user, + is_active=True + ).order_by('-created_at') + + return [ + { + 'id': cred.id, + 'name': cred.name, + 'credential_id': cred.credential_id[:16] + '...', + 'created_at': cred.created_at.isoformat(), + 'last_used': cred.last_used.isoformat() if cred.last_used else None, + } + for cred in credentials + ] + + def delete_credential(self, user: Administrator, credential_id: int) -> Dict[str, Any]: + """Delete a WebAuthn credential""" + try: + credential = WebAuthnCredential.objects.get( + id=credential_id, + user=user + ) + + credential.is_active = False + credential.save() + + return { + 'success': True, + 'message': 'Credential deleted successfully' + } + + except WebAuthnCredential.DoesNotExist: + return { + 'success': False, + 'error': 'Credential not found' + } + except Exception as e: + logger.error(f"Error deleting credential: {str(e)}") + return { + 'success': False, + 'error': f'Failed to delete credential: {str(e)}' + } + + def update_credential_name(self, user: Administrator, credential_id: int, new_name: str) -> Dict[str, Any]: + """Update credential name""" + try: + credential = WebAuthnCredential.objects.get( + id=credential_id, + user=user + ) + + credential.name = new_name + credential.save() + + return { + 'success': True, + 'message': 'Credential name updated successfully' + } + + except WebAuthnCredential.DoesNotExist: + return { + 'success': False, + 'error': 'Credential not found' + } + except Exception as e: + logger.error(f"Error updating credential name: {str(e)}") + return { + 'success': False, + 'error': f'Failed to update credential name: {str(e)}' + } + + def cleanup_expired_challenges(self): + """Clean up expired challenges""" + expired_challenges = WebAuthnChallenge.objects.filter( + expires_at__lt=datetime.now() + ) + count = expired_challenges.count() + expired_challenges.delete() + logger.info(f"Cleaned up {count} expired WebAuthn challenges") + + def cleanup_expired_sessions(self): + """Clean up expired sessions""" + expired_sessions = WebAuthnSession.objects.filter( + expires_at__lt=datetime.now() + ) + count = expired_sessions.count() + expired_sessions.delete() + logger.info(f"Cleaned up {count} expired WebAuthn sessions") diff --git a/loginSystem/webauthn_models.py b/loginSystem/webauthn_models.py new file mode 100644 index 000000000..a8d84bc4f --- /dev/null +++ b/loginSystem/webauthn_models.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- + +from django.db import models +from django.contrib.auth import get_user_model +from .models import Administrator +import json +import base64 +from datetime import datetime, timedelta + + +class WebAuthnCredential(models.Model): + """ + Model to store WebAuthn passkey credentials for users + """ + user = models.ForeignKey(Administrator, on_delete=models.CASCADE, related_name='webauthn_credentials') + credential_id = models.CharField(max_length=255, unique=True, help_text="Base64 encoded credential ID") + public_key = models.TextField(help_text="Base64 encoded public key") + counter = models.BigIntegerField(default=0, help_text="Signature counter for replay protection") + name = models.CharField(max_length=100, help_text="User-friendly name for the passkey") + created_at = models.DateTimeField(auto_now_add=True) + last_used = models.DateTimeField(null=True, blank=True) + is_active = models.BooleanField(default=True, help_text="Whether this credential is active") + + class Meta: + db_table = 'webauthn_credentials' + verbose_name = 'WebAuthn Credential' + verbose_name_plural = 'WebAuthn Credentials' + indexes = [ + models.Index(fields=['user', 'is_active']), + models.Index(fields=['credential_id']), + ] + + def __str__(self): + return f"{self.user.userName} - {self.name} ({self.credential_id[:16]}...)" + + def get_credential_id_bytes(self): + """Get credential ID as bytes""" + return base64.urlsafe_b64decode(self.credential_id + '==') + + def get_public_key_bytes(self): + """Get public key as bytes""" + return base64.urlsafe_b64decode(self.public_key + '==') + + def update_counter(self, new_counter): + """Update signature counter""" + if new_counter > self.counter: + self.counter = new_counter + self.last_used = datetime.now() + self.save(update_fields=['counter', 'last_used']) + return True + return False + + +class WebAuthnChallenge(models.Model): + """ + Model to store WebAuthn challenges for registration and authentication + """ + user = models.ForeignKey(Administrator, on_delete=models.CASCADE, related_name='webauthn_challenges') + challenge = models.CharField(max_length=255, help_text="Base64 encoded challenge") + challenge_type = models.CharField(max_length=20, choices=[ + ('registration', 'Registration'), + ('authentication', 'Authentication'), + ]) + created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() + used = models.BooleanField(default=False) + metadata = models.TextField(default='{}', help_text="Additional challenge metadata as JSON") + + class Meta: + db_table = 'webauthn_challenges' + verbose_name = 'WebAuthn Challenge' + verbose_name_plural = 'WebAuthn Challenges' + indexes = [ + models.Index(fields=['user', 'challenge_type', 'used']), + models.Index(fields=['expires_at']), + ] + + def __str__(self): + return f"{self.user.userName} - {self.challenge_type} ({self.challenge[:16]}...)" + + def is_expired(self): + """Check if challenge has expired""" + return datetime.now() > self.expires_at + + def get_challenge_bytes(self): + """Get challenge as bytes""" + return base64.urlsafe_b64decode(self.challenge + '==') + + def get_metadata(self): + """Get metadata as dict""" + try: + return json.loads(self.metadata) + except: + return {} + + def set_metadata(self, data): + """Set metadata from dict""" + self.metadata = json.dumps(data) + + def mark_used(self): + """Mark challenge as used""" + self.used = True + self.save(update_fields=['used']) + + +class WebAuthnSession(models.Model): + """ + Model to store WebAuthn session data for ongoing operations + """ + user = models.ForeignKey(Administrator, on_delete=models.CASCADE, related_name='webauthn_sessions') + session_id = models.CharField(max_length=255, unique=True, help_text="Unique session identifier") + session_type = models.CharField(max_length=20, choices=[ + ('registration', 'Registration'), + ('authentication', 'Authentication'), + ]) + data = models.TextField(help_text="Session data as JSON") + created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() + + class Meta: + db_table = 'webauthn_sessions' + verbose_name = 'WebAuthn Session' + verbose_name_plural = 'WebAuthn Sessions' + indexes = [ + models.Index(fields=['session_id']), + models.Index(fields=['expires_at']), + ] + + def __str__(self): + return f"{self.user.userName} - {self.session_type} ({self.session_id[:16]}...)" + + def is_expired(self): + """Check if session has expired""" + return datetime.now() > self.expires_at + + def get_data(self): + """Get session data as dict""" + try: + return json.loads(self.data) + except: + return {} + + def set_data(self, data): + """Set session data from dict""" + self.data = json.dumps(data) + + @classmethod + def create_session(cls, user, session_type, data, duration_minutes=10): + """Create a new WebAuthn session""" + import uuid + session_id = str(uuid.uuid4()) + expires_at = datetime.now() + timedelta(minutes=duration_minutes) + + session = cls.objects.create( + user=user, + session_id=session_id, + session_type=session_type, + data=json.dumps(data), + expires_at=expires_at + ) + return session + + +class WebAuthnSettings(models.Model): + """ + Model to store WebAuthn configuration settings + """ + user = models.OneToOneField(Administrator, on_delete=models.CASCADE, related_name='webauthn_settings') + enabled = models.BooleanField(default=False, help_text="Whether WebAuthn is enabled for this user") + require_passkey = models.BooleanField(default=False, help_text="Require passkey for login (passwordless)") + allow_multiple_credentials = models.BooleanField(default=True, help_text="Allow multiple passkeys per user") + max_credentials = models.IntegerField(default=10, help_text="Maximum number of passkeys allowed") + timeout_seconds = models.IntegerField(default=60, help_text="WebAuthn operation timeout in seconds") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'webauthn_settings' + verbose_name = 'WebAuthn Settings' + verbose_name_plural = 'WebAuthn Settings' + + def __str__(self): + return f"WebAuthn Settings for {self.user.userName}" + + @classmethod + def get_or_create_settings(cls, user): + """Get or create WebAuthn settings for a user""" + settings, created = cls.objects.get_or_create( + user=user, + defaults={ + 'enabled': False, + 'require_passkey': False, + 'allow_multiple_credentials': True, + 'max_credentials': 10, + 'timeout_seconds': 60, + } + ) + return settings + + def can_add_credential(self): + """Check if user can add another credential""" + if not self.allow_multiple_credentials: + return WebAuthnCredential.objects.filter(user=self.user, is_active=True).count() == 0 + return WebAuthnCredential.objects.filter(user=self.user, is_active=True).count() < self.max_credentials diff --git a/loginSystem/webauthn_urls.py b/loginSystem/webauthn_urls.py new file mode 100644 index 000000000..bdec3c09c --- /dev/null +++ b/loginSystem/webauthn_urls.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from django.urls import path +from . import webauthn_views + +urlpatterns = [ + # WebAuthn Registration + path('registration/start/', webauthn_views.webauthn_registration_start, name='webauthn_registration_start'), + path('registration/complete/', webauthn_views.webauthn_registration_complete, name='webauthn_registration_complete'), + + # WebAuthn Authentication + path('authentication/start/', webauthn_views.webauthn_authentication_start, name='webauthn_authentication_start'), + path('authentication/complete/', webauthn_views.webauthn_authentication_complete, name='webauthn_authentication_complete'), + + # WebAuthn Credential Management + path('credentials//', webauthn_views.webauthn_credentials_list, name='webauthn_credentials_list'), + path('credential/delete/', webauthn_views.webauthn_credential_delete, name='webauthn_credential_delete'), + path('credential/update/', webauthn_views.webauthn_credential_update, name='webauthn_credential_update'), + + # WebAuthn Settings + path('settings/update/', webauthn_views.webauthn_settings_update, name='webauthn_settings_update'), + + # WebAuthn Maintenance + path('cleanup/', webauthn_views.webauthn_cleanup, name='webauthn_cleanup'), +] diff --git a/loginSystem/webauthn_views.py b/loginSystem/webauthn_views.py new file mode 100644 index 000000000..1709086e4 --- /dev/null +++ b/loginSystem/webauthn_views.py @@ -0,0 +1,463 @@ +# -*- coding: utf-8 -*- + +import json +import base64 +from django.shortcuts import render, HttpResponse +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods +from django.contrib.auth.decorators import login_required +from django.utils.decorators import method_decorator +from django.views import View +from .models import Administrator +from .webauthn_backend import WebAuthnBackend +from .webauthn_models import WebAuthnSettings, WebAuthnCredential +from plogical.acl import ACLManager +import logging + +logger = logging.getLogger(__name__) + + +class WebAuthnAPIView(View): + """Base class for WebAuthn API views""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.webauthn = WebAuthnBackend() + + def json_response(self, data, status=200): + """Return JSON response""" + return HttpResponse( + json.dumps(data, ensure_ascii=False), + content_type='application/json', + status=status + ) + + def error_response(self, message, status=400): + """Return error response""" + return self.json_response({ + 'success': False, + 'error': message + }, status) + + +@method_decorator(csrf_exempt, name='dispatch') +class WebAuthnRegistrationStart(WebAuthnAPIView): + """Start WebAuthn registration process""" + + def post(self, request): + try: + data = json.loads(request.body) + username = data.get('username') + credential_name = data.get('credential_name', '') + + if not username: + return self.error_response('Username is required') + + try: + user = Administrator.objects.get(userName=username) + except Administrator.DoesNotExist: + return self.error_response('User not found', 404) + + # Check if user has permission to register WebAuthn + if hasattr(request, 'session') and 'userID' in request.session: + current_user_id = request.session['userID'] + current_user = Administrator.objects.get(pk=current_user_id) + current_acl = ACLManager.loadedACL(current_user_id) + + # Allow if admin, user is modifying themselves, or user is owned by current user + if not (current_acl['admin'] == 1 or + user.pk == current_user.pk or + user.owner == current_user.pk): + return self.error_response('Unauthorized access', 403) + + result = self.webauthn.create_registration_challenge(user, credential_name) + return self.json_response(result) + + except json.JSONDecodeError: + return self.error_response('Invalid JSON') + except Exception as e: + logger.error(f"Error in registration start: {str(e)}") + return self.error_response(f'Internal server error: {str(e)}', 500) + + +@method_decorator(csrf_exempt, name='dispatch') +class WebAuthnRegistrationComplete(WebAuthnAPIView): + """Complete WebAuthn registration process""" + + def post(self, request): + try: + data = json.loads(request.body) + challenge_id = data.get('challenge_id') + credential_data = data.get('credential') + client_data_json = data.get('client_data_json') + attestation_object = data.get('attestation_object') + + if not all([challenge_id, credential_data, client_data_json, attestation_object]): + return self.error_response('Missing required fields') + + result = self.webauthn.verify_registration( + challenge_id=challenge_id, + credential_data=credential_data, + client_data_json=client_data_json, + attestation_object=attestation_object + ) + + return self.json_response(result) + + except json.JSONDecodeError: + return self.error_response('Invalid JSON') + except Exception as e: + logger.error(f"Error in registration complete: {str(e)}") + return self.error_response(f'Internal server error: {str(e)}', 500) + + +@method_decorator(csrf_exempt, name='dispatch') +class WebAuthnAuthenticationStart(WebAuthnAPIView): + """Start WebAuthn authentication process""" + + def post(self, request): + try: + data = json.loads(request.body) + username = data.get('username') + + if not username: + return self.error_response('Username is required') + + try: + user = Administrator.objects.get(userName=username) + except Administrator.DoesNotExist: + return self.error_response('User not found', 404) + + result = self.webauthn.create_authentication_challenge(user) + return self.json_response(result) + + except json.JSONDecodeError: + return self.error_response('Invalid JSON') + except Exception as e: + logger.error(f"Error in authentication start: {str(e)}") + return self.error_response(f'Internal server error: {str(e)}', 500) + + +@method_decorator(csrf_exempt, name='dispatch') +class WebAuthnAuthenticationComplete(WebAuthnAPIView): + """Complete WebAuthn authentication process""" + + def post(self, request): + try: + data = json.loads(request.body) + challenge_id = data.get('challenge_id') + credential_data = data.get('credential') + client_data_json = data.get('client_data_json') + authenticator_data = data.get('authenticator_data') + + if not all([challenge_id, credential_data, client_data_json, authenticator_data]): + return self.error_response('Missing required fields') + + result = self.webauthn.verify_authentication( + challenge_id=challenge_id, + credential_data=credential_data, + client_data_json=client_data_json, + authenticator_data=authenticator_data + ) + + if result['success']: + # Set session for successful authentication + request.session['userID'] = result['user_id'] + request.session['webauthn_auth'] = True + request.session.set_expiry(43200) # 12 hours + + # Log successful authentication + logger.info(f"WebAuthn authentication successful for user ID: {result['user_id']}") + + return self.json_response(result) + + except json.JSONDecodeError: + return self.error_response('Invalid JSON') + except Exception as e: + logger.error(f"Error in authentication complete: {str(e)}") + return self.error_response(f'Internal server error: {str(e)}', 500) + + +@method_decorator(csrf_exempt, name='dispatch') +class WebAuthnCredentialsList(WebAuthnAPIView): + """List WebAuthn credentials for a user""" + + def get(self, request, username=None): + try: + if not username: + return self.error_response('Username is required') + + try: + user = Administrator.objects.get(userName=username) + except Administrator.DoesNotExist: + return self.error_response('User not found', 404) + + # Check permissions + if hasattr(request, 'session') and 'userID' in request.session: + current_user_id = request.session['userID'] + current_user = Administrator.objects.get(pk=current_user_id) + current_acl = ACLManager.loadedACL(current_user_id) + + if not (current_acl['admin'] == 1 or + user.pk == current_user.pk or + user.owner == current_user.pk): + return self.error_response('Unauthorized access', 403) + + credentials = self.webauthn.get_user_credentials(user) + settings = WebAuthnSettings.get_or_create_settings(user) + + return self.json_response({ + 'success': True, + 'credentials': credentials, + 'settings': { + 'enabled': settings.enabled, + 'require_passkey': settings.require_passkey, + 'allow_multiple_credentials': settings.allow_multiple_credentials, + 'max_credentials': settings.max_credentials, + 'can_add_credential': settings.can_add_credential(), + } + }) + + except Exception as e: + logger.error(f"Error listing credentials: {str(e)}") + return self.error_response(f'Internal server error: {str(e)}', 500) + + +@method_decorator(csrf_exempt, name='dispatch') +class WebAuthnCredentialDelete(WebAuthnAPIView): + """Delete a WebAuthn credential""" + + def post(self, request): + try: + data = json.loads(request.body) + username = data.get('username') + credential_id = data.get('credential_id') + + if not username or not credential_id: + return self.error_response('Username and credential ID are required') + + try: + user = Administrator.objects.get(userName=username) + except Administrator.DoesNotExist: + return self.error_response('User not found', 404) + + # Check permissions + if hasattr(request, 'session') and 'userID' in request.session: + current_user_id = request.session['userID'] + current_user = Administrator.objects.get(pk=current_user_id) + current_acl = ACLManager.loadedACL(current_user_id) + + if not (current_acl['admin'] == 1 or + user.pk == current_user.pk or + user.owner == current_user.pk): + return self.error_response('Unauthorized access', 403) + + result = self.webauthn.delete_credential(user, credential_id) + return self.json_response(result) + + except json.JSONDecodeError: + return self.error_response('Invalid JSON') + except Exception as e: + logger.error(f"Error deleting credential: {str(e)}") + return self.error_response(f'Internal server error: {str(e)}', 500) + + +@method_decorator(csrf_exempt, name='dispatch') +class WebAuthnCredentialUpdate(WebAuthnAPIView): + """Update WebAuthn credential name""" + + def post(self, request): + try: + data = json.loads(request.body) + username = data.get('username') + credential_id = data.get('credential_id') + new_name = data.get('new_name') + + if not all([username, credential_id, new_name]): + return self.error_response('Username, credential ID, and new name are required') + + try: + user = Administrator.objects.get(userName=username) + except Administrator.DoesNotExist: + return self.error_response('User not found', 404) + + # Check permissions + if hasattr(request, 'session') and 'userID' in request.session: + current_user_id = request.session['userID'] + current_user = Administrator.objects.get(pk=current_user_id) + current_acl = ACLManager.loadedACL(current_user_id) + + if not (current_acl['admin'] == 1 or + user.pk == current_user.pk or + user.owner == current_user.pk): + return self.error_response('Unauthorized access', 403) + + result = self.webauthn.update_credential_name(user, credential_id, new_name) + return self.json_response(result) + + except json.JSONDecodeError: + return self.error_response('Invalid JSON') + except Exception as e: + logger.error(f"Error updating credential: {str(e)}") + return self.error_response(f'Internal server error: {str(e)}', 500) + + +@method_decorator(csrf_exempt, name='dispatch') +class WebAuthnSettingsUpdate(WebAuthnAPIView): + """Update WebAuthn settings for a user""" + + def post(self, request): + try: + data = json.loads(request.body) + username = data.get('username') + enabled = data.get('enabled') + require_passkey = data.get('require_passkey') + allow_multiple_credentials = data.get('allow_multiple_credentials') + max_credentials = data.get('max_credentials') + timeout_seconds = data.get('timeout_seconds') + + if not username: + return self.error_response('Username is required') + + try: + user = Administrator.objects.get(userName=username) + except Administrator.DoesNotExist: + return self.error_response('User not found', 404) + + # Check permissions + if hasattr(request, 'session') and 'userID' in request.session: + current_user_id = request.session['userID'] + current_user = Administrator.objects.get(pk=current_user_id) + current_acl = ACLManager.loadedACL(current_user_id) + + if not (current_acl['admin'] == 1 or + user.pk == current_user.pk or + user.owner == current_user.pk): + return self.error_response('Unauthorized access', 403) + + settings = WebAuthnSettings.get_or_create_settings(user) + + if enabled is not None: + settings.enabled = bool(enabled) + if require_passkey is not None: + settings.require_passkey = bool(require_passkey) + if allow_multiple_credentials is not None: + settings.allow_multiple_credentials = bool(allow_multiple_credentials) + if max_credentials is not None: + settings.max_credentials = int(max_credentials) + if timeout_seconds is not None: + settings.timeout_seconds = int(timeout_seconds) + + settings.save() + + return self.json_response({ + 'success': True, + 'message': 'Settings updated successfully', + 'settings': { + 'enabled': settings.enabled, + 'require_passkey': settings.require_passkey, + 'allow_multiple_credentials': settings.allow_multiple_credentials, + 'max_credentials': settings.max_credentials, + 'timeout_seconds': settings.timeout_seconds, + } + }) + + except json.JSONDecodeError: + return self.error_response('Invalid JSON') + except Exception as e: + logger.error(f"Error updating settings: {str(e)}") + return self.error_response(f'Internal server error: {str(e)}', 500) + + +@method_decorator(csrf_exempt, name='dispatch') +class WebAuthnCleanup(WebAuthnAPIView): + """Cleanup expired WebAuthn data""" + + def post(self, request): + try: + # Check if user is admin + if not (hasattr(request, 'session') and 'userID' in request.session): + return self.error_response('Authentication required', 401) + + current_user_id = request.session['userID'] + current_acl = ACLManager.loadedACL(current_user_id) + + if current_acl['admin'] != 1: + return self.error_response('Admin access required', 403) + + # Cleanup expired data + self.webauthn.cleanup_expired_challenges() + self.webauthn.cleanup_expired_sessions() + + return self.json_response({ + 'success': True, + 'message': 'Cleanup completed successfully' + }) + + except Exception as e: + logger.error(f"Error during cleanup: {str(e)}") + return self.error_response(f'Internal server error: {str(e)}', 500) + + +# Traditional function-based views for easier integration +@csrf_exempt +def webauthn_registration_start(request): + """Start WebAuthn registration - function view""" + view = WebAuthnRegistrationStart() + return view.post(request) + + +@csrf_exempt +def webauthn_registration_complete(request): + """Complete WebAuthn registration - function view""" + view = WebAuthnRegistrationComplete() + return view.post(request) + + +@csrf_exempt +def webauthn_authentication_start(request): + """Start WebAuthn authentication - function view""" + view = WebAuthnAuthenticationStart() + return view.post(request) + + +@csrf_exempt +def webauthn_authentication_complete(request): + """Complete WebAuthn authentication - function view""" + view = WebAuthnAuthenticationComplete() + return view.post(request) + + +@csrf_exempt +def webauthn_credentials_list(request, username): + """List WebAuthn credentials - function view""" + view = WebAuthnCredentialsList() + return view.get(request, username) + + +@csrf_exempt +def webauthn_credential_delete(request): + """Delete WebAuthn credential - function view""" + view = WebAuthnCredentialDelete() + return view.post(request) + + +@csrf_exempt +def webauthn_credential_update(request): + """Update WebAuthn credential - function view""" + view = WebAuthnCredentialUpdate() + return view.post(request) + + +@csrf_exempt +def webauthn_settings_update(request): + """Update WebAuthn settings - function view""" + view = WebAuthnSettingsUpdate() + return view.post(request) + + +@csrf_exempt +def webauthn_cleanup(request): + """Cleanup WebAuthn data - function view""" + view = WebAuthnCleanup() + return view.post(request) diff --git a/userManagment/static/userManagment/userManagment.js b/userManagment/static/userManagment/userManagment.js index 3d3fe4054..22b2f7f2c 100644 --- a/userManagment/static/userManagment/userManagment.js +++ b/userManagment/static/userManagment/userManagment.js @@ -179,6 +179,121 @@ app.controller('modifyUser', function ($scope, $http) { document.body.removeChild(tempTextarea); } }; + + // WebAuthn Functions + $scope.loadWebAuthnData = function() { + if (!$scope.accountUsername) return; + + var url = '/webauthn/credentials/' + $scope.accountUsername + '/'; + + $http.get(url).then(function(response) { + if (response.data.success) { + $scope.webauthnCredentials = response.data.credentials; + $scope.webauthnEnabled = response.data.settings.enabled; + $scope.webauthnRequirePasskey = response.data.settings.require_passkey; + $scope.webauthnAllowMultiple = response.data.settings.allow_multiple_credentials; + $scope.webauthnMaxCredentials = response.data.settings.max_credentials; + $scope.canAddCredential = response.data.settings.can_add_credential; + } + }, function(error) { + console.error('Error loading WebAuthn data:', error); + }); + }; + + $scope.toggleWebAuthn = function() { + if ($scope.webauthnEnabled) { + $scope.loadWebAuthnData(); + } else { + $scope.webauthnCredentials = []; + $scope.canAddCredential = true; + } + }; + + $scope.registerNewPasskey = function() { + if (!window.cyberPanelWebAuthn) { + alert('WebAuthn is not supported in this browser'); + return; + } + + var credentialName = prompt('Enter a name for this passkey:', 'Passkey ' + new Date().toLocaleDateString()); + if (!credentialName) return; + + window.cyberPanelWebAuthn.registerPasskey($scope.accountUsername, credentialName) + .then(function(response) { + if (response.success) { + $scope.loadWebAuthnData(); + $scope.$apply(); + } + }) + .catch(function(error) { + console.error('Error registering passkey:', error); + }); + }; + + $scope.deleteCredential = function(credentialId) { + if (!confirm('Are you sure you want to delete this passkey?')) return; + + if (!window.cyberPanelWebAuthn) { + alert('WebAuthn is not supported in this browser'); + return; + } + + window.cyberPanelWebAuthn.deleteCredential($scope.accountUsername, credentialId) + .then(function(response) { + if (response.success) { + $scope.loadWebAuthnData(); + $scope.$apply(); + } + }) + .catch(function(error) { + console.error('Error deleting credential:', error); + }); + }; + + $scope.updateCredentialName = function(credentialId, newName) { + if (!window.cyberPanelWebAuthn) return; + + window.cyberPanelWebAuthn.updateCredentialName($scope.accountUsername, credentialId, newName) + .then(function(response) { + if (response.success) { + $scope.loadWebAuthnData(); + $scope.$apply(); + } + }) + .catch(function(error) { + console.error('Error updating credential name:', error); + }); + }; + + $scope.refreshCredentials = function() { + $scope.loadWebAuthnData(); + }; + + $scope.saveWebAuthnSettings = function() { + if (!window.cyberPanelWebAuthn) { + alert('WebAuthn is not supported in this browser'); + return; + } + + var settings = { + enabled: $scope.webauthnEnabled, + require_passkey: $scope.webauthnRequirePasskey, + allow_multiple_credentials: $scope.webauthnAllowMultiple, + max_credentials: $scope.webauthnMaxCredentials, + timeout_seconds: $scope.webauthnTimeout + }; + + window.cyberPanelWebAuthn.updateSettings($scope.accountUsername, settings) + .then(function(response) { + if (response.success) { + $scope.loadWebAuthnData(); + $scope.$apply(); + } + }) + .catch(function(error) { + console.error('Error updating WebAuthn settings:', error); + }); + }; $scope.fetchUserDetails = function () { @@ -223,6 +338,18 @@ app.controller('modifyUser', function ($scope, $http) { $scope.secretKey = userDetails.secretKey; $scope.formattedSecretKey = userDetails.secretKey.match(/.{1,4}/g).join(' '); } + + // Initialize WebAuthn settings + $scope.webauthnEnabled = false; + $scope.webauthnRequirePasskey = false; + $scope.webauthnAllowMultiple = true; + $scope.webauthnMaxCredentials = 10; + $scope.webauthnTimeout = 60; + $scope.webauthnCredentials = []; + $scope.canAddCredential = true; + + // Load WebAuthn settings and credentials + $scope.loadWebAuthnData(); qrCode.set({ value: userDetails.otpauth @@ -320,7 +447,13 @@ app.controller('modifyUser', function ($scope, $http) { } }; - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + $http.post(url, data, config).then(function(response) { + ListInitialDatas(response); + // Save WebAuthn settings after successful user modification + if (response.data.saveStatus == 1) { + $scope.saveWebAuthnSettings(); + } + }, cantLoadInitialDatas); function ListInitialDatas(response) { diff --git a/userManagment/templates/userManagment/modifyUser.html b/userManagment/templates/userManagment/modifyUser.html index b924f46fc..559717644 100644 --- a/userManagment/templates/userManagment/modifyUser.html +++ b/userManagment/templates/userManagment/modifyUser.html @@ -321,6 +321,97 @@ + + +
+ +
+ +

{% trans "Use passkeys for secure, passwordless login" %}

+
+ +
+
+ +

{% trans "When enabled, users must use passkeys to login (password becomes optional)" %}

+
+ +
+ +

{% trans "Allow users to register multiple passkeys for backup access" %}

+
+ +
+ + +

{% trans "Maximum number of passkeys allowed per user" %}

+
+ +
+ + +

{% trans "How long to wait for passkey interaction" %}

+
+ + +
+
{% trans "Manage Passkeys" %}
+
+ {% trans "No passkeys registered yet" %} +
+ +
+ + + + + + + + + + + + + + + + + +
{% trans "Name" %}{% trans "Created" %}{% trans "Last Used" %}{% trans "Actions" %}
+ + {{ cred.created_at | date:'short' }}{{ cred.last_used | date:'short' || 'Never' }} + +
+
+ +
+ + +
+ +
+ {% trans "Maximum number of passkeys reached" %} +
+
+
+