diff --git a/composer.json b/composer.json
index 268376b2ae1fe029744bc10416b77211a47d0784..9d35afc132e58a9f71d9c20a9e3a308add2bc10c 100644
--- a/composer.json
+++ b/composer.json
@@ -11,7 +11,8 @@
         "unicaen/app":                                  "^1.3",
         "zf-commons/zfc-user-doctrine-orm":             ">=0.1",
         "jasig/phpcas":                                 ">=1.3.3",
-        "bjyoungblood/bjy-authorize":                   ">=1.4"
+        "bjyoungblood/bjy-authorize":                   ">=1.4",
+        "ramsey/uuid":                                  "^3.8"
     },
     "require-dev": {
         "phpunit/PHPUnit":                              ">=3.7"
diff --git a/config/module.config.php b/config/module.config.php
index f6ea7d6c8e3b6e3822e1b8b0a737dad220c4b369..52902653c1ad69f1fddd88132421772a4f32f35e 100644
--- a/config/module.config.php
+++ b/config/module.config.php
@@ -5,6 +5,7 @@ use UnicaenAuth\Controller\AuthControllerFactory;
 use UnicaenAuth\Service\ShibService;
 use UnicaenAuth\Service\ShibServiceFactory;
 use UnicaenAuth\Service\UserContextFactory;
+use UnicaenAuth\Service\UserMapperFactory;
 use UnicaenAuth\View\Helper\LdapConnectViewHelperFactory;
 use UnicaenAuth\View\Helper\LocalConnectViewHelperFactory;
 use UnicaenAuth\View\Helper\ShibConnectViewHelperFactory;
@@ -154,6 +155,8 @@ return [
                 ['controller' => 'UnicaenAuth\Controller\Utilisateur', 'action' => 'selectionner-profil', 'roles' => []],
 
                 ['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'shibboleth', 'roles' => []],
+                ['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'requestPasswordReset', 'roles' => []],
+                ['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'changePassword', 'roles' => []],
             ],
         ],
     ],
@@ -221,6 +224,24 @@ return [
                             ],
                         ],
                     ],
