$status, 'code' => $code, 'message' => $message, 'data' => $data ]); 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.'); } /** * 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; } // Must be authenticated or pending TOTP if ( !( (isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']) ) ) { respond('error', 403, 'Not authenticated'); } $headers = array_change_key_case(getallheaders(), CASE_LOWER); $csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['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( new GoogleChartsQrCodeProvider(), // QR code provider 'FileRise', // issuer 6, // number of digits 30, // period in seconds Algorithm::Sha1 // Correct enum case name from your enum ); 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( new GoogleChartsQrCodeProvider(), // QR code provider 'FileRise', // issuer 6, // number of digits 30, // period in seconds Algorithm::Sha1 // Correct enum case name from your enum ); 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([ 'status' => 'error', 'code' => 500, 'message' => 'Internal server error' ]); exit; }