diff --git a/.htaccess b/.htaccess index a9531d2..dd58d5e 100644 --- a/.htaccess +++ b/.htaccess @@ -4,26 +4,32 @@ Options -Indexes # ----------------------------- -# 2) Default index files +# Default index files # ----------------------------- DirectoryIndex index.html # ----------------------------- -# 3) Deny access to hidden files +# Deny access to hidden files # ----------------------------- -# (blocks access to .htaccess, .gitignore, etc.) Require all denied # ----------------------------- -# 4) Enforce HTTPS (optional) +# Enforce HTTPS (optional) # ----------------------------- -# Uncomment if you have SSL configured -#RewriteEngine On +RewriteEngine On #RewriteCond %{HTTPS} off #RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + + # Allow requests from a specific origin + #Header set Access-Control-Allow-Origin "https://demo.filerise.net" + Header set Access-Control-Allow-Methods "GET, POST, OPTIONS" + Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, X-CSRF-Token" + Header set Access-Control-Allow-Credentials "true" + + # Prevent clickjacking Header always set X-Frame-Options "SAMEORIGIN" @@ -40,9 +46,30 @@ DirectoryIndex index.html Header set Pragma "no-cache" Header set Expires "0" - # JS/CSS: short‑term cache, revalidate regularly Header set Cache-Control "public, max-age=3600, must-revalidate" - \ No newline at end of file + + +# ----------------------------- +# Additional Security Headers +# ----------------------------- + + # Enforce HTTPS for a year with subdomains and preload option. + Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + # Set a Referrer Policy. + Header always set Referrer-Policy "strict-origin-when-cross-origin" + # Permissions Policy: disable features you don't need. + Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()" + # IE-specific header to prevent downloads from opening in IE. + Header always set X-Download-Options "noopen" + # Expect-CT header for Certificate Transparency (optional). + Header always set Expect-CT "max-age=86400, enforce" + + +# ----------------------------- +# Disable TRACE method +# ----------------------------- +RewriteCond %{REQUEST_METHOD} ^TRACE +RewriteRule .* - [F] \ No newline at end of file diff --git a/auth.php b/auth.php index 972b736..d6c9d45 100644 --- a/auth.php +++ b/auth.php @@ -1,16 +1,25 @@ getMessage()); + http_response_code(500); + echo json_encode(["error" => "Internal Server Error"]); + exit(); +}); + /** * Helper: Get the user's role from users.txt. */ function getUserRole($username) { $usersFile = USERS_DIR . USERS_FILE; if (file_exists($usersFile)) { - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { + foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { $parts = explode(":", trim($line)); if (count($parts) >= 3 && $parts[0] === $username) { return trim($parts[2]); @@ -21,37 +30,25 @@ function getUserRole($username) { } /* --- OIDC Authentication Flow --- */ -if (isset($_GET['oidc'])) { - // Read and decrypt OIDC configuration from JSON file. +// Detect either ?oidc=… or a callback that only has ?code= +$oidcAction = $_GET['oidc'] ?? null; +if (!$oidcAction && isset($_GET['code'])) { + $oidcAction = 'callback'; +} +if ($oidcAction) { $adminConfigFile = USERS_DIR . 'adminConfig.json'; if (file_exists($adminConfigFile)) { - $encryptedContent = file_get_contents($adminConfigFile); - $decryptedContent = decryptData($encryptedContent, $encryptionKey); - if ($decryptedContent === false) { - // Log internal error and return a generic message. - error_log("Failed to decrypt admin configuration."); - echo json_encode(['error' => 'Internal error.']); - exit; - } - $adminConfig = json_decode($decryptedContent, true); - if (isset($adminConfig['oidc'])) { - $oidcConfig = $adminConfig['oidc']; - $oidc_provider_url = !empty($oidcConfig['providerUrl']) ? $oidcConfig['providerUrl'] : 'https://your-oidc-provider.com'; - $oidc_client_id = !empty($oidcConfig['clientId']) ? $oidcConfig['clientId'] : 'YOUR_CLIENT_ID'; - $oidc_client_secret = !empty($oidcConfig['clientSecret']) ? $oidcConfig['clientSecret'] : 'YOUR_CLIENT_SECRET'; - $oidc_redirect_uri = !empty($oidcConfig['redirectUri']) ? $oidcConfig['redirectUri'] : 'https://yourdomain.com/auth.php?oidc=callback'; - } else { - $oidc_provider_url = 'https://your-oidc-provider.com'; - $oidc_client_id = 'YOUR_CLIENT_ID'; - $oidc_client_secret = 'YOUR_CLIENT_SECRET'; - $oidc_redirect_uri = 'https://yourdomain.com/auth.php?oidc=callback'; - } + $enc = file_get_contents($adminConfigFile); + $dec = decryptData($enc, $encryptionKey); + $cfg = $dec !== false ? json_decode($dec, true) : []; } else { - $oidc_provider_url = 'https://your-oidc-provider.com'; - $oidc_client_id = 'YOUR_CLIENT_ID'; - $oidc_client_secret = 'YOUR_CLIENT_SECRET'; - $oidc_redirect_uri = 'https://yourdomain.com/auth.php?oidc=callback'; + $cfg = []; } + $oidc_provider_url = $cfg['oidc']['providerUrl'] ?? 'https://your-oidc-provider.com'; + $oidc_client_id = $cfg['oidc']['clientId'] ?? 'YOUR_CLIENT_ID'; + $oidc_client_secret = $cfg['oidc']['clientSecret'] ?? 'YOUR_CLIENT_SECRET'; + // Use your production domain for redirect URI. + $oidc_redirect_uri = $cfg['oidc']['redirectUri'] ?? 'https://yourdomain.com/auth.php?oidc=callback'; $oidc = new Jumbojett\OpenIDConnectClient( $oidc_provider_url, @@ -60,31 +57,54 @@ if (isset($_GET['oidc'])) { ); $oidc->setRedirectURL($oidc_redirect_uri); - if ($_GET['oidc'] === 'callback') { + if ($oidcAction === 'callback') { try { $oidc->authenticate(); $username = $oidc->requestUserInfo('preferred_username'); + + // Check if this user has a TOTP secret. + $usersFile = USERS_DIR . USERS_FILE; + $totp_secret = null; + if (file_exists($usersFile)) { + foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $parts = explode(":", trim($line)); + if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { + $totp_secret = decryptData($parts[3], $encryptionKey); + break; + } + } + } + if ($totp_secret) { + // Hold pending login & prompt for TOTP. + $_SESSION['pending_login_user'] = $username; + $_SESSION['pending_login_secret'] = $totp_secret; + header("Location: index.html?totp_required=1"); + exit(); + } + + // No TOTP → finalize login. session_regenerate_id(true); $_SESSION["authenticated"] = true; - $_SESSION["username"] = $username; - // Determine the user role from users.txt. - $userRole = getUserRole($username); - $_SESSION["isAdmin"] = ($userRole === "1"); - // *** Use loadUserPermissions() here instead of loadFolderPermission() *** - $_SESSION["folderOnly"] = loadUserPermissions($username); + $_SESSION["username"] = $username; + $_SESSION["isAdmin"] = (getUserRole($username) === "1"); + $_SESSION["folderOnly"] = loadUserPermissions($username); + header("Location: index.html"); exit(); } catch (Exception $e) { error_log("OIDC authentication error: " . $e->getMessage()); + http_response_code(401); echo json_encode(["error" => "Authentication failed."]); exit(); } } else { + // Initiate OIDC authentication. try { $oidc->authenticate(); exit(); } catch (Exception $e) { error_log("OIDC initiation error: " . $e->getMessage()); + http_response_code(401); echo json_encode(["error" => "Authentication initiation failed."]); exit(); } @@ -92,10 +112,9 @@ if (isset($_GET['oidc'])) { } /* --- Fallback: Form-based Authentication --- */ -// (Form-based branch code remains unchanged. It calls loadUserPermissions() in its basic auth branch.) $usersFile = USERS_DIR . USERS_FILE; $maxAttempts = 5; -$lockoutTime = 30 * 60; +$lockoutTime = 30 * 60; // 30 minutes $attemptsFile = USERS_DIR . 'failed_logins.json'; $failedLogFile = USERS_DIR . 'failed_login.log'; $persistentTokensFile = USERS_DIR . 'persistent_tokens.json'; @@ -111,7 +130,7 @@ function loadFailedAttempts($file) { } function saveFailedAttempts($file, $data) { - file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); + file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX); } $ip = $_SERVER['REMOTE_ADDR']; @@ -121,6 +140,7 @@ $failedAttempts = loadFailedAttempts($attemptsFile); if (isset($failedAttempts[$ip])) { $attemptData = $failedAttempts[$ip]; if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) { + http_response_code(429); echo json_encode(["error" => "Too many failed login attempts. Please try again later."]); exit(); } @@ -137,11 +157,9 @@ function authenticate($username, $password) { if (count($parts) < 3) continue; if ($username === $parts[0] && password_verify($password, $parts[1])) { $result = ['role' => $parts[2]]; - if (isset($parts[3]) && !empty($parts[3])) { - $result['totp_secret'] = decryptData($parts[3], $encryptionKey); - } else { - $result['totp_secret'] = null; - } + $result['totp_secret'] = (isset($parts[3]) && !empty($parts[3])) + ? decryptData($parts[3], $encryptionKey) + : null; return $result; } } @@ -154,11 +172,13 @@ $password = trim($data["password"] ?? ""); $rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true; if (!$username || !$password) { + http_response_code(400); echo json_encode(["error" => "Username and password are required"]); exit(); } if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) { + http_response_code(400); echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]); exit(); } @@ -167,6 +187,7 @@ $user = authenticate($username, $password); if ($user !== false) { if (!empty($user['totp_secret'])) { if (empty($data['totp_code'])) { + http_response_code(401); echo json_encode([ "totp_required" => true, "message" => "TOTP code required" @@ -176,6 +197,7 @@ if ($user !== false) { $tfa = new \RobThree\Auth\TwoFactorAuth('FileRise'); $providedCode = trim($data['totp_code']); if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) { + http_response_code(401); echo json_encode(["error" => "Invalid TOTP code"]); exit(); } @@ -229,6 +251,7 @@ if ($user !== false) { saveFailedAttempts($attemptsFile, $failedAttempts); $logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL; file_put_contents($failedLogFile, $logLine, FILE_APPEND); + http_response_code(401); echo json_encode(["error" => "Invalid credentials"]); } ?> \ No newline at end of file diff --git a/js/auth.js b/js/auth.js index 4dfb593..47cbe58 100644 --- a/js/auth.js +++ b/js/auth.js @@ -1,11 +1,16 @@ import { sendRequest } from './networkUtils.js'; -import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmModal } from './domUtils.js'; +import { + toggleVisibility, + showToast as originalShowToast, + attachEnterKeyListener, + showCustomConfirmModal +} from './domUtils.js'; import { loadFileList } from './fileListView.js'; import { initFileActions } from './fileActions.js'; import { renderFileTable } from './fileListView.js'; import { loadFolderTree } from './folderManager.js'; import { - openTOTPLoginModal, + openTOTPLoginModal as originalOpenTOTPLoginModal, openUserPanel, openTOTPModal, closeTOTPModal, @@ -24,6 +29,43 @@ const currentOIDCConfig = { }; window.currentOIDCConfig = currentOIDCConfig; +/* ----------------- TOTP & Toast Overrides ----------------- */ +// detect if we’re in a pending‑TOTP state +window.pendingTOTP = new URLSearchParams(window.location.search).get('totp_required') === '1'; + +// override showToast to suppress the "Please log in to continue." toast during TOTP +function showToast(msg) { + if (window.pendingTOTP && msg === "Please log in to continue.") { + return; + } + originalShowToast(msg); +} +window.showToast = showToast; + +// wrap the TOTP modal opener to disable other login buttons only for Basic/OIDC flows +function openTOTPLoginModal() { + originalOpenTOTPLoginModal(); + + const isFormLogin = Boolean(window.__lastLoginData); + if (!isFormLogin) { + // disable Basic‑Auth link + const basicLink = document.querySelector("a[href='login_basic.php']"); + if (basicLink) { + basicLink.style.pointerEvents = 'none'; + basicLink.style.opacity = '0.5'; + } + // disable OIDC button + const oidcBtn = document.getElementById("oidcLoginBtn"); + if (oidcBtn) { + oidcBtn.disabled = true; + oidcBtn.style.opacity = '0.5'; + } + // hide the form login + const authForm = document.getElementById("authForm"); + if (authForm) authForm.style.display = 'none'; + } +} + /* ----------------- Utility Functions ----------------- */ function updateItemsPerPageSelect() { const selectElem = document.querySelector(".form-control.bottom-select"); @@ -85,7 +127,6 @@ function updateAuthenticatedUI(data) { if (typeof data.totp_enabled !== "undefined") { localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false"); } - if (data.username) { localStorage.setItem("username", data.username); } @@ -103,11 +144,8 @@ function updateAuthenticatedUI(data) { restoreBtn.id = "restoreFilesBtn"; restoreBtn.classList.add("btn", "btn-warning"); restoreBtn.innerHTML = 'restore_from_trash'; - if (firstButton) { - insertAfter(restoreBtn, firstButton); - } else { - headerButtons.appendChild(restoreBtn); - } + if (firstButton) insertAfter(restoreBtn, firstButton); + else headerButtons.appendChild(restoreBtn); } restoreBtn.style.display = "block"; @@ -128,7 +166,7 @@ function updateAuthenticatedUI(data) { const adminPanelBtn = document.getElementById("adminPanelBtn"); if (adminPanelBtn) adminPanelBtn.style.display = "none"; } - + if (window.location.hostname !== "demo.filerise.net") { let userPanelBtn = document.getElementById("userPanelBtn"); if (!userPanelBtn) { @@ -136,17 +174,10 @@ function updateAuthenticatedUI(data) { userPanelBtn.id = "userPanelBtn"; userPanelBtn.classList.add("btn", "btn-user"); userPanelBtn.innerHTML = 'account_circle'; - let adminPanelBtn = document.getElementById("adminPanelBtn"); - if (adminPanelBtn) { - insertAfter(userPanelBtn, adminPanelBtn); - } else { - const firstButton = headerButtons.firstElementChild; - if (firstButton) { - insertAfter(userPanelBtn, firstButton); - } else { - headerButtons.appendChild(userPanelBtn); - } - } + const adminBtn = document.getElementById("adminPanelBtn"); + if (adminBtn) insertAfter(userPanelBtn, adminBtn); + else if (firstButton) insertAfter(userPanelBtn, firstButton); + else headerButtons.appendChild(userPanelBtn); userPanelBtn.addEventListener("click", openUserPanel); } else { userPanelBtn.style.display = "block"; @@ -193,6 +224,7 @@ function checkAuthentication(showLoginToast = true) { /* ----------------- Authentication Submission ----------------- */ function submitLogin(data) { setLastLoginData(data); + window.__lastLoginData = data; sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken }) .then(response => { if (response.success) { @@ -220,7 +252,7 @@ function submitLogin(data) { } window.submitLogin = submitLogin; -/* ----------------- Other Helpers and Initialization ----------------- */ +/* ----------------- Other Helpers ----------------- */ window.changeItemsPerPage = function (value) { localStorage.setItem("itemsPerPage", value); if (typeof renderFileTable === "function") renderFileTable(window.currentFolder || "root"); @@ -259,7 +291,7 @@ function loadUserList() { closeRemoveUserModal(); } }) - .catch(() => { }); + .catch(() => {}); } window.loadUserList = loadUserList; @@ -286,7 +318,7 @@ function initAuth() { method: "POST", credentials: "include", headers: { "X-CSRF-Token": window.csrfToken } - }).then(() => window.location.reload(true)).catch(() => { }); + }).then(() => window.location.reload(true)).catch(() => {}); }); document.getElementById("addUserBtn").addEventListener("click", function () { resetUserForm(); @@ -352,7 +384,7 @@ function initAuth() { showToast("Error: " + (data.error || "Could not remove user")); } }) - .catch(() => { }); + .catch(() => {}); }); document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal); document.getElementById("changePasswordBtn").addEventListener("click", function () { @@ -404,13 +436,19 @@ document.addEventListener("DOMContentLoaded", function () { disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true", disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true" }); + const oidcLoginBtn = document.getElementById("oidcLoginBtn"); if (oidcLoginBtn) { oidcLoginBtn.addEventListener("click", () => { - // Redirect to the OIDC auth endpoint. The endpoint can be adjusted if needed. window.location.href = "auth.php?oidc=initiate"; }); } + + // If TOTP is pending, show modal and skip normal auth init + if (window.pendingTOTP) { + openTOTPLoginModal(); + return; + } }); export { initAuth, checkAuthentication }; \ No newline at end of file diff --git a/js/authModals.js b/js/authModals.js index b143278..78bbd8a 100644 --- a/js/authModals.js +++ b/js/authModals.js @@ -3,61 +3,102 @@ import { sendRequest } from './networkUtils.js'; const version = "v1.0.7"; const adminTitle = `Admin Panel ${version}`; -let lastLoginData = null; +let lastLoginData = null; export function setLastLoginData(data) { - lastLoginData = data; + lastLoginData = data; + // expose to auth.js so it can tell form-login vs basic/oidc + window.__lastLoginData = data; } export function openTOTPLoginModal() { - let totpLoginModal = document.getElementById("totpLoginModal"); - const isDarkMode = document.body.classList.contains("dark-mode"); - const modalBg = isDarkMode ? "#2c2c2c" : "#fff"; - const textColor = isDarkMode ? "#e0e0e0" : "#000"; + let totpLoginModal = document.getElementById("totpLoginModal"); + const isDarkMode = document.body.classList.contains("dark-mode"); + const modalBg = isDarkMode ? "#2c2c2c" : "#fff"; + const textColor = isDarkMode ? "#e0e0e0" : "#000"; - if (!totpLoginModal) { - totpLoginModal = document.createElement("div"); - totpLoginModal.id = "totpLoginModal"; - totpLoginModal.style.cssText = ` + if (!totpLoginModal) { + totpLoginModal = document.createElement("div"); + totpLoginModal.id = "totpLoginModal"; + totpLoginModal.style.cssText = ` position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; + top: 0; left: 0; + width: 100vw; height: 100vh; background-color: rgba(0,0,0,0.5); - display: flex; - justify-content: center; - align-items: center; + display: flex; justify-content: center; align-items: center; z-index: 3200; `; - totpLoginModal.innerHTML = ` -
- × + totpLoginModal.innerHTML = ` +
+ ×

Enter TOTP Code

- +
`; - document.body.appendChild(totpLoginModal); - document.getElementById("closeTOTPLoginModal").addEventListener("click", () => { - totpLoginModal.style.display = "none"; - }); - const totpInput = document.getElementById("totpLoginInput"); - totpInput.focus(); - totpInput.addEventListener("input", function () { - if (this.value.trim().length === 6 && lastLoginData) { - lastLoginData.totp_code = this.value.trim(); - totpLoginModal.style.display = "none"; - if (typeof window.submitLogin === "function") { - window.submitLogin(lastLoginData); - } - } - }); - } else { - totpLoginModal.style.display = "flex"; - const modalContent = totpLoginModal.firstElementChild; - modalContent.style.background = modalBg; - modalContent.style.color = textColor; + document.body.appendChild(totpLoginModal); + + document.getElementById("closeTOTPLoginModal").addEventListener("click", () => { + totpLoginModal.style.display = "none"; + }); + + const totpInput = document.getElementById("totpLoginInput"); + totpInput.focus(); + + totpInput.addEventListener("input", function () { + const code = this.value.trim(); + if (code.length === 6) { + // FORM-BASED LOGIN + if (lastLoginData) { + totpLoginModal.style.display = "none"; + lastLoginData.totp_code = code; + window.submitLogin(lastLoginData); + + // BASIC-AUTH / OIDC LOGIN + } else { + // keep modal open until we know the result + fetch("totp_verify.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ totp_code: code }) + }) + .then(res => res.json()) + .then(json => { + if (json.success) { + window.location.href = "index.html"; + } else { + showToast(json.error || json.message || "TOTP verification failed"); + this.value = ""; + totpLoginModal.style.display = "flex"; + totpInput.focus(); + } + }) + .catch(() => { + showToast("TOTP verification failed"); + this.value = ""; + totpLoginModal.style.display = "flex"; + totpInput.focus(); + }); + } + } + }); + } else { + totpLoginModal.style.display = "flex"; + const modalContent = totpLoginModal.firstElementChild; + modalContent.style.background = modalBg; + modalContent.style.color = textColor; + // reset input if reopening + const totpInput = document.getElementById("totpLoginInput"); + if (totpInput) { + totpInput.value = ""; + totpInput.focus(); } + } } export function openUserPanel() { diff --git a/js/fileListView.js b/js/fileListView.js index 89c41c9..4672202 100644 --- a/js/fileListView.js +++ b/js/fileListView.js @@ -297,7 +297,7 @@ export function renderGalleryView(folder) { -
@@ -306,10 +306,23 @@ export function renderGalleryView(folder) { }); galleryHTML += ""; + fileListContainer.innerHTML = galleryHTML; createViewToggleButton(); updateFileActionButtons(); + + // Bind share button clicks + document.querySelectorAll(".share-btn").forEach(btn => { + btn.addEventListener("click", e => { + e.stopPropagation(); + const fileName = btn.getAttribute("data-file"); + const file = fileData.find(f => f.name === fileName); + import('./filePreview.js').then(module => { + module.openShareModal(file, folder); + }); + }); + }); } export function sortFiles(column, folder) { diff --git a/js/filePreview.js b/js/filePreview.js index 07325cb..65f425a 100644 --- a/js/filePreview.js +++ b/js/filePreview.js @@ -253,4 +253,5 @@ export function displayFilePreview(file, container) { } } -window.previewFile = previewFile; \ No newline at end of file +window.previewFile = previewFile; +window.openShareModal = openShareModal; \ No newline at end of file diff --git a/login_basic.php b/login_basic.php index 389e146..f2b938f 100644 --- a/login_basic.php +++ b/login_basic.php @@ -3,6 +3,19 @@ require_once 'config.php'; $usersFile = USERS_DIR . USERS_FILE; // Make sure the users file path is defined +// Helper: retrieve a user's TOTP secret from users.txt +function getUserTOTPSecret($username) { + global $encryptionKey, $usersFile; + if (!file_exists($usersFile)) return null; + foreach (file($usersFile, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $line) { + $parts = explode(':', trim($line)); + if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { + return decryptData($parts[3], $encryptionKey); + } + } + return null; +} + // Reuse the same authentication function function authenticate($username, $password) { @@ -43,15 +56,9 @@ function loadFolderPermission($username) { $permissionsFile = USERS_DIR . 'userPermissions.json'; if (file_exists($permissionsFile)) { $content = file_get_contents($permissionsFile); - // Try to decrypt the content. - $decryptedContent = decryptData($content, $encryptionKey); - if ($decryptedContent !== false) { - $permissions = json_decode($decryptedContent, true); - } else { - $permissions = json_decode($content, true); - } + $decrypted = decryptData($content, $encryptionKey); + $permissions = $decrypted !== false ? json_decode($decrypted, true) : json_decode($content, true); if (is_array($permissions)) { - // Use case-insensitive comparison. foreach ($permissions as $storedUsername => $data) { if (strcasecmp($storedUsername, $username) === 0 && isset($data['folderOnly'])) { return (bool)$data['folderOnly']; @@ -59,7 +66,7 @@ function loadFolderPermission($username) { } } } - return false; // Default if not set. + return false; } // Check if the user has sent HTTP Basic auth credentials. @@ -68,39 +75,46 @@ if (!isset($_SERVER['PHP_AUTH_USER'])) { header('HTTP/1.0 401 Unauthorized'); echo 'Authorization Required'; exit; -} else { - $username = trim($_SERVER['PHP_AUTH_USER']); - $password = trim($_SERVER['PHP_AUTH_PW']); - - // Validate username format (optional) - if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) { - header('WWW-Authenticate: Basic realm="FileRise Login"'); - header('HTTP/1.0 401 Unauthorized'); - echo 'Invalid username format'; - exit; - } - - // Attempt authentication - $roleFromAuth = authenticate($username, $password); - if ($roleFromAuth !== false) { - // Use getUserRole() to determine the user's role from the file - $actualRole = getUserRole($username); - session_regenerate_id(true); - $_SESSION["authenticated"] = true; - $_SESSION["username"] = $username; - $_SESSION["isAdmin"] = ($actualRole === "1"); - // Set the folderOnly flag based on userPermissions.json. - $_SESSION["folderOnly"] = loadFolderPermission($username); - - // Redirect to the main page (or output JSON for testing) - header("Location: index.html"); - exit; - } else { - // Invalid credentials; prompt again - header('WWW-Authenticate: Basic realm="FileRise Login"'); - header('HTTP/1.0 401 Unauthorized'); - echo 'Invalid credentials'; - exit; - } } + +$username = trim($_SERVER['PHP_AUTH_USER']); +$password = trim($_SERVER['PHP_AUTH_PW']); + +// Validate username format (optional) +if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) { + header('WWW-Authenticate: Basic realm="FileRise Login"'); + header('HTTP/1.0 401 Unauthorized'); + echo 'Invalid username format'; + exit; +} + +// Attempt authentication +$roleFromAuth = authenticate($username, $password); +if ($roleFromAuth !== false) { + // --- NEW: check for TOTP secret --- + $secret = getUserTOTPSecret($username); + if ($secret) { + // hold user & secret in session and ask client for TOTP + $_SESSION['pending_login_user'] = $username; + $_SESSION['pending_login_secret'] = $secret; + header("Location: index.html?totp_required=1"); + exit; + } + + // no TOTP, proceed as before + session_regenerate_id(true); + $_SESSION["authenticated"] = true; + $_SESSION["username"] = $username; + $_SESSION["isAdmin"] = (getUserRole($username) === "1"); + $_SESSION["folderOnly"] = loadFolderPermission($username); + + header("Location: index.html"); + exit; +} + +// Invalid credentials; prompt again +header('WWW-Authenticate: Basic realm="FileRise Login"'); +header('HTTP/1.0 401 Unauthorized'); +echo 'Invalid credentials'; +exit; ?> \ No newline at end of file diff --git a/totp_verify.php b/totp_verify.php index 532aae6..96d6322 100644 --- a/totp_verify.php +++ b/totp_verify.php @@ -1,84 +1,153 @@ "Not authenticated"]); - exit; +// Secure session cookie +session_set_cookie_params([ + 'lifetime' => 0, + 'path' => '/', + 'domain' => '', // your domain + 'secure' => true, // only over HTTPS + 'httponly' => true, + 'samesite' => 'Lax' +]); +if (session_status() !== PHP_SESSION_ACTIVE) { + session_start(); } -// Verify CSRF token from request headers. -$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; -if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { - http_response_code(403); - echo json_encode(["error" => "Invalid CSRF token"]); - exit; -} - -// Ensure Content-Type is JSON. +// JSON + CSP header('Content-Type: application/json'); +header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';"); -// Read and decode the JSON request body. -$input = json_decode(file_get_contents("php://input"), true); -if (!isset($input['totp_code']) || strlen(trim($input['totp_code'])) !== 6 || !ctype_digit(trim($input['totp_code']))) { - http_response_code(400); - echo json_encode(["error" => "A valid 6-digit TOTP code is required"]); - exit; -} +try { + // standardized error helper + function respond($status, $code, $message, $data = []) { + http_response_code($code); + echo json_encode([ + 'status' => $status, + 'code' => $code, + 'message' => $message, + 'data' => $data + ]); + exit; + } -$totpCode = trim($input['totp_code']); -$username = $_SESSION['username'] ?? ''; -if (empty($username)) { - http_response_code(400); - echo json_encode(["error" => "Username not found in session"]); - exit; -} + // Rate‑limit TOTP attempts + if (!isset($_SESSION['totp_failures'])) { + $_SESSION['totp_failures'] = 0; + } + if ($_SESSION['totp_failures'] >= 5) { + respond('error', 429, 'Too many TOTP attempts. Please try again later.'); + } -/** - * Retrieves the current user's TOTP secret from users.txt. - * - * @param string $username - * @return string|null The decrypted TOTP secret or null if not found. - */ -function getUserTOTPSecret($username) { - global $encryptionKey; - // Define the path to your users file. - $usersFile = USERS_DIR . USERS_FILE; - if (!file_exists($usersFile)) { + /** + * Helper: Get a user's role from users.txt + */ + function getUserRole(string $username): ?string { + $usersFile = USERS_DIR . USERS_FILE; + if (!file_exists($usersFile)) return null; + foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $parts = explode(':', trim($line)); + if (count($parts) >= 3 && $parts[0] === $username) { + return trim($parts[2]); + } + } return null; } - $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - $parts = explode(':', trim($line)); - // Assuming format: username:hashedPassword:role:encryptedTOTPSecret - if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { - return decryptData($parts[3], $encryptionKey); - } + + // Must be authenticated or pending TOTP + if ( + !( + (isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) + || isset($_SESSION['pending_login_user']) + ) + ) { + respond('error', 403, 'Not authenticated'); } - return null; -} -// Retrieve the user's TOTP secret. -$totpSecret = getUserTOTPSecret($username); -if (!$totpSecret) { + // CSRF check + $csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; + if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { + respond('error', 403, 'Invalid CSRF token'); + } + + // Parse & validate input + $input = json_decode(file_get_contents("php://input"), true); + $code = trim($input['totp_code'] ?? ''); + if (!preg_match('/^\d{6}$/', $code)) { + respond('error', 400, 'A valid 6-digit TOTP code is required'); + } + + // LOGIN flow (Basic‑Auth or OIDC) + if (isset($_SESSION['pending_login_user'])) { + $username = $_SESSION['pending_login_user']; + $totpSecret = $_SESSION['pending_login_secret']; + $tfa = new \RobThree\Auth\TwoFactorAuth('FileRise'); + + if (!$tfa->verifyCode($totpSecret, $code)) { + $_SESSION['totp_failures']++; + respond('error', 400, 'Invalid TOTP code'); + } + + // success → complete login + session_regenerate_id(true); + $_SESSION['authenticated'] = true; + $_SESSION['username'] = $username; + $_SESSION['isAdmin'] = (getUserRole($username) === "1"); + $_SESSION['folderOnly'] = loadUserPermissions($username); + + unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'], $_SESSION['totp_failures']); + + respond('ok', 200, 'Login successful'); + } + + // SETUP‑VERIFICATION flow + $username = $_SESSION['username'] ?? ''; + if (!$username) { + respond('error', 400, 'Username not found in session'); + } + + /** + * Helper: retrieve the user's TOTP secret from users.txt + */ + function getUserTOTPSecret(string $username): ?string { + global $encryptionKey; + $usersFile = USERS_DIR . USERS_FILE; + if (!file_exists($usersFile)) return null; + foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $parts = explode(':', trim($line)); + if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { + return decryptData($parts[3], $encryptionKey); + } + } + return null; + } + + $totpSecret = getUserTOTPSecret($username); + if (!$totpSecret) { + respond('error', 500, 'TOTP secret not found. Please set up TOTP again.'); + } + + $tfa = new \RobThree\Auth\TwoFactorAuth('FileRise'); + if (!$tfa->verifyCode($totpSecret, $code)) { + $_SESSION['totp_failures']++; + respond('error', 400, 'Invalid TOTP code'); + } + + // success + unset($_SESSION['totp_failures']); + respond('ok', 200, 'TOTP successfully verified'); + +} catch (\Throwable $e) { + // log error internally, then generic response + error_log("totp_verify error: " . $e->getMessage()); http_response_code(500); - echo json_encode(["error" => "TOTP secret not found. Please try setting up TOTP again."]); + echo json_encode([ + 'status' => 'error', + 'code' => 500, + 'message' => 'Internal server error' + ]); exit; -} - -// Verify the provided TOTP code. -$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise'); -if (!$tfa->verifyCode($totpSecret, $totpCode)) { - http_response_code(400); - echo json_encode(["error" => "Invalid TOTP code."]); - exit; -} - -// If needed, you could update a flag or store the confirmation in the user record here. - -// Return a successful response. -echo json_encode(["success" => true, "message" => "TOTP successfully verified."]); -?> \ No newline at end of file +} \ No newline at end of file diff --git a/uploads/.gitkeep b/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/uploads/.htaccess b/uploads/.htaccess new file mode 100644 index 0000000..651f7db --- /dev/null +++ b/uploads/.htaccess @@ -0,0 +1,7 @@ + + php_flag engine off + + + php_flag engine off + + Options -Indexes \ No newline at end of file diff --git a/users/.gitkeep b/users/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/users/.htaccess b/users/.htaccess new file mode 100644 index 0000000..b94f347 --- /dev/null +++ b/users/.htaccess @@ -0,0 +1,3 @@ + + Require all denied + \ No newline at end of file