diff --git a/VERSIONS_WIP/passhash b/VERSIONS_WIP/passhash
new file mode 100644
index 0000000000000000000000000000000000000000..6832bc78e94a99b1f46ade03ecbbd999a11a2f06
--- /dev/null
+++ b/VERSIONS_WIP/passhash
@@ -0,0 +1 @@
+ - Administration : hashage des mots de passe administrateurs
\ No newline at end of file
diff --git a/application/modules/admin/controllers/IndexController.php b/application/modules/admin/controllers/IndexController.php
index 016d6dca266f9f144642eff5f107f0d683a594c6..a40df7c01d396605b42a52979c0bfac47d83dbe4 100644
--- a/application/modules/admin/controllers/IndexController.php
+++ b/application/modules/admin/controllers/IndexController.php
@@ -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
diff --git a/application/modules/opac/controllers/AuthController.php b/application/modules/opac/controllers/AuthController.php
index b3a171a11f486d4b119605eff23bde8932dd5a2f..62cdd9891fdcaf108fd5c15badffcb12cbe6264b 100644
--- a/application/modules/opac/controllers/AuthController.php
+++ b/application/modules/opac/controllers/AuthController.php
@@ -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');
   }
 
 
diff --git a/application/modules/opac/views/scripts/auth/reset-password.phtml b/application/modules/opac/views/scripts/auth/reset-password.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..e41e768a2d04b4662d7c8e33e3d76a67aaeeaff8
--- /dev/null
+++ b/application/modules/opac/views/scripts/auth/reset-password.phtml
@@ -0,0 +1,10 @@
+<?php
+$this->openBoite($this->title);
+
+if ($this->message)
+  echo $this->tag('div', $this->message);
+
+if ($this->form)
+  echo $this->renderForm($this->form);
+
+$this->closeBoite();
diff --git a/cosmogramme/php/_identification.php b/cosmogramme/php/_identification.php
index 310d0dcb0e06200e4f77c18362e70afb462c81f6..a02e72b04d4b41e28f3a87cd31cc891161825437 100644
--- a/cosmogramme/php/_identification.php
+++ b/cosmogramme/php/_identification.php
@@ -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';
 }
diff --git a/cosmogramme/sql/patch/patch_338.php b/cosmogramme/sql/patch/patch_338.php
new file mode 100644
index 0000000000000000000000000000000000000000..aaa9e0fe7fe61eaea145698130cc95c15ff2fb96
--- /dev/null
+++ b/cosmogramme/sql/patch/patch_338.php
@@ -0,0 +1,2 @@
+<?php
+(new Class_Migration_HashAdminPasswords)->run();
\ No newline at end of file
diff --git a/library/Class/Crypt.php b/library/Class/Crypt.php
new file mode 100644
index 0000000000000000000000000000000000000000..37684aa60f4160946c50eb0f7630a801c77a0559
--- /dev/null
+++ b/library/Class/Crypt.php
@@ -0,0 +1,35 @@
+<?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);
+  }
+}
diff --git a/library/Class/Mail.php b/library/Class/Mail.php
index 35f72b7f376c3ee9e10017024ac8691de864b1d1..c8a262bed768d5ad150403727062a289519561a2 100644
--- a/library/Class/Mail.php
+++ b/library/Class/Mail.php
@@ -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
diff --git a/library/Class/Migration/HashAdminPasswords.php b/library/Class/Migration/HashAdminPasswords.php
new file mode 100644
index 0000000000000000000000000000000000000000..600968002b8c79a390835b25e8f0436701eaccbc
--- /dev/null
+++ b/library/Class/Migration/HashAdminPasswords.php
@@ -0,0 +1,28 @@
+<?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();
+  }
+}
diff --git a/library/Class/User/LostPass.php b/library/Class/User/LostPass.php
new file mode 100644
index 0000000000000000000000000000000000000000..eb3b83dde9199302efd94c82815483aece3736d7
--- /dev/null
+++ b/library/Class/User/LostPass.php
@@ -0,0 +1,144 @@
+<?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
diff --git a/library/Class/User/Password.php b/library/Class/User/Password.php
new file mode 100644
index 0000000000000000000000000000000000000000..eedb739ae717c6042cf7f7316edf0afc442ad26d
--- /dev/null
+++ b/library/Class/User/Password.php
@@ -0,0 +1,83 @@
+<?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());
+  }
+
+
+  public function asBlowFish() {
+    return $this->crypt()->blowFishHashOf(strtolower($this->password()));
+  }
+
+
+  public function crypt() {
+    return new Class_Crypt();
+  }
+}
diff --git a/library/Class/Users.php b/library/Class/Users.php
index c5c17052a5216d40e2db294fbf960da88f0108cd..9dc2857b98ee7c4c3a996571e88e146516cd3c1f 100644
--- a/library/Class/Users.php
+++ b/library/Class/Users.php
@@ -241,10 +241,11 @@ class UsersLoader extends Storm_Model_Loader {
 
 
   public function findBlowfish($login) {
+    $crypt = new Class_Crypt();
     return array_filter(
                         Class_Users::findAllBy(['login' => $login]),
-                        function($user) {
-                          return substr($user->getPassword(), 0, 4) === '$2a$';
+                        function($user) use($crypt) {
+                          return $crypt->isBlowFish($user->getPassword());
                         });
   }
 
@@ -320,6 +321,14 @@ class UsersLoader extends Storm_Model_Loader {
     $count_params = $this->_buildSearchParams($defaults, $where, $valide_subscription);
     return Class_Users::countBy($count_params);
   }
+
+
+  public function findFirstValidOrNotBy($params) {
+    if (!$user = Class_Users::findFirstBy($params))
+      $user = Class_UsersNonValid::findFirstBy($params);
+
+    return $user;
+  }
 }
 
 
@@ -1114,7 +1123,7 @@ class Class_Users extends Storm_Model_Abstract {
   }
 
 
-  function lostpass($login) {
+  public function lostpass($login) {
     if(!trim($login))
       return ['error' => 1];
 
@@ -1378,6 +1387,8 @@ class Class_Users extends Storm_Model_Abstract {
   public function beforeSave() {
     $this->setDateMaj(Class_Multimedia_Utils_DateTimeFormat::getInstance()
                       ->getCurrentDateFormatInYmdHMS());
+
+    $this->setPassword((new Class_User_Password($this))->format());
   }
 
 
@@ -1859,4 +1870,9 @@ class Class_Users extends Storm_Model_Abstract {
   public function hasPaniers() {
     return (0 < $this->numberOfPaniers());
   }
+
+
+  public function verifyPassword($value) {
+    return (new Class_User_Password($this))->verify($value);
+  }
 }
diff --git a/library/ZendAfi/Auth/Adapter/Success.php b/library/ZendAfi/Auth/Adapter/Success.php
new file mode 100644
index 0000000000000000000000000000000000000000..c7d2b58b76a356a434591aebbf72d98e58ec1a36
--- /dev/null
+++ b/library/ZendAfi/Auth/Adapter/Success.php
@@ -0,0 +1,45 @@
+<?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 ZendAfi_Auth_Adapter_Success implements Zend_Auth_Adapter_Interface {
+  protected $_user;
+
+  /**
+   * Performs an authentication attempt
+   *
+   * @throws Zend_Auth_Adapter_Exception If authentication cannot be performed
+   * @return Zend_Auth_Result
+   */
+  public function authenticate() {
+    return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $this->_user->getId());
+  }
+
+
+  public function __construct($user) {
+    $this->_user = $user;
+  }
+
+
+  public function getResultObject() {
+    return $this->_user->toStdClass();
+  }
+}
diff --git a/library/ZendAfi/Auth/TryHarder.php b/library/ZendAfi/Auth/TryHarder.php
index 6e54e799149bb8eab0fc8e162d767659f23a8e7a..aae00cb897cd5867637bb6e5970f1a52628418b8 100644
--- a/library/ZendAfi/Auth/TryHarder.php
+++ b/library/ZendAfi/Auth/TryHarder.php
@@ -16,7 +16,7 @@
  *
  * 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
  */
 
 
@@ -31,14 +31,18 @@ class ZendAfi_Auth_TryHarder {
 
   public function tryHarder($login, $password) {
     $this->_adapter->setIdentity($login);
-    return 
-      $this->trySimpleFormats($login, $password) or $this->tryBlowfish($login, $password);
+
+    return $this->trySimpleFormats($login, $password)
+      or $this->tryBlowfish($login, $password);
   }
 
 
   protected function trySimpleFormats($login, $password) {
-    $formats = [function ($password) {return $password;},
-                function ($password) {return $this->md5_base64($password);}];
+    if ((new Class_Crypt)->isBlowFish($password))
+      return false;
+
+    $formats = [function ($password) { return $password; },
+                function ($password) { return $this->md5_base64($password); }];
 
     foreach($formats as $format) {
       $this->_adapter->setCredential($format($password));
@@ -55,10 +59,10 @@ class ZendAfi_Auth_TryHarder {
       return false;
 
     foreach(Class_Users::findBlowfish($login) as $user) {
-      $salt = substr($user->getPassword(), 0, 29);
-      $this->_adapter->setCredential(crypt($password, $salt));
-      if ($this->authenticate())
-        return true;
+      if ($user->verifyPassword($password)) {
+        $this->_adapter = new ZendAfi_Auth_Adapter_Success($user);
+        return $this->authenticate();
+      }
     }
 
     return false;
diff --git a/library/ZendAfi/Form/LostPassword.php b/library/ZendAfi/Form/LostPassword.php
index 3bebbbde9382b786558ee47b73704c93b92e13b7..e51c8b1d79c86a8a8965a70cf6d08815cc262763 100644
--- a/library/ZendAfi/Form/LostPassword.php
+++ b/library/ZendAfi/Form/LostPassword.php
@@ -29,11 +29,12 @@ class ZendAfi_Form_LostPassword extends ZendAfi_Form {
                    ['label' => $this->getAttrib('username_label'),
                     'placeholder' => $this->getAttrib('username_placeholder'),
                     'required' => true,
-                    'allowEmpty' => false])
+                    'allowEmpty' => false,
+                    'validators' => ['LostUsername']])
 
       ->addDisplayGroup(['lost_username'],
                         'lostpass_display_group',
-                        ['legend' => $this->_('Récupération du mot de passe')])
+                        ['legend' => ''])
       ->setAttribs([])
       ->setName('form_lostpass');
   }
diff --git a/library/ZendAfi/Form/ResetPassword.php b/library/ZendAfi/Form/ResetPassword.php
new file mode 100644
index 0000000000000000000000000000000000000000..062a853a71ea26d161e694c547d53c03c42c28d1
--- /dev/null
+++ b/library/ZendAfi/Form/ResetPassword.php
@@ -0,0 +1,42 @@
+<?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 ZendAfi_Form_ResetPassword extends ZendAfi_Form {
+  public function init() {
+    parent::init();
+
+    $this
+      ->addElement('password', 'new_pass',
+                   ['label' => $this->_('Nouveau mot de passe'),
+                    'required' => true,
+                    'allowEmpty' => false])
+
+      ->addElement('password', 'confirm_pass',
+                   ['label' => $this->_('Confirmez le nouveau mot de passe'),
+                    'required' => true,
+                    'allowEmpty' => false,
+                    'validators' => [new ZendAfi_Validate_PasswordEquals('new_pass')]])
+
+      ->addUniqDisplayGroup('reset-password')
+      ;
+  }
+}
diff --git a/library/ZendAfi/Validate/LostUsername.php b/library/ZendAfi/Validate/LostUsername.php
new file mode 100644
index 0000000000000000000000000000000000000000..eead94453952c1080477cccb6a55b4ddceb2fe0d
--- /dev/null
+++ b/library/ZendAfi/Validate/LostUsername.php
@@ -0,0 +1,57 @@
+<?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 ZendAfi_Validate_LostUsername extends Zend_Validate_Abstract {
+  use Trait_Translator;
+
+  const
+    NO_USER = 'no_user',
+    NO_MAIL = 'no_mail';
+
+
+  public function __construct() {
+    $this->_messageTemplates =
+      [static::NO_USER => $this->_('Identifiant inconnu.'),
+       static::NO_MAIL => $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.")];
+  }
+
+
+  /**
+   * @param $value mixed
+   * @param $fields array
+   * @return boolean
+   */
+  public function isValid($value, array $fields_values = array()) {
+    $value = ZendAfi_Filters_Post::filterStatic($value);
+    if (!$user = Class_Users::findFirstValidOrNotBy(['login' => $value])) {
+      $this->_error(static::NO_USER);
+      return false;
+    }
+
+    if (!$user->hasMail()) {
+      $this->_error(static::NO_MAIL);
+      return false;
+    }
+
+    return true;
+  }
+}
diff --git a/tests/application/modules/admin/controllers/UsersControllerTest.php b/tests/application/modules/admin/controllers/UsersControllerTest.php
index 474f9204ce05210dce0e546d91d9ef2105dd3d95..a755d6c67afe88b56a26e071ebfec56130ec67c9 100644
--- a/tests/application/modules/admin/controllers/UsersControllerTest.php
+++ b/tests/application/modules/admin/controllers/UsersControllerTest.php
@@ -456,7 +456,7 @@ class UsersControllerPostMarcusDataTest extends UsersControllerWithMarcusTestCas
 
 
   /** @test */
-  function marcusShouldHaveBeenSaved() {
+  public function marcusShouldHaveBeenSaved() {
     $this->assertEquals($this->marcus,
                         Class_Users::find(10));
   }
@@ -966,7 +966,6 @@ class Admin_UsersControllerFormEditAdminTest extends Admin_UsersControllerEditAd
 
 
 class UsersControllerPostEditAdminTest extends Admin_UsersControllerEditAdminTestCase {
-
   public function setUp() {
     parent::setUp();
     $this->postDispatch('/admin/users/edit/id/10', ['login' => 'Tom',
@@ -998,10 +997,58 @@ class UsersControllerPostEditAdminTest extends Admin_UsersControllerEditAdminTes
     $this->assertEquals($this->_admin->getRedmineLibrary()->getLibelle(),
                         $this->_lib_po->getLibelle());
   }
+
+
+  /** @test */
+  public function passwordShouldBeHashed() {
+    $this->assertTrue((new Class_Crypt())->isBlowFish($this->_admin->getPassword()));
+  }
+
+
+  /** @test */
+  public function hashShouldNotBeCaseSensitive() {
+    $this->assertTrue($this->_admin->verifyPassword('TuTu'));
+  }
 }
 
 
 
+class UsersControllerPostEditWithHashedPasswordAdminTest extends Admin_UsersControllerEditAdminTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->postDispatch('/admin/users/edit/id/10',
+                        ['login' => 'Tom',
+                         'password' => '$2y$10$VDozEsvlDhp80/qz9qqOqOwq0uV/jxV2ayONdeacSY.fGipYMjvSe',
+                         'nom' => 'Davis',
+                         'prenom' => 'Miles',
+                         'pseudo' => 'Dave',
+                         'mail' => 'mdavis@free.fr',
+                         'role_level' => ZendAfi_Acl_AdminControllerRoles::MODO_PORTAIL,
+                         'id_site' => '7',
+                         'idabon' => '2341',
+                         'ordreabon' => '2',
+                         'telephone' => '',
+                         'adresse' => '12 rue miles',
+                         'code_postal' => '75000',
+                         'ville' => 'Paris',
+                         'civilite' => 1,
+                         'mobile' => '',
+                         'naissance' => '1976-02-17',
+                         'date_debut' => '',
+                         'date_fin' => '',
+                         'user_group_ids' => '',
+                         'redmine_library' => '15']);
+  }
+
+
+  /** @test */
+  public function passwordShouldStayTheSame() {
+    $this->assertEquals('$2y$10$VDozEsvlDhp80/qz9qqOqOwq0uV/jxV2ayONdeacSY.fGipYMjvSe',
+                        $this->_admin->getPassword());
+  }
+}
+
+
 
 class UsersControllerEditSuperAdminTest extends Admin_AbstractControllerTestCase {
   protected $_storm_default_to_volatile = true;
diff --git a/tests/application/modules/opac/controllers/AbonneControllerNewslettersTest.php b/tests/application/modules/opac/controllers/AbonneControllerNewslettersTest.php
index b83784d68c7cba02c5f844cb6799f28e84c3df8a..06789ff79f94f373aaae47073b3f47d96bdbda5e 100644
--- a/tests/application/modules/opac/controllers/AbonneControllerNewslettersTest.php
+++ b/tests/application/modules/opac/controllers/AbonneControllerNewslettersTest.php
@@ -41,7 +41,7 @@ abstract class AbstractAbonneControllerNewslettersTestCase extends AbstractContr
                                                   'password' => 'mysecret',
                                                   'date_debut' => null,
                                                   'role' => 'abonne_sigb',
-                                                  'role_level' => 3,
+                                                  'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
                                                   'id_site' => 999,
                                                   'idabon' => '00123',
                                                   'fiche_SIGB' => ['type_comm' => 0]
diff --git a/tests/application/modules/opac/controllers/AbonneControllerSuggestionAchatNanookTest.php b/tests/application/modules/opac/controllers/AbonneControllerSuggestionAchatNanookTest.php
index dea46bd3212d816b44d1a50d10a77ddcf9a4591a..d2f908e942357a1ad67776f3c82db406084cea83 100644
--- a/tests/application/modules/opac/controllers/AbonneControllerSuggestionAchatNanookTest.php
+++ b/tests/application/modules/opac/controllers/AbonneControllerSuggestionAchatNanookTest.php
@@ -54,9 +54,12 @@ abstract class AbstractAbonneControllerSuggestionAchatNanookTestCase extends Abs
     $sigb_conf->setIdBib(12);
 
     $this->_francis = $this->fixture('Class_Users', ['id' => 3,
+                                                     'id_site' => 12,
                                                      'id_sigb' => '187',
                                                      'login' => 'francis',
                                                      'password' => 'test',
+                                                     'idabon' => 'francis',
+                                                     'role_level' => 2,
                                                      'int_bib' => $sigb_conf]);
 
     ZendAfi_Auth::getInstance()->logUser($this->_francis);
diff --git a/tests/application/modules/opac/controllers/AuthControllerResetPasswordTest.php b/tests/application/modules/opac/controllers/AuthControllerResetPasswordTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c37537bff1a543a5c53ca9dda8568e05a11b7f4d
--- /dev/null
+++ b/tests/application/modules/opac/controllers/AuthControllerResetPasswordTest.php
@@ -0,0 +1,133 @@
+<?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
+ */
+
+
+abstract class AuthControllerResetPasswordTestCase extends AbstractControllerTestCase {
+  protected
+    $_storm_default_to_volatile=true,
+    $_user;
+
+  public function setUp() {
+    parent::setUp();
+
+    Class_User_LostPass::setTimeSource((new TimeSourceForTest('2017-11-30'))
+                                       ->atCoffeeTime());
+
+    $this->_user = $this->fixture('Class_Users',
+                                  ['id' => 45,
+                                   'login' => 'sysnoadm',
+                                   'password' => 'supersecret']);
+  }
+
+
+  public function tearDown() {
+    Class_User_LostPass::setTimeSource(null);
+    parent::tearDown();
+  }
+
+
+  protected function urlFor($id, $token, $created) {
+    return sprintf('/opac/auth/reset-password/id/%s/token/%s/created/%s',
+                   $id, $token, $created);
+  }
+}
+
+
+
+class AuthControllerResetPasswordActionTest
+  extends AuthControllerResetPasswordTestCase {
+
+  /** @test */
+  public function unknownUserShouldHaveError() {
+    $this->dispatch($this->urlFor(99999999, sha1('test'), '20231207040545'), true);
+    $this->assertXPathContentContains('//div', 'Utilisateur inconnu',
+                                      $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function badTokenShouldHaveError() {
+    $this->dispatch($this->urlFor($this->_user->getId(), sha1('test'),'20231207040545'), true);
+    $this->assertXPathContentContains('//div', 'Jeton de réinitialisation invalide',
+                                      $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function outdatedTokenShouldHaveError() {
+    $token = (new Class_User_LostPass($this->_user))->tokenAt('20071207040545');
+
+    $this->dispatch($this->urlFor($this->_user->getId(), $token, '20071207040545'), true);
+    $this->assertXPathContentContains('//div', 'Jeton de réinitialisation expiré',
+                                      $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function validTokenShouldDisplayForm() {
+    $token = (new Class_User_LostPass($this->_user))->tokenAt('20171130154500');
+
+    $this->dispatch($this->urlFor($this->_user->getId(), $token, '20171130154500'), true);
+    $this->assertXPath('//form[contains(@action, "auth/reset-password")]',
+                       $this->_response->getBody());
+  }
+}
+
+
+
+class AuthControllerResetPasswordActionPostTest
+  extends AuthControllerResetPasswordTestCase {
+  protected $_url;
+
+  public function setUp() {
+    parent::setUp();
+
+    $token = (new Class_User_LostPass($this->_user))->tokenAt('20171130154500');
+    $this->_url = $this->urlFor($this->_user->getId(), $token, '20171130154500');
+  }
+
+
+  /** @test */
+  public function withoutPasswordsShouldHaveError() {
+    $this->postDispatch($this->_url, ['new_pass' => '', 'confirm_pass' => '']);
+    $this->assertXPathContentContains('//div', 'Une valeur est requise');
+  }
+
+
+  /** @test */
+  public function withDifferentPasswordsShouldHaveError() {
+    $this->postDispatch($this->_url, ['new_pass' => 'secret',
+                                      'confirm_pass' => 'terces']);
+
+    $this->assertXPathContentContains('//div', 'Les champs \'Mot de passe\' sont différents');
+  }
+
+
+  /** @test */
+  public function withSamePasswordShouldUpdateUserAndRedirectToLogin() {
+    $this->postDispatch($this->_url, ['new_pass' => 'secret',
+                                      'confirm_pass' => 'secret']);
+
+    $this->assertTrue($this->_user->verifyPassword('secret'));
+    $this->assertRedirectTo('/auth/login');
+    $this->assertFlashMessengerContentContains('Votre mot de passe a été réinitialisé, vous pouvez vous connecter.');
+  }
+}
\ No newline at end of file
diff --git a/tests/application/modules/opac/controllers/AuthControllerTest.php b/tests/application/modules/opac/controllers/AuthControllerTest.php
index fc725cc8659a33e366c099e97c1c1757b3a69b99..816969a0dd1737347c0ba10644064dc7e62c8434 100644
--- a/tests/application/modules/opac/controllers/AuthControllerTest.php
+++ b/tests/application/modules/opac/controllers/AuthControllerTest.php
@@ -1316,27 +1316,27 @@ class AuthControllerLoginActionWithDefaultPreferencesRenderTest extends AuthCont
   }
 }
 
-class AuthControllerLostPasswordUnknownPostTest extends AbstractControllerTestCase{
-  protected $_storm_default_to_volatile = true;
 
-  public function setUp() {
-    parent::setUp();
 
-  }
+class AuthControllerLostPasswordUnknownPostTest extends AbstractControllerTestCase{
+  protected $_storm_default_to_volatile = true;
 
   /** @test */
   public function withUnknowUserShouldDisplayError() {
-    $this->postDispatch('/opac/auth/lostpass',['lost_username' => 'unknown']);
-    $this->assertXPathContentContains('//div', 'Identifiant inconnu',$this->_response->getBody());
+    $this->postDispatch('/opac/auth/lostpass', ['lost_username' => 'unknown']);
+    $this->assertXPathContentContains('//div',
+                                      'Identifiant inconnu',
+                                      $this->_response->getBody());
   }
 
 
   /** @test */
   public function withEmptyUserShouldDisplayError() {
-    $this->postDispatch('/opac/auth/lostpass',['lost_username' => '']);
-    $this->assertXPathContentContains('//div', 'Veuillez saisir votre identifiant.',$this->_response->getBody());
+    $this->postDispatch('/opac/auth/lostpass', ['lost_username' => '']);
+    $this->assertXPathContentContains('//div',
+                                      'Une valeur est requise',
+                                      $this->_response->getBody());
   }
-
 }
 
 
diff --git a/tests/application/modules/opac/controllers/MultimediaControllerTest.php b/tests/application/modules/opac/controllers/MultimediaControllerTest.php
index 7bb1f5039962303c66f0fbd2d8e38d9f9a9eb7a7..22fa897a70ec6efea747767a3e705b9d6428a83a 100644
--- a/tests/application/modules/opac/controllers/MultimediaControllerTest.php
+++ b/tests/application/modules/opac/controllers/MultimediaControllerTest.php
@@ -105,10 +105,10 @@ abstract class MultimediaControllerAuthenticateTestCase extends MultimediaContro
   /**
    * @param $user Class_Users
    */
-  protected function _expectUserToLoad($user) {
+  protected function _expectUserToLoad($user, $pass=null) {
     $this->_auth
       ->whenCalled('authenticateLoginPassword')
-      ->with($user->getLogin(), $user->getPassword(), ['auth_sigb', 'auth_db'])
+      ->with($user->getLogin(), $pass ? $pass : $user->getPassword(), ['auth_sigb', 'auth_db'])
       ->willDo(
         function() use ($user) {
           $this->_auth
@@ -215,7 +215,7 @@ class MultimediaControllerAuthenticateInviteNonAfiMultimediaTest extends Multime
   public function setUp() {
     parent::setUp();
     $user = MultimediaControllerUsersFixtures::getInvite();
-    $this->_expectUserToLoad($user);
+    $this->_expectUserToLoad($user, 'invite');
 
     $this->_json = $this->getJson('/multimedia/authenticate/login/invite/password/invite');
   }
@@ -225,7 +225,6 @@ class MultimediaControllerAuthenticateInviteNonAfiMultimediaTest extends Multime
   public function shouldReturnSubscriptionExpired() {
     $this->assertEquals('SubscriptionExpired', $this->_json->error);
   }
-
 }
 
 
@@ -233,7 +232,7 @@ class MultimediaControllerAuthenticateInviteAfiMultimediaTest extends Multimedia
   public function setUp() {
     parent::setUp();
     $user = MultimediaControllerUsersFixtures::getInvite();
-    $this->_expectUserToLoad($user);
+    $this->_expectUserToLoad($user, 'invite');
     $this->_expectGroupForUser($user, 'Abonne multimedia');
 
     $this->_json = $this->getJson('/multimedia/authenticate/login/invite/password/invite');
diff --git a/tests/db/UpgradeDBTest.php b/tests/db/UpgradeDBTest.php
index 83773d7f2582ee4a954a1d59af1e467f921c5838..5dcef8ae354ddb6934a43b4a81391119c5d3cc35 100644
--- a/tests/db/UpgradeDBTest.php
+++ b/tests/db/UpgradeDBTest.php
@@ -1774,3 +1774,22 @@ class UpgradeDB_337_Test extends UpgradeDBTestCase {
     $this->assertColumn('multimedia_device', 'note');
   }
 }
+
+
+
+class UpgradeDB_338_Test extends UpgradeDBTestCase {
+  public function prepare() {
+    try {
+      $this->query('update bib_admin_users set password="pass" where id_user=1');
+    } catch(Exception $e) {}
+  }
+
+
+  /** @test */
+  public function adminPasswordShouldBeHashed() {
+    $datas = $this->query('select password from bib_admin_users where id_user=1')
+                  ->fetch();
+
+    $this->assertEquals('$2y$', substr($datas['password'], 0, 4));
+  }
+}
diff --git a/tests/library/Class/MailTest.php b/tests/library/Class/MailTest.php
index 8e6a5133eb76ead842e33d584980ab3d539bc0e1..ba5fec229aa939cd3106c706d7758cef90d455b9 100644
--- a/tests/library/Class/MailTest.php
+++ b/tests/library/Class/MailTest.php
@@ -16,22 +16,21 @@
  *
  * 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
  */
 
 class Class_MailTesting extends Class_Mail {
-  protected 
-    $_destinataire, 
-    $_sujet, 
-    $_body, 
-    $_headers, 
+  protected
+    $_destinataire,
+    $_sujet,
+    $_body,
+    $_headers,
     $_mail_return_value;
 
-  public function mail($destinataire, $sujet, $body, $headers) {
+  public function mail($destinataire, $sujet, $body) {
     $this->_destinataire = $destinataire;
     $this->_sujet = $sujet;
     $this->_body = $body;
-    $this->_headers = $headers;
     return $this->_mail_return_value;
   }
 
@@ -58,21 +57,22 @@ class Class_MailTesting extends Class_Mail {
 
 
 
-class MailToZorkFromFlorenceTest extends Storm_Test_ModelTestCase {
+class MailToZorkFromFlorenceTest extends ModelTestCase {
+  protected $_storm_default_to_volatile=true;
+
   public function setUp() {
     parent::setUp();
     Class_CosmoVar::getLoader()
       ->newInstanceWithId('mail_admin')
       ->setValeur('florence@astrolabe-melun.fr');
 
-
-    Class_Profil::getCurrentProfil()->setMailSite('');
-    Class_Profil::getPortail()->setMailSite('');
+    $this->fixture('Class_Profil', ['id' => 1,
+                                    'mail_site' => '']);
 
     $this->_mail_testing = new Class_MailTesting();
     $this->_mail_testing->mailReturns(true);
 
-    $this->_status = $this->_mail_testing->sendMail('Bienvenue !', 
+    $this->_status = $this->_mail_testing->sendMail('Bienvenue !',
                                                     'Vous êtes inscrit',
                                                     'zork@gmail.com');
   }
@@ -100,35 +100,28 @@ class MailToZorkFromFlorenceTest extends Storm_Test_ModelTestCase {
   function bodyShouldBeVousEtesInscrit() {
     $this->assertEquals('Vous êtes inscrit', $this->_mail_testing->getBody());
   }
-
-
-  /** @test */
-  function headersShouldContainSenderFlorence() {
-    $this->assertContains('From: florence@astrolabe-melun.fr', 
-                          $this->_mail_testing->getMailHeaders());
-  }
 }
 
 
 
 
-class MailErrorsTest extends Storm_Test_ModelTestCase {
+class MailErrorsTest extends ModelTestCase {
+  protected $_storm_default_to_volatile=true;
+
   public function setUp() {
     parent::setUp();
-    Class_Profil::getCurrentProfil()->setMailSite('');
-    Class_Profil::getPortail()->setMailSite('');
+
+    $profil = $this->fixture('Class_Profil', ['id' => 1, 'mail_site' => '']);
+    Class_Profil::setCurrentProfil($profil);
   }
 
   /** @test */
   function withoutMailAdminShouldReturnErrorMessage() {
-    Class_CosmoVar::getLoader()
-      ->newInstanceWithId('mail_admin')
-      ->setValeur('');
-
+    Class_CosmoVar::setValueOf('mail_admin', '');
     $mail_testing = new Class_MailTesting();
     $mail_testing->mailReturns(true);
 
-    $status = $mail_testing->sendMail('Bienvenue !', 
+    $status = $mail_testing->sendMail('Bienvenue !',
                                       'Vous êtes inscrit',
                                       'zork@gmail.com');
     $this->assertContains("Les paramètres d'envoi de mails du portail sont incomplets.",
@@ -156,16 +149,15 @@ class MailErrorsTest extends Storm_Test_ModelTestCase {
     Class_CosmoVar::getLoader()
       ->newInstanceWithId('mail_admin')
       ->setValeur('laurent@gmail.com');
+
     $mail_testing = new Class_MailTesting();
     $mail_testing->mailReturns(false);
 
-    $status = $mail_testing->sendMail('Bienvenue !', 
+    $status = $mail_testing->sendMail('Bienvenue !',
                                       'Vous êtes inscrit',
                                       'zork@gmail.com');
+
     $this->assertContains("Les paramètres d'envoi de mails du portail sont incomplets.",
                           $status);
   }
 }
-
-
-?>
\ No newline at end of file
diff --git a/tests/library/Class/WebService/SIGB/CarthameTest.php b/tests/library/Class/WebService/SIGB/CarthameTest.php
index 66293b684a06f6b616f23f161bfe63bec67959cc..91100a3aa05280091a7d37fb39173b28dfa4c455 100644
--- a/tests/library/Class/WebService/SIGB/CarthameTest.php
+++ b/tests/library/Class/WebService/SIGB/CarthameTest.php
@@ -218,6 +218,9 @@ class CarthameKarviPickupLocationsTest extends CarthameOperationTestCase {
     $user = $this->fixture('Class_Users',
                            ['id' => 34,
                             'login' => '90100000119049',
+                            'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                            'id_site' => 3,
+                            'idabon' => '90100000119049',
                             'password' => 'pass']);
 
     $item = $this->fixture('Class_Exemplaire',
@@ -388,6 +391,9 @@ class CarthameEmprunteurPatrickBTest extends CarthameOperationTestCase {
                            ['id' => 56,
                             'login' => 'pbarroca',
                             'password' => '1974',
+                            'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                            'idabon' => 'pbarroca',
+                            'id_site' => 3,
                             'int_bib' => $library]);
 
     ZendAfi_Auth::getInstance()->logUser($user);
diff --git a/tests/library/Class/WebService/SIGB/KohaTest.php b/tests/library/Class/WebService/SIGB/KohaTest.php
index c1a66ec1b44de5f22bf9dc35a25be13fe6417720..32ddda29a2d271af734bb2c9a2bf0730e99b17fb 100644
--- a/tests/library/Class/WebService/SIGB/KohaTest.php
+++ b/tests/library/Class/WebService/SIGB/KohaTest.php
@@ -658,6 +658,9 @@ class KohaGetEmprunteurJeanAndreWithIdSIGBTest extends KohaTestCase {
                                      ['id' => 43,
                                       'login' => 'JEAN',
                                       'password' => 'zork',
+                                      'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                                      'idabon' => 'Jean',
+                                      'id_site' => 3,
                                       'id_sigb' => '01234']));
   }
 
@@ -1146,17 +1149,20 @@ class KohaAuthenticateWSTest extends KohaTestCase {
       ->with('http://cat-aficg55.biblibre.com/cgi-bin/koha/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=1&show_holds=1')
       ->answers(KohaFixtures::xmlGetPatronInfoDupont());
 
-    $this->emprunteur = $this->service->getEmprunteur( $this->user = $this->fixture('Class_Users',
-                                                                                    [
-                                                                                     'id' => 10,
-                                                                                     'login' => 'john',
-                                                                                     'password' => '1989']));
+    $this->user = $this->fixture('Class_Users', ['id' => 10,
+                                                 'login' => 'john',
+                                                 'password' => '1989',
+                                                 'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                                                 'idabon' => 'john',
+                                                 'id_site' => 3]);
+
+    $this->emprunteur = $this->service->getEmprunteur($this->user);
 
     $this->emprunteur->updateUser($this->user);
   }
 
   public function expectedUser() {
-    return [[ 'nom', 'DUPONT'],
+    return [['nom', 'DUPONT'],
             ['prenom' ,'Jean'],
             ['login' , 'john'],
             ['password' , '1989'],
diff --git a/tests/library/ZendAfi/Auth/TryHarderTest.php b/tests/library/ZendAfi/Auth/TryHarderTest.php
index 6b74cf407ca18d39a82ff54f7efb320a66aab28c..b4dd1decb742fb044d1fd27ee3431dc380f80839 100644
--- a/tests/library/ZendAfi/Auth/TryHarderTest.php
+++ b/tests/library/ZendAfi/Auth/TryHarderTest.php
@@ -16,38 +16,46 @@
  *
  * 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
  */
 
 
 class ZendAfi_Auth_TryHarderTest extends ModelTestCase {
-  protected $_auth, $_adapter, $_try_harder;
+  protected
+    $_storm_default_to_volatile = true,
+    $_auth,
+    $_adapter,
+    $_try_harder;
 
   public function setUp() {
     parent::setUp();
-    $this->fixture('Class_Users', ['id' => 1,
-                                   'login' => 'Biquette',
-                                   'password' => '$2a$08$gPuCFG7FU2psZ52z5R.ACeW295qSfRJuTd04i/zwjjNI67ZUmVIHe']);
 
+    $this->fixture('Class_Users',
+                   ['id' => 1,
+                    'login' => 'Biquette',
+                    'password' => '$2a$08$gPuCFG7FU2psZ52z5R.ACeFgtjLo1D/7C5goi0KCPcmAbG.zwGog2']);
+
+    $this->fixture('Class_Users',
+                   ['id' => 2,
+                    'login' => 'Other',
+                    'password' => 'testFeelsGood']);
 
     $this->_auth = $this->mock()
-      ->whenCalled('authenticate')
-      ->willDo(function () {return $this->_adapter->authenticate();});
+                        ->whenCalled('authenticate')
+                        ->willDo(function ($adapter) { return $adapter->authenticate(); });
 
-    $this->_adapter = $this->mock()
-      ->whenCalled('setIdentity')->answers(true)
-      ->whenCalled('setCredential')->answers(true);
+    $this->_adapter = new Class_Entity();
 
     $this->_try_harder = new ZendAfi_Auth_TryHarder($this->_auth, $this->_adapter);
 
-    $this->_adapter
-      ->whenCalled('authenticate')
-      ->willDo(
-        function() {
-          return $this->mock()
-            ->whenCalled('isValid')
-            ->answers($this->_adapter->methodHasBeenCalledWithParams('setCredential', ['$2a$08$gPuCFG7FU2psZ52z5R.ACeFgtjLo1D/7C5goi0KCPcmAbG.zwGog2']));
-        });
+    $authenticate = function() {
+      return ($user = Class_Users::findFirstBy(['login' => $this->_adapter->getIdentity(),
+                                                'password' => $this->_adapter->getCredential()]))
+      ? new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $user->getId())
+      : new Zend_Auth_Result(Zend_Auth_Result::FAILURE, '');
+    };
+
+    $this->_adapter->whenCalledDo('authenticate', $authenticate);
   }
 
 
@@ -58,9 +66,22 @@ class ZendAfi_Auth_TryHarderTest extends ModelTestCase {
 
 
   /** @test */
-  public function withoutBlowfishInstalledShouldReturnFalse() {
+  public function blowfishBiquetteWithHashShouldNotBeLogged() {
+    $this->assertFalse($this->_try_harder
+                       ->tryHarder('Biquette',
+                                   '$2a$08$gPuCFG7FU2psZ52z5R.ACeFgtjLo1D/7C5goi0KCPcmAbG.zwGog2'));
+  }
+
+
+  /** @test */
+  public function withoutBlowfishInstalledBiquetteShouldNotBeAbleToLogin() {
     $this->_try_harder->setBlowfishEnabled(false);
     $this->assertFalse($this->_try_harder->tryHarder('Biquette', 'password'));
   }
+
+
+  /** @test */
+  public function withClearPassOtherShouldBeAbleToLogin() {
+    $this->assertTrue($this->_try_harder->tryHarder('Other', 'testFeelsGood'));
+  }
 }
-?>
\ No newline at end of file