aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2014-03-01 14:45:58 +0100
committerGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2014-03-01 14:45:58 +0100
commit29b3bbfe284a6e56413a2e89b740ffc4172c6847 (patch)
tree5e1b74f889f071e3e45beca09673304629e79f74
parentf44683b5671b323ba96f0c4cd47ba9458e934679 (diff)
API: Real password system
https://github.com/marienfressinaud/FreshRSS/issues/13 Expiring token not implemented yet
-rw-r--r--app/Controllers/usersController.php12
-rw-r--r--app/Models/Configuration.php4
-rw-r--r--app/i18n/en.php1
-rw-r--r--app/i18n/fr.php1
-rw-r--r--app/views/configure/users.phtml12
-rw-r--r--p/api/greader.php89
6 files changed, 82 insertions, 37 deletions
diff --git a/app/Controllers/usersController.php b/app/Controllers/usersController.php
index bb4f34c5e..b03989cd7 100644
--- a/app/Controllers/usersController.php
+++ b/app/Controllers/usersController.php
@@ -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));
}
diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php
index 48efe3bf6..827a1d166 100644
--- a/app/Models/Configuration.php
+++ b/app/Models/Configuration.php
@@ -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) {
diff --git a/app/i18n/en.php b/app/i18n/en.php
index e67447520..d504ffc11 100644
--- a/app/i18n/en.php
+++ b/app/i18n/en.php
@@ -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',
diff --git a/app/i18n/fr.php b/app/i18n/fr.php
index 2bd4fabab..c5581a78b 100644
--- a/app/i18n/fr.php
+++ b/app/i18n/fr.php
@@ -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 l’utilisateur 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 l’utilisateur par défaut (%s)',
diff --git a/app/views/configure/users.phtml b/app/views/configure/users.phtml
index 0677db881..f5c7dff17 100644
--- a/app/views/configure/users.phtml
+++ b/app/views/configure/users.phtml
@@ -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>
diff --git a/p/api/greader.php b/p/api/greader.php
index e99e1c0c8..035a031dd 100644
--- a/p/api/greader.php
+++ b/p/api/greader.php
@@ -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') {