Bladeren bron

Implement sudo mode / reauthentication (#7753)

* Implement sudo mode / reauthentication

* i18n: fr

* generate flags

* Improvements

* Remove HMAC check
* Don't require reauth to access logs when signed in as admin
* Notify user of bad login via notification instead

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Inverle 8 maanden geleden
bovenliggende
commit
3ce64d271b

+ 33 - 0
app/Controllers/authController.php

@@ -21,6 +21,10 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
 			Minz_Error::error(403);
 		}
 
+		if (FreshRSS_Auth::requestReauth()) {
+			return;
+		}
+
 		FreshRSS_View::prependTitle(_t('admin.auth.title') . ' · ');
 
 		if (Minz_Request::isPost()) {
@@ -219,6 +223,35 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
 		}
 	}
 
+	public function reauthAction(): void {
+		if (!FreshRSS_Auth::hasAccess()) {
+			Minz_Error::error(403);
+			return;
+		}
+		/** @var array{c?: string, a?: string, params?: array<string, mixed>} $redirect */
+		$redirect = Minz_Url::unserialize(Minz_Request::paramString('r'));
+		if (!FreshRSS_Auth::needsReauth()) {
+			Minz_Request::forward($redirect, true);
+			return;
+		}
+		if (Minz_Request::isPost()) {
+			$username = Minz_User::name() ?? '';
+			$nonce = Minz_Session::paramString('nonce');
+			$challenge = Minz_Request::paramString('challenge');
+			if (!FreshRSS_FormAuth::checkCredentials(
+				$username, FreshRSS_Context::userConf()->passwordHash, $nonce, $challenge
+				)) {
+				Minz_Request::setBadNotification(_t('feedback.auth.login.invalid'));
+			} else {
+				Minz_Session::_param('lastReauth', time());
+				Minz_Request::forward($redirect, true);
+				return;
+			}
+		}
+		FreshRSS_View::prependTitle(_t('gen.auth.reauth.title') . ' · ');
+		FreshRSS_View::appendScript(Minz_Url::display('/scripts/vendor/bcrypt.js?' . @filemtime(PUBLIC_PATH . '/scripts/vendor/bcrypt.js')));
+	}
+
 	/**
 	 * This action removes all accesses of the current user.
 	 */

+ 4 - 0
app/Controllers/updateController.php

@@ -270,6 +270,10 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
 			Minz_Request::forward(['c' => 'update'], true);
 		}
 
+		if (FreshRSS_Auth::requestReauth()) {
+			return;
+		}
+
 		if (Minz_Request::paramBoolean('post_conf')) {
 			if (self::isGit()) {
 				$res = !self::hasGitUpdate();

+ 50 - 7
app/Controllers/userController.php

@@ -72,6 +72,10 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 		}
 
 		if (Minz_Request::isPost()) {
+			if (self::reauthRedirect()) {
+				return;
+			}
+
 			$username = Minz_Request::paramString('username');
 			$newPasswordPlain = Minz_User::name() !== $username ? Minz_Request::paramString('newPasswordPlain', true) : '';
 
@@ -190,21 +194,41 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 		}
 	}
 