+                    'requestPasswordReset' => [
+                        'type'    => 'Segment',
+                        'options' => [
+                            'route'    => '/request-password-reset',
+                            'defaults' => [
+                                'action'     => 'requestPasswordReset',
+                            ],
+                        ],
+                    ],
+                    'changePassword' => [
+                        'type'    => 'Segment',
+                        'options' => [
+                            'route'    => '/change-password/:token',
+                            'defaults' => [
+                                'action'     => 'changePassword',
+                            ],
+                        ],
+                    ],
                 ],
             ],
             'zfcuser'     => [
@@ -428,6 +449,7 @@ return [
             'zfcuser_redirect_callback'                => 'UnicaenAuth\Authentication\RedirectCallbackFactory', // substituion
             ShibService::class                         => ShibServiceFactory::class,
             'UnicaenAuth\Service\UserContext'          => UserContextFactory::class,
+            'zfcuser_user_mapper'                      => UserMapperFactory::class,
             'MouchardCompleterAuth'        => 'UnicaenAuth\Mouchard\MouchardCompleterAuthFactory',
         ],
         'shared' => [
diff --git a/data/schema_mysql.sql b/data/schema_mysql.sql
index 7d22ce0e15a9f7b24352ed65f230a561ce4826fc..adc2cfcd2ace5d75276777d3b90181c9bab8a8f1 100644
--- a/data/schema_mysql.sql
+++ b/data/schema_mysql.sql
@@ -9,6 +9,10 @@ CREATE TABLE user (
   UNIQUE INDEX `unique_username` (`username` ASC)
 ) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_unicode_ci;
 
+alter table user add PASSWORD_RESET_TOKEN varchar2(256) default null;
+
+create unique index USER_PASSWORD_RESET_TOKEN_UN on user (PASSWORD_RESET_TOKEN);
+
 
 CREATE TABLE IF NOT EXISTS `user_role` (
   `id` INT(11) NOT NULL AUTO_INCREMENT,
diff --git a/data/schema_oracle.sql b/data/schema_oracle.sql
index 5410e1ed8bd04608e472bd535ff597f03839f592..4570eb35425a8f23b480c694e7ceac67e8d6d410 100644
--- a/data/schema_oracle.sql
+++ b/data/schema_oracle.sql
@@ -10,6 +10,10 @@ CREATE TABLE "USER"
 );
 CREATE SEQUENCE "USER_ID_SEQ" ;
 
+alter table "USER" add PASSWORD_RESET_TOKEN varchar2(256) default null;
+
+create unique index USER_PASSWORD_RESET_TOKEN_UN on "USER" (PASSWORD_RESET_TOKEN);
+
 
 CREATE TABLE USER_ROLE
 (	"ID" NUMBER(*,0) NOT NULL ENABLE,
diff --git a/data/schema_postgresql.sql b/data/schema_postgresql.sql
index 59dd14b76d9f8f0122a638f4d893f45b4cf0e904..64f30c9254e5374ae73d57c6b26c1b543969f5a5 100644
--- a/data/schema_postgresql.sql
+++ b/data/schema_postgresql.sql
@@ -8,6 +8,10 @@ CREATE TABLE "user" (
 ) ;
 CREATE UNIQUE INDEX user_username_unique ON "user" (username);
 
+alter table "user" add PASSWORD_RESET_TOKEN varchar2(256) default null;
+
+create unique index USER_PASSWORD_RESET_TOKEN_UN on "user" (PASSWORD_RESET_TOKEN);
+
 CREATE TABLE user_role (
   id BIGSERIAL PRIMARY KEY,
   role_id VARCHAR(64) NOT NULL,
diff --git a/src/UnicaenAuth/Controller/AuthController.php b/src/UnicaenAuth/Controller/AuthController.php
index 986c50e5c7496601cb1e10338c7f02ae62cb31b9..33747a8659674b6081988871cdb2c7021c819d36 100644
--- a/src/UnicaenAuth/Controller/AuthController.php
+++ b/src/UnicaenAuth/Controller/AuthController.php
@@ -2,19 +2,27 @@
 
 namespace UnicaenAuth\Controller;
 
+use Doctrine\ORM\NoResultException;
+use UnicaenApp\Controller\Plugin\AppInfos;
+use UnicaenApp\Controller\Plugin\Mail;
 use UnicaenApp\Exception\RuntimeException;
 use UnicaenAuth\Service\Traits\ShibServiceAwareTrait;
 use UnicaenAuth\Service\Traits\UserServiceAwareTrait;
 use Zend\Authentication\AuthenticationService;
 use Zend\Authentication\Exception\ExceptionInterface;
+use Zend\Http\Request;
 use Zend\Http\Response;
 use Zend\Mvc\Controller\AbstractActionController;
+use Zend\View\Model\ViewModel;
 use ZfcUser\Controller\Plugin\ZfcUserAuthentication;
 
 /**
  * Classe ajoutée lors de l'implémentation de l'auth Shibboleth.
  *
  * @method ZfcUserAuthentication zfcUserAuthentication()
+ * @method AppInfos appInfos()
+ * @method Mail mail()
+ *
  * @author Bertrand GAUTHIER <bertrand.gauthier at unicaen.fr>
  */
 class AuthController extends AbstractActionController
@@ -108,25 +116,113 @@ class AuthController extends AbstractActionController
         }
     }
 
-    public function sendPasswordRenewalMailAction()
+    /**
+     * @return Response|ViewModel
+     */
+    public function requestPasswordResetAction()
     {
-        // lecture email fourni
+        $form = $this->userService->createResetPasswordEmailForm();
+
+        $view = new ViewModel();
+        $view->setVariable('form', $form);
+        $view->setTemplate('unicaen-auth/auth/request-password-reset-form');
+
+        /** @var Request $request */
+        $request = $this->getRequest();
+
+        if ($request->isPost()) {
+            $data = $request->getPost();
+            $form->setData($data);
+            if ($form->isValid()) {
+                $email = $data['email'];
+                $this->processPasswordResetRequest($email);
+
+                $view->setVariable('email', $email);
+                $view->setTemplate('unicaen-auth/auth/request-password-reset-success');
+            }
+        }
 
-        // tester email connu dans table utilisateur
+        return $view;
+    }
 
-        // générer / enregistrer token dans table utilisateur
+    private function processPasswordResetRequest($email)
+    {
+        try {
+            $token = $this->userService->updateUserPasswordResetToken($email);
+        } catch (NoResultException $nre) {
+            // aucun utilisateur trouvé tel que username = $email
+            return;
+        }
 
-        // envoyer mail avec lien/token
+        // envoi du mail contenant le lien de changement de mdp
+        $app = $this->appInfos()->getNom();
+        $subject = "[$app] Demande de changement de mot de passe";
+        $changePasswordUrl = $this->url()->fromRoute('auth/changePassword', ['token' => $token], ['force_canonical' => true]);
+        $body = <<<EOS
+<p>Une demande de changement de mot de passe a été faite sur l'application $app.</p>
+<p>Si vous n'en êtes pas l'auteur, vous pouvez ignorer ce message.</p>
+<p>Cliquez sur le lien suivant pour accéder au formulaire de changement de votre mot de passe :<br><a href='$changePasswordUrl'>$changePasswordUrl</a></p>
+EOS;
+        $message = $this->mail()->createNewMessage($body, $subject);
+        $message->setTo($email);
+        $this->mail()->send($message);
     }
 
+    /**
+     * @return array|ViewModel
+     */
     public function changePasswordAction()
     {
-        // lecture token fourni
+        $token = $this->params()->fromRoute('token');
+        $view = new ViewModel();
 
-        // test token fourni existe dans table utilisateur
+        // recherche du token spécifié dans table utilisateur
+        $user = $this->userService->getUserMapper()->findOneByPasswordResetToken($token);
+        if ($user === null) {
+            // token inexistant
+            $view->setVariable('result', 'unknown_token');
+            $view->setTemplate('unicaen-auth/auth/change-password-result');
+
+            return $view;
+        }
+
+        $form = $this->userService->createPasswordChangeForm();
+
+        /** @var Request $request */
+        $request = $this->getRequest();
+
+        if ($request->isPost()) {
+            $data = $request->getPost();
+            $form->setData($data);
+            if ($form->isValid()) {
+                // màj password
+                $password = $this->params()->fromPost('password');
+                $this->userService->updateUserPassword($user, $password);
+
+                $view->setVariable('result', 'success');
+                $view->setTemplate('unicaen-auth/auth/change-password-result');
+
+                // todo: faut-il déconnecter l'utilisateur (attention au logout shib différent) ?
+
+                return $view;
+            }
+        }
+
+        // test durée de vie du token
+        $date = $this->userService->extractDateFromResetPasswordToken($token);
+        if ($date < date_create()) {
+            // token expiré, on le raz
+            $this->userService->clearUserPasswordResetToken($user);
+
+            $view->setVariable('result', 'dead_token');
+            $view->setTemplate('unicaen-auth/auth/change-password-result');
+
+            return $view;
+        }
 
-        // afficher formulaire de màj
+        $view->setVariable('form', $form);
+        $view->setTemplate('unicaen-auth/auth/change-password-form');
 
-        // màj password
+        return $view;
     }
 }
\ No newline at end of file
diff --git a/src/UnicaenAuth/Entity/Db/AbstractUser.php b/src/UnicaenAuth/Entity/Db/AbstractUser.php
index e19aba664c5006252e0ad54fd581e13bf2ef83a0..a8adfa17b9254fb4ee540ce1ac35af59a872889d 100644
--- a/src/UnicaenAuth/Entity/Db/AbstractUser.php
+++ b/src/UnicaenAuth/Entity/Db/AbstractUser.php
@@ -52,6 +52,12 @@ abstract class AbstractUser implements UserInterface, ProviderInterface
      */
     protected $state;
 
+    /**
+     * @var string
+     * @ORM\Column(type="string", length=256)
+     */
+    protected $passwordResetToken;
+
     /**
      * @var Collection
      * @ORM\ManyToMany(targetEntity="UnicaenAuth\Entity\Db\Role")
@@ -202,6 +208,22 @@ abstract class AbstractUser implements UserInterface, ProviderInterface
         $this->state = $state;
     }
 
+    /**
+     * @return string
+     */
+    public function getPasswordResetToken()
+    {
+        return $this->passwordResetToken;
+    }
+
+    /**
+     * @param string $passwordResetToken
+     */
+    public function setPasswordResetToken($passwordResetToken = null)
+    {
+        $this->passwordResetToken = $passwordResetToken;
+    }
+
     /**
      * Get role.
      *
diff --git a/src/UnicaenAuth/Service/User.php b/src/UnicaenAuth/Service/User.php
index c1a483d607d4ef4e582df4bbe7c9a14c71768d4f..d566f10ef17a53edce6af21c620630db19e2db66 100644
--- a/src/UnicaenAuth/Service/User.php
+++ b/src/UnicaenAuth/Service/User.php
@@ -2,24 +2,34 @@
 
 namespace UnicaenAuth\Service;
 
-use UnicaenAuth\Event\UserAuthenticatedEvent;
-use PDOException;
+use DateTime;
+use Doctrine\ORM\NoResultException;
+use Ramsey\Uuid\Uuid;
 use UnicaenApp\Entity\Ldap\People;
 use UnicaenApp\Exception\RuntimeException;
 use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper;
 use UnicaenAuth\Entity\Shibboleth\ShibUser;
+use UnicaenAuth\Event\UserAuthenticatedEvent;
 use UnicaenAuth\Options\ModuleOptions;
+use Zend\Crypt\Password\Bcrypt;
 use Zend\EventManager\EventManagerAwareInterface;
 use Zend\EventManager\EventManagerInterface;
+use Zend\Form\Element\Csrf;
+use Zend\Form\Element\Password;
+use Zend\Form\Element\Submit;
+use Zend\Form\Element\Text;
+use Zend\Form\Form;
+use Zend\InputFilter\Input;
 use Zend\ServiceManager\ServiceLocatorAwareInterface;
 use Zend\ServiceManager\ServiceLocatorAwareTrait;
+use Zend\Validator\Identical;
 use ZfcUser\Entity\UserInterface;
 use ZfcUser\Options\AuthenticationOptionsInterface;
 use ZfcUser\Options\ModuleOptions as ZfcUserModuleOptions;
+use UnicaenAuth\Entity\Db\AbstractUser;
 
 /**
- * Service d'enregistrement dans la table des utilisateurs de l'application
- * de l'utilisateur authentifié avec succès.
+ * Service traitant des utilisateurs locaux de l'application.
  *
  * @see \UnicaenAuth\Authentication\Adapter\AbstractFactory
  * @author Unicaen
@@ -45,15 +55,20 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
      */
     protected $zfcUserOptions;
 
+    /**
+     * @var UserMapper
+     */
+    protected $userMapper;
+
     /**
      * @var LdapPeopleMapper
      */
     protected $ldapPeopleMapper;
 
     /**
-     * Save authenticated user in database from LDAP data.
+     * Save authenticated user in database from LDAP or Shibboleth data.
      *
-     * @param UserInterface|People $userData
+     * @param People|ShibUser $userData
      * @return bool
      */
     public function userAuthenticated($userData)
@@ -68,7 +83,6 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
                 $email = $userData->getMail();
                 $password = 'ldap';
                 $state = in_array('deactivated', ldap_explode_dn($userData->getDn(), 1)) ? 0 : 1;
-
                 break;
             case $userData instanceof ShibUser:
                 $username = $userData->getUsername();
@@ -94,46 +108,42 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
             throw new RuntimeException("Identité rencontrée inattendue.");
         }
 
