From a81d9cb940d4a01ff2603e14c00931d27f2880b9 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 23 Apr 2025 01:47:27 -0400 Subject: [PATCH] Enhance remember me --- CHANGELOG.md | 16 +++++++ public/js/auth.js | 1 + src/controllers/authController.php | 77 +++++++++++++++++++++++------- src/models/AuthModel.php | 60 ++++++++++++++++++----- 4 files changed, 124 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 834c81c..5148c1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## Changes 4/23/2025 + +**AuthModel** + +- **Added** `validateRememberToken(string $token): ?array` + - Reads and decrypts `persistent_tokens.json` + - Verifies token exists and hasn’t expired + - Returns stored payload (`username`, `expiry`, `isAdmin`, etc.) or `null` if invalid + +**authController (checkAuth)** + +- **Enhanced** “remember-me” re-login path at top of `checkAuth()` + - Calls `AuthModel::validateRememberToken()` when session is missing but `remember_me_token` cookie present + - Repopulates `$_SESSION['authenticated']`, `username`, `isAdmin`, `folderOnly`, `readOnly`, `disableUpload` from payload + - Regenerates session ID and CSRF token, then immediately returns JSON and exits + ## Changes 4/22/2025 v1.2.3 - Support for custom PUID/PGID via `PUID`/`PGID` environment variables, replacing the need to run the container with `--user` diff --git a/public/js/auth.js b/public/js/auth.js index ed85832..ba59cc2 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -228,6 +228,7 @@ function checkAuthentication(showLoginToast = true) { } window.setupMode = false; if (data.authenticated) { + localStorage.setItem('isAdmin', data.isAdmin ? 'true' : 'false'); localStorage.setItem("folderOnly", data.folderOnly); localStorage.setItem("readOnly", data.readOnly); localStorage.setItem("disableUpload", data.disableUpload); diff --git a/src/controllers/authController.php b/src/controllers/authController.php index 5645dfa..3166ffa 100644 --- a/src/controllers/authController.php +++ b/src/controllers/authController.php @@ -238,28 +238,28 @@ class AuthController $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(), @@ -269,7 +269,7 @@ class AuthController $secure, true ); - + session_regenerate_id(true); } @@ -341,40 +341,83 @@ class AuthController public function checkAuth(): void { - header('Content-Type: application/json'); + + // 1) Remember-me re-login + if (empty($_SESSION['authenticated']) && !empty($_COOKIE['remember_me_token'])) { + $payload = AuthModel::validateRememberToken($_COOKIE['remember_me_token']); + if ($payload) { + session_regenerate_id(true); + $_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 + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + + // 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, + 'isAdmin' => $_SESSION['isAdmin'], + 'totp_enabled' => $totp, + 'username' => $_SESSION['username'], + 'folderOnly' => $_SESSION['folderOnly'], + 'readOnly' => $_SESSION['readOnly'], + 'disableUpload' => $_SESSION['disableUpload'] + ]); + exit(); + } + } + $usersFile = USERS_DIR . USERS_FILE; - // setup mode? + // 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(); } - // TOTP enabled? + // 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])) { + if ($parts[0] === ($_SESSION['username'] ?? '') && !empty($parts[3])) { $totp = true; break; } } - $isAdmin = ((int)AuthModel::getUserRole($_SESSION['username']) === 1); + // 5) Final response $resp = [ 'authenticated' => true, - 'isAdmin' => $isAdmin, - 'totp_enabled' => $totp, - 'username' => $_SESSION['username'], - 'folderOnly' => $_SESSION['folderOnly'] ?? false, - 'readOnly' => $_SESSION['readOnly'] ?? false, + '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(); } diff --git a/src/models/AuthModel.php b/src/models/AuthModel.php index 37c8cf8..957ef92 100644 --- a/src/models/AuthModel.php +++ b/src/models/AuthModel.php @@ -3,7 +3,8 @@ require_once PROJECT_ROOT . '/config/config.php'; -class AuthModel { +class AuthModel +{ /** * Retrieves the user's role from the users file. @@ -11,7 +12,8 @@ class AuthModel { * @param string $username * @return string|null The role string (e.g. "1" for admin) or null if not found. */ - public static function getUserRole(string $username): ?string { + public static function getUserRole(string $username): ?string + { $usersFile = USERS_DIR . USERS_FILE; if (file_exists($usersFile)) { foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { @@ -23,7 +25,7 @@ class AuthModel { } return null; } - + /** * Authenticates the user using form-based credentials. * @@ -31,7 +33,8 @@ class AuthModel { * @param string $password * @return array|false Returns an associative array with user data (role, totp_secret) on success or false on failure. */ - public static function authenticate(string $username, string $password) { + public static function authenticate(string $username, string $password) + { $usersFile = USERS_DIR . USERS_FILE; if (!file_exists($usersFile)) { return false; @@ -51,14 +54,15 @@ class AuthModel { } return false; } - + /** * Loads failed login attempts from a file. * * @param string $file * @return array */ - public static function loadFailedAttempts(string $file): array { + public static function loadFailedAttempts(string $file): array + { if (file_exists($file)) { $data = json_decode(file_get_contents($file), true); if (is_array($data)) { @@ -67,7 +71,7 @@ class AuthModel { } return []; } - + /** * Saves failed login attempts into a file. * @@ -75,17 +79,19 @@ class AuthModel { * @param array $data * @return void */ - public static function saveFailedAttempts(string $file, array $data): void { + public static function saveFailedAttempts(string $file, array $data): void + { file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX); } - + /** * Retrieves a user's TOTP secret from the users file. * * @param string $username * @return string|null Returns the decrypted TOTP secret or null if not set. */ - public static function getUserTOTPSecret(string $username): ?string { + public static function getUserTOTPSecret(string $username): ?string + { $usersFile = USERS_DIR . USERS_FILE; if (!file_exists($usersFile)) { return null; @@ -98,14 +104,15 @@ class AuthModel { } return null; } - + /** * Loads the folder-only permission for a given user. * * @param string $username * @return bool */ - public static function loadFolderPermission(string $username): bool { + public static function loadFolderPermission(string $username): bool + { $permissionsFile = USERS_DIR . 'userPermissions.json'; if (file_exists($permissionsFile)) { $content = file_get_contents($permissionsFile); @@ -121,4 +128,31 @@ class AuthModel { } return false; } -} \ No newline at end of file + + /** + * Validate a remember-me token and return its stored payload. + * + * @param string $token + * @return array|null Returns ['username'=>…, 'expiry'=>…, 'isAdmin'=>…] or null if invalid/expired. + */ + public static function validateRememberToken(string $token): ?array + { + $tokFile = USERS_DIR . 'persistent_tokens.json'; + if (! file_exists($tokFile)) { + return null; + } + + // Decrypt and decode the full token store + $encrypted = file_get_contents($tokFile); + $json = decryptData($encrypted, $GLOBALS['encryptionKey']); + $all = json_decode($json, true) ?: []; + + // Lookup and expiry check + if (empty($all[$token]) || !isset($all[$token]['expiry']) || $all[$token]['expiry'] < time()) { + return null; + } + + // Valid token—return its payload + return $all[$token]; + } +}