diff options
| author | 2014-10-06 23:29:20 +0200 | |
|---|---|---|
| committer | 2014-10-06 23:29:20 +0200 | |
| commit | 79aa5beaf44af13a1828bfa5fc824a08c62054dc (patch) | |
| tree | 53841c4ba4af03498c9005ced85cd1996cb9ca9b /app | |
| parent | 530a1d4b6b043f6b6976bb7ad25b380c29d5b5a4 (diff) | |
Refactor authentication system.
Big work, not finished. A lot of features have been removed.
See https://github.com/marienfressinaud/FreshRSS/issues/655
Diffstat (limited to 'app')
| -rw-r--r-- | app/Controllers/categoryController.php | 2 | ||||
| -rwxr-xr-x | app/Controllers/configureController.php | 2 | ||||
| -rwxr-xr-x | app/Controllers/entryController.php | 2 | ||||
| -rwxr-xr-x | app/Controllers/feedController.php | 2 | ||||
| -rw-r--r-- | app/Controllers/importExportController.php | 2 | ||||
| -rwxr-xr-x | app/Controllers/indexController.php | 296 | ||||
| -rw-r--r-- | app/Controllers/statsController.php | 2 | ||||
| -rw-r--r-- | app/Controllers/subscriptionController.php | 2 | ||||
| -rw-r--r-- | app/Controllers/updateController.php | 2 | ||||
| -rw-r--r-- | app/Controllers/usersController.php | 2 | ||||
| -rw-r--r-- | app/FreshRSS.php | 135 | ||||
| -rw-r--r-- | app/Models/Auth.php | 209 | ||||
| -rw-r--r-- | app/layout/aside_flux.phtml | 6 | ||||
| -rw-r--r-- | app/layout/header.phtml | 32 | ||||
| -rw-r--r-- | app/layout/nav_menu.phtml | 4 | ||||
| -rw-r--r-- | app/views/helpers/view/normal_view.phtml | 6 | ||||
| -rw-r--r-- | app/views/index/index.phtml | 2 | ||||
| -rw-r--r-- | app/views/index/login.phtml | 1 | ||||
| -rw-r--r-- | app/views/index/logout.phtml | 1 | ||||
| -rw-r--r-- | app/views/index/resetAuth.phtml | 33 |
20 files changed, 309 insertions, 434 deletions
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 @@ +<?php + +/** + * This class handles all authentication process. + */ +class FreshRSS_Auth { + /** + * Determines if user is connected. + */ + private static $login_ok = false; + + /** + * This method initializes authentication system. + */ + public static function init() { + self::$login_ok = Minz_Session::param('loginOk', false); + $current_user = Minz_Session::param('currentUser', ''); + if ($current_user === '') { + $current_user = Minz_Configuration::defaultUser(); + Minz_Session::_param('currentUser', $current_user); + } + + $access_ok = self::accessControl($current_user); + + if ($access_ok) { + self::giveAccess(); + } else { + // Be sure all accesses are removed! + self::removeAccess(); + } + } + + /** + * This method checks if user is allowed to connect. + * + * Required session parameters are also set in this method (such as + * currentUser). + * + * @param string $username username of the user to check access. + * @return boolean true if user can be connected, false else. + */ + public static function accessControl($username) { + if (self::$login_ok) { + return true; + } + + switch (Minz_Configuration::authType()) { + case 'form': + $credentials = FreshRSS_FormAuth::getCredentialsFromCookie(); + $current_user = ''; + if (isset($credentials[1])) { + $current_user = trim($credentials[0]); + Minz_Session::_param('currentUser', $current_user); + Minz_Session::_param('passwordHash', trim($credentials[1])); + } + return $current_user != ''; + case 'http_auth': + $current_user = httpAuthUser(); + $login_ok = $current_user != ''; + if ($login_ok) { + Minz_Session::_param('currentUser', $current_user); + } + return $login_ok; + case 'none': + return true; + default: + // TODO load extension + return false; + } + } + + /** + * Gives access to the current user. + */ + public static function giveAccess() { + $current_user = Minz_Session::param('currentUser'); + try { + $conf = new FreshRSS_Configuration($current_user); + } catch(Minz_Exception $e) { + die($e->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 @@ <a class="toggle_aside" href="#close"><?php echo _i('close'); ?></a> <ul class="categories"> - <?php if ($this->loginOk) { ?> + <?php if (FreshRSS_Auth::hasAccess()) { ?> <form id="mark-read-aside" method="post" style="display: none"></form> <li> @@ -83,11 +83,11 @@ <ul class="dropdown-menu"> <li class="dropdown-close"><a href="#close">❌</a></li> <li class="item"><a href="<?php echo _url('index', 'index', 'get', 'f_!!!!!!'); ?>"><?php echo _t('filter'); ?></a></li> - <?php if ($this->loginOk) { ?> + <?php if (FreshRSS_Auth::hasAccess()) { ?> <li class="item"><a href="<?php echo _url('stats', 'repartition', 'id', '!!!!!!'); ?>"><?php echo _t('stats'); ?></a></li> <?php } ?> <li class="item"><a target="_blank" href="http://example.net/"><?php echo _t('see_website'); ?></a></li> - <?php if ($this->loginOk) { ?> + <?php if (FreshRSS_Auth::hasAccess()) { ?> <li class="separator"></li> <li class="item"><a href="<?php echo _url('subscription', 'index', 'id', '!!!!!!'); ?>"><?php echo _t('administration'); ?></a></li> <li class="item"><a href="<?php echo _url('feed', 'actualize', 'id', '!!!!!!'); ?>"><?php echo _t('actualize'); ?></a></li> diff --git a/app/layout/header.phtml b/app/layout/header.phtml index 4b571ef06..fadfd13d7 100644 --- a/app/layout/header.phtml +++ b/app/layout/header.phtml @@ -1,22 +1,11 @@ <?php if (Minz_Configuration::canLogIn()) { ?><ul class="nav nav-head nav-login"><?php - switch (Minz_Configuration::authType()) { - case 'form': - if ($this->loginOk) { - ?><li class="item"><?php echo _i('logout'); ?> <a class="signout" href="<?php echo _url('index', 'formLogout'); ?>"><?php echo _t('logout'); ?></a></li><?php + if (FreshRSS_Auth::hasAccess()) { + ?><li class="item"><?php echo _i('logout'); ?> <a class="signout" href="<?php echo _url('index', 'logout'); ?>"><?php echo _t('logout'); ?></a></li><?php } else { - ?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="<?php echo _url('index', 'formLogin'); ?>"><?php echo _t('login'); ?></a></li><?php + ?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="<?php echo _url('index', 'login'); ?>"><?php echo _t('login'); ?></a></li><?php } - break; - case 'persona': - if ($this->loginOk) { - ?><li class="item"><?php echo _i('logout'); ?> <a class="signout" href="#"><?php echo _t('logout'); ?></a></li><?php - } else { - ?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="#"><?php echo _t('login'); ?></a></li><?php - } - break; - } ?></ul><?php } ?> @@ -32,7 +21,7 @@ if (Minz_Configuration::canLogIn()) { </div> <div class="item search"> - <?php if ($this->loginOk || Minz_Configuration::allowAnonymous()) { ?> + <?php if (FreshRSS_Auth::hasAccess() || Minz_Configuration::allowAnonymous()) { ?> <form action="<?php echo _url('index', 'index'); ?>" method="get"> <div class="stick"> <?php $search = Minz_Request::param('search', ''); ?> @@ -59,7 +48,7 @@ if (Minz_Configuration::canLogIn()) { <?php } ?> </div> - <?php if ($this->loginOk) { ?> + <?php if (FreshRSS_Auth::hasAccess()) { ?> <div class="item configure"> <div class="dropdown"> <div id="dropdown-configure" class="dropdown-target"></div> @@ -87,15 +76,8 @@ if (Minz_Configuration::canLogIn()) { <li class="item"><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about'); ?></a></li> <?php if (Minz_Configuration::canLogIn()) { - ?><li class="separator"></li><?php - switch (Minz_Configuration::authType()) { - case 'form': - ?><li class="item"><a class="signout" href="<?php echo _url('index', 'formLogout'); ?>"><?php echo _i('logout'), ' ', _t('logout'); ?></a></li><?php - break; - case 'persona': - ?><li class="item"><a class="signout" href="#"><?php echo _i('logout'), ' ', _t('logout'); ?></a></li><?php - break; - } + ?><li class="separator"></li> + <li class="item"><a class="signout" href="<?php echo _url('index', 'logout'); ?>"><?php echo _i('logout'), ' ', _t('logout'); ?></a></li><?php } ?> </ul> </div> diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml index a9e6614e7..090b55785 100644 --- a/app/layout/nav_menu.phtml +++ b/app/layout/nav_menu.phtml @@ -6,7 +6,7 @@ <a class="btn toggle_aside" href="#aside_flux"><?php echo _i('category'); ?></a> <?php } ?> - <?php if ($this->loginOk) { ?> + <?php if (FreshRSS_Auth::hasAccess()) { ?> <div id="nav_menu_actions" class="stick"> <?php $url_state = $this->url; @@ -300,7 +300,7 @@ <?php echo _i($icon); ?> </a> - <?php if ($this->loginOk || Minz_Configuration::allowAnonymousRefresh()) { ?> + <?php if (FreshRSS_Auth::hasAccess() || Minz_Configuration::allowAnonymousRefresh()) { ?> <a id="actualize" class="btn" href="<?php echo _url('feed', 'actualize'); ?>"><?php echo _i('refresh'); ?></a> <?php } ?> </div> diff --git a/app/views/helpers/view/normal_view.phtml b/app/views/helpers/view/normal_view.phtml index 109fad0eb..db25714bb 100644 --- a/app/views/helpers/view/normal_view.phtml +++ b/app/views/helpers/view/normal_view.phtml @@ -7,7 +7,7 @@ if (!empty($this->entries)) { $display_today = true; $display_yesterday = true; $display_others = true; - if ($this->loginOk) { + if (FreshRSS_Auth::hasAccess()) { $sharing = $this->conf->sharing; } else { $sharing = array(); @@ -58,7 +58,7 @@ if (!empty($this->entries)) { } ?><div class="flux<?php echo !$item->isRead() ? ' not_read' : ''; ?><?php echo $item->isFavorite() ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id(); ?>"> <ul class="horizontal-list flux_header"><?php - if ($this->loginOk) { + if (FreshRSS_Auth::hasAccess()) { if ($topline_read) { ?><li class="item manage"><?php $arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id())); @@ -103,7 +103,7 @@ if (!empty($this->entries)) { ?> </div> <ul class="horizontal-list bottom"><?php - if ($this->loginOk) { + if (FreshRSS_Auth::hasAccess()) { if ($bottomline_read) { ?><li class="item manage"><?php $arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id())); diff --git a/app/views/index/index.phtml b/app/views/index/index.phtml index 584792e29..a59063557 100644 --- a/app/views/index/index.phtml +++ b/app/views/index/index.phtml @@ -2,7 +2,7 @@ $output = Minz_Request::param('output', 'normal'); -if ($this->loginOk || Minz_Configuration::allowAnonymous()) { +if (FreshRSS_Auth::hasAccess() || Minz_Configuration::allowAnonymous()) { if ($output === 'normal') { $this->renderHelper('view/normal_view'); } elseif ($output === 'reader') { diff --git a/app/views/index/login.phtml b/app/views/index/login.phtml deleted file mode 100644 index 79fbe9d21..000000000 --- a/app/views/index/login.phtml +++ /dev/null @@ -1 +0,0 @@ -<?php print_r($this->res); ?> diff --git a/app/views/index/logout.phtml b/app/views/index/logout.phtml deleted file mode 100644 index a0aba9318..000000000 --- a/app/views/index/logout.phtml +++ /dev/null @@ -1 +0,0 @@ -OK
\ No newline at end of file diff --git a/app/views/index/resetAuth.phtml b/app/views/index/resetAuth.phtml deleted file mode 100644 index 6d4282c14..000000000 --- a/app/views/index/resetAuth.phtml +++ /dev/null @@ -1,33 +0,0 @@ -<div class="prompt"> - <h1><?php echo _t('auth_reset'); ?></h1> - - <?php if (!empty($this->message)) { ?> - <p class="alert <?php echo $this->message['status'] === 'bad' ? 'alert-error' : 'alert-warn'; ?>"> - <span class="alert-head"><?php echo $this->message['title']; ?></span><br /> - <?php echo $this->message['body']; ?> - </p> - <?php } ?> - - <?php if (!$this->no_form) { ?> - <form id="crypto-form" method="post" action="<?php echo _url('index', 'resetAuth'); ?>"> - <p class="alert alert-warn"> - <span class="alert-head"><?php echo _t('attention'); ?></span><br /> - <?php echo _t('auth_will_reset'); ?> - </p> - - <div> - <label for="username"><?php echo _t('username_admin'); ?></label> - <input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" /> - </div> - <div> - <label for="passwordPlain"><?php echo _t('password'); ?></label> - <input type="password" id="passwordPlain" required="required" /> - <input type="hidden" id="challenge" name="challenge" /><br /> - <noscript><strong><?php echo _t('javascript_should_be_activated'); ?></strong></noscript> - </div> - <div> - <button id="loginButton" type="submit" class="btn btn-important"><?php echo _t('submit'); ?></button> - </div> - </form> - <?php } ?> -</div> |