+	public static function reauthRedirect(): bool {
+		$url_redirect = [
+			'c' => 'user',
+			'a' => 'manage',
+			'params' => [],
+		];
+		$username = Minz_Request::paramStringNull('username');
+		if ($username !== null) {
+			$url_redirect['a'] = 'details';
+			$url_redirect['params']['username'] = $username;
+		}
+		return FreshRSS_Auth::requestReauth($url_redirect);
+	}
+
 	public function purgeAction(): void {
 		if (!FreshRSS_Auth::hasAccess('admin')) {
 			Minz_Error::error(403);
 		}
 
-		if (Minz_Request::isPost()) {
-			$username = Minz_Request::paramString('username');
+		if (!Minz_Request::isPost()) {
+			Minz_Error::error(403);
+		}
 
-			if (!FreshRSS_UserDAO::exists($username)) {
-				Minz_Error::error(404);
-			}
+		if (self::reauthRedirect()) {
+			return;
+		}
+
+		$username = Minz_Request::paramString('username');
 
-			$feedDAO = FreshRSS_Factory::createFeedDao($username);
-			$feedDAO->purge();
+		if (!FreshRSS_UserDAO::exists($username)) {
+			Minz_Error::error(404);
 		}
+
+		$feedDAO = FreshRSS_Factory::createFeedDao($username);
+		$feedDAO->purge();
 	}
 
 	/**
@@ -215,6 +239,10 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 			Minz_Error::error(403);
 		}
 
+		if (self::reauthRedirect()) {
+			return;
+		}
+
 		FreshRSS_View::prependTitle(_t('admin.user.title') . ' · ');
 
 		if (Minz_Request::isPost()) {
@@ -337,6 +365,10 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 			Minz_Error::error(403);
 		}
 
+		if (self::reauthRedirect()) {
+			return;
+		}
+
 		if (Minz_Request::isPost()) {
 			$new_user_name = Minz_Request::paramString('new_user_name');
 			$email = Minz_Request::paramString('new_user_email');
@@ -602,7 +634,10 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 					$username, FreshRSS_Context::userConf()->passwordHash,
 					$nonce, $challenge
 				);
+			} elseif (self::reauthRedirect()) {
+				return;
 			}
+
 			if ($ok) {
 				$ok &= self::deleteUser($username);
 			}
@@ -647,6 +682,10 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 			Minz_Error::error(403);
 		}
 
+		if (self::reauthRedirect()) {
+			return;
+		}
+
 		$username = Minz_Request::paramString('username');
 		if (!FreshRSS_UserDAO::exists($username)) {
 			Minz_Error::error(404);
@@ -682,6 +721,10 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 			Minz_Error::error(403);
 		}
 
+		if (self::reauthRedirect()) {
+			return;
+		}
+
 		$username = Minz_Request::paramString('username');
 		if (!FreshRSS_UserDAO::exists($username)) {
 			Minz_Error::error(404);

+ 51 - 0
app/Models/Auth.php

@@ -165,6 +165,7 @@ class FreshRSS_Auth {
 		self::$login_ok = false;
 		Minz_Session::_params([
 			'loginOk' => false,
+			'lastReauth' => false,
 			'csrf' => false,
 			'REMOTE_USER' => false,
 		]);
@@ -230,4 +231,54 @@ class FreshRSS_Auth {
 		}
 		return $token != '' && $token === $csrf;
 	}
+
+	public static function needsReauth(): bool {
+		$auth_type = FreshRSS_Context::systemConf()->auth_type;
+		$reauth_required = FreshRSS_Context::systemConf()->reauth_required;
+		$reauth_time = FreshRSS_Context::systemConf()->reauth_time;
+
+		if (!$reauth_required) {
+			return false;
+		}
+
+		$last_reauth = Minz_Session::paramInt('lastReauth');
+
+		if ($auth_type !== 'none' && time() - $last_reauth > $reauth_time) {
+			if ($auth_type === 'http_auth') {
+				// TODO: not implemented - just let the user through
+				return false;
+			}
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Return if user needs reauth and got redirected to login page.
+	 *
+	 * @param array{c?: string, a?: string, params?: array<string, mixed>}|null $redirect
+	 */
+	public static function requestReauth(?array $redirect = null): bool {
+		if (self::needsReauth()) {
+			if (Minz_Request::paramBoolean('ajax')) {
+				// Send 403 and exit instead of redirect with Minz_Error::error()
+				header('HTTP/1.1 403 Forbidden');
+				exit();
+			}
+
+			$redirect = Minz_Url::serialize($redirect ?? Minz_Request::currentRequest());
+
+			Minz_Request::forward([
+				'c' => 'auth',
+				'a' => 'reauth',
+				'params' => [
+					'r' => $redirect,
+				],
+			], true);
+
+			return true;
+		}
+
+		return false;
+	}
 }

+ 2 - 0
app/Models/SystemConfiguration.php

@@ -9,6 +9,8 @@ declare(strict_types=1);
  * @property bool $api_enabled
  * @property string $archiving
  * @property 'form'|'http_auth'|'none' $auth_type
+ * @property-read bool $reauth_required
+ * @property-read int $reauth_time
  * @property-read string $auto_update_url
  * @property-read array<int,mixed> $curl_options
  * @property string $default_user

+ 5 - 0
app/i18n/cs/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Heslo',
 			'format' => '<small>Alespoň 7 znaků</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Nový účet',
 			'ask' => 'Vytvořit účet?',

+ 5 - 0
app/i18n/de/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Passwort',
 			'format' => '<small>mindestens 7 Zeichen</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Neuer Account',
 			'ask' => 'Erstelle einen Account?',

+ 5 - 0
app/i18n/el/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Password',	// TODO
 			'format' => '<small>At least 7 characters</small>',	// TODO
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'New account',	// TODO
 			'ask' => 'Create an account?',	// TODO

