From dc797034630ebb300df3001d00bdfd86431134e4 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sat, 7 Mar 2026 02:46:15 +0100 Subject: [PATCH] 2FA/WebAuthn, user management, deploy and fix scripts - loginSystem: WebAuthn (webauthn backend, models, urls, views), login template and webauthn.js - baseTemplate: index.html updates - docs: 2FA_AUTHENTICATION_GUIDE.md - userManagment: createUser/modifyUser templates, userManagment.js, views, tests; check_modify_users_page.py - requirments.txt: add webauthn>=2.0.0 - deploy-templates.sh: deploy templates/static to live CyberCP - fix-cyberpanel-500.sh: script for common HTTP 500 login fixes (MariaDB, configservercsf, cache, restart) --- .../templates/baseTemplate/index.html | 1 + deploy-templates.sh | 52 ++ docs/2FA_AUTHENTICATION_GUIDE.md | 2 + fix-cyberpanel-500.sh | 45 ++ loginSystem/static/loginSystem/webauthn.js | 156 ++-- loginSystem/templates/loginSystem/login.html | 24 +- loginSystem/views.py | 30 +- loginSystem/webauthn_backend.py | 689 ++++++++---------- loginSystem/webauthn_models.py | 6 +- loginSystem/webauthn_urls.py | 1 + loginSystem/webauthn_views.py | 99 ++- requirments.txt | 1 + userManagment/check_modify_users_page.py | 57 ++ .../static/userManagment/userManagment.js | 105 ++- .../templates/userManagment/createUser.html | 5 + .../templates/userManagment/modifyUser.html | 183 ++++- userManagment/tests.py | 13 + userManagment/views.py | 3 +- 18 files changed, 940 insertions(+), 532 deletions(-) create mode 100755 deploy-templates.sh create mode 100644 fix-cyberpanel-500.sh create mode 100644 userManagment/check_modify_users_page.py diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index b075692b2..a3a2f71b4 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -2221,6 +2221,7 @@ + diff --git a/deploy-templates.sh b/deploy-templates.sh new file mode 100755 index 000000000..544b8d995 --- /dev/null +++ b/deploy-templates.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Deploy updated templates (and related static) from this repo to live CyberPanel. +# Use after pulling template changes so the panel at /usr/local/CyberCP shows the new UI. +# Usage: sudo bash deploy-templates.sh + +set -e + +CYBERCP_ROOT="${CYBERCP_ROOT:-/usr/local/CyberCP}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "[$(date +%Y-%m-%d\ %H:%M:%S)] Deploying templates to $CYBERCP_ROOT..." + +# userManagment templates +for name in modifyUser createUser; do + SRC="$SCRIPT_DIR/userManagment/templates/userManagment/${name}.html" + DST="$CYBERCP_ROOT/userManagment/templates/userManagment/${name}.html" + if [ -f "$SRC" ]; then + cp -f "$SRC" "$DST" + echo " Copied userManagment ${name}.html" + fi +done + +# loginSystem login template +if [ -f "$SCRIPT_DIR/loginSystem/templates/loginSystem/login.html" ]; then + cp -f "$SCRIPT_DIR/loginSystem/templates/loginSystem/login.html" \ + "$CYBERCP_ROOT/loginSystem/templates/loginSystem/login.html" + echo " Copied loginSystem login.html" +fi + +# Optional: userManagment static (if you change JS) +if [ -f "$SCRIPT_DIR/userManagment/static/userManagment/userManagment.js" ]; then + mkdir -p "$CYBERCP_ROOT/userManagment/static/userManagment" + cp -f "$SCRIPT_DIR/userManagment/static/userManagment/userManagment.js" \ + "$CYBERCP_ROOT/userManagment/static/userManagment/userManagment.js" + echo " Copied userManagment.js" +fi +if [ -f "$SCRIPT_DIR/loginSystem/static/loginSystem/webauthn.js" ]; then + mkdir -p "$CYBERCP_ROOT/loginSystem/static/loginSystem" + cp -f "$SCRIPT_DIR/loginSystem/static/loginSystem/webauthn.js" \ + "$CYBERCP_ROOT/loginSystem/static/loginSystem/webauthn.js" + echo " Copied webauthn.js" +fi + +# Run collectstatic if manage.py exists (so static changes are served) +if [ -f "$CYBERCP_ROOT/manage.py" ]; then + PYTHON="${CYBERCP_ROOT}/bin/python" + [ -x "$PYTHON" ] || PYTHON="python3" + echo " Running collectstatic..." + (cd "$CYBERCP_ROOT" && "$PYTHON" manage.py collectstatic --noinput --clear 2>&1) | tail -5 +fi + +echo "[$(date +%Y-%m-%d\ %H:%M:%S)] Templates deployed. Hard-refresh (Ctrl+F5) on Modify User / Create User / Login if needed." diff --git a/docs/2FA_AUTHENTICATION_GUIDE.md b/docs/2FA_AUTHENTICATION_GUIDE.md index f240c1465..cdf71aaac 100644 --- a/docs/2FA_AUTHENTICATION_GUIDE.md +++ b/docs/2FA_AUTHENTICATION_GUIDE.md @@ -97,6 +97,8 @@ TOTP generates time-based codes that change every 30 seconds. Users scan a QR co ### 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. +**Login behaviour**: The login page supports **passkey-first** sign-in: users can click "Login with Passkey" without entering a username. Passkeys are managed under **User Management → Modify User**. The relying party ID (`rp_id`) and origin are derived from the current request host only (never hardcoded), so WebAuthn works on any domain or IP (e.g. `https://207.180.193.210:2087`). + ### Setting Up WebAuthn #### Prerequisites diff --git a/fix-cyberpanel-500.sh b/fix-cyberpanel-500.sh new file mode 100644 index 000000000..2e9a672d1 --- /dev/null +++ b/fix-cyberpanel-500.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# fix-cyberpanel-500.sh – Apply common fixes for CyberPanel HTTP 500 on login. +# Run on the server: sudo bash fix-cyberpanel-500.sh +# See: to-do/CYBERPANEL-HTTP-500-LOGIN-FIX.md + +set -e +LOG="/var/log/cyberpanel_500_fix.log" +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; } + +if [[ $EUID -ne 0 ]]; then + echo "Run as root: sudo bash $0" + exit 1 +fi + +log "=== CyberPanel 500 fix script started ===" + +# 0. Ensure MariaDB is running (common cause: DB down -> 500) +log "Step 0: Ensuring MariaDB is running..." +systemctl start mariadb 2>/dev/null || systemctl start mysql 2>/dev/null || true +systemctl enable mariadb 2>/dev/null || systemctl enable mysql 2>/dev/null || true +log "Step 0 done." + +# 1. Remove or neutralize configservercsf (common cause of 500) +log "Step 1: Cleaning configservercsf references..." +rm -rf /usr/local/CyberCP/configservercsf 2>/dev/null || true +rm -f /home/cyberpanel/plugins/configservercsf 2>/dev/null || true +rm -rf /usr/local/CyberCP/public/static/configservercsf 2>/dev/null || true +sed -i '/configservercsf/d' /usr/local/CyberCP/CyberCP/settings.py 2>/dev/null || true +sed -i '/configservercsf/d' /usr/local/CyberCP/CyberCP/urls.py 2>/dev/null || true +log "Step 1 done." + +# 2. Clear Python cache +log "Step 2: Clearing __pycache__..." +find /usr/local/CyberCP -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true +log "Step 2 done." + +# 3. Restart panel and web server +log "Step 3: Restarting lscpd and lsws..." +systemctl restart lscpd +systemctl restart lsws +killall lsphp 2>/dev/null || true +log "Step 3 done." + +log "=== Fix script finished. Try https://YOUR_IP:2087 or :8090 ===" +log "If 500 persists, enable DEBUG in /usr/local/CyberCP/CyberCP/settings.py and check logs (see to-do/CYBERPANEL-HTTP-500-LOGIN-FIX.md)." diff --git a/loginSystem/static/loginSystem/webauthn.js b/loginSystem/static/loginSystem/webauthn.js index a9d4eddc5..9cc4ea5a4 100644 --- a/loginSystem/static/loginSystem/webauthn.js +++ b/loginSystem/static/loginSystem/webauthn.js @@ -10,6 +10,7 @@ class CyberPanelWebAuthn { this.apiEndpoints = { registrationStart: '/webauthn/registration/start/', registrationComplete: '/webauthn/registration/complete/', + authenticationOptions: '/webauthn/authentication/options/', authenticationStart: '/webauthn/authentication/start/', authenticationComplete: '/webauthn/authentication/complete/', credentialsList: '/webauthn/credentials/', @@ -60,18 +61,10 @@ class CyberPanelWebAuthn { 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); + const existingBtn = document.getElementById('webauthn-login-btn'); + if (existingBtn && !existingBtn.dataset.bound) { + existingBtn.dataset.bound = '1'; + existingBtn.onclick = () => this.startPasskeyFirstLogin(); } } @@ -80,6 +73,76 @@ class CyberPanelWebAuthn { // Implementation depends on the specific UI structure } + arrayBufferToBase64url(buffer) { + const bytes = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]); + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + } + + base64urlToArrayBuffer(str) { + let base64 = str.replace(/-/g, '+').replace(/_/g, '/'); + const pad = (4 - (base64.length % 4)) % 4; + for (let i = 0; i < pad; i++) base64 += '='; + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return bytes.buffer; + } + + async startPasskeyFirstLogin() { + try { + this.showLoading('Signing in with passkey...'); + const optsUrl = this.apiEndpoints.authenticationOptions + '?return=' + encodeURIComponent(window.location.pathname || '/'); + const optsResponse = await fetch(optsUrl, { method: 'GET', credentials: 'same-origin' }); + const optsData = await optsResponse.json(); + if (!optsData.publicKey) { + throw new Error(optsData.error || 'Failed to get options'); + } + const publicKey = optsData.publicKey; + publicKey.challenge = this.base64urlToArrayBuffer(publicKey.challenge); + if (publicKey.allowCredentials && publicKey.allowCredentials.length) { + publicKey.allowCredentials = publicKey.allowCredentials.map(function(c) { + return { + type: c.type || 'public-key', + id: typeof c.id === 'string' ? this.base64urlToArrayBuffer(c.id) : c.id, + transports: c.transports + }; + }.bind(this)); + } + const credential = await navigator.credentials.get({ publicKey }); + if (!credential) throw new Error('No credential'); + const credentialJson = { + id: credential.id, + rawId: this.arrayBufferToBase64url(credential.rawId), + type: credential.type, + response: { + clientDataJSON: this.arrayBufferToBase64url(credential.response.clientDataJSON), + authenticatorData: this.arrayBufferToBase64url(credential.response.authenticatorData), + signature: this.arrayBufferToBase64url(credential.response.signature), + userHandle: credential.response.userHandle ? this.arrayBufferToBase64url(credential.response.userHandle) : null + } + }; + const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, { + credential: credentialJson + }); + if (authResponse.success && authResponse.redirect) { + window.location.href = authResponse.redirect; + return; + } + throw new Error(authResponse.error || 'Verification failed'); + } catch (error) { + if (error.name === 'NotAllowedError' || (error.message && (error.message.indexOf('cancel') !== -1 || error.message.indexOf('timed out') !== -1))) { + this.hideLoading(); + return; + } + console.error('WebAuthn passkey-first error:', error); + this.showError(error.message || 'Passkey sign-in failed'); + } finally { + this.hideLoading(); + } + } + async startPasswordlessLogin() { try { const username = document.querySelector('input[name="username"]').value; @@ -87,27 +150,13 @@ class CyberPanelWebAuthn { 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 - }); - + 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 credential = await navigator.credentials.get({ publicKey: challenge }); const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, { challenge_id: challengeResponse.challenge_id, credential: { @@ -117,19 +166,14 @@ class CyberPanelWebAuthn { 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 + user_handle: credential.response.userHandle ? this.arrayBufferToBase64(credential.response.userHandle) : null }); - if (authResponse.success) { this.showSuccess('Authentication successful! Redirecting...'); - setTimeout(() => { - window.location.href = '/'; - }, 1000); + setTimeout(() => { window.location.href = authResponse.redirect || '/'; }, 1000); } else { throw new Error(authResponse.error || 'Authentication failed'); } - } catch (error) { console.error('WebAuthn authentication error:', error); this.showError(error.message || 'Authentication failed'); @@ -138,9 +182,10 @@ class CyberPanelWebAuthn { } } - async registerPasskey(username, credentialName = '') { + async registerPasskey(username, credentialName = '', options = {}) { + const silent = options && options.silent === true; try { - this.showLoading('Starting passkey registration...'); + if (!silent) this.showLoading('Starting passkey registration...'); // Get registration challenge const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.registrationStart, { @@ -172,7 +217,7 @@ class CyberPanelWebAuthn { }); if (regResponse.success) { - this.showSuccess('Passkey registered successfully!'); + if (!silent) this.showSuccess('Passkey registered successfully!'); return regResponse; } else { throw new Error(regResponse.error || 'Registration failed'); @@ -180,10 +225,10 @@ class CyberPanelWebAuthn { } catch (error) { console.error('WebAuthn registration error:', error); - this.showError(error.message || 'Registration failed'); + if (!silent) this.showError(error.message || 'Registration failed'); throw error; } finally { - this.hideLoading(); + if (!silent) this.hideLoading(); } } @@ -265,23 +310,25 @@ class CyberPanelWebAuthn { } convertChallenge(challenge) { - // Convert base64 challenge to ArrayBuffer - const challengeBytes = this.base64ToArrayBuffer(challenge.challenge); - + const ch = challenge.challenge; + const challengeBytes = (typeof ch === 'string' && (ch.indexOf('-') !== -1 || ch.indexOf('_') !== -1)) + ? this.base64urlToArrayBuffer(ch) : this.base64ToArrayBuffer(ch); + const userId = challenge.user && challenge.user.id; + const userIdBuf = !userId ? undefined : (typeof userId === 'string' && (userId.indexOf('-') !== -1 || userId.indexOf('_') !== -1) + ? this.base64urlToArrayBuffer(userId) : this.base64ToArrayBuffer(userId)); return { ...challenge, challenge: challengeBytes, - user: { - ...challenge.user, - id: this.base64ToArrayBuffer(challenge.user.id) - }, + user: challenge.user ? { ...challenge.user, id: userIdBuf } : undefined, excludeCredentials: challenge.excludeCredentials?.map(cred => ({ ...cred, - id: this.base64ToArrayBuffer(cred.id) + id: typeof cred.id === 'string' && (cred.id.indexOf('-') !== -1 || cred.id.indexOf('_') !== -1) + ? this.base64urlToArrayBuffer(cred.id) : this.base64ToArrayBuffer(cred.id) })) || [], allowCredentials: challenge.allowCredentials?.map(cred => ({ ...cred, - id: this.base64ToArrayBuffer(cred.id) + id: typeof cred.id === 'string' && (cred.id.indexOf('-') !== -1 || cred.id.indexOf('_') !== -1) + ? this.base64urlToArrayBuffer(cred.id) : this.base64ToArrayBuffer(cred.id) })) || [] }; } @@ -383,12 +430,17 @@ class CyberPanelWebAuthn { } } -// Initialize WebAuthn when DOM is loaded -document.addEventListener('DOMContentLoaded', function() { +// Initialize WebAuthn - run now if DOM ready, else on DOMContentLoaded (script often loads after DOM is ready) +function initCyberPanelWebAuthn() { if (CyberPanelWebAuthn.isSupported()) { window.cyberPanelWebAuthn = new CyberPanelWebAuthn(); } -}); +} +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCyberPanelWebAuthn); +} else { + initCyberPanelWebAuthn(); +} // Export for use in other scripts if (typeof module !== 'undefined' && module.exports) { diff --git a/loginSystem/templates/loginSystem/login.html b/loginSystem/templates/loginSystem/login.html index 14d373aff..39127b45c 100644 --- a/loginSystem/templates/loginSystem/login.html +++ b/loginSystem/templates/loginSystem/login.html @@ -352,7 +352,8 @@ class="btn btn-success btn-block btn-login">Sign In - + {% if passkey_login_available %} + + {% endif %} @@ -414,29 +416,17 @@ } {% get_current_language as LANGUAGE_CODE %} @@ -234,14 +235,30 @@
- +

{% trans "Select the user account you want to modify" %}

+
@@ -294,6 +311,11 @@ class="form-control">

{% trans "Choose the security level for this account" %}

+
+ {% trans "What's the difference?" %} + {% trans "High" %}: {% trans "Session is tied to the IP address used at login. If the user logs in from one IP and later visits from another (e.g. different network, VPN, or mobile), the session is invalidated and they must log in again." %}

+ {% trans "Low" %}: {% trans "Session is not tied to IP. The user can stay logged in when their IP changes (e.g. switching networks or using VPN). More convenient but slightly less strict against session reuse from another location." %} +
@@ -352,27 +374,7 @@

{% 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" %}
@@ -407,21 +409,136 @@
-
-
- -
- {% trans "Maximum number of passkeys reached" %} -
+ +