added authentication logic and password change enforcement

This commit is contained in:
Edmund Tan 2025-07-23 17:26:50 +08:00
parent 9128fce5c2
commit 23b38483b8
7 changed files with 813 additions and 3 deletions

View File

@ -19,6 +19,11 @@ class AuthManager {
$_SESSION['last_activity'] = time(); $_SESSION['last_activity'] = time();
$_SESSION['login_time'] = time(); $_SESSION['login_time'] = time();
// Check if this is first login with default credentials
if (USER_FIRST_LOGIN) {
$_SESSION['requires_setup'] = true;
}
// Generate new CSRF token // Generate new CSRF token
generateCSRFToken(); generateCSRFToken();
@ -79,6 +84,75 @@ class AuthManager {
} }
} }
} }
/**
* Change user credentials (username and/or password)
*/
public static function changeCredentials($currentPassword, $newUsername, $newPassword) {
// Verify current password
if (!password_verify($currentPassword, ADMIN_PASSWORD_HASH)) {
return ['success' => false, 'error' => 'Current password is incorrect'];
}
// Validate new username
$newUsername = sanitizeInput($newUsername);
if (strlen($newUsername) < 3 || strlen($newUsername) > 20) {
return ['success' => false, 'error' => 'Username must be between 3 and 20 characters'];
}
if (!preg_match('/^[a-zA-Z0-9_]+$/', $newUsername)) {
return ['success' => false, 'error' => 'Username can only contain letters, numbers, and underscores'];
}
// Validate new password
if (strlen($newPassword) < 6) {
return ['success' => false, 'error' => 'Password must be at least 6 characters long'];
}
try {
// Load current user config
$userConfig = loadUserConfig();
// Update credentials
$userConfig['username'] = $newUsername;
$userConfig['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
$userConfig['first_login'] = false;
$userConfig['last_updated'] = time();
// Save updated config
saveUserConfig($userConfig);
logMessage('INFO', "User credentials changed successfully. New username: '$newUsername'");
return ['success' => true, 'message' => 'Credentials updated successfully'];
} catch (Exception $e) {
logMessage('ERROR', "Failed to update user credentials: " . $e->getMessage());
return ['success' => false, 'error' => 'Failed to save new credentials'];
}
}
/**
* Check if user requires setup (first login)
*/
public static function requiresSetup() {
return isset($_SESSION['requires_setup']) && $_SESSION['requires_setup'] === true;
}
/**
* Complete initial setup
*/
public static function completeSetup($currentPassword, $newUsername, $newPassword) {
$result = self::changeCredentials($currentPassword, $newUsername, $newPassword);
if ($result['success']) {
// Clear setup requirement
unset($_SESSION['requires_setup']);
logMessage('INFO', "Initial setup completed for user: '$newUsername'");
}
return $result;
}
} }
/** /**

View File

@ -27,9 +27,51 @@ define('SYSTEMD_SERVICE_FILE', '/etc/systemd/system/ziti-router.service');
define('UI_LOG_DIR', __DIR__ . '/../logs'); define('UI_LOG_DIR', __DIR__ . '/../logs');
define('UI_TEMP_DIR', __DIR__ . '/../temp'); define('UI_TEMP_DIR', __DIR__ . '/../temp');
// Authentication // Load user configuration
define('ADMIN_USERNAME', 'admin'); function loadUserConfig() {
define('ADMIN_PASSWORD_HASH', password_hash('admin123', PASSWORD_DEFAULT)); // Change this in production $userConfigFile = __DIR__ . '/user_config.php';
// Create default user config if it doesn't exist
if (!file_exists($userConfigFile)) {
$defaultConfig = [
'username' => 'admin',
'password_hash' => password_hash('admin123', PASSWORD_DEFAULT),
'first_login' => true,
'created_at' => time(),
'last_updated' => time()
];
saveUserConfig($defaultConfig);
return $defaultConfig;
}
return include $userConfigFile;
}
function saveUserConfig($config) {
$userConfigFile = __DIR__ . '/user_config.php';
// Create backup if file exists
if (file_exists($userConfigFile)) {
copy($userConfigFile, $userConfigFile . '.backup');
}
$configContent = "<?php\n/**\n * User Configuration File for ZitiNexus Router Enrollment UI\n * This file stores user-changeable settings like username and password\n */\n\nreturn " . var_export($config, true) . ";\n?>";
if (file_put_contents($userConfigFile, $configContent, LOCK_EX) === false) {
throw new Exception('Failed to save user configuration');
}
// Set proper permissions
chmod($userConfigFile, 0644);
}
// Load user configuration
$userConfig = loadUserConfig();
// Authentication - Use user config values
define('ADMIN_USERNAME', $userConfig['username']);
define('ADMIN_PASSWORD_HASH', $userConfig['password_hash']);
define('USER_FIRST_LOGIN', $userConfig['first_login']);
// Security settings // Security settings
define('SESSION_TIMEOUT', 3600); // 1 hour define('SESSION_TIMEOUT', 3600); // 1 hour