+ 5 - 0
app/i18n/en-us/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Password',	// IGNORE
 			'format' => '<small>At least 7 characters</small>',	// IGNORE
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// IGNORE
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// IGNORE
+			'title' => 'Reauthentication',	// IGNORE
+		),
 		'registration' => array(
 			'_' => 'New account',	// IGNORE
 			'ask' => 'Create an account?',	// IGNORE

+ 5 - 0
app/i18n/en/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Password',
 			'format' => '<small>At least 7 characters</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',
+			'title' => 'Reauthentication',
+		),
 		'registration' => array(
 			'_' => 'New account',
 			'ask' => 'Create an account?',

+ 5 - 0
app/i18n/es/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Contraseña',
 			'format' => '<small>Mínimo de 7 caracteres</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Nueva cuenta',
 			'ask' => '¿Crear una cuenta?',

+ 5 - 0
app/i18n/fa/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => ' رمز عبور',
 			'format' => '<small>حداقل 7 نویسه</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => ' حساب جدید',
 			'ask' => ' یک حساب کاربری ایجاد کنید؟',

+ 5 - 0
app/i18n/fi/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Salasana',
 			'format' => '<small>Vähintään 7 merkkiä</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Uusi tili',
 			'ask' => 'Haluatko luoda tilin?',

+ 5 - 0
app/i18n/fr/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Mot de passe',
 			'format' => '<small>7 caractères minimum</small>',
 		),
+		'reauth' => array(
+			'header' => 'Une réauthentification est requise',
+			'tip' => 'La réauthentification sera valide pendant <u>%d minutes</u>',
+			'title' => 'Réauthentification',
+		),
 		'registration' => array(
 			'_' => 'Nouveau compte',
 			'ask' => 'Créer un compte ?',

+ 5 - 0
app/i18n/he/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'סיסמה',
 			'format' => '<small>At least 7 characters</small>',	// TODO
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'New account',	// TODO
 			'ask' => 'Create an account?',	// TODO

+ 5 - 0
app/i18n/hu/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Jelszó',
 			'format' => '<small>Legalább 7 karakter hosszú</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Új fiók',
 			'ask' => 'Létrehoz egy új fiókot?',

+ 5 - 0
app/i18n/id/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Kata sandi',
 			'format' => '<small>Paling tidak 7 karakter</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Akun baru',
 			'ask' => 'Buat akun?',

+ 5 - 0
app/i18n/it/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Password',	// IGNORE
 			'format' => '<small>almeno 7 caratteri</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Nuovo profilo',
 			'ask' => 'Vuoi creare un nuovo profilo?',

+ 5 - 0
app/i18n/ja/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'パスワード',
 			'format' => '<small>最低7文字必要です</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => '新規アカウント',
 			'ask' => 'アカウントを作りますか?',

+ 5 - 0
app/i18n/ko/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => '암호',
 			'format' => '<small>7 글자 이상이어야 합니다</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => '새 계정',
 			'ask' => '새 계정을 만들까요?',

+ 5 - 0
app/i18n/lv/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Parole',
 			'format' => '<small>Vismaz 7 rakstzīmes</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Jauns konts',
 			'ask' => 'Uztaisīt kontu?',

+ 5 - 0
app/i18n/nl/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Wachtwoord',
 			'format' => '<small>Ten minste 7 tekens</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Nieuw account',
 			'ask' => 'Maak een account?',

+ 5 - 0
app/i18n/oc/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Senhal',
 			'format' => '<small>Almens 7 caractèrs</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Compte nòu',
 			'ask' => 'Crear un compte?',

+ 5 - 0
app/i18n/pl/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Hasło',
 			'format' => '<small>przynajmniej 7 znaków</small>',
 		),
+		'reauth' => array(
+			'header' => 'Wymagane ponowne logowanie',
+			'tip' => 'Nie będziesz proszony o ponowne logowanie przez <u>%d minut</u>',
+			'title' => 'Ponowne logowanie',
+		),
 		'registration' => array(
 			'_' => 'Tworzenie konta',
 			'ask' => 'Nie masz konta?',

+ 5 - 0
app/i18n/pt-br/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Senha',
 			'format' => '<small>Ao menos 7 caracteres</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Nova conta',
 			'ask' => 'Criar novoa conta?',

+ 5 - 0
app/i18n/pt-pt/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Senha',
 			'format' => '<small>Pelo menos 7 caracteres</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Nova conta',
 			'ask' => 'Criar novoa conta?',

+ 5 - 0
app/i18n/ru/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Пароль',
 			'format' => '<small>Не менее 7 символов</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Новый аккаунт',
 			'ask' => 'Создать аккаунт?',

+ 5 - 0
app/i18n/sk/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Heslo',
 			'format' => '<small>Najmenej 7 znakov</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Nový účet',
 			'ask' => 'Vytvoriť účet?',

+ 5 - 0
app/i18n/tr/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => 'Parola',
 			'format' => '<small>En az 7 karakter</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => 'Yeni hesap',
 			'ask' => 'Hesap oluştur?',

+ 5 - 0
app/i18n/zh-cn/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => '密码',
 			'format' => '<small>至少 7 个字符</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => '新用户',
 			'ask' => '创建新用户?',

+ 5 - 0
app/i18n/zh-tw/gen.php

@@ -61,6 +61,11 @@ return array(
 			'_' => '密碼',
 			'format' => '<small>至少 7 個字元</small>',
 		),
+		'reauth' => array(
+			'header' => 'Reauthentication is required',	// TODO
+			'tip' => 'You won’t be asked to sign in again for <u>%d minutes</u>',	// TODO
+			'title' => 'Reauthentication',	// TODO
+		),
 		'registration' => array(
 			'_' => '新使用者',
 			'ask' => '創建新使用者?',

+ 32 - 0
app/views/auth/reauth.phtml

@@ -0,0 +1,32 @@
+<?php
+	declare(strict_types=1);
+	/** @var FreshRSS_View $this */
+?>
+
+<main class="prompt">
+	<h1><?= _t('gen.auth.reauth.header') ?></h1>
+
+	<form id="crypto-form" method="post">
+		<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
+		<input type="hidden" id="username" value="<?= Minz_User::name() ?>" />
+		<div class="form-group">
+			<label for="passwordPlain"><?= _t('gen.auth.password') ?></label>
+			<div class="stick">
+				<input type="password" id="passwordPlain" required="required" />
+				<button type="button" class="btn toggle-password" data-toggle="passwordPlain"><?= _i('key') ?></button>
+			</div>
+			<input type="hidden" id="challenge" name="challenge" />
+			<noscript><strong><?= _t('gen.js.should_be_activated') ?></strong></noscript>
+		</div>
+		<?php
+			$reauth_time = FreshRSS_Context::systemConf()->reauth_time;
+		?>
+		<p class="help"><?= _i('help') ?> <?= _t('gen.auth.reauth.tip', intval($reauth_time / 60)) ?></p>
+		<div class="form-group form-group-actions">
+			<button id="loginButton" type="submit" class="btn btn-important" disabled="disabled">
+				<?= _t('gen.auth.login') ?>
+			</button>
+		</div>
+	</form>
+</main>
+

+ 7 - 0
config.default.php

@@ -59,6 +59,13 @@ return [
 	#		and in particular not protect `/FreshRSS/p/api/` if you would like to use the API (different login system).
 	'auth_type' => 'form',
 
+	# Whether reauthentication is required for performing sensitive actions e.g. promoting a user or applying an update
+	'reauth_required' => true,
+
+	# Time before asking for reauth
+	#    Default: 1200s (20 min)
+	'reauth_time' => 1200,
+
 	# When using http_auth, automatically register any unknown user
 	'http_auth_auto_register' => true,
 

+ 1 - 1
docs/i18n/flags/gen/de.svg

@@ -2,6 +2,6 @@
 <svg xmlns="http://www.w3.org/2000/svg" width="70" height="20">
 	<g fill="white" font-size="12" font-family="Verdana" text-anchor="middle">
 		<rect rx="3" width="70" height="20" fill="green" />
-		<text x="34" y="14">🇩🇪 96%</text>
+		<text x="34" y="14">🇩🇪 95%</text>
 	</g>
 </svg>

+ 1 - 1
docs/i18n/flags/gen/id.svg

@@ -2,6 +2,6 @@
 <svg xmlns="http://www.w3.org/2000/svg" width="70" height="20">
 	<g fill="white" font-size="12" font-family="Verdana" text-anchor="middle">
 		<rect rx="3" width="70" height="20" fill="green" />
-		<text x="34" y="14">🇮🇩 99%</text>
+		<text x="34" y="14">🇮🇩 98%</text>
 	</g>
 </svg>

+ 4 - 0
lib/core-extensions/UserJS/extension.php

@@ -21,6 +21,10 @@ final class UserJSExtension extends Minz_Extension {
 
 		$this->registerTranslates();
 
+		if (FreshRSS_Auth::requestReauth()) {
+			return;
+		}
+
 		if (Minz_Request::isPost()) {
 			$js_rules = Minz_Request::paramString('js-rules', plaintext: true);
 			$this->saveFile(self::FILENAME, $js_rules);

+ 1 - 1
lib/core-extensions/UserJS/metadata.json

@@ -2,7 +2,7 @@
 	"name": "User JS",
 	"author": "hkcomori, Frans de Jonge",
 	"description": "Apply user JS.",
-	"version": "1.1.0",
+	"version": "1.1.1",
 	"entrypoint": "UserJS",
 	"type": "user"
 }

+ 5 - 0
p/scripts/extra.js

@@ -327,6 +327,11 @@ function open_slider_listener(ev) {
 			req.open('GET', ahref, true);
 			req.responseType = 'document';
 			req.onload = function (e) {
+				if (this.status === 403) {
+					// Redirect to reauth page (or fail if session expired)
+					location.href = a.href;
+					return;
+				}
 				location.href = '#slider'; // close menu/dropdown
 				document.documentElement.classList.add('slider-active');
 				slider.classList.add('active');