API: Real password system

https://github.com/marienfressinaud/FreshRSS/issues/13
Expiring token not implemented yet
This commit is contained in:
Alexandre Alapetite
2014-03-01 14:45:58 +01:00
parent f44683b567
commit 29b3bbfe28
6 changed files with 82 additions and 37 deletions

View File

@@ -32,6 +32,18 @@ class FreshRSS_users_Controller extends Minz_ActionController {
}
Minz_Session::_param('passwordHash', $this->view->conf->passwordHash);
$passwordPlain = Minz_Request::param('apiPasswordPlain', false);
if ($passwordPlain != '') {
if (!function_exists('password_hash')) {
include_once(LIB_PATH . '/password_compat.php');
}
$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
$passwordPlain = '';
$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
$ok &= ($passwordHash != '');
$this->view->conf->_apiPasswordHash($passwordHash);
}
if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
$this->view->conf->_mail_login(Minz_Request::param('mail_login', false));
}

View File

@@ -10,6 +10,7 @@ class FreshRSS_Configuration {
'mail_login' => '',
'token' => '',
'passwordHash' => '', //CRYPT_BLOWFISH
'apiPasswordHash' => '', //CRYPT_BLOWFISH
'posts_per_page' => 20,
'view_mode' => 'normal',
'default_view' => 'not_read',
@@ -165,6 +166,9 @@ class FreshRSS_Configuration {
public function _passwordHash ($value) {
$this->data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
}
public function _apiPasswordHash ($value) {
$this->data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
}
public function _mail_login ($value) {
$value = filter_var($value, FILTER_VALIDATE_EMAIL);
if ($value) {

View File

@@ -176,6 +176,7 @@ return array (
'current_user' => 'Current user',
'default_user' => 'Username of the default user <small>(maximum 16 alphanumeric characters)</small>',
'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
'password_api' => 'Password API<br /><small>(e.g., for mobile apps)</small>',
'persona_connection_email' => 'Login mail address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
'allow_anonymous' => 'Allow anonymous reading of the articles of the default user (%s)',
'allow_anonymous_refresh' => 'Allow anonymous refresh of the articles',

View File

@@ -175,6 +175,7 @@ return array (
'current_user' => 'Utilisateur actuel',
'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
'password_api' => 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</small>',
'default_user' => 'Nom de lutilisateur par défaut <small>(16 caractères alphanumériques maximum)</small>',
'persona_connection_email' => 'Adresse courriel de connexion<br /><small>(pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
'allow_anonymous' => 'Autoriser la lecture anonyme des articles de lutilisateur par défaut (%s)',

View File

@@ -20,7 +20,15 @@
<div class="form-group">
<label class="group-name" for="passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label>
<div class="group-controls">
<input type="password" id="passwordPlain" name="passwordPlain" autocomplete="off" pattern=".{7,}" />
<input type="password" id="passwordPlain" name="passwordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
</div>
</div>
<div class="form-group">
<label class="group-name" for="apiPasswordPlain"><?php echo Minz_Translate::t('password_api'); ?></label>
<div class="group-controls">
<input type="password" id="apiPasswordPlain" name="apiPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
</div>
</div>
@@ -85,7 +93,7 @@
<label class="group-name" for="token"><?php echo Minz_Translate::t('auth_token'); ?></label>
<?php $token = $this->conf->token; ?>
<div class="group-controls">
<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo Minz_Translate::t('blank_to_disable'); ?>"<?php
<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo Minz_Translate::t('blank_to_disable'); ?>"<?php
echo Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('explain_token', Minz_Url::display(null, 'html', true), $token); ?>
</div>

View File

@@ -20,9 +20,6 @@ Server-side API compatible with Google Reader API layer 2
* https://github.com/theoldreader/api
*/
define('TEMP_PASSWORD', 'temp123'); //Change to another ASCII password
define('TEMP_AUTH', 'XtofqkkOkCULRLH8'); //Change to another random ASCII auth
require('../../constants.php');
require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
@@ -119,14 +116,28 @@ function checkCompatibility() {
exit();
}
function authorizationToUser() {
function authorizationToUserConf() {
$headerAuth = headerVariable('Authorization', 'GoogleLogin_auth'); //Input is 'GoogleLogin auth', but PHP replaces spaces by '_' http://php.net/language.variables.external
if ($headerAuth != '') {
$headerAuthX = explode('/', $headerAuth, 2);
if ((count($headerAuthX) === 2) && ($headerAuthX[1] === TEMP_AUTH)) {
if (count($headerAuthX) === 2) {
$user = $headerAuthX[0];
if (ctype_alnum($user)) {
return $user;
try {
$conf = new FreshRSS_Configuration($user);
} catch (Exception $e) {
logMe($e->getMessage() . "\n");
unauthorized();
}
if ($headerAuthX[1] === sha1(Minz_Configuration::salt() . $conf->user . $conf->apiPasswordHash)) {
return $conf;
} else {
logMe('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1] . "\n");
Minz_Log::record('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1], Minz_Log::WARNING);
unauthorized();
}
} else {
badRequest();
}
}
}
@@ -135,28 +146,45 @@ function authorizationToUser() {
function clientLogin($email, $pass) { //http://web.archive.org/web/20130604091042/http://undoc.in/clientLogin.html
logMe('clientLogin(' . $email . ")\n");
if ($pass !== TEMP_PASSWORD) {
unauthorized();
if (ctype_alnum($email)) {
if (!function_exists('password_verify')) {
include_once(LIB_PATH . '/password_compat.php');
}
try {
$conf = new FreshRSS_Configuration($email);
} catch (Exception $e) {
logMe($e->getMessage() . "\n");
Minz_Log::record('Invalid API user ' . $email, Minz_Log::WARNING);
unauthorized();
}
if ($conf->apiPasswordHash != '' && password_verify($pass, $conf->apiPasswordHash)) {
header('Content-Type: text/plain; charset=UTF-8');
$auth = $email . '/' . sha1(Minz_Configuration::salt() . $conf->user . $conf->apiPasswordHash);
echo 'SID=', $auth, "\n",
'Auth=', $auth, "\n";
exit();
} else {
Minz_Log::record('Password API mismatch for user ' . $email, Minz_Log::WARNING);
unauthorized();
}
} else {
badRequest();
}
header('Content-Type: text/plain; charset=UTF-8');
$auth = $email . '/' . TEMP_AUTH;
echo 'SID=', $auth, "\n",
'Auth=', $auth, "\n";
exit();
die();
}
function token($user) {
function token($conf) {
//http://blog.martindoms.com/2009/08/15/using-the-google-reader-api-part-1/ https://github.com/ericmann/gReader-Library/blob/master/greader.class.php
logMe('token('. $user . ")\n");
$token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ01234'; //Must have 57 characters...
logMe('token('. $conf->user . ")\n"); //TODO: Implement real token that expires
$token = str_pad(sha1(Minz_Configuration::salt() . $conf->user . $conf->apiPasswordHash), 57, 'Z'); //Must have 57 characters
echo $token, "\n";
exit();
}
function checkToken($user, $token) {
function checkToken($conf, $token) {
//http://code.google.com/p/google-reader-api/wiki/ActionToken
logMe('checkToken(' . $token . ")\n");
if ($token === 'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ01234') {
if ($token === str_pad(sha1(Minz_Configuration::salt() . $conf->user . $conf->apiPasswordHash), 57, 'Z')) {
return true;
}
unauthorized();
@@ -462,32 +490,23 @@ if (!Minz_Configuration::apiEnabled()) {
Minz_Session::init('FreshRSS');
$user = authorizationToUser();
$conf = null;
$conf = authorizationToUserConf();
$user = $conf == null ? '' : $conf->user;
logMe('User => ' . $user . "\n");
if ($user != null) {
try {
$conf = new FreshRSS_Configuration($user);
} catch (Exception $e) {
logMe($e->getMessage());
$user = null;
badRequest();
}
}
Minz_Session::_param('currentUser', $user);
if (count($pathInfos) < 3) {
badRequest();
}
elseif ($pathInfos[1] === 'accounts') {
if (($pathInfos[2] === 'ClientLogin') && isset($_REQUEST['Email']) && isset($_REQUEST['Passwd']))
if (($pathInfos[2] === 'ClientLogin') && isset($_REQUEST['Email']) && isset($_REQUEST['Passwd'])) {
clientLogin($_REQUEST['Email'], $_REQUEST['Passwd']);
}
}
elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfos[3]) && $pathInfos[3] === '0' && isset($pathInfos[4])) {
if ($user == null) {
if ($user == '') {
unauthorized();
}
$timestamp = isset($_GET['ck']) ? intval($_GET['ck']) : 0; //ck=[unix timestamp] : Use the current Unix time here, helps Google with caching.
@@ -543,7 +562,7 @@ elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfo
break;
case 'edit-tag': //http://blog.martindoms.com/2010/01/20/using-the-google-reader-api-part-3/
$token = isset($_POST['T']) ? trim($_POST['T']) : '';
checkToken($user, $token);
checkToken($conf, $token);
$a = isset($_POST['a']) ? $_POST['a'] : ''; //Add: user/-/state/com.google/read user/-/state/com.google/starred
$r = isset($_POST['r']) ? $_POST['r'] : ''; //Remove: user/-/state/com.google/read user/-/state/com.google/starred
$e_ids = multiplePosts('i'); //item IDs
@@ -551,7 +570,7 @@ elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfo
break;
case 'mark-all-as-read':
$token = isset($_POST['T']) ? trim($_POST['T']) : '';
checkToken($user, $token);
checkToken($conf, $token);
$streamId = $_POST['s']; //StreamId
$ts = isset($_POST['ts']) ? $_POST['ts'] : '0'; //Older than timestamp in nanoseconds
if (!ctype_digit($ts)) {
@@ -560,7 +579,7 @@ elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfo
markAllAsRead($streamId, $ts);
break;
case 'token':
Token($user);
Token($conf);
break;
}
} elseif ($pathInfos[1] === 'check' && $pathInfos[2] === 'compatibility') {