getMessage()); http_response_code(500); echo json_encode(['error' => 'Internal Server Error']); exit(); }); // Decode any JSON payload $data = json_decode(file_get_contents('php://input'), true) ?: []; $username = trim($data['username'] ?? ''); $password = trim($data['password'] ?? ''); $totpCode = trim($data['totp_code'] ?? ''); $rememberMe = !empty($data['remember_me']); // // 1) TOTP‑only step: user already passed credentials and we asked for TOTP, // now they POST just totp_code. // if ($totpCode && isset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'])) { $username = $_SESSION['pending_login_user']; $secret = $_SESSION['pending_login_secret']; $rememberMe = $_SESSION['pending_login_remember_me'] ?? false; $tfa = new TwoFactorAuth(new GoogleChartsQrCodeProvider(), 'FileRise', 6, 30, Algorithm::Sha1); if (! $tfa->verifyCode($secret, $totpCode)) { echo json_encode(['error' => 'Invalid TOTP code']); exit(); } // clear the pending markers unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret']); // now finish login $this->finalizeLogin($username, $rememberMe); } // // 2) OIDC flow // $oidcAction = $_GET['oidc'] ?? null; if (! $oidcAction && isset($_GET['code'])) { $oidcAction = 'callback'; } if ($oidcAction) { $cfg = AdminModel::getConfig(); $oidc = new OpenIDConnectClient( $cfg['oidc']['providerUrl'], $cfg['oidc']['clientId'], $cfg['oidc']['clientSecret'] ); $oidc->setRedirectURL($cfg['oidc']['redirectUri']); $oidc->addScope(['openid','profile','email']); if ($oidcAction === 'callback') { try { $oidc->authenticate(); $username = $oidc->requestUserInfo('preferred_username') ?: $oidc->requestUserInfo('email') ?: $oidc->requestUserInfo('sub'); // check if this user has a TOTP secret $totp_secret = null; $usersFile = USERS_DIR . USERS_FILE; 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 && $parts[3] !== '') { $totp_secret = decryptData($parts[3], $GLOBALS['encryptionKey']); break; } } } if ($totp_secret) { $_SESSION['pending_login_user'] = $username; $_SESSION['pending_login_secret'] = $totp_secret; header('Location: /index.html?totp_required=1'); exit(); } // no TOTP → finish immediately $this->finishBrowserLogin($username); } catch (\Exception $e) { error_log("OIDC auth error: " . $e->getMessage()); http_response_code(401); echo json_encode(['error' => 'Authentication failed.']); exit(); } } else { // initial OIDC redirect 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(); } } } // // 3) Form‑based / AJAX login // if (! $username || ! $password) { http_response_code(400); echo json_encode(['error' => 'Username and password are required']); exit(); } if (! preg_match(REGEX_USER, $username)) { http_response_code(400); echo json_encode(['error' => 'Invalid username format']); exit(); } // rate‑limit $ip = $_SERVER['REMOTE_ADDR']; $attemptsFile = USERS_DIR . 'failed_logins.json'; $failed = AuthModel::loadFailedAttempts($attemptsFile); if ( isset($failed[$ip]) && $failed[$ip]['count'] >= 5 && time() - $failed[$ip]['last_attempt'] < 30 * 60 ) { http_response_code(429); echo json_encode(['error' => 'Too many failed login attempts. Please try again later.']); exit(); } $user = AuthModel::authenticate($username, $password); if ($user === false) { // record failure $failed[$ip] = [ 'count' => ($failed[$ip]['count'] ?? 0) + 1, 'last_attempt' => time() ]; AuthModel::saveFailedAttempts($attemptsFile, $failed); http_response_code(401); echo json_encode(['error' => 'Invalid credentials']); exit(); } // if this account has TOTP, ask for it if (! empty($user['totp_secret'])) { $_SESSION['pending_login_user'] = $username; $_SESSION['pending_login_secret'] = $user['totp_secret']; $_SESSION['pending_login_remember_me'] = $rememberMe; echo json_encode(['totp_required' => true]); exit(); } // otherwise clear rate‑limit & finish if (isset($failed[$ip])) { unset($failed[$ip]); AuthModel::saveFailedAttempts($attemptsFile, $failed); } $this->finalizeLogin($username, $rememberMe); } /** * Finalize an AJAX‐style login (form/basic/TOTP) by * issuing the session, remember‑me cookie, and JSON payload. */ protected function finalizeLogin(string $username, bool $rememberMe): void { session_regenerate_id(true); $_SESSION['authenticated'] = true; $_SESSION['username'] = $username; $_SESSION['isAdmin'] = (AuthModel::getUserRole($username) === '1'); $perms = loadUserPermissions($username); $_SESSION['folderOnly'] = $perms['folderOnly'] ?? false; $_SESSION['readOnly'] = $perms['readOnly'] ?? false; $_SESSION['disableUpload'] = $perms['disableUpload'] ?? false; // remember‑me if ($rememberMe) { $tokFile = USERS_DIR . 'persistent_tokens.json'; $token = bin2hex(random_bytes(32)); $expiry = time() + 30 * 24 * 60 * 60; $all = []; if (file_exists($tokFile)) { $dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']); $all = json_decode($dec, true) ?: []; } $all[$token] = [ 'username' => $username, 'expiry' => $expiry, 'isAdmin' => $_SESSION['isAdmin'] ]; file_put_contents( $tokFile, encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']), LOCK_EX ); $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true); setcookie( session_name(), session_id(), $expiry, '/', '', $secure, true ); session_regenerate_id(true); } echo json_encode([ 'status' => 'ok', 'success' => 'Login successful', 'isAdmin' => $_SESSION['isAdmin'], 'folderOnly' => $_SESSION['folderOnly'], 'readOnly' => $_SESSION['readOnly'], 'disableUpload' => $_SESSION['disableUpload'], 'username' => $username ]); exit(); } /** * A version of finalizeLogin() that ends in a browser redirect * (used for OIDC non‑AJAX flows). */ protected function finishBrowserLogin(string $username): void { session_regenerate_id(true); $_SESSION['authenticated'] = true; $_SESSION['username'] = $username; $_SESSION['isAdmin'] = (AuthModel::getUserRole($username) === '1'); $perms = loadUserPermissions($username); $_SESSION['folderOnly'] = $perms['folderOnly'] ?? false; $_SESSION['readOnly'] = $perms['readOnly'] ?? false; $_SESSION['disableUpload'] = $perms['disableUpload'] ?? false; header('Location: /index.html'); exit(); } public function checkAuth(): void { // 1) Remember-me re-login if (empty($_SESSION['authenticated']) && !empty($_COOKIE['remember_me_token'])) { $payload = AuthModel::validateRememberToken($_COOKIE['remember_me_token']); if ($payload) { $old = $_SESSION['csrf_token'] ?? bin2hex(random_bytes(32)); session_regenerate_id(true); $_SESSION['csrf_token'] = $old; $_SESSION['authenticated'] = true; $_SESSION['username'] = $payload['username']; $_SESSION['isAdmin'] = !empty($payload['isAdmin']); $_SESSION['folderOnly'] = $payload['folderOnly'] ?? false; $_SESSION['readOnly'] = $payload['readOnly'] ?? false; $_SESSION['disableUpload'] = $payload['disableUpload'] ?? false; // regenerate CSRF if you use one // TOTP enabled? (same logic as below) $usersFile = USERS_DIR . USERS_FILE; $totp = false; if (file_exists($usersFile)) { foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { $parts = explode(':', trim($line)); if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) { $totp = true; break; } } } echo json_encode([ 'authenticated' => true, 'csrf_token' => $_SESSION['csrf_token'], 'isAdmin' => $_SESSION['isAdmin'], 'totp_enabled' => $totp, 'username' => $_SESSION['username'], 'folderOnly' => $_SESSION['folderOnly'], 'readOnly' => $_SESSION['readOnly'], 'disableUpload' => $_SESSION['disableUpload'] ]); exit(); } } $usersFile = USERS_DIR . USERS_FILE; // 2) Setup mode? if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') { error_log("checkAuth: setup mode"); echo json_encode(['setup' => true]); exit(); } // 3) Session-based auth if (empty($_SESSION['authenticated'])) { echo json_encode(['authenticated' => false]); exit(); } // 4) TOTP enabled? $totp = false; foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { $parts = explode(':', trim($line)); if ($parts[0] === ($_SESSION['username'] ?? '') && !empty($parts[3])) { $totp = true; break; } } // 5) Final response $resp = [ 'authenticated' => true, 'isAdmin' => !empty($_SESSION['isAdmin']), 'totp_enabled' => $totp, 'username' => $_SESSION['username'], 'folderOnly' => $_SESSION['folderOnly'] ?? false, 'readOnly' => $_SESSION['readOnly'] ?? false, 'disableUpload' => $_SESSION['disableUpload'] ?? false ]; echo json_encode($resp); exit(); } public function getToken(): void { // 1) Ensure session and CSRF token exist if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } // 2) Emit headers header('Content-Type: application/json'); header('X-CSRF-Token: ' . $_SESSION['csrf_token']); // 3) Return JSON payload echo json_encode([ 'csrf_token' => $_SESSION['csrf_token'], 'share_url' => SHARE_URL ]); exit; } public function loginBasic(): void { // Set header for plain-text or JSON as needed. header('Content-Type: application/json'); // Check for HTTP Basic auth credentials. if (!isset($_SERVER['PHP_AUTH_USER'])) { header('WWW-Authenticate: Basic realm="FileRise Login"'); header('HTTP/1.0 401 Unauthorized'); echo 'Authorization Required'; exit; } $username = trim($_SERVER['PHP_AUTH_USER']); $password = trim($_SERVER['PHP_AUTH_PW']); // Validate username format. if (!preg_match(REGEX_USER, $username)) { header('WWW-Authenticate: Basic realm="FileRise Login"'); header('HTTP/1.0 401 Unauthorized'); echo 'Invalid username format'; exit; } // Attempt authentication. $role = AuthModel::authenticate($username, $password); if ($role !== false) { // Check for TOTP secret. $secret = AuthModel::getUserTOTPSecret($username); if ($secret) { // If TOTP is required, store pending values and redirect to prompt for TOTP. $_SESSION['pending_login_user'] = $username; $_SESSION['pending_login_secret'] = $secret; header("Location: /index.html?totp_required=1"); exit; } // Finalize login. session_regenerate_id(true); $_SESSION["authenticated"] = true; $_SESSION["username"] = $username; $_SESSION["isAdmin"] = (AuthModel::getUserRole($username) === "1"); // load _all_ the permissions $userPerms = loadUserPermissions($username); $_SESSION["folderOnly"] = $userPerms["folderOnly"] ?? false; $_SESSION["readOnly"] = $userPerms["readOnly"] ?? false; $_SESSION["disableUpload"] = $userPerms["disableUpload"] ?? false; 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; } public function logout(): void { // Retrieve headers and check CSRF token. $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; // Log mismatch but do not prevent logout. if (isset($_SESSION['csrf_token']) && $receivedToken !== $_SESSION['csrf_token']) { error_log("CSRF token mismatch on logout. Proceeding with logout."); } // Remove the "remember_me_token" from persistent tokens. if (isset($_COOKIE['remember_me_token'])) { $token = $_COOKIE['remember_me_token']; $persistentTokensFile = USERS_DIR . 'persistent_tokens.json'; if (file_exists($persistentTokensFile)) { $encryptedContent = file_get_contents($persistentTokensFile); $decryptedContent = decryptData($encryptedContent, $GLOBALS['encryptionKey']); $persistentTokens = json_decode($decryptedContent, true); if (is_array($persistentTokens) && isset($persistentTokens[$token])) { unset($persistentTokens[$token]); $newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']); file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX); } } // Clear the cookie. $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true); } // Clear session data. $_SESSION = []; // Clear the session cookie. if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); setcookie( session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"] ); } // Destroy the session. session_destroy(); // Redirect the user to the login page (or index) with a logout flag. header("Location: /index.html?logout=1"); exit; } }