Enhance remember me

This commit is contained in:
Ryan
2025-04-23 01:47:27 -04:00
committed by GitHub
parent 13b8871200
commit a81d9cb940
4 changed files with 124 additions and 30 deletions

View File

@@ -1,5 +1,21 @@
# Changelog # Changelog
## Changes 4/23/2025
**AuthModel**
- **Added** `validateRememberToken(string $token): ?array`
- Reads and decrypts `persistent_tokens.json`
- Verifies token exists and hasnt 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 ## 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` - Support for custom PUID/PGID via `PUID`/`PGID` environment variables, replacing the need to run the container with `--user`

View File

@@ -228,6 +228,7 @@ function checkAuthentication(showLoginToast = true) {
} }
window.setupMode = false; window.setupMode = false;
if (data.authenticated) { if (data.authenticated) {
localStorage.setItem('isAdmin', data.isAdmin ? 'true' : 'false');
localStorage.setItem("folderOnly", data.folderOnly); localStorage.setItem("folderOnly", data.folderOnly);
localStorage.setItem("readOnly", data.readOnly); localStorage.setItem("readOnly", data.readOnly);
localStorage.setItem("disableUpload", data.disableUpload); localStorage.setItem("disableUpload", data.disableUpload);

View File

@@ -341,40 +341,83 @@ class AuthController
public function checkAuth(): void 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; $usersFile = USERS_DIR . USERS_FILE;
// setup mode? // 2) Setup mode?
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') { if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
error_log("checkAuth: setup mode"); error_log("checkAuth: setup mode");
echo json_encode(['setup' => true]); echo json_encode(['setup' => true]);
exit(); exit();
} }
// 3) Session-based auth
if (empty($_SESSION['authenticated'])) { if (empty($_SESSION['authenticated'])) {
echo json_encode(['authenticated' => false]); echo json_encode(['authenticated' => false]);
exit(); exit();
} }
// TOTP enabled? // 4) TOTP enabled?
$totp = false; $totp = false;
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$parts = explode(':', trim($line)); $parts = explode(':', trim($line));
if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) { if ($parts[0] === ($_SESSION['username'] ?? '') && !empty($parts[3])) {
$totp = true; $totp = true;
break; break;
} }
} }
$isAdmin = ((int)AuthModel::getUserRole($_SESSION['username']) === 1); // 5) Final response
$resp = [ $resp = [
'authenticated' => true, 'authenticated' => true,
'isAdmin' => $isAdmin, 'isAdmin' => !empty($_SESSION['isAdmin']),
'totp_enabled' => $totp, 'totp_enabled' => $totp,
'username' => $_SESSION['username'], 'username' => $_SESSION['username'],
'folderOnly' => $_SESSION['folderOnly'] ?? false, 'folderOnly' => $_SESSION['folderOnly'] ?? false,
'readOnly' => $_SESSION['readOnly'] ?? false, 'readOnly' => $_SESSION['readOnly'] ?? false,
'disableUpload' => $_SESSION['disableUpload'] ?? false 'disableUpload' => $_SESSION['disableUpload'] ?? false
]; ];
echo json_encode($resp); echo json_encode($resp);
exit(); exit();
} }

View File

@@ -3,7 +3,8 @@
require_once PROJECT_ROOT . '/config/config.php'; require_once PROJECT_ROOT . '/config/config.php';
class AuthModel { class AuthModel
{
/** /**
* Retrieves the user's role from the users file. * Retrieves the user's role from the users file.
@@ -11,7 +12,8 @@ class AuthModel {
* @param string $username * @param string $username
* @return string|null The role string (e.g. "1" for admin) or null if not found. * @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; $usersFile = USERS_DIR . USERS_FILE;
if (file_exists($usersFile)) { if (file_exists($usersFile)) {
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
@@ -31,7 +33,8 @@ class AuthModel {
* @param string $password * @param string $password
* @return array|false Returns an associative array with user data (role, totp_secret) on success or false on failure. * @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; $usersFile = USERS_DIR . USERS_FILE;
if (!file_exists($usersFile)) { if (!file_exists($usersFile)) {
return false; return false;
@@ -58,7 +61,8 @@ class AuthModel {
* @param string $file * @param string $file
* @return array * @return array
*/ */
public static function loadFailedAttempts(string $file): array { public static function loadFailedAttempts(string $file): array
{
if (file_exists($file)) { if (file_exists($file)) {
$data = json_decode(file_get_contents($file), true); $data = json_decode(file_get_contents($file), true);
if (is_array($data)) { if (is_array($data)) {
@@ -75,7 +79,8 @@ class AuthModel {
* @param array $data * @param array $data
* @return void * @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); file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX);
} }
@@ -85,7 +90,8 @@ class AuthModel {
* @param string $username * @param string $username
* @return string|null Returns the decrypted TOTP secret or null if not set. * @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; $usersFile = USERS_DIR . USERS_FILE;
if (!file_exists($usersFile)) { if (!file_exists($usersFile)) {
return null; return null;
@@ -105,7 +111,8 @@ class AuthModel {
* @param string $username * @param string $username
* @return bool * @return bool
*/ */
public static function loadFolderPermission(string $username): bool { public static function loadFolderPermission(string $username): bool
{
$permissionsFile = USERS_DIR . 'userPermissions.json'; $permissionsFile = USERS_DIR . 'userPermissions.json';
if (file_exists($permissionsFile)) { if (file_exists($permissionsFile)) {
$content = file_get_contents($permissionsFile); $content = file_get_contents($permissionsFile);
@@ -121,4 +128,31 @@ class AuthModel {
} }
return false; return false;
} }
/**
* 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];
}
} }