View File

@ -0,0 +1,14 @@
<?php
/**
* User Configuration File for ZitiNexus Router Enrollment UI
* This file stores user-changeable settings like username and password
*/
return [
'username' => 'admin',
'password_hash' => password_hash('admin123', PASSWORD_DEFAULT),
'first_login' => true,
'created_at' => time(),
'last_updated' => time()
];
?>

View File

@ -492,3 +492,124 @@ body {
.text-sm { font-size: 0.875rem; } .text-sm { font-size: 0.875rem; }
.text-xs { font-size: 0.75rem; } .text-xs { font-size: 0.75rem; }
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--card-background);
border-radius: 12px;
box-shadow: var(--shadow-lg);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-50px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid var(--border-color);
background: #f8fafc;
border-radius: 12px 12px 0 0;
}
.modal-header h2 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
}
.modal-close:hover {
background-color: var(--error-color);
color: white;
}
.modal-body {
padding: 2rem;
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
/* Password strength and match indicators */
.password-strength,
.password-match {
margin-top: 0.5rem;
font-size: 0.875rem;
min-height: 1.2rem;
}
/* Responsive modal */
@media (max-width: 768px) {
.modal-content {
width: 95%;
margin: 1rem;
}
.modal-header,
.modal-body {
padding: 1rem;
}
.modal-actions {
flex-direction: column;
}
.modal-actions .btn {
width: 100%;
}
}

View File

@ -74,6 +74,17 @@ class EnrollmentUI {
this.showCleanupConfirmation(); this.showCleanupConfirmation();
}); });
} }
// Settings button
const settingsBtn = document.getElementById('settingsBtn');
if (settingsBtn) {
settingsBtn.addEventListener('click', () => {
this.showSettingsModal();
});
}
// Settings modal events
this.bindSettingsModalEvents();
} }
validateHashKey(input) { validateHashKey(input) {
@ -574,6 +585,213 @@ class EnrollmentUI {
formatTimestamp(timestamp) { formatTimestamp(timestamp) {
return new Date(timestamp * 1000).toLocaleString(); return new Date(timestamp * 1000).toLocaleString();
} }
// Settings Modal Methods
bindSettingsModalEvents() {
const modal = document.getElementById('settingsModal');
const closeBtn = document.getElementById('closeSettingsModal');
const cancelBtn = document.getElementById('cancelSettings');
const settingsForm = document.getElementById('settingsForm');
// Close modal events
if (closeBtn) {
closeBtn.addEventListener('click', () => {
this.hideSettingsModal();
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
this.hideSettingsModal();
});
}
// Close modal when clicking outside
if (modal) {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.hideSettingsModal();
}
});
}
// Settings form submission
if (settingsForm) {
settingsForm.addEventListener('submit', (e) => {
e.preventDefault();
this.handleSettingsSubmit();
});
}
// Password strength and match validation
const newPasswordInput = document.getElementById('settings_new_password');
const confirmPasswordInput = document.getElementById('settings_confirm_password');
if (newPasswordInput) {
newPasswordInput.addEventListener('input', () => {
this.updatePasswordStrength('settings');
this.checkPasswordMatch('settings');
});
}
if (confirmPasswordInput) {
confirmPasswordInput.addEventListener('input', () => {
this.checkPasswordMatch('settings');
});
}
}
showSettingsModal() {
const modal = document.getElementById('settingsModal');
if (modal) {
modal.classList.add('active');
// Focus on first input
const firstInput = modal.querySelector('input[type="password"]');
if (firstInput) {
setTimeout(() => firstInput.focus(), 100);
}
}
}
hideSettingsModal() {
const modal = document.getElementById('settingsModal');
if (modal) {
modal.classList.remove('active');
// Reset form
const form = document.getElementById('settingsForm');
if (form) {
form.reset();
// Reset password indicators
document.getElementById('settingsPasswordStrength').innerHTML = '';
document.getElementById('settingsPasswordMatch').innerHTML = '';
}
}
}
updatePasswordStrength(prefix) {
const passwordInput = document.getElementById(`${prefix}_new_password`);
const strengthDiv = document.getElementById(`${prefix}PasswordStrength`);
if (!passwordInput || !strengthDiv) return;
const password = passwordInput.value;
let strength = 0;
let feedback = '';
if (password.length >= 6) strength++;
if (password.length >= 8) strength++;
if (/[A-Z]/.test(password)) strength++;
if (/[0-9]/.test(password)) strength++;
if (/[^A-Za-z0-9]/.test(password)) strength++;
switch (strength) {
case 0:
case 1:
feedback = '<span style="color: #ef4444;">Weak</span>';
break;
case 2:
case 3:
feedback = '<span style="color: #f59e0b;">Medium</span>';
break;
case 4:
case 5:
feedback = '<span style="color: #10b981;">Strong</span>';
break;
}
strengthDiv.innerHTML = password.length > 0 ? 'Strength: ' + feedback : '';
}
checkPasswordMatch(prefix) {
const passwordInput = document.getElementById(`${prefix}_new_password`);
const confirmInput = document.getElementById(`${prefix}_confirm_password`);
const matchDiv = document.getElementById(`${prefix}PasswordMatch`);
if (!passwordInput || !confirmInput || !matchDiv) return;
const password = passwordInput.value;
const confirm = confirmInput.value;
if (confirm.length === 0) {
matchDiv.innerHTML = '';
return;
}
if (password === confirm) {
matchDiv.innerHTML = '<span style="color: #10b981;">✓ Passwords match</span>';
} else {
matchDiv.innerHTML = '<span style="color: #ef4444;">✗ Passwords do not match</span>';
}
}
async handleSettingsSubmit() {
const form = document.getElementById('settingsForm');
const submitBtn = form.querySelector('button[type="submit"]');
// Get form data
const currentPassword = document.getElementById('settings_current_password').value;
const newUsername = document.getElementById('settings_new_username').value;
const newPassword = document.getElementById('settings_new_password').value;
const confirmPassword = document.getElementById('settings_confirm_password').value;
// Validate passwords match
if (newPassword !== confirmPassword) {
this.showAlert('New passwords do not match', 'error');
return;
}
// Validate password length
if (newPassword.length < 6) {
this.showAlert('Password must be at least 6 characters long', 'error');
return;
}
// Update UI
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner"></span>Saving...';
try {
const formData = new FormData();
formData.append('action', 'change_credentials');
formData.append('current_password', currentPassword);
formData.append('new_username', newUsername);
formData.append('new_password', newPassword);
formData.append('csrf_token', document.querySelector('input[name="csrf_token"]').value);
const response = await fetch('dashboard.php', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = await response.json();
if (result.success) {
this.showAlert('Credentials updated successfully! You will be logged out in 3 seconds.', 'success');
this.hideSettingsModal();
// Redirect to login page after 3 seconds
setTimeout(() => {
window.location.href = 'index.php?message=credentials_changed';
}, 3000);
} else {
throw new Error(result.error || 'Failed to update credentials');
}
} catch (error) {
console.error('Settings update failed:', error);
this.showAlert(`Failed to update credentials: ${error.message}`, 'error');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '💾 Save Changes';
}
}
} }
// Initialize the application when DOM is loaded // Initialize the application when DOM is loaded

