Ver código fonte

API: Real password system

https://github.com/marienfressinaud/FreshRSS/issues/13
Expiring token not implemented yet
Alexandre Alapetite 12 anos atrás
pai
commit
29b3bbfe28

+ 12 - 0
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));
 			}

+ 4 - 0
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) {

+ 1 - 0
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',

+ 1 - 0
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)',

+ 10 - 2
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>

+ 54 - 35
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') {