mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-05-18 05:14:34 -04:00
Refactor authentication system.
Big work, not finished. A lot of features have been removed. See https://github.com/marienfressinaud/FreshRSS/issues/655
This commit is contained in:
@@ -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')))
|
||||
|
||||
@@ -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')))
|
||||
|
||||
@@ -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')))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')))
|
||||
|
||||
@@ -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);
|
||||
if (FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
|
||||
}
|
||||
|
||||
$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);
|
||||
invalidateHttpCache();
|
||||
|
||||
$res = json_decode($result, true);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
$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 . ']!';
|
||||
}
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function formLoginAction() {
|
||||
if (FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
|
||||
}
|
||||
|
||||
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', '');
|
||||
$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' => 'login'));
|
||||
}
|
||||
|
||||
$ok = FreshRSS_FormAuth::checkCredentials(
|
||||
$username, $conf->passwordHash, $nonce, $challenge
|
||||
);
|
||||
if ($ok) {
|
||||
// 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 {
|
||||
FreshRSS_FormAuth::deleteCookie();
|
||||
}
|
||||
|
||||
// All is good, go back to the index.
|
||||
Minz_Request::good(_t('login'),
|
||||
array('c' => 'index', 'a' => 'index'));
|
||||
} else {
|
||||
$reason = 'Invalid email format [' . $res['email'] . ']!';
|
||||
Minz_Log::warning('Password mismatch for' .
|
||||
' user=' . $username .
|
||||
', nonce=' . $nonce .
|
||||
', c=' . $challenge);
|
||||
Minz_Request::bad(_t('invalid_login'),
|
||||
array('c' => 'index', 'a' => 'login'));
|
||||
}
|
||||
}
|
||||
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']);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function formLoginAction() {
|
||||
if ($this->view->loginOk) {
|
||||
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();
|
||||
|
||||
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);
|
||||
Minz_Request::bad(_t('invalid_login'),
|
||||
array('c' => 'index', 'a' => 'resetAuth'));
|
||||
}
|
||||
|
||||
if (!function_exists('password_verify')) {
|
||||
include_once(LIB_PATH . '/password_compat.php');
|
||||
}
|
||||
|
||||
$s = $conf->passwordHash;
|
||||
$ok = password_verify($nonce . $s, $c);
|
||||
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' => 'index', 'a' => 'resetAuth'));
|
||||
}
|
||||
} else {
|
||||
Minz_Log::debug('Password mismatch for user ' . $username .
|
||||
', nonce=' . $nonce . ', c=' . $c);
|
||||
|
||||
Minz_Request::bad(_t('invalid_login'),
|
||||
array('c' => 'index', 'a' => 'resetAuth'));
|
||||
}
|
||||
}
|
||||
FreshRSS_Auth::removeAccess();
|
||||
Minz_Request::good(_t('disconnected'),
|
||||
array('c' => 'index', 'a' => 'index'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')))
|
||||
);
|
||||
|
||||
@@ -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')))
|
||||
|
||||
@@ -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')))
|
||||
|
||||
@@ -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')))
|
||||
|
||||
135
app/FreshRSS.php
135
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());
|
||||
}
|
||||
} catch(Minz_Exception $e) {
|
||||
Minz_Log::error('Cannot load configuration file of user `' . $current_user . '`');
|
||||
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;
|
||||
}
|
||||
}
|
||||
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')));
|
||||
|
||||
209
app/Models/Auth.php
Normal file
209
app/Models/Auth.php
Normal file
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<?php print_r($this->res); ?>
|
||||
@@ -1 +0,0 @@
|
||||
OK
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user