View File

@ -9,6 +9,12 @@ require_once '../includes/enrollment.php';
// Require authentication // Require authentication
AuthManager::requireAuth(); AuthManager::requireAuth();
// Check if user requires initial setup
if (AuthManager::requiresSetup()) {
header('Location: setup.php');
exit;
}
// Get current user // Get current user
$currentUser = AuthManager::getCurrentUser(); $currentUser = AuthManager::getCurrentUser();
@ -59,6 +65,19 @@ if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQU
echo json_encode($result); echo json_encode($result);
exit; exit;
} }
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'change_credentials') {
// Handle credentials change request
AuthManager::requireCSRF();
$currentPassword = $_POST['current_password'] ?? '';
$newUsername = $_POST['new_username'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$result = AuthManager::changeCredentials($currentPassword, $newUsername, $newPassword);
echo json_encode($result);
exit;
}
} }
// Get system status for initial page load // Get system status for initial page load
@ -89,6 +108,9 @@ $systemStatus = $enrollmentManager->getSystemStatus();
<button id="refreshStatus" class="btn btn-secondary" title="Refresh System Status"> <button id="refreshStatus" class="btn btn-secondary" title="Refresh System Status">
🔄 Refresh 🔄 Refresh
</button> </button>
<button id="settingsBtn" class="btn btn-secondary" title="Account Settings">
⚙️ Settings
</button>
<a href="index.php?action=logout" class="btn btn-secondary"> <a href="index.php?action=logout" class="btn btn-secondary">
Logout Logout
</a> </a>
@ -319,6 +341,92 @@ $systemStatus = $enrollmentManager->getSystemStatus();
</main> </main>
</div> </div>
<!-- Settings Modal -->
<div id="settingsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>⚙️ Account Settings</h2>
<button class="modal-close" id="closeSettingsModal">&times;</button>
</div>
<div class="modal-body">
<form id="settingsForm">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<div class="form-group">
<label for="settings_current_password" class="form-label">Current Password</label>
<input
type="password"
id="settings_current_password"
name="current_password"
class="form-input"
placeholder="Enter current password"
required
autocomplete="current-password"
>
<small class="text-sm text-secondary">Required to verify your identity</small>
</div>
<div class="form-group">
<label for="settings_new_username" class="form-label">Username</label>
<input
type="text"
id="settings_new_username"
name="new_username"
class="form-input"
placeholder="Enter new username"
value="<?php echo htmlspecialchars($currentUser['username']); ?>"
minlength="3"
maxlength="20"
pattern="[a-zA-Z0-9_]+"
required
autocomplete="username"
>
<small class="text-sm text-secondary">3-20 characters, letters, numbers, and underscores only</small>
</div>
<div class="form-group">
<label for="settings_new_password" class="form-label">New Password</label>
<input
type="password"
id="settings_new_password"
name="new_password"
class="form-input"
placeholder="Enter new password"
minlength="6"
required
autocomplete="new-password"
>
<small class="text-sm text-secondary">Minimum 6 characters</small>
<div id="settingsPasswordStrength" class="password-strength"></div>
</div>
<div class="form-group">
<label for="settings_confirm_password" class="form-label">Confirm New Password</label>
<input
type="password"
id="settings_confirm_password"
name="confirm_password"
class="form-input"
placeholder="Confirm new password"
minlength="6"
required
autocomplete="new-password"
>
<small class="text-sm text-secondary">Re-enter your new password</small>
<div id="settingsPasswordMatch" class="password-match"></div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" id="cancelSettings">Cancel</button>
<button type="submit" class="btn btn-primary">
💾 Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script src="assets/js/app.js"></script> <script src="assets/js/app.js"></script>
</body> </body>
</html> </html>

233
UI/public/setup.php Normal file
View File

@ -0,0 +1,233 @@
<?php
/**
* Initial Setup Page for ZitiNexus Router Enrollment UI
* Forces users to change default credentials on first login
*/
require_once '../includes/auth.php';
// Require authentication but allow setup
AuthManager::requireAuth();
// If user doesn't require setup, redirect to dashboard
if (!AuthManager::requiresSetup()) {
header('Location: dashboard.php');
exit;
}
// Handle setup form submission
$setupError = '';
$setupSuccess = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'setup') {
AuthManager::requireCSRF();
$currentPassword = $_POST['current_password'] ?? '';
$newUsername = $_POST['new_username'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
// Validate password confirmation
if ($newPassword !== $confirmPassword) {
$setupError = 'New passwords do not match';
} else {
$result = AuthManager::completeSetup($currentPassword, $newUsername, $newPassword);
if ($result['success']) {
$setupSuccess = $result['message'];
// Redirect to login page after successful setup
header('refresh:3;url=index.php?message=setup_complete');
} else {
$setupError = $result['error'];
}
}
}
$currentUser = AuthManager::getCurrentUser();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo APP_NAME; ?> - Initial Setup</title>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="icon" type="image/x-icon" href="assets/images/favicon.ico">
</head>
<body>
<div class="login-container">
<div class="login-card" style="max-width: 500px;">
<div class="login-header">
<h1>🔐 Security Setup Required</h1>
<p>For security reasons, you must change the default credentials before proceeding.</p>
</div>
<?php if ($setupError): ?>
<div class="alert alert-error">
<?php echo htmlspecialchars($setupError); ?>
</div>
<?php endif; ?>
<?php if ($setupSuccess): ?>
<div class="alert alert-success">
<?php echo htmlspecialchars($setupSuccess); ?>
<br><small>You will be redirected to login with your new credentials...</small>
</div>
<?php else: ?>
<form method="POST" id="setupForm">
<input type="hidden" name="action" value="setup">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<div class="form-group">
<label for="current_password" class="form-label">Current Password</label>
<input
type="password"
id="current_password"
name="current_password"
class="form-input"
placeholder="Enter current password (admin123)"
required
autocomplete="current-password"
>
<small class="text-sm text-secondary">Enter your current password to verify identity</small>
</div>
<div class="form-group">
<label for="new_username" class="form-label">New Username</label>
<input
type="text"
id="new_username"
name="new_username"
class="form-input"
placeholder="Enter new username"
value="<?php echo htmlspecialchars($currentUser['username']); ?>"
minlength="3"
maxlength="20"
pattern="[a-zA-Z0-9_]+"
required
autocomplete="username"
>
<small class="text-sm text-secondary">3-20 characters, letters, numbers, and underscores only</small>
</div>
<div class="form-group">
<label for="new_password" class="form-label">New Password</label>
<input
type="password"
id="new_password"
name="new_password"
class="form-input"
placeholder="Enter new password"
minlength="6"
required
autocomplete="new-password"
>
<small class="text-sm text-secondary">Minimum 6 characters</small>
<div id="passwordStrength" class="password-strength"></div>
</div>
<div class="form-group">
<label for="confirm_password" class="form-label">Confirm New Password</label>
<input
type="password"
id="confirm_password"
name="confirm_password"
class="form-input"
placeholder="Confirm new password"
minlength="6"
required
autocomplete="new-password"
>
<small class="text-sm text-secondary">Re-enter your new password</small>
<div id="passwordMatch" class="password-match"></div>
</div>
<button type="submit" class="btn btn-primary btn-full">
🔒 Complete Setup
</button>
</form>
<div style="margin-top: 2rem; padding: 1rem; background: #f8fafc; border-radius: 8px; border-left: 4px solid #2563eb;">
<h4 style="margin: 0 0 0.5rem 0; color: #1e293b;">Why is this required?</h4>
<p style="margin: 0; font-size: 0.875rem; color: #64748b;">
Default credentials pose a security risk. Changing them ensures only authorized users can access your router enrollment system.
</p>
</div>
<?php endif; ?>
</div>
</div>
<script>
// Password strength indicator
document.getElementById('new_password').addEventListener('input', function() {
const password = this.value;
const strengthDiv = document.getElementById('passwordStrength');
let strength = 0;
let feedback = '';
if (password.length >= 6) strength++;
if (password.length >= 8) strength++;
if (/[A-Z]/.test(password)) strength++;
if (/[0-9]/.test(password)) strength++;
if (/[^A-Za-z0-9]/.test(password)) strength++;
switch (strength) {
case 0:
case 1:
feedback = '<span style="color: #ef4444;">Weak</span>';
break;
case 2:
case 3:
feedback = '<span style="color: #f59e0b;">Medium</span>';
break;
case 4:
case 5:
feedback = '<span style="color: #10b981;">Strong</span>';
break;
}
strengthDiv.innerHTML = password.length > 0 ? 'Strength: ' + feedback : '';
});
// Password match indicator
function checkPasswordMatch() {
const password = document.getElementById('new_password').value;
const confirm = document.getElementById('confirm_password').value;
const matchDiv = document.getElementById('passwordMatch');
if (confirm.length === 0) {
matchDiv.innerHTML = '';
return;
}
if (password === confirm) {
matchDiv.innerHTML = '<span style="color: #10b981;">✓ Passwords match</span>';
} else {
matchDiv.innerHTML = '<span style="color: #ef4444;">✗ Passwords do not match</span>';
}
}
document.getElementById('new_password').addEventListener('input', checkPasswordMatch);
document.getElementById('confirm_password').addEventListener('input', checkPasswordMatch);
// Form validation
document.getElementById('setupForm').addEventListener('submit', function(e) {
const password = document.getElementById('new_password').value;
const confirm = document.getElementById('confirm_password').value;
if (password !== confirm) {
e.preventDefault();
alert('Passwords do not match!');
return false;
}
if (password.length < 6) {
e.preventDefault();
alert('Password must be at least 6 characters long!');
return false;
}
});
</script>
</body>
</html>