Commit 89455a49 authored by Patrick Barroca's avatar Patrick Barroca 😠
Browse files

Merge branch 'sandbox-user-password-hash' into 'stable'

Sandbox user password hash

See merge request afi/opacce!2424
parents b66adccf 368f9027
- Administration : hashage des mots de passe administrateurs
\ No newline at end of file
......@@ -161,23 +161,35 @@ class Admin_IndexController extends ZendAfi_Controller_Action {
public function becomeAction() {
if(!Class_Users::getIdentity()->isSuperAdmin()) {
if (!Class_Users::getIdentity()->isSuperAdmin()) {
$this->_helper->notify($this->_('Vous n\'avez pas les droits suffisants pour utiliser cette fonctionnalité.'));
return $this->_redirectToIndex();
}
if(!$user = Class_Users::find($this->_getParam('id', null))) {
if (!$user = Class_Users::find($this->_getParam('id', null))) {
$this->_helper->notify($this->_('Utilisateur invalide'));
return $this->_redirectToIndex();
}
if(!ZendAfi_Auth::getInstance()->authenticateLoginPassword($user->getLogin(), $user->getPassword())) {
if ($user->isAbonne())
return $this->_becomePatron($user);
ZendAfi_Auth::getInstance()->logUser($user);
$this->_helper->notify($this->_('Vous êtes maintenant connecté avec l\'utilisateur "%s"',
$user->getNomComplet()));
$this->_redirectToIndex();
}
protected function _becomePatron($user) {
if (!ZendAfi_Auth::getInstance()->authenticateLoginPassword($user->getLogin(), $user->getPassword())) {
$this->_helper->notify($this->_('Nom d\'utilisateur ou mot de passe invalide'));
return $this->_redirectToIndex();
}
$this->_helper->notify($this->_('Vous êtes maintenant connecté avec l\'utilisateur "%s"',
$user->getNomComplet()));
$this->_redirectToIndex();
return $this->_redirectToIndex();
}
}
\ No newline at end of file
......@@ -20,6 +20,10 @@
*/
class AuthController extends ZendAfi_Controller_Action {
const
LOST_PASS_NOLOGIN = 1,
LOST_PASS_NOUSER = 2,
LOST_PASS_NOMAIL = 4;
public function init() {
$this->view->locale = Zend_Registry::get('locale');
......@@ -54,12 +58,13 @@ class AuthController extends ZendAfi_Controller_Action {
public function getErrorMessages($error_code) {
$messages= [1 => $this->_('Veuillez saisir votre identifiant.'),
2 => $this->_('Identifiant inconnu.'),
4 => $this->_("Votre mail n'est pas renseigné dans votre compte lecteur. Merci de vous adresser à la bibliothèque pour récupérer votre mot de passe ou bien le remplacer par un nouveau.")];
if (isset($messages[$error_code]))
return $messages[$error_code];
return '';
$messages = [static::LOST_PASS_NOLOGIN => $this->_('Veuillez saisir votre identifiant.'),
static::LOST_PASS_NOUSER => $this->_('Identifiant inconnu.'),
static::LOST_PASS_NOMAIL => $this->_("Votre mail n'est pas renseigné dans votre compte lecteur. Merci de vous adresser à la bibliothèque pour récupérer votre mot de passe ou bien le remplacer par un nouveau.")];
return isset($messages[$error_code])
? $messages[$error_code]
: '';
}
......@@ -84,13 +89,13 @@ class AuthController extends ZendAfi_Controller_Action {
$password = $f->filter($this->_request->getPost('password'));
if (empty($username))
return $this->view->_('Entrez votre identifiant S.V.P.');
return $this->_('Entrez votre identifiant S.V.P.');
if (empty($password))
return $this->view->_('Entrez votre mot de passe S.V.P.');
return $this->_('Entrez votre mot de passe S.V.P.');
if (!ZendAfi_Auth::getInstance()->authenticateLoginPassword($username, $password))
return $this->view->_('Identifiant ou mot de passe incorrect.');
return $this->_('Identifiant ou mot de passe incorrect.');
$user = Class_Users::getIdentity();
$this->_helper->trackEvent('authentification', 'connexion', 'utilisateur', $user->getId());
......@@ -296,25 +301,54 @@ class AuthController extends ZendAfi_Controller_Action {
public function lostpassAction() {
$preferences = $this->_loginPrefFromWidgetOrModule();
$options = ['username_label' => $preferences["identifiant"],
'username_placeholder' => $preferences["identifiant_exemple"]];
$this->view->form = ZendAfi_Form_LostPassword::newWithOptions(['username_label' => $preferences["identifiant"],
'username_placeholder' => $preferences["identifiant_exemple"]])
$form = ZendAfi_Form_LostPassword::newWithOptions($options)
->setAction($this->view->url(['module' => 'opac',
'controller' => 'auth',
'action' => 'lostpass'], null , true));
$this->view->form = $form;
if(!$this->_request->isPost())
if(!$this->_request->isPost()
|| !$form->isValid($this->_request->getPost()))
return;
$user = ZendAfi_Filters_Post::filterStatic($this->_request->getPost('lost_username'));
$classe_user = new Class_Users();
$ret = $classe_user->lostpass($user);
$login = ZendAfi_Filters_Post::filterStatic($this->_request->getPost('lost_username'));
$this->view->username = $login;
$this->view->message = $this->getErrorMessages($ret["error"]);
$this->view->message_mail = isset($ret["message_mail"])
? $ret["message_mail"]
: '';
$this->view->username=$user;
$user = Class_Users::findFirstValidOrNotBy(['login' => $login]);
$this->view->message_mail = (new Class_User_LostPass($user))->send();
}
public function resetPasswordAction() {
$this->view->title = $this->_('Réinitialisation du mot de passe');
if (!$user = Class_Users::find((int)$this->_getParam('id')))
return $this->view->message = $this->_('Utilisateur inconnu');
$lostpass = new Class_User_LostPass($user);
$created = $this->_getParam('created');
if ($this->_getParam('token', '') != $lostpass->tokenAt($created))
return $this->view->message = $this->_('Jeton de réinitialisation invalide');
if ($lostpass->tokenHasExpiredFrom($created))
return $this->view->message = $this->_('Jeton de réinitialisation expiré');
$this->view->form = $form = new ZendAfi_Form_ResetPassword();
$form->setAction($this->view->url());
if (!$this->_request->isPost()
|| !$form->isValid($this->_request->getPost()))
return;
$user->setPassword($form->getValue('new_pass'))
->save();
$this->_helper->notify($this->_('Votre mot de passe a été réinitialisé, vous pouvez vous connecter.'));
$this->_redirect('/auth/login');
}
......
<?php
$this->openBoite($this->title);
if ($this->message)
echo $this->tag('div', $this->message);
if ($this->form)
echo $this->renderForm($this->form);
$this->closeBoite();
......@@ -37,7 +37,8 @@ function cosmo_auth($user, $passe, $cfg) {
if ($user == getVariable('catalog_login') && $passe == getVariable('catalog_pwd'))
return 'catalogueur';
if (($bokeh_user = Class_Users::findFirstBy(['login' => $user, 'password' => $passe]))
if (($bokeh_user = Class_Users::findFirstBy(['login' => $user]))
&& $bokeh_user->verifyPassword($passe)
&& $bokeh_user->isSuperAdmin())
return 'admin_systeme';
}
......
<?php
(new Class_Migration_HashAdminPasswords)->run();
\ No newline at end of file
<?php
/**
* Copyright (c) 2012-2017, Agence Française Informatique (AFI). All rights reserved.
*
* BOKEH is free software; you can redistribute it and/or modify
* it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
* the Free Software Foundation.
*
* There are special exceptions to the terms and conditions of the AGPL as it
* is applied to this software (see README file).
*
* BOKEH is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
* along with BOKEH; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
class Class_Crypt {
/** @see http://php.net/manual/en/function.crypt.php CRYPT_BLOWFISH section */
const BLOWFISH_PATTERN = '/^\$2[axy]\$[0-9]{2}\$/';
public function isBlowFish($value) {
return 1 === preg_match(static::BLOWFISH_PATTERN, $value);
}
public function blowFishHashOf($value) {
return password_hash($value, PASSWORD_BCRYPT);
}
}
......@@ -16,29 +16,22 @@
*
* You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
* along with BOKEH; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
////////////////////////////////////////////////////////////////////////////////////////
// OPAC3 : ENVOI DE MAILS
///////////////////////////////////////////////////////////////////////////////////////
class Class_Mail
{
class Class_Mail {
use Trait_Translator;
private $params_ok=false; // Flag de controle si un mail peut etre envoyé
private $mail_from; // Header from
private $_translate;
//---------------------------------------------------------------------------------
// constructeur : init des parametres
//---------------------------------------------------------------------------------
public function __construct() {
$this->_translate = Zend_Registry::get('translate');
$this->params_ok = false;
$this->mail_from = Class_Profil::getCurrentProfil()->getMailSiteOrPortail();
if (!$this->mail_from)
$this->mail_from = Class_CosmoVar::get('mail_admin');
if ($this->isMailValid($this->mail_from)) {
ini_set('sendmail_from', $this->mail_from);
$this->params_ok=true;
......@@ -46,12 +39,7 @@ class Class_Mail
}
public function _translate($message) {
return $this->_translate->_($message);
}
public function mail($destinataire, $sujet, $body, $headers) {
public function mail($destinataire, $sujet, $body) {
$mail = new ZendAfi_Mail('utf8');
$mail
->setSubject($sujet)
......@@ -67,21 +55,21 @@ class Class_Mail
}
}
//---------------------------------------------------------------------------------
// Envoi de mail
//---------------------------------------------------------------------------------
public function sendMail($sujet,$body,$destinataire,$data=false)
{
public function sendMail($sujet, $body, $destinataire, $data=false) {
$error_message = sprintf('%s <br/> %s',
$this->_translate("Les paramètres d'envoi de mails du portail sont incomplets."),
$this->_translate("Merci de le signaler aux responsables de la bibliothèque."));
$this->_("Les paramètres d'envoi de mails du portail sont incomplets."),
$this->_("Merci de le signaler aux responsables de la bibliothèque."));
if(!$this->params_ok or !trim($body))
return $error_message;
// Controle des parametres
if(!trim($destinataire))
return $this->_translate->_("Adresse du destinataire absente.");
return $this->_("L'Adresse du destinataire est absente.");
if (!$this->isMailValid($destinataire))
return $this->_("L'adresse e-mail du destinataire est incorrecte.");
if($this->params_ok==false or !trim($body))
return $error_message;
// Fusion
if($data)
......@@ -93,20 +81,12 @@ class Class_Mail
}
}
$body = wordwrap($body, 60);
// Envoi du mail
$ret=$this->getHeaders($destinataire);
if (array_isset("erreur", $ret))
return $ret["erreur"];
else
$headers=$ret["headers"];
$statut = $this->mail($destinataire, $sujet, $body, $headers);
if($statut == false)
return $error_message;
$statut = $this->mail($destinataire, $sujet, $body);
return "";
return (true === $statut)
? ''
: $error_message;
}
......@@ -114,27 +94,4 @@ class Class_Mail
$validator = new Zend_Validate_EmailAddress();
return $validator->isValid($mail);
}
//---------------------------------------------------------------------------------
// Constitution des headers
//---------------------------------------------------------------------------------
protected function getHeaders($destinataire)
{
$ret = array('headers' => '');
if (!$this->isMailValid($destinataire)) {
$ret["erreur"]= "L'adresse e-mail du destinataire est incorrecte.";
return $ret;
}
// Headers
$headers = 'MIME-Version: 1.0' . "\r\n";
$headers .= 'Content-type: text/html; charset=UTF-8' . "\r\n";
$headers .= 'To: '.$destinataire. "\r\n";
$headers .= 'From: '.$this->mail_from . "\r\n";
$ret["headers"]=$headers;
return $ret;
}
}
?>
\ No newline at end of file
}
\ No newline at end of file
<?php
/**
* Copyright (c) 2012-2017, Agence Française Informatique (AFI). All rights reserved.
*
* BOKEH is free software; you can redistribute it and/or modify
* it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
* the Free Software Foundation.
*
* There are special exceptions to the terms and conditions of the AGPL as it
* is applied to this software (see README file).
*
* BOKEH is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
* along with BOKEH; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
class Class_Migration_HashAdminPasswords {
public function run() {
foreach(Class_Users::findAllBy(['where' => 'role_level > 2']) as $user)
$user->save();
}
}
<?php
/**
* Copyright (c) 2012-2017, Agence Française Informatique (AFI). All rights reserved.
*
* BOKEH is free software; you can redistribute it and/or modify
* it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
* the Free Software Foundation.
*
* There are special exceptions to the terms and conditions of the AGPL as it
* is applied to this software (see README file).
*
* BOKEH is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
* along with BOKEH; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
class Class_User_LostPass {
use Trait_TimeSource;
const MAX_MINUTES = 30;
const TOKEN_SEPARATOR = '@';
const TOKEN_DATE_FORMAT = 'YmdHis';
protected $_user;
public function __construct($user) {
$this->_user = $user;
}
public function send() {
$strategy = $this->_user->isAbonne()
? new Class_User_LostPassPatron($this)
: new Class_User_LostPassLocal($this);
return $strategy->sendTo($this->_user);
}
public function tokenAt($date) {
$parts = [$this->_user->getId(), $this->_user->getLogin(), $date];
return sha1(implode(static::TOKEN_SEPARATOR, $parts));
}
public function tokenHasExpiredFrom($date) {
$from = DateTime::createFromFormat(static::TOKEN_DATE_FORMAT, $date);
$now = new DateTime($this->getCurrentDateTime());
$from->add(new DateInterval('PT' . static::MAX_MINUTES . 'M'));
return $from < $now;
}
}
abstract class Class_User_LostPassSender {
use Trait_Translator;
protected $_lostpass;
public function __construct($lostpass) {
$this->_lostpass = $lostpass;
}
public function sendTo($user) {
$error = (new Class_Mail())
->sendMail(Class_Profil::getCurrentProfil()->getTitreSite(),
$this->_contentFor($user),
$user->getMail());
return $error
? '<p class="error">' . $error . '</p>'
: $this->_successMessage();
}
protected function _contentFor($user) {
return '';
}
protected function _successMessage() {
return '';
}
}
class Class_User_LostPassPatron extends Class_User_LostPassSender {
protected function _contentFor($user) {
return
sprintf("%s\n\n",
$this->_('Vous avez fait une demande de mot de passe sur le portail.'))
. $this->_("Votre identifiant : %s\n", $user->getLogin())
. $this->_("Votre mot de passe : %s\n", $user->getPassword())
. sprintf("%s\n\n", $this->_('Bonne navigation sur le portail'))
;
}
protected function _successMessage() {
return $this->_('Un mail vient de vous être envoyé avec vos paramètres de connexion.');
}
}
class Class_User_LostPassLocal extends Class_User_LostPassSender{
use Trait_TimeSource;
protected function _contentFor($user) {
$created_at = $this->getCurrentTime();
$created_at_part = date(Class_User_LostPass::TOKEN_DATE_FORMAT, $created_at);
$token = $this->_lostpass->tokenAt($created_at_part);
return
sprintf("%s\n\n",
$this->_('Vous avez fait une demande de réinitialisation de votre mot de passe sur le portail.'))
. $this->_("Votre lien de réinitialisation : %s\n",
Class_Url::absolute(['module' => 'opac',
'controller' => 'auth',
'action' => 'reset-password',
'id' => $user->getId(),
'token' => $token,
'created' => $created_at_part], null, true))
. $this->_("ATTENTION : ce lien créé à %s est valide pendant %s minutes\n",
date('H:i', $created_at),
Class_User_LostPass::MAX_MINUTES)
. sprintf("%s\n\n", $this->_('Bonne navigation sur le portail'));
}
protected function _successMessage() {
return $this->_('Un mail vient de vous être envoyé contenant un lien de réinitialisation du mot de passe.');
}
}
\ No newline at end of file
<?php
/**
* Copyright (c) 2012-2017, Agence Française Informatique (AFI). All rights reserved.
*
* BOKEH is free software; you can redistribute it and/or modify
* it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
* the Free Software Foundation.
*
* There are special exceptions to the terms and conditions of the AGPL as it
* is applied to this software (see README file).
*
* BOKEH is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
* along with BOKEH; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
class Class_User_Password {
protected $_user;
public function __construct($user) {
$this->_user = $user;
}
public function format() {
return $this->shouldHash()
? $this->asBlowFish()
: $this->password();
}
public function verify($value) {
if (!$value)
return false;
$value = strtolower($value);
return $this->isBlowFish()
? password_verify(strtolower($value), $this->password())
: $value == strtolower($this->password());
}
public function shouldHash() {
return $this->hasRoleLevel() && !$this->isPatron() && !$this->isBlowFish();
}
public function password() {
return $this->_user->getPassword();
}
public function isPatron() {
return $this->_user->isAbonne();
}
public function hasRoleLevel() {
return $this->_user->hasRoleLevel();
}
public function isBlowFish() {
return $this->crypt()->isBlowFish($this->password());
}