From 79aa5beaf44af13a1828bfa5fc824a08c62054dc Mon Sep 17 00:00:00 2001
From: Marien Fressinaud
Date: Mon, 6 Oct 2014 23:29:20 +0200
Subject: Refactor authentication system.
Big work, not finished. A lot of features have been removed.
See https://github.com/marienfressinaud/FreshRSS/issues/655
---
app/Controllers/categoryController.php | 2 +-
app/Controllers/configureController.php | 2 +-
app/Controllers/entryController.php | 2 +-
app/Controllers/feedController.php | 2 +-
app/Controllers/importExportController.php | 2 +-
app/Controllers/indexController.php | 296 ++++++-----------------------
app/Controllers/statsController.php | 2 +-
app/Controllers/subscriptionController.php | 2 +-
app/Controllers/updateController.php | 2 +-
app/Controllers/usersController.php | 2 +-
app/FreshRSS.php | 135 ++-----------
app/Models/Auth.php | 209 ++++++++++++++++++++
app/layout/aside_flux.phtml | 6 +-
app/layout/header.phtml | 32 +---
app/layout/nav_menu.phtml | 4 +-
app/views/helpers/view/normal_view.phtml | 6 +-
app/views/index/index.phtml | 2 +-
app/views/index/login.phtml | 1 -
app/views/index/logout.phtml | 1 -
app/views/index/resetAuth.phtml | 33 ----
20 files changed, 309 insertions(+), 434 deletions(-)
create mode 100644 app/Models/Auth.php
delete mode 100644 app/views/index/login.phtml
delete mode 100644 app/views/index/logout.phtml
delete mode 100644 app/views/index/resetAuth.phtml
diff --git a/app/Controllers/categoryController.php b/app/Controllers/categoryController.php
index c79f37fa4..537a2b210 100644
--- a/app/Controllers/categoryController.php
+++ b/app/Controllers/categoryController.php
@@ -12,7 +12,7 @@ class FreshRSS_category_Controller extends Minz_ActionController {
*
*/
public function firstAction() {
- if (!$this->view->loginOk) {
+ if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(
403,
array('error' => array(_t('access_denied')))
diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php
index 789e9dfb0..7e77a757a 100755
--- a/app/Controllers/configureController.php
+++ b/app/Controllers/configureController.php
@@ -10,7 +10,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
* underlying framework.
*/
public function firstAction() {
- if (!$this->view->loginOk) {
+ if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(
403,
array('error' => array(_t('access_denied')))
diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php
index c46fbf346..a1dfacb4d 100755
--- a/app/Controllers/entryController.php
+++ b/app/Controllers/entryController.php
@@ -10,7 +10,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
* underlying framework.
*/
public function firstAction() {
- if (!$this->view->loginOk) {
+ if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(
403,
array('error' => array(_t('access_denied')))
diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php
index 18829d252..2a7238eaf 100755
--- a/app/Controllers/feedController.php
+++ b/app/Controllers/feedController.php
@@ -10,7 +10,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
* underlying framework.
*/
public function firstAction() {
- if (!$this->view->loginOk) {
+ if (!FreshRSS_Auth::hasAccess()) {
// Token is useful in the case that anonymous refresh is forbidden
// and CRON task cannot be used with php command so the user can
// set a CRON task to refresh his feeds by using token inside url
diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php
index 57759f277..aaac1b68b 100644
--- a/app/Controllers/importExportController.php
+++ b/app/Controllers/importExportController.php
@@ -10,7 +10,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
* underlying framework.
*/
public function firstAction() {
- if (!$this->view->loginOk) {
+ if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(
403,
array('error' => array(_t('access_denied')))
diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php
index 0d2eff700..3006480f9 100755
--- a/app/Controllers/indexController.php
+++ b/app/Controllers/indexController.php
@@ -8,7 +8,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
$token = $this->view->conf->token;
// check if user is logged in
- if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous()) {
+ if (!FreshRSS_Auth::hasAccess() && !Minz_Configuration::allowAnonymous()) {
$token_param = Minz_Request::param('token', '');
$token_is_ok = ($token != '' && $token === $token_param);
if ($output === 'rss' && !$token_is_ok) {
@@ -20,7 +20,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
} elseif ($output !== 'rss') {
// "hard" redirection is not required, just ask dispatcher to
// forward to the login form without 302 redirection
- Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'));
+ Minz_Request::forward(array('c' => 'index', 'a' => 'login'));
return;
}
}
@@ -207,7 +207,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
}
public function logsAction() {
- if (!$this->view->loginOk) {
+ if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(
403,
array('error' => array(_t('access_denied')))
@@ -229,265 +229,91 @@ class FreshRSS_index_Controller extends Minz_ActionController {
$this->view->logsPaginator->_currentPage($page);
}
+ /**
+ * This action handles the login page.
+ */
public function loginAction() {
- $this->view->_useLayout(false);
-
- $url = 'https://verifier.login.persona.org/verify';
- $assert = Minz_Request::param('assertion');
- $params = 'assertion=' . $assert . '&audience=' .
- urlencode(Minz_Url::display(null, 'php', true));
- $ch = curl_init();
- $options = array(
- CURLOPT_URL => $url,
- CURLOPT_RETURNTRANSFER => TRUE,
- CURLOPT_POST => 2,
- CURLOPT_POSTFIELDS => $params
- );
- curl_setopt_array($ch, $options);
- $result = curl_exec($ch);
- curl_close($ch);
-
- $res = json_decode($result, true);
-
- $loginOk = false;
- $reason = '';
- if ($res['status'] === 'okay') {
- $email = filter_var($res['email'], FILTER_VALIDATE_EMAIL);
- if ($email != '') {
- $personaFile = DATA_PATH . '/persona/' . $email . '.txt';
- if (($currentUser = @file_get_contents($personaFile)) !== false) {
- $currentUser = trim($currentUser);
- if (ctype_alnum($currentUser)) {
- try {
- $this->conf = new FreshRSS_Configuration($currentUser);
- $loginOk = strcasecmp($email, $this->conf->mail_login) === 0;
- } catch (Minz_Exception $e) {
- $reason = 'Invalid configuration for user [' . $currentUser . ']! ' . $e->getMessage(); //Permission denied or conf file does not exist
- }
- } else {
- $reason = 'Invalid username format [' . $currentUser . ']!';
- }
- }
- } else {
- $reason = 'Invalid email format [' . $res['email'] . ']!';
- }
- }
- if ($loginOk) {
- Minz_Session::_param('currentUser', $currentUser);
- Minz_Session::_param('mail', $email);
- $this->view->loginOk = true;
- invalidateHttpCache();
- } else {
- $res = array();
- $res['status'] = 'failure';
- $res['reason'] = $reason == '' ? _t('invalid_login') : $reason;
- Minz_Log::warning('Persona: ' . $res['reason']);
+ if (FreshRSS_Auth::hasAccess()) {
+ Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
}
- header('Content-Type: application/json; charset=UTF-8');
- $this->view->res = json_encode($res);
- }
-
- public function logoutAction() {
- $this->view->_useLayout(false);
invalidateHttpCache();
- Minz_Session::_param('currentUser');
- Minz_Session::_param('mail');
- Minz_Session::_param('passwordHash');
- }
-
- private static function makeLongTermCookie($username, $passwordHash) {
- do {
- $token = sha1(Minz_Configuration::salt() . $username . uniqid(mt_rand(), true));
- $tokenFile = DATA_PATH . '/tokens/' . $token . '.txt';
- } while (file_exists($tokenFile));
- if (@file_put_contents($tokenFile, $username . "\t" . $passwordHash) === false) {
- return false;
- }
- $expire = time() + 2629744; //1 month //TODO: Use a configuration instead
- Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
- Minz_Session::_param('token', $token);
- return $token;
- }
-
- private static function deleteLongTermCookie() {
- Minz_Session::deleteLongTermCookie('FreshRSS_login');
- $token = Minz_Session::param('token', null);
- if (ctype_alnum($token)) {
- @unlink(DATA_PATH . '/tokens/' . $token . '.txt');
- }
- Minz_Session::_param('token');
- if (rand(0, 10) === 1) {
- self::purgeTokens();
- }
- }
- private static function purgeTokens() {
- $oldest = time() - 2629744; //1 month //TODO: Use a configuration instead
- foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $fileInfo) {
- if ($fileInfo->getExtension() === 'txt' && $fileInfo->getMTime() < $oldest) {
- @unlink($fileInfo->getPathname());
- }
+ $auth_type = Minz_Configuration::authType();
+ switch ($auth_type) {
+ case 'form':
+ Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'));
+ break;
+ case 'http_auth':
+ case 'none':
+ // It should not happened!
+ Minz_Error::error(404);
+ default:
+ // TODO load plugin instead
+ Minz_Error::error(404);
}
}
+ /**
+ *
+ */
public function formLoginAction() {
- if ($this->view->loginOk) {
+ if (FreshRSS_Auth::hasAccess()) {
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
}
- if (Minz_Request::isPost()) {
- $ok = false;
- $nonce = Minz_Session::param('nonce');
- $username = Minz_Request::param('username', '');
- $c = Minz_Request::param('challenge', '');
- if (ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce)) {
- if (!function_exists('password_verify')) {
- include_once(LIB_PATH . '/password_compat.php');
- }
- try {
- $conf = new FreshRSS_Configuration($username);
- $s = $conf->passwordHash;
- $ok = password_verify($nonce . $s, $c);
- if ($ok) {
- Minz_Session::_param('currentUser', $username);
- Minz_Session::_param('passwordHash', $s);
- if (Minz_Request::param('keep_logged_in', false)) {
- self::makeLongTermCookie($username, $s);
- } else {
- self::deleteLongTermCookie();
- }
- } else {
- Minz_Log::warning('Password mismatch for user ' . $username . ', nonce=' . $nonce . ', c=' . $c);
- }
- } catch (Minz_Exception $me) {
- Minz_Log::warning('Login failure: ' . $me->getMessage());
- }
- } else {
- Minz_Log::debug('Invalid credential parameters: user=' . $username . ' challenge=' . $c . ' nonce=' . $nonce);
- }
- if (!$ok) {
- $notif = array(
- 'type' => 'bad',
- 'content' => _t('invalid_login')
- );
- Minz_Session::_param('notification', $notif);
- }
- $this->view->_useLayout(false);
- Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
- } elseif (Minz_Configuration::unsafeAutologinEnabled() && isset($_GET['u']) && isset($_GET['p'])) {
- Minz_Session::_param('currentUser');
- Minz_Session::_param('mail');
- Minz_Session::_param('passwordHash');
- $username = ctype_alnum($_GET['u']) ? $_GET['u'] : '';
- $passwordPlain = $_GET['p'];
- Minz_Request::_param('p'); //Discard plain-text password ASAP
- $_GET['p'] = '';
- if (!function_exists('password_verify')) {
- include_once(LIB_PATH . '/password_compat.php');
- }
- try {
- $conf = new FreshRSS_Configuration($username);
- $s = $conf->passwordHash;
- $ok = password_verify($passwordPlain, $s);
- unset($passwordPlain);
- if ($ok) {
- Minz_Session::_param('currentUser', $username);
- Minz_Session::_param('passwordHash', $s);
- } else {
- Minz_Log::warning('Unsafe password mismatch for user ' . $username);
- }
- } catch (Minz_Exception $me) {
- Minz_Log::warning('Unsafe login failure: ' . $me->getMessage());
- }
- Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
- } elseif (!Minz_Configuration::canLogIn()) {
- Minz_Error::error(
- 403,
- array('error' => array(_t('access_denied')))
- );
- }
invalidateHttpCache();
- }
- public function formLogoutAction() {
- $this->view->_useLayout(false);
- invalidateHttpCache();
- Minz_Session::_param('currentUser');
- Minz_Session::_param('mail');
- Minz_Session::_param('passwordHash');
- self::deleteLongTermCookie();
- Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
- }
-
- public function resetAuthAction() {
- Minz_View::prependTitle(_t('auth_reset') . ' · ');
- Minz_View::appendScript(Minz_Url::display(
- '/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')
- ));
-
- $this->view->no_form = false;
- // Enable changement of auth only if Persona!
- if (Minz_Configuration::authType() != 'persona') {
- $this->view->message = array(
- 'status' => 'bad',
- 'title' => _t('damn'),
- 'body' => _t('auth_not_persona')
- );
- $this->view->no_form = true;
- return;
- }
-
- $conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser());
- // Admin user must have set its master password.
- if (!$conf->passwordHash) {
- $this->view->message = array(
- 'status' => 'bad',
- 'title' => _t('damn'),
- 'body' => _t('auth_no_password_set')
- );
- $this->view->no_form = true;
- return;
- }
-
- invalidateHttpCache();
+ $file_mtime = @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js');
+ Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . $file_mtime));
if (Minz_Request::isPost()) {
$nonce = Minz_Session::param('nonce');
$username = Minz_Request::param('username', '');
- $c = Minz_Request::param('challenge', '');
- if (!(ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce))) {
- Minz_Log::debug('Invalid credential parameters:' .
- ' user=' . $username .
- ' challenge=' . $c .
- ' nonce=' . $nonce);
+ $challenge = Minz_Request::param('challenge', '');
+ try {
+ $conf = new FreshRSS_Configuration($username);
+ } catch(Minz_Exception $e) {
+ // $username is not a valid user, nor the configuration file!
+ Minz_Log::warning('Login failure: ' . $e->getMessage());
Minz_Request::bad(_t('invalid_login'),
- array('c' => 'index', 'a' => 'resetAuth'));
- }
-
- if (!function_exists('password_verify')) {
- include_once(LIB_PATH . '/password_compat.php');
+ array('c' => 'index', 'a' => 'login'));
}
- $s = $conf->passwordHash;
- $ok = password_verify($nonce . $s, $c);
+ $ok = FreshRSS_FormAuth::checkCredentials(
+ $username, $conf->passwordHash, $nonce, $challenge
+ );
if ($ok) {
- Minz_Configuration::_authType('form');
- $ok = Minz_Configuration::writeFile();
-
- if ($ok) {
- Minz_Request::good(_t('auth_form_set'));
+ // Set session parameter to give access to the user.
+ Minz_Session::_param('currentUser', $username);
+ Minz_Session::_param('passwordHash', $conf->passwordHash);
+ FreshRSS_Auth::giveAccess();
+
+ // Set cookie parameter if nedded.
+ if (Minz_Request::param('keep_logged_in', false)) {
+ FreshRSS_FormAuth::makeCookie($username, $conf->passwordHash);
} else {
- Minz_Request::bad(_t('auth_form_not_set'),
- array('c' => 'index', 'a' => 'resetAuth'));
+ FreshRSS_FormAuth::deleteCookie();
}
- } else {
- Minz_Log::debug('Password mismatch for user ' . $username .
- ', nonce=' . $nonce . ', c=' . $c);
+ // All is good, go back to the index.
+ Minz_Request::good(_t('login'),
+ array('c' => 'index', 'a' => 'index'));
+ } else {
+ Minz_Log::warning('Password mismatch for' .
+ ' user=' . $username .
+ ', nonce=' . $nonce .
+ ', c=' . $challenge);
Minz_Request::bad(_t('invalid_login'),
- array('c' => 'index', 'a' => 'resetAuth'));
+ array('c' => 'index', 'a' => 'login'));
}
}
}
+
+ public function logoutAction() {
+ invalidateHttpCache();
+ FreshRSS_Auth::removeAccess();
+ Minz_Request::good(_t('disconnected'),
+ array('c' => 'index', 'a' => 'index'));
+ }
}
diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php
index 99c57c809..0e3430fcc 100644
--- a/app/Controllers/statsController.php
+++ b/app/Controllers/statsController.php
@@ -118,7 +118,7 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
* underlying framework.
*/
public function firstAction() {
- if (!$this->view->loginOk) {
+ if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(
403, array('error' => array(_t('access_denied')))
);
diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php
index 7cc8179a0..a89168eb3 100644
--- a/app/Controllers/subscriptionController.php
+++ b/app/Controllers/subscriptionController.php
@@ -10,7 +10,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
* underlying framework.
*/
public function firstAction() {
- if (!$this->view->loginOk) {
+ if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(
403,
array('error' => array(_t('access_denied')))
diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php
index da5bddc65..9da1e8657 100644
--- a/app/Controllers/updateController.php
+++ b/app/Controllers/updateController.php
@@ -3,7 +3,7 @@
class FreshRSS_update_Controller extends Minz_ActionController {
public function firstAction() {
$current_user = Minz_Session::param('currentUser', '');
- if (!$this->view->loginOk && Minz_Configuration::isAdmin($current_user)) {
+ if (!FreshRSS_Auth::hasAccess() && Minz_Configuration::isAdmin($current_user)) {
Minz_Error::error(
403,
array('error' => array(_t('access_denied')))
diff --git a/app/Controllers/usersController.php b/app/Controllers/usersController.php
index 7d0171bc7..c2b1d163f 100644
--- a/app/Controllers/usersController.php
+++ b/app/Controllers/usersController.php
@@ -5,7 +5,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
const BCRYPT_COST = 9; //Will also have to be computed client side on mobile devices, so do not use a too high cost
public function firstAction() {
- if (!$this->view->loginOk) {
+ if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(
403,
array('error' => array(_t('access_denied')))
diff --git a/app/FreshRSS.php b/app/FreshRSS.php
index efd302ecc..35a37b887 100644
--- a/app/FreshRSS.php
+++ b/app/FreshRSS.php
@@ -4,130 +4,33 @@ class FreshRSS extends Minz_FrontController {
if (!isset($_SESSION)) {
Minz_Session::init('FreshRSS');
}
- $loginOk = $this->accessControl(Minz_Session::param('currentUser', ''));
+
+ FreshRSS_Auth::init();
+ $this->loadConfiguration();
$this->loadParamsView();
if (Minz_Request::isPost() && !is_referer_from_same_domain()) {
- $loginOk = false; //Basic protection against XSRF attacks
+ //Basic protection against XSRF attacks
+ FreshRSS_Auth::removeAccess();
Minz_Error::error(
403,
array('error' => array(_t('access_denied') . ' [HTTP_REFERER=' .
- htmlspecialchars(empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER']) . ']'))
+ htmlspecialchars(empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER']) . ']'))
);
}
- Minz_View::_param('loginOk', $loginOk);
- $this->loadStylesAndScripts($loginOk); //TODO: Do not load that when not needed, e.g. some Ajax requests
+ $this->loadStylesAndScripts();
$this->loadNotifications();
$this->loadExtensions();
}
- private static function getCredentialsFromLongTermCookie() {
- $token = Minz_Session::getLongTermCookie('FreshRSS_login');
- if (!ctype_alnum($token)) {
- return array();
- }
- $tokenFile = DATA_PATH . '/tokens/' . $token . '.txt';
- $mtime = @filemtime($tokenFile);
- if ($mtime + 2629744 < time()) { //1 month //TODO: Use a configuration instead
- @unlink($tokenFile);
- return array(); //Expired or token does not exist
- }
- $credentials = @file_get_contents($tokenFile);
- return $credentials === false ? array() : explode("\t", $credentials, 2);
- }
-
- private function accessControl($currentUser) {
- if ($currentUser == '') {
- switch (Minz_Configuration::authType()) {
- case 'form':
- $credentials = self::getCredentialsFromLongTermCookie();
- if (isset($credentials[1])) {
- $currentUser = trim($credentials[0]);
- Minz_Session::_param('passwordHash', trim($credentials[1]));
- }
- $loginOk = $currentUser != '';
- if (!$loginOk) {
- $currentUser = Minz_Configuration::defaultUser();
- Minz_Session::_param('passwordHash');
- }
- break;
- case 'http_auth':
- $currentUser = httpAuthUser();
- $loginOk = $currentUser != '';
- break;
- case 'persona':
- $loginOk = false;
- $email = filter_var(Minz_Session::param('mail'), FILTER_VALIDATE_EMAIL);
- if ($email != '') { //TODO: Remove redundancy with indexController
- $personaFile = DATA_PATH . '/persona/' . $email . '.txt';
- if (($currentUser = @file_get_contents($personaFile)) !== false) {
- $currentUser = trim($currentUser);
- $loginOk = true;
- }
- }
- if (!$loginOk) {
- $currentUser = Minz_Configuration::defaultUser();
- }
- break;
- case 'none':
- $currentUser = Minz_Configuration::defaultUser();
- $loginOk = true;
- break;
- default:
- $currentUser = Minz_Configuration::defaultUser();
- $loginOk = false;
- break;
- }
- } else {
- $loginOk = true;
- }
-
- if (!ctype_alnum($currentUser)) {
- Minz_Session::_param('currentUser', '');
- die('Invalid username [' . $currentUser . ']!');
- }
-
+ private function loadConfiguration() {
+ $current_user = Minz_Session::param('currentUser');
try {
- $this->conf = new FreshRSS_Configuration($currentUser);
+ $this->conf = new FreshRSS_Configuration($current_user);
Minz_View::_param('conf', $this->conf);
- Minz_Session::_param('currentUser', $currentUser);
- } catch (Minz_Exception $me) {
- $loginOk = false;
- try {
- $this->conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser());
- Minz_Session::_param('currentUser', Minz_Configuration::defaultUser());
- Minz_View::_param('conf', $this->conf);
- $notif = array(
- 'type' => 'bad',
- 'content' => 'Invalid configuration for user [' . $currentUser . ']!',
- );
- Minz_Session::_param('notification', $notif);
- Minz_Log::warning($notif['content'] . ' ' . $me->getMessage());
- Minz_Session::_param('currentUser', '');
- } catch (Exception $e) {
- die($e->getMessage());
- }
- }
-
- if ($loginOk) {
- switch (Minz_Configuration::authType()) {
- case 'form':
- $loginOk = Minz_Session::param('passwordHash') === $this->conf->passwordHash;
- break;
- case 'http_auth':
- $loginOk = strcasecmp($currentUser, httpAuthUser()) === 0;
- break;
- case 'persona':
- $loginOk = strcasecmp(Minz_Session::param('mail'), $this->conf->mail_login) === 0;
- break;
- case 'none':
- $loginOk = true;
- break;
- default:
- $loginOk = false;
- break;
- }
+ } catch(Minz_Exception $e) {
+ Minz_Log::error('Cannot load configuration file of user `' . $current_user . '`');
+ die($e->getMessage());
}
- return $loginOk;
}
private function loadParamsView() {
@@ -140,7 +43,7 @@ class FreshRSS extends Minz_FrontController {
}
}
- private function loadStylesAndScripts($loginOk) {
+ private function loadStylesAndScripts() {
$theme = FreshRSS_Themes::load($this->conf->theme);
if ($theme) {
foreach($theme['files'] as $file) {
@@ -158,16 +61,6 @@ class FreshRSS extends Minz_FrontController {
}
}
- switch (Minz_Configuration::authType()) {
- case 'form':
- if (!$loginOk) {
- Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
- }
- break;
- case 'persona':
- Minz_View::appendScript('https://login.persona.org/include.js');
- break;
- }
Minz_View::appendScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')));
Minz_View::appendScript(Minz_Url::display('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js')));
Minz_View::appendScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
diff --git a/app/Models/Auth.php b/app/Models/Auth.php
new file mode 100644
index 000000000..c4a3abd98
--- /dev/null
+++ b/app/Models/Auth.php
@@ -0,0 +1,209 @@
+getMessage());
+ }
+
+ switch (Minz_Configuration::authType()) {
+ case 'form':
+ self::$login_ok = Minz_Session::param('passwordHash') === $conf->passwordHash;
+ break;
+ case 'http_auth':
+ self::$login_ok = strcasecmp($current_user, httpAuthUser()) === 0;
+ break;
+ case 'none':
+ self::$login_ok = true;
+ break;
+ default:
+ // TODO: extensions
+ self::$login_ok = false;
+ }
+
+ Minz_Session::_param('loginOk', self::$login_ok);
+ }
+
+ /**
+ * Returns if current user is connected.
+ *
+ * @return boolean true if user is connected, false else.
+ */
+ public static function hasAccess() {
+ return self::$login_ok;
+ }
+
+ /**
+ * Removes all accesses for the current user.
+ */
+ public static function removeAccess() {
+ Minz_Session::_param('loginOk');
+ self::$login_ok = false;
+ Minz_Session::_param('currentUser', Minz_Configuration::defaultUser());
+
+ switch (Minz_Configuration::authType()) {
+ case 'form':
+ Minz_Session::_param('passwordHash');
+ FreshRSS_FormAuth::deleteCookie();
+ break;
+ case 'http_auth':
+ case 'none':
+ // Nothing to do...
+ break;
+ default:
+ // TODO: extensions
+ }
+ }
+}
+
+
+class FreshRSS_FormAuth {
+ public static function checkCredentials($username, $hash, $nonce, $challenge) {
+ if (!ctype_alnum($username) ||
+ !ctype_graph($challenge) ||
+ !ctype_alnum($nonce)) {
+ Minz_Log::debug('Invalid credential parameters:' .
+ ' user=' . $username .
+ ' challenge=' . $challenge .
+ ' nonce=' . $nonce);
+ return false;
+ }
+
+ if (!function_exists('password_verify')) {
+ include_once(LIB_PATH . '/password_compat.php');
+ }
+
+ return password_verify($nonce . $hash, $challenge);
+ }
+
+ public static function getCredentialsFromCookie() {
+ $token = Minz_Session::getLongTermCookie('FreshRSS_login');
+ if (!ctype_alnum($token)) {
+ return array();
+ }
+
+ $token_file = DATA_PATH . '/tokens/' . $token . '.txt';
+ $mtime = @filemtime($token_file);
+ if ($mtime + 2629744 < time()) {
+ // Token has expired (> 1 month) or does not exist.
+ // TODO: 1 month -> use a configuration instead
+ @unlink($token_file);
+ return array();
+ }
+
+ $credentials = @file_get_contents($token_file);
+ return $credentials === false ? array() : explode("\t", $credentials, 2);
+ }
+
+ public static function makeCookie($username, $password_hash) {
+ do {
+ $token = sha1(Minz_Configuration::salt() . $username . uniqid(mt_rand(), true));
+ $token_file = DATA_PATH . '/tokens/' . $token . '.txt';
+ } while (file_exists($token_file));
+
+ if (@file_put_contents($token_file, $username . "\t" . $password_hash) === false) {
+ return false;
+ }
+
+ $expire = time() + 2629744; //1 month //TODO: Use a configuration instead
+ Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
+ return $token;
+ }
+
+ public static function deleteCookie() {
+ $token = Minz_Session::getLongTermCookie('FreshRSS_login');
+ Minz_Session::deleteLongTermCookie('FreshRSS_login');
+ if (ctype_alnum($token)) {
+ @unlink(DATA_PATH . '/tokens/' . $token . '.txt');
+ }
+
+ if (rand(0, 10) === 1) {
+ self::purgeTokens();
+ }
+ }
+
+ public static function purgeTokens() {
+ $oldest = time() - 2629744; // 1 month // TODO: Use a configuration instead
+ foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $file_info) {
+ // $extension = $file_info->getExtension(); doesn't work in PHP < 5.3.7
+ $extension = pathinfo($file_info->getFilename(), PATHINFO_EXTENSION);
+ if ($extension === 'txt' && $file_info->getMTime() < $oldest) {
+ @unlink($file_info->getPathname());
+ }
+ }
+ }
+}
diff --git a/app/layout/aside_flux.phtml b/app/layout/aside_flux.phtml
index a8ae2f424..a66be2ed9 100644
--- a/app/layout/aside_flux.phtml
+++ b/app/layout/aside_flux.phtml
@@ -2,7 +2,7 @@
@@ -83,11 +83,11 @@
-
+
+
+
diff --git a/app/views/auth/formLogin.phtml b/app/views/auth/formLogin.phtml
new file mode 100644
index 000000000..0194a11a5
--- /dev/null
+++ b/app/views/auth/formLogin.phtml
@@ -0,0 +1,28 @@
+
diff --git a/app/views/auth/logout.phtml b/app/views/auth/logout.phtml
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/views/auth/personaLogin.phtml b/app/views/auth/personaLogin.phtml
new file mode 100644
index 000000000..d62fe5818
--- /dev/null
+++ b/app/views/auth/personaLogin.phtml
@@ -0,0 +1,24 @@
+res === false) { ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+res);
+}
+?>
diff --git a/app/views/helpers/javascript_vars.phtml b/app/views/helpers/javascript_vars.phtml
index 8f615ed87..3bbcc3848 100644
--- a/app/views/helpers/javascript_vars.phtml
+++ b/app/views/helpers/javascript_vars.phtml
@@ -8,6 +8,15 @@ $hide_posts = ($this->conf->display_posts ||
Minz_Request::param('output') === 'reader');
$s = $this->conf->shortcuts;
+$url_login = Minz_Url::display(array(
+ 'c' => 'auth',
+ 'a' => 'login'
+), 'php');
+$url_logout = Minz_Url::display(array(
+ 'c' => 'auth',
+ 'a' => 'logout'
+), 'php');
+
echo 'var context={',
'hide_posts:', $hide_posts ? 'false' : 'true', ',',
'display_order:"', Minz_Request::param('order', $this->conf->sort_order), '",',
@@ -43,8 +52,8 @@ echo 'shortcuts={',
echo 'url={',
'index:"', _url('index', 'index'), '",',
- 'login:"', _url('index', 'login'), '",',
- 'logout:"', _url('index', 'logout'), '",',
+ 'login:"', $url_login, '",',
+ 'logout:"', $url_logout, '",',
'help:"', FRESHRSS_WIKI, '"',
"},\n";
diff --git a/app/views/index/formLogin.phtml b/app/views/index/formLogin.phtml
deleted file mode 100644
index b05cdced4..000000000
--- a/app/views/index/formLogin.phtml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/p/scripts/main.js b/p/scripts/main.js
index b01a3a34d..77e1e3f77 100644
--- a/p/scripts/main.js
+++ b/p/scripts/main.js
@@ -1034,67 +1034,7 @@ function init_crypto_form() {
}
//
-//
-function init_persona() {
- if (!(navigator.id)) {
- if (window.console) {
- console.log('FreshRSS waiting for Persona…');
- }
- window.setTimeout(init_persona, 100);
- return;
- }
- $('a.signin').click(function() {
- navigator.id.request();
- return false;
- });
-
- $('a.signout').click(function() {
- navigator.id.logout();
- return false;
- });
- navigator.id.watch({
- loggedInUser: context['current_user_mail'],
-
- onlogin: function(assertion) {
- // A user has logged in! Here you need to:
- // 1. Send the assertion to your backend for verification and to create a session.
- // 2. Update your UI.
- $.ajax ({
- type: 'POST',
- url: url['login'],
- data: {assertion: assertion},
- success: function(res, status, xhr) {
- /*if (res.status === 'failure') {
- alert (res_obj.reason);
- } else*/ if (res.status === 'okay') {
- location.href = url['index'];
- }
- },
- error: function(res, status, xhr) {
- alert("Login failure: " + res);
- }
- });
- },
- onlogout: function() {
- // A user has logged out! Here you need to:
- // Tear down the user's session by redirecting the user or making a call to your backend.
- // Also, make sure loggedInUser will get set to null on the next page load.
- // (That's a literal JavaScript null. Not false, 0, or undefined. null.)
- $.ajax ({
- type: 'POST',
- url: url['logout'],
- success: function(res, status, xhr) {
- location.href = url['index'];
- },
- error: function(res, status, xhr) {
- //alert("logout failure" + res);
- }
- });
- }
- });
-}
-//
function init_confirm_action() {
$('body').on('click', '.confirm', function () {
@@ -1274,11 +1214,6 @@ function init_all() {
return;
}
init_notifications();
- switch (context['auth_type']) {
- case 'persona':
- init_persona();
- break;
- }
init_confirm_action();
$stream = $('#stream');
if ($stream.length > 0) {
diff --git a/p/scripts/persona.js b/p/scripts/persona.js
new file mode 100644
index 000000000..36aeeaf56
--- /dev/null
+++ b/p/scripts/persona.js
@@ -0,0 +1,76 @@
+"use strict";
+
+function init_persona() {
+ if (!(navigator.id && window.$)) {
+ if (window.console) {
+ console.log('FreshRSS (Persona) waiting for JS…');
+ }
+ window.setTimeout(init_persona, 100);
+ return;
+ }
+
+ $('a.signin').click(function() {
+ navigator.id.request();
+ return false;
+ });
+
+ $('a.signout').click(function() {
+ navigator.id.logout();
+ return false;
+ });
+
+ navigator.id.watch({
+ loggedInUser: context['current_user_mail'],
+
+ onlogin: function(assertion) {
+ // A user has logged in! Here you need to:
+ // 1. Send the assertion to your backend for verification and to create a session.
+ // 2. Update your UI.
+ $.ajax ({
+ type: 'POST',
+ url: url['login'],
+ data: {assertion: assertion},
+ success: function(res, status, xhr) {
+ if (res.status === 'failure') {
+ openNotification(res.reason, 'bad');
+ } else if (res.status === 'okay') {
+ location.href = url['index'];
+ }
+ },
+ error: function(res, status, xhr) {
+ // alert(res);
+ }
+ });
+ },
+ onlogout: function() {
+ // A user has logged out! Here you need to:
+ // Tear down the user's session by redirecting the user or making a call to your backend.
+ // Also, make sure loggedInUser will get set to null on the next page load.
+ // (That's a literal JavaScript null. Not false, 0, or undefined. null.)
+ $.ajax ({
+ type: 'POST',
+ url: url['logout'],
+ success: function(res, status, xhr) {
+ location.href = url['index'];
+ },
+ error: function(res, status, xhr) {
+ // alert(res);
+ }
+ });
+ }
+ });
+}
+
+if (document.readyState && document.readyState !== 'loading') {
+ if (window.console) {
+ console.log('FreshRSS (Persona) immediate init…');
+ }
+ init_persona();
+} else if (document.addEventListener) {
+ document.addEventListener('DOMContentLoaded', function () {
+ if (window.console) {
+ console.log('FreshRSS (Persona) waiting for DOMContentLoaded…');
+ }
+ init_persona();
+ }, false);
+}
--
cgit v1.2.3
From dbf57266b297c3f831602ec4f451c27a5ad71e6b Mon Sep 17 00:00:00 2001
From: Marien Fressinaud
Date: Tue, 7 Oct 2014 16:58:11 +0200
Subject: Reset auth system comes back!
It has moved to authController.
---
app/Controllers/authController.php | 68 ++++++++++++++++++++++++++++++++++++++
app/views/auth/personaLogin.phtml | 2 +-
app/views/auth/reset.phtml | 33 ++++++++++++++++++
3 files changed, 102 insertions(+), 1 deletion(-)
create mode 100644 app/views/auth/reset.phtml
diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php
index 2b67e34b8..e30fa4b72 100644
--- a/app/Controllers/authController.php
+++ b/app/Controllers/authController.php
@@ -179,4 +179,72 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
Minz_Request::good(_t('disconnected'),
array('c' => 'index', 'a' => 'index'));
}
+
+ /**
+ * This action resets the authentication system.
+ *
+ * After reseting, form auth is set by default.
+ */
+ public function resetAction() {
+ Minz_View::prependTitle(_t('auth_reset') . ' · ');
+
+ Minz_View::appendScript(Minz_Url::display(
+ '/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')
+ ));
+
+ $this->view->no_form = false;
+ // Enable changement of auth only if Persona!
+ if (Minz_Configuration::authType() != 'persona') {
+ $this->view->message = array(
+ 'status' => 'bad',
+ 'title' => _t('damn'),
+ 'body' => _t('auth_not_persona')
+ );
+ $this->view->no_form = true;
+ return;
+ }
+
+ $conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser());
+ // Admin user must have set its master password.
+ if (!$conf->passwordHash) {
+ $this->view->message = array(
+ 'status' => 'bad',
+ 'title' => _t('damn'),
+ 'body' => _t('auth_no_password_set')
+ );
+ $this->view->no_form = true;
+ return;
+ }
+
+ invalidateHttpCache();
+
+ if (Minz_Request::isPost()) {
+ $nonce = Minz_Session::param('nonce');
+ $username = Minz_Request::param('username', '');
+ $challenge = Minz_Request::param('challenge', '');
+
+ $ok = FreshRSS_FormAuth::checkCredentials(
+ $username, $conf->passwordHash, $nonce, $challenge
+ );
+
+ if ($ok) {
+ Minz_Configuration::_authType('form');
+ $ok = Minz_Configuration::writeFile();
+
+ if ($ok) {
+ Minz_Request::good(_t('auth_form_set'));
+ } else {
+ Minz_Request::bad(_t('auth_form_not_set'),
+ array('c' => 'auth', 'a' => 'reset'));
+ }
+ } else {
+ Minz_Log::warning('Password mismatch for' .
+ ' user=' . $username .
+ ', nonce=' . $nonce .
+ ', c=' . $challenge);
+ Minz_Request::bad(_t('invalid_login'),
+ array('c' => 'auth', 'a' => 'reset'));
+ }
+ }
+ }
}
diff --git a/app/views/auth/personaLogin.phtml b/app/views/auth/personaLogin.phtml
index d62fe5818..dd3e22b52 100644
--- a/app/views/auth/personaLogin.phtml
+++ b/app/views/auth/personaLogin.phtml
@@ -11,7 +11,7 @@
-
+
diff --git a/app/views/auth/reset.phtml b/app/views/auth/reset.phtml
new file mode 100644
index 000000000..e501555c4
--- /dev/null
+++ b/app/views/auth/reset.phtml
@@ -0,0 +1,33 @@
+
+
+
+ message)) { ?>
+
+ message['title']; ?>
+ message['body']; ?>
+
+
+
+ no_form) { ?>
+
+
+
--
cgit v1.2.3