-        // update/insert de l'utilisateur dans la table de l'appli
-        $mapper = $this->getServiceLocator()->get('zfcuser_user_mapper'); /* @var $mapper \ZfcUserDoctrineORM\Mapper\User */
-        try {
-            /** @var UserInterface $entity */
-            $entity = $mapper->findByUsername($username);
-            if (!$entity) {
-                $entityClass = $this->getZfcUserOptions()->getUserEntityClass();
-                $entity = new $entityClass;
-                $entity->setUsername($username);
-                $method = 'insert';
-            }
-            else {
-                $method = 'update';
-            }
-            $entity->setEmail($email);
-            $entity->setDisplayName($userData->getDisplayName());
-            $entity->setPassword($password);
-            $entity->setState($state);
-
-            // pre-persist
-            $event = new UserAuthenticatedEvent(UserAuthenticatedEvent::PRE_PERSIST);
-            $this->triggerEvent($event, $entity, $userData);
-
-            // persist
-            $mapper->$method($entity);
-
-            // post-persist
-            $event = new UserAuthenticatedEvent(UserAuthenticatedEvent::POST_PERSIST);
-            $this->triggerEvent($event, $entity, $userData);
+        $mapper = $this->getUserMapper();
+
+        /** @var UserInterface $entity */
+        $entity = $mapper->findByUsername($username);
+        if (!$entity) {
+            $entityClass = $this->getZfcUserOptions()->getUserEntityClass();
+            $entity = new $entityClass;
+            $entity->setUsername($username);
+            $method = 'insert';
         }
-        catch (PDOException $pdoe) {
-            throw new RuntimeException("Impossible d'enregistrer l'utilisateur authentifié dans la base de données.", null, $pdoe);
+        else {
+            $method = 'update';
         }
+        $entity->setEmail($email);
+        $entity->setDisplayName($userData->getDisplayName());
+        $entity->setPassword($password);
+        $entity->setState($state);
+
+        // pre-persist
+        $event = new UserAuthenticatedEvent(UserAuthenticatedEvent::PRE_PERSIST);
+        $this->triggerEvent($event, $entity, $userData);
+
+        // persist
+        $mapper->$method($entity);
+
+        // post-persist
+        $event = new UserAuthenticatedEvent(UserAuthenticatedEvent::POST_PERSIST);
+        $this->triggerEvent($event, $entity, $userData);
 
         return true;
     }
 
     /**
-     * @param UserInterface $entity
-     * @param People|ShibUser $userData
+     * @param UserAuthenticatedEvent $event
+     * @param UserInterface          $entity
+     * @param People|ShibUser        $userData
      */
     private function triggerEvent(UserAuthenticatedEvent $event, $entity, $userData)
     {
@@ -148,6 +158,18 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
         $this->getEventManager()->trigger($event);
     }
 
+    /**
+     * @return UserMapper
+     */
+    public function getUserMapper()
+    {
+        if ($this->userMapper === null) {
+            $this->userMapper = $this->getServiceLocator()->get('zfcuser_user_mapper');
+        }
+
+        return $this->userMapper;
+    }
+
     /**
      * Retrieve the event manager
      *
@@ -222,4 +244,137 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
 
         return $this->zfcUserOptions;
     }
+
+    const PASSWORD_RESET_TOKEN_SEP = '-';
+    const PASSWORD_RESET_TOKEN_DATE_FORMAT = 'YmdHis';
+
+    /**
+     * Construit le formulaire de saisie de l'adresse électronique à laquelle envoyer le lien de
+     * changement de mot de passe.
+     *
+     * @return Form
+     */
+    public function createResetPasswordEmailForm()
+    {
+        $form = new Form();
+        $form->add((new Text('email'))->setLabel("Adresse électronique :"));
+        $form->add((new Csrf('csrf')));
+        $form->add((new Submit('submit'))->setLabel("Envoyer le lien"));
+        $form->getInputFilter()->add((new Input('email'))->setRequired(true));
+
+        return $form;
+    }
+
+    /**
+     * Construit le formulaire de saisie d'un nouveau mot de passe.
+     *
+     * @return Form
+     */
+    public function createPasswordChangeForm()
+    {
+        $form = new Form();
+        $form->add((new Password('password'))->setLabel("Nouveau mot de passe :"));
+        $form->add((new Password('passwordbis'))->setLabel("Confirmation du nouveau mot de passe :"));
+        $form->add((new Csrf('csrf')));
+        $form->add((new Submit('submit'))->setLabel("Enregistrer"));
+        $form->getInputFilter()->add((new Input('password'))->setRequired(true));
+
+        $passwordbisInput = (new Input('passwordbis'))->setRequired(true);
+        $passwordbisInput->getValidatorChain()->attach(new Identical('password'));
+        $form->getInputFilter()->add($passwordbisInput);
+
+        return $form;
+    }
+
+    /**
+     * Si l'utilisateur dont le username égale l'email spécifié est trouvé,
+     * génère puis enregistre le token permettant d'autoriser cet utilisateur à changer son mot de passe.
+     *
+     * @param string $email Email de l'utilisateur qui doit être aussi son username
+     * @return string|null Token généré
+     * @throws NoResultException Aucun utilisateur trouvé avec cet email
+     */
+    public function updateUserPasswordResetToken($email)
+    {
+        // Si l'email est inconnu, on ne fera rien mais on ne le signale pas sinon le formulaire permettrait
+        // de tester si des emails potentiellement valides existent dans la base.
+        $user = $this->getUserMapper()->findByEmail($email); /** @var User $user */
+        if ($user === null) {
+            throw new NoResultException();
+        }
+
+        // Génération du token.
+        $token = $this->generatePasswordResetToken();
+
+        // Enregistrement du token dans la table des utilisateurs
+        $user->setPasswordResetToken($token);
+        $this->getUserMapper()->update($user);
+
+        return $token;
+    }
+
+    /**
+     * @param AbstractUser $user
+     */
+    public function clearUserPasswordResetToken(AbstractUser $user)
+    {
+        $user->setPasswordResetToken(null);
+        $this->getUserMapper()->update($user);
+    }
+
+    /**
+     * @param AbstractUser $user
+     * @param string       $password
+     */
+    public function updateUserPassword(AbstractUser $user, $password)
+    {
+        $bcrypt = new Bcrypt();
+        $bcrypt->setCost($this->getZfcUserOptions()->getPasswordCost());
+        $password = $bcrypt->create($password);
+
+        $user->setPasswordResetToken(null);
+        $user->setPassword($password);
+        $this->getUserMapper()->update($user);
+    }
+
+    /**
+     * Génération d'un token pour la demande de renouvellement de mot de passe.
+     *
+     * @return string
+     */
+    public function generatePasswordResetToken()
+    {
+        // NB: la date de fin de vie du token est concaténée à la fin.
+        $token = Uuid::uuid4()->toString() . self::PASSWORD_RESET_TOKEN_SEP . date('YmdHis', time() + 3600*24);
+        // durée de vie = 24h
+
+        return $token;
+    }
+
+    /**
+     * Génération du motif permettant de rechercher un token dans la table des utilisateurs.
+     *
+     * Rappel: la date de génération est concaténée à la fin.
+     *
+     * @param string $tokenUnderTest Le token recherché
+     * @return string
+     */
+    public function generatePasswordResetTokenSearchPattern($tokenUnderTest)
+    {
+        return $tokenUnderTest . self::PASSWORD_RESET_TOKEN_SEP . '%';
+    }
+
+    /**
+     * Extrait la date de fin de vie d'un token.
+     *
+     * @param string $token
+     * @return DateTime
+     */
+    public function extractDateFromResetPasswordToken($token)
+    {
+        $ts = ltrim(strrchr($token, $sep = self::PASSWORD_RESET_TOKEN_SEP), $sep);
+        $date = DateTime::createFromFormat(self::PASSWORD_RESET_TOKEN_DATE_FORMAT, $ts);
+
+        return $date;
+    }
 }
\ No newline at end of file
diff --git a/src/UnicaenAuth/Service/UserMapper.php b/src/UnicaenAuth/Service/UserMapper.php
new file mode 100644
index 0000000000000000000000000000000000000000..b41c49b66dc40ccdeac7cc4b36fee6767d07db44
--- /dev/null
+++ b/src/UnicaenAuth/Service/UserMapper.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace UnicaenAuth\Service;
+
+use UnicaenAuth\Entity\Db\User;
+use ZfcUserDoctrineORM\Mapper\User as ZfcUserDoctrineORMUserMapper;
+
+class UserMapper extends ZfcUserDoctrineORMUserMapper
+{
+    /**
+     * @param string $token
+     * @return User
+     */
+    public function findOneByPasswordResetToken($token)
+    {
+        /** @var User $user */
+        $user = $this->em->getRepository($this->options->getUserEntityClass())->findOneBy(['passwordResetToken' => $token]);
+
+        return $user;
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenAuth/Service/UserMapperFactory.php b/src/UnicaenAuth/Service/UserMapperFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..a6c83548907240466c147fc1a74228d62fa0d06d
--- /dev/null
+++ b/src/UnicaenAuth/Service/UserMapperFactory.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace UnicaenAuth\Service;
+
+use Doctrine\ORM\EntityManagerInterface;
+use Zend\ServiceManager\FactoryInterface;
+use Zend\ServiceManager\ServiceLocatorInterface;
+
+class UserMapperFactory implements FactoryInterface
+{
+    /**
+     * Create service
+     *
+     * @param ServiceLocatorInterface $serviceLocator
+     * @return UserMapper
+     */
+    public function createService(ServiceLocatorInterface $serviceLocator)
+    {
+        /** @var EntityManagerInterface $em */
+        $em = $serviceLocator->get('zfcuser_doctrine_em');
+
+        /** @var \ZfcUserDoctrineORM\Options\ModuleOptions $options */
+        $options = $serviceLocator->get('zfcuser_module_options');
+
+        return new UserMapper($em, $options);
+    }
+}
diff --git a/src/UnicaenAuth/View/Helper/LocalConnectViewHelper.php b/src/UnicaenAuth/View/Helper/LocalConnectViewHelper.php
index e6dd8047cccd3307831b2432458ddb0375e1369d..b2475ed8867a5b22a1ebeec08e66631f330fb5a9 100644
--- a/src/UnicaenAuth/View/Helper/LocalConnectViewHelper.php
+++ b/src/UnicaenAuth/View/Helper/LocalConnectViewHelper.php
@@ -67,7 +67,7 @@ class LocalConnectViewHelper extends AbstractHelper
                 'enabled' => $this->enabled,
                 'form' => $this->form,
                 'redirect' => null,
-                'password_reset' => true,
+                'passwordReset' => true,
             ]);
         } catch (\Exception $e) {
             return '<p>' . $e->getMessage() . '</p><p>' . $e->getTraceAsString() . '</p>';
diff --git a/src/UnicaenAuth/View/Helper/partial/connect.phtml b/src/UnicaenAuth/View/Helper/partial/connect.phtml
index f216e21f7f2ade485af54a49b694623bf000d6ff..7d758b6ec3ffe9e5ca9c8119599b8742c1b3c0b2 100644
--- a/src/UnicaenAuth/View/Helper/partial/connect.phtml
+++ b/src/UnicaenAuth/View/Helper/partial/connect.phtml
@@ -7,6 +7,7 @@ use Zend\Form\Form;
  * @var Form   $form
  * @var string $title
  * @var string $redirect
+ * @var bool   $passwordReset
  */
 ?>
 
@@ -34,6 +35,9 @@ use Zend\Form\Form;
     echo $this->formLabel($identity);
     echo $this->formInput($identity);
     ?>
+    <?php if ($passwordReset): ?>
+        <a class="connect-credentials-lost" href="<?php echo $this->url('auth/requestPasswordReset') ?>">Mot de passe oublié</a>
+    <?php endif ?>
 </p>
 <?php if ($redirect): ?>
     <input type="hidden" name="redirect" value="<?php echo $redirect ?>"/>
diff --git a/src/UnicaenAuth/View/Helper/partial/reset-password.phtml b/src/UnicaenAuth/View/Helper/partial/reset-password.phtml
deleted file mode 100644
index ef61977c15f5810981ad18ef90e2595513dc3687..0000000000000000000000000000000000000000
--- a/src/UnicaenAuth/View/Helper/partial/reset-password.phtml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-use Zend\Form\Form;
-
-/**
- * @var bool   $enabled
- * @var Form   $form
- * @var string $title
- * @var string $redirect
- */
-?>
-
-<?php if ($title): ?>
-    <h3 class="password-reset-title">
-        <?php echo $title ?>
-    </h3>
-<?php endif ?>
-
-<?php echo $this->form()->openTag($form) ?>
-
-<?php if (($errors = $this->formErrors($form))): ?>
-    <p><?php echo $errors ?></p>
-<?php endif ?>
-<p class="password-reset-identity">
-    <?php
-    $identity = $form->get($name = 'identity')->setAttributes(['id' => $name, 'class' => 'form-control']);
-    echo $this->formLabel($identity);
-    echo $this->formInput($identity);
-    ?>
-</p>
-<?php if ($redirect): ?>
-    <input type="hidden" name="redirect" value="<?php echo $redirect ?>"/>
-<?php endif ?>
-
-<p class="password-reset-submit">
-    <?php echo $this->formButton($form->get('submit')->setAttribute('class', 'btn btn-primary')) ?>
-</p>
-
-<?php echo $this->form()->closeTag() ?>
diff --git a/view/unicaen-auth/auth/change-password-form.phtml b/view/unicaen-auth/auth/change-password-form.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..5054ec457d51c2519e12534d1345d91d26c78f7c
--- /dev/null
+++ b/view/unicaen-auth/auth/change-password-form.phtml
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @var string $status
+ */
+?>
+
+<h2 class="page-header password-change-title">
+    Formulaire de changement de mot de passe
+</h2>
+
+<p class="lead">
+    Veuillez choisir puis confirmer votre nouveau mot de passe.<br>
+</p>
+
+<div class="col-sm-4">
+
+    <?php echo $this->form()->openTag($form) ?>
+
+    <p class="password-change-password">
+        <?php
+        $password = $form->get($name = 'password')->setAttributes(['id' => $name, 'class' => 'form-control']);
+        echo $this->formLabel($password);
+        echo $this->formInput($password);
+        echo $this->formElementErrors($password, ['class' => 'text-danger']);
+        ?>
+    </p>
+    <p class="password-change-passwordbis">
+        <?php
+        $passwordbis = $form->get($name = 'passwordbis')->setAttributes(['id' => $name, 'class' => 'form-control']);
+        echo $this->formLabel($passwordbis);
+        echo $this->formInput($passwordbis);
+        echo $this->formElementErrors($passwordbis, ['class' => 'text-danger']);
+        ?>
+    </p>
+
+    <?php echo $this->formInput($form->get('csrf')); ?>
+
+    <p class="password-change-submit">
+        <?php echo $this->formButton($form->get('submit')->setAttribute('class', 'btn btn-primary')) ?>
+    </p>
+
+    <?php echo $this->form()->closeTag() ?>
+
+</div>
\ No newline at end of file
diff --git a/view/unicaen-auth/auth/change-password-result.phtml b/view/unicaen-auth/auth/change-password-result.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..4de2e9fe3a3f67fefef2a877c1fd61850eb49bc2
--- /dev/null
+++ b/view/unicaen-auth/auth/change-password-result.phtml
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @var string $result
+ */
+?>
+
+<h2 class="page-header password-reset-title">
+    Changement de mot de passe
+</h2>
+
+<?php if (in_array($result, ['unknown_token', 'dead_token'])): ?>
+    <p class="lead text-danger">
+        <strong>Impossible!</strong> Le lien que vous avez utilisé n'est plus valide. <br>
+    </p>
+    <p>
+        Vous pouvez refaire une demande de changement de mot de passe <a href="<?php echo $this->url('auth/requestPasswordReset') ?>">ici</a>.
+    </p>
+<?php else: ?>
+    <p class="lead text-success">
+        <strong>C'est fait!</strong> Votre nouveau mot de passe a bien été enregistré.
+    </p>
+<?php endif ?>
+
+<a class="btn btn-primary" href="<?php echo $this->url('home') ?>">Revenir à l'accueil</a>
diff --git a/view/unicaen-auth/auth/request-password-reset-form.phtml b/view/unicaen-auth/auth/request-password-reset-form.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..c297a4de736e83b809ac79f31fca8ff00cd44747
--- /dev/null
+++ b/view/unicaen-auth/auth/request-password-reset-form.phtml
@@ -0,0 +1,43 @@
+<?php
+
+use Zend\Form\Form;
+
+/**
+ * @var bool   $enabled
+ * @var Form   $form
+ * @var string $title
+ * @var string $redirect
+ */
+?>
+
+<h2 class="page-header password-reset-title">
+    Demande de changement de mot de passe
+</h2>
+
+<p class="lead">
+    Un lien permettant de changer votre mot de passe vous sera envoyé à l'adresse que vous renseignerez ci-dessous.<br>
+    <strong>NB:</strong> Cette adresse doit correspondre au compte que vous utilisez pour vous connecter à l'application.
+</p>
+
+<div class="col-sm-4">
+
+    <?php echo $this->form()->openTag($form) ?>
+
+    <p class="password-reset-email">
+        <?php
+        $email = $form->get($name = 'email')->setAttributes(['id' => $name, 'class' => 'form-control']);
+        echo $this->formLabel($email);
+        echo $this->formInput($email);
+        echo $this->formElementErrors($email, ['class' => 'text-danger']);
+        ?>
+    </p>
+
+    <?php echo $this->formInput($form->get('csrf')); ?>
+
+    <p class="password-reset-submit">
+        <?php echo $this->formButton($form->get('submit')->setAttribute('class', 'btn btn-primary')) ?>
+    </p>
+
+    <?php echo $this->form()->closeTag() ?>
+
+</div>
\ No newline at end of file
diff --git a/view/unicaen-auth/auth/request-password-reset-success.phtml b/view/unicaen-auth/auth/request-password-reset-success.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..34c79c06bb2696cf974affb6adf7cca1e05dd3c3
--- /dev/null
+++ b/view/unicaen-auth/auth/request-password-reset-success.phtml
@@ -0,0 +1,25 @@
+<?php
+
+use Zend\Form\Form;
+
+/**
+ * @var Form   $form
+ * @var string $email
+ */
+?>
+
+<h2 class="page-header password-reset-title">
+    Demande de changement de mot de passe
+</h2>
+
+<p class="lead text-success">
+    <strong>Relevez votre courier!</strong><br>
+    Un lien permettant de changer votre mot de passe a été envoyé à l'adresse <strong><?php echo $email ?></strong>...
+</p>
+
+<p>
+    Si besoin, vous pouvez refaire une demande de changement en cliquant
+    <a href="<?php echo $this->url('auth/requestPasswordReset') ?>">ici</a>.
+</p>
+
+<a class="btn btn-primary" href="<?php echo $this->url('home') ?>">Revenir à l'accueil</a>