New User Panel with TOTP & change password. Admin Panel added Global OTPAuth URL

This commit is contained in:
Ryan
2025-03-30 14:44:53 -04:00
committed by GitHub
parent 27de0a9a48
commit 3f1007b1b3
13 changed files with 918 additions and 351 deletions

View File

@@ -137,20 +137,26 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
- **Seamless Interaction:**
- Both drop zones support smooth drag and drop interactions with animations and pointer event adjustments to prevent interference, ensuring that cards can be dropped reliably regardless of screen position.
### 🔒 Admin Panel & OpenID Connect (OIDC) Integration
# 🔒 Admin Panel, TOTP & OpenID Connect (OIDC) Integration
- **Flexible Authentication:**
- Supports multiple authentication methods including Form-based Login, Basic Auth, and OpenID Connect (OIDC). Allow disable of only two login options.
- Supports multiple authentication methods including Form-based Login, Basic Auth, OpenID Connect (OIDC), and TOTP-based Two-Factor Authentication.
- Ensures continuous secure access by allowing administrators to disable only two of the available login options at any time.
- **Secure OIDC Authentication:**
- Integrates seamlessly with OIDC providers (e.g., Keycloak, Okta).
- Admin-configurable OIDC settings, including Provider URL, Client ID, Client Secret, and Redirect URI.
- All sensitive configurations are securely stored in an encrypted JSON file.
- Seamlessly integrates with OIDC providers (e.g., Keycloak, Okta).
- Provides admin-configurable OIDC settingsincluding Provider URL, Client ID, Client Secret, and Redirect URI.
- Stores all sensitive configurations in an encrypted JSON file.
- **TOTP Two-Factor Authentication:**
- Enhances security by integrating Time-based One-Time Password (TOTP) functionality.
- The new User Panel automatically displays the TOTP setup modal when users enable TOTP, presenting a QR code for easy configuration in authenticator apps.
- Administrators can customize a global OTPAuth URL template for consistent TOTP provisioning across accounts.
- **Dynamic Admin Panel:**
- Intuitive Admin Panel with Material Icons for quick recognition and access.
- Allows administrators to easily manage authentication settings, user management, and login methods.
- Real-time validation prevents disabling all authentication methods simultaneously, ensuring continuous secure access.
- Features an intuitive interface with Material Icons for quick recognition and access.
- Allows administrators to manage authentication settings, user management, and login methods in real time.
- Includes real-time validation that prevents the accidental disabling of all authentication methods simultaneously.
---

886
auth.js

File diff suppressed because it is too large Load Diff

View File

@@ -104,16 +104,31 @@ if (isset($failedAttempts[$ip])) {
}
}
/*
* Updated authenticate() function:
* It reads each line from users.txt.
* It expects records in the format:
* username:hashed_password:role[:encrypted_totp_secret]
* If a fourth field is present and non-empty, it decrypts it to obtain the TOTP secret.
*/
function authenticate($username, $password) {
global $usersFile;
global $usersFile, $encryptionKey;
if (!file_exists($usersFile)) {
return false;
}
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3);
if ($username === $storedUser && password_verify($password, $storedPass)) {
return $storedRole;
$parts = explode(':', trim($line));
if (count($parts) < 3) continue; // Skip invalid lines.
if ($username === $parts[0] && password_verify($password, $parts[1])) {
$result = ['role' => $parts[2]];
// If there's a fourth field, decrypt it to get the TOTP secret.
if (isset($parts[3]) && !empty($parts[3])) {
$result['totp_secret'] = decryptData($parts[3], $encryptionKey);
} else {
$result['totp_secret'] = null;
}
return $result;
}
}
return false;
@@ -134,8 +149,27 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
exit();
}
$userRole = authenticate($username, $password);
if ($userRole !== false) {
$user = authenticate($username, $password);
if ($user !== false) {
// Only require TOTP if the user's TOTP secret is set.
if (!empty($user['totp_secret'])) {
if (empty($data['totp_code'])) {
echo json_encode([
"totp_required" => true,
"message" => "TOTP code required"
]);
exit();
} else {
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
$providedCode = trim($data['totp_code']);
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
echo json_encode(["error" => "Invalid TOTP code"]);
exit();
}
}
}
// --- End TOTP Integration ---
if (isset($failedAttempts[$ip])) {
unset($failedAttempts[$ip]);
saveFailedAttempts($attemptsFile, $failedAttempts);
@@ -143,7 +177,7 @@ if ($userRole !== false) {
session_regenerate_id(true);
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $username;
$_SESSION["isAdmin"] = ($userRole === "1");
$_SESSION["isAdmin"] = ($user['role'] === "1");
if ($rememberMe) {
$token = bin2hex(random_bytes(32));
@@ -160,7 +194,7 @@ if ($userRole !== false) {
$persistentTokens[$token] = [
"username" => $username,
"expiry" => $expiry,
"isAdmin" => ($userRole === "1")
"isAdmin" => ($user['role'] === "1")
];
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);

View File

@@ -54,7 +54,19 @@ $userFound = false;
$newLines = [];
foreach ($lines as $line) {
list($storedUser, $storedHash, $storedRole) = explode(':', trim($line));
$parts = explode(':', trim($line));
// Expect at least 3 parts: username, hashed password, and role.
if (count($parts) < 3) {
// Skip invalid lines.
$newLines[] = $line;
continue;
}
$storedUser = $parts[0];
$storedHash = $parts[1];
$storedRole = $parts[2];
// Preserve TOTP secret if it exists.
$totpSecret = (count($parts) >= 4) ? $parts[3] : "";
if ($storedUser === $username) {
$userFound = true;
// Verify the old password.
@@ -64,8 +76,12 @@ foreach ($lines as $line) {
}
// Hash the new password.
$newHashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
// Rebuild the line with the new hash.
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole;
// Rebuild the line with the new hash and preserve TOTP secret if present.
if ($totpSecret !== "") {
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole . ":" . $totpSecret;
} else {
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole;
}
} else {
$newLines[] = $line;
}

View File

@@ -15,8 +15,25 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
exit;
}
$totp_enabled = false;
$username = $_SESSION['username'] ?? '';
if ($username) {
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$parts = explode(":", trim($line));
// Assuming first field is username and fourth (if exists) is the TOTP secret.
if ($parts[0] === $username) {
if (isset($parts[3]) && trim($parts[3]) !== "") {
$totp_enabled = true;
}
break;
}
}
}
echo json_encode([
"authenticated" => true,
"isAdmin" => isset($_SESSION["isAdmin"]) ? $_SESSION["isAdmin"] : false
"isAdmin" => isset($_SESSION["isAdmin"]) ? $_SESSION["isAdmin"] : false,
"totp_enabled" => $totp_enabled
]);
?>

View File

@@ -258,7 +258,7 @@ function previewFile(fileUrl, fileName) {
embed.style.height = "80vh";
embed.style.border = "none";
container.appendChild(embed);
} else if (/\.(mp4|webm|mov|ogg)$/i.test(fileName)) {
} else if (/\.(mp4|webm|mov)$/i.test(fileName)) {
const video = document.createElement("video");
video.src = fileUrl;
video.controls = true;

View File

@@ -11,7 +11,12 @@ if (file_exists($configFile)) {
echo json_encode(['error' => 'Failed to decrypt configuration.']);
exit;
}
echo $decryptedContent;
// Decode the configuration and ensure globalOtpauthUrl is set
$config = json_decode($decryptedContent, true);
if (!isset($config['globalOtpauthUrl'])) {
$config['globalOtpauthUrl'] = "";
}
echo json_encode($config);
} else {
echo json_encode([
'oidc' => [
@@ -24,7 +29,8 @@ if (file_exists($configFile)) {
'disableFormLogin' => false,
'disableBasicAuth' => false,
'disableOIDCLogin' => false
]
],
'globalOtpauthUrl' => ""
]);
}
?>

View File

@@ -99,7 +99,7 @@
<button id="logoutBtn" title="Logout">
<i class="material-icons">exit_to_app</i>
</button>
<button id="changePasswordBtn" title="Change Password">
<button id="changePasswordBtn" title="Change Password" style="display: none;">
<i class="material-icons">vpn_key</i>
</button>
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
@@ -390,8 +390,7 @@
</div>
</div>
</div>
<!-- JavaScript Files -->
<script type="module" src="main.js"></script>
</body>

View File

@@ -2047,4 +2047,15 @@ body.dark-mode .admin-panel-content textarea {
body.dark-mode .admin-panel-content label {
color: #e0e0e0;
}
#openChangePasswordModalBtn {
width: auto;
padding: 5px 10px;
font-size: 14px;
margin-right: 300px;
}
#changePasswordModal {
z-index: 9999;
}

152
totp_setup.php Normal file
View File

@@ -0,0 +1,152 @@
<?php
// totp_setup.php
require_once 'vendor/autoload.php';
require 'config.php';
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Writer\PngWriter;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
// For debugging purposes, you might enable error reporting temporarily:
// ini_set('display_errors', 1);
// error_reporting(E_ALL);
// Start the session and ensure the user is authenticated.
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
http_response_code(403);
exit;
}
// Verify CSRF token provided as a GET parameter.
if (!isset($_GET['csrf']) || $_GET['csrf'] !== $_SESSION['csrf_token']) {
http_response_code(403);
exit;
}
$username = $_SESSION['username'] ?? '';
if (!$username) {
http_response_code(400);
exit;
}
// Set header to output a PNG image.
header("Content-Type: image/png");
// Define the path to your users.txt file.
$usersFile = USERS_DIR . USERS_FILE;
/**
* Updates the TOTP secret for the given user in users.txt.
*
* @param string $username
* @param string $encryptedSecret The encrypted TOTP secret.
*/
function updateUserTOTPSecret($username, $encryptedSecret) {
global $usersFile;
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$newLines = [];
foreach ($lines as $line) {
$parts = explode(':', trim($line));
if (count($parts) < 3) {
$newLines[] = $line;
continue;
}
if ($parts[0] === $username) {
// If a fourth field exists, update it; otherwise, append it.
if (count($parts) >= 4) {
$parts[3] = $encryptedSecret;
} else {
$parts[] = $encryptedSecret;
}
$newLines[] = implode(':', $parts);
} else {
$newLines[] = $line;
}
}
file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX);
}
/**
* Retrieves the current user's TOTP secret from users.txt (if present).
*
* @param string $username
* @return string|null The decrypted TOTP secret or null if not found.
*/
function getUserTOTPSecret($username) {
global $usersFile, $encryptionKey;
if (!file_exists($usersFile)) {
return null;
}
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$parts = explode(':', trim($line));
if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
return decryptData($parts[3], $encryptionKey);
}
}
return null;
}
/**
* Retrieves the global OTPAuth URL from admin configuration.
*
* @return string Global OTPAuth URL template or an empty string if not set.
*/
function getGlobalOtpauthUrl() {
global $encryptionKey;
$adminConfigFile = USERS_DIR . 'adminConfig.json';
if (file_exists($adminConfigFile)) {
$encryptedContent = file_get_contents($adminConfigFile);
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
if ($decryptedContent !== false) {
$config = json_decode($decryptedContent, true);
if (isset($config['globalOtpauthUrl']) && !empty($config['globalOtpauthUrl'])) {
return $config['globalOtpauthUrl'];
}
}
}
return "";
}
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
// Retrieve the current TOTP secret for the user.
$totpSecret = getUserTOTPSecret($username);
if (!$totpSecret) {
// If no TOTP secret exists, generate a new one.
$totpSecret = $tfa->createSecret();
$encryptedSecret = encryptData($totpSecret, $encryptionKey);
updateUserTOTPSecret($username, $encryptedSecret);
}
// Determine the otpauth URL to use.
// If a global OTPAuth URL template is defined, replace placeholders {label} and {secret}.
// Otherwise, use the default method.
$globalOtpauthUrl = getGlobalOtpauthUrl();
if (!empty($globalOtpauthUrl)) {
$label = "FileRise:" . $username;
$otpauthUrl = str_replace(
["{label}", "{secret}"],
[urlencode($label), $totpSecret],
$globalOtpauthUrl
);
} else {
$label = urlencode("FileRise:" . $username);
$issuer = urlencode("FileRise");
$otpauthUrl = "otpauth://totp/{$label}?secret={$totpSecret}&issuer={$issuer}";
}
// Build the QR code using Endroid QR Code.
$result = Builder::create()
->writer(new PngWriter())
->data($otpauthUrl)
->errorCorrectionLevel(new ErrorCorrectionLevelHigh())
->build();
header('Content-Type: ' . $result->getMimeType());
echo $result->getString();
?>

View File

@@ -51,6 +51,9 @@ $disableFormLogin = isset($data['disableFormLogin']) ? filter_var($data['disable
$disableBasicAuth = isset($data['disableBasicAuth']) ? filter_var($data['disableBasicAuth'], FILTER_VALIDATE_BOOLEAN) : false;
$disableOIDCLogin = isset($data['disableOIDCLogin']) ? filter_var($data['disableOIDCLogin'], FILTER_VALIDATE_BOOLEAN) : false;
// Retrieve the global OTPAuth URL (new field). If not provided, default to an empty string.
$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : "";
// Prepare configuration array.
$configUpdate = [
'oidc' => [
@@ -63,7 +66,8 @@ $configUpdate = [
'disableFormLogin' => $disableFormLogin,
'disableBasicAuth' => $disableBasicAuth,
'disableOIDCLogin' => $disableOIDCLogin,
]
],
'globalOtpauthUrl' => $globalOtpauthUrl
];
// Define the configuration file path.

82
updateUserPanel.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
// updateUserPanel.php
require 'config.php';
header('Content-Type: application/json');
session_start();
// Ensure the user is authenticated.
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
http_response_code(403);
echo json_encode(["error" => "Unauthorized"]);
exit;
}
// Verify the CSRF token from headers.
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$csrfToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if (!isset($_SESSION['csrf_token']) || $csrfToken !== $_SESSION['csrf_token']) {
http_response_code(403);
echo json_encode(["error" => "Invalid CSRF token"]);
exit;
}
$data = json_decode(file_get_contents("php://input"), true);
if (!is_array($data)) {
http_response_code(400);
echo json_encode(["error" => "Invalid input"]);
exit;
}
$username = $_SESSION['username'] ?? '';
if (!$username) {
http_response_code(400);
echo json_encode(["error" => "No username in session"]);
exit;
}
$totp_enabled = isset($data['totp_enabled']) ? filter_var($data['totp_enabled'], FILTER_VALIDATE_BOOLEAN) : false;
$usersFile = USERS_DIR . USERS_FILE;
/**
* Clears the TOTP secret for a given user by removing or emptying the fourth field.
*
* @param string $username
*/
function disableUserTOTP($username) {
global $usersFile;
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$newLines = [];
foreach ($lines as $line) {
$parts = explode(':', trim($line));
// If the line doesn't have at least three parts, leave it alone.
if (count($parts) < 3) {
$newLines[] = $line;
continue;
}
if ($parts[0] === $username) {
// If a fourth field exists, clear it; otherwise, append an empty field.
if (count($parts) >= 4) {
$parts[3] = "";
} else {
$parts[] = "";
}
$newLines[] = implode(':', $parts);
} else {
$newLines[] = $line;
}
}
file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX);
}
// If TOTP is disabled, clear the user's TOTP secret.
if (!$totp_enabled) {
disableUserTOTP($username);
echo json_encode(["success" => "User panel updated: TOTP disabled"]);
exit;
} else {
// If TOTP is enabled, do not change the stored secret.
echo json_encode(["success" => "User panel updated: TOTP remains enabled"]);
exit;
}
?>

View File

@@ -407,7 +407,7 @@ function initResumableUpload() {
resumableInstance = new Resumable({
target: "upload.php",
query: { folder: window.currentFolder || "root", upload_token: window.csrfToken },
chunkSize: 3 * 1024 * 1024, // 3 MB chunks
chunkSize: 1.5 * 1024 * 1024, // 1.5 MB chunks
simultaneousUploads: 3,
testChunks: false,
throttleProgressCallbacks: 1,