Commit f38c4ce0 authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Merge branch 'local-auth'

parents b5f31ecc afb95057
Pipeline #3392 failed with stages
in 2 minutes and 10 seconds
......@@ -5,8 +5,15 @@
- Authentification locale activable/désactivable dans la config.
- Clé de config `unicaen-auth` > `local` > `enabled`.
- Fonctionnalité "Mot de passe oublié" pour l'authentification locale.
- Ajout de la fonctionnalité "Mot de passe oublié" pour l'authentification locale.
- Principe: un lien contenant un token est envoyé par mail à l'utilisateur.
- NB: Le username de l'utilisateur doit être une adresse électronique.
- NB: nécessitéd de créer une nouvelle colonne dans la table des utilisateurs,
- NB: nécessité de créer une nouvelle colonne dans la table des utilisateurs,
cf. [répertoire data](./data).
## 1.3.1 - 25/01/2019
- Fonctionnalité "Mot de passe oublié" :
- Correction: l'utilisateur n'était pas recherché par son username!
- Ajout d'un validateur sur le formulaire de saisie de l'adresse électronique.
- Vérification que le compte utilisateur est bien local.
......@@ -2,7 +2,7 @@
namespace UnicaenAuth\Controller;
use Doctrine\ORM\NoResultException;
use DomainException;
use UnicaenApp\Controller\Plugin\AppInfos;
use UnicaenApp\Controller\Plugin\Mail;
use UnicaenApp\Exception\RuntimeException;
......@@ -135,24 +135,42 @@ class AuthController extends AbstractActionController
$form->setData($data);
if ($form->isValid()) {
$email = $data['email'];
try {
$this->processPasswordResetRequest($email);
$view->setVariable('email', $email);
$view->setTemplate('unicaen-auth/auth/request-password-reset-success');
} catch (DomainException $de) {
// affichage de l'erreur comme une erreur de validation
$form->get('email')->setMessages([$de->getMessage()]);
}
}
}
return $view;
}
/**
* @param string $email
*/
private function processPasswordResetRequest($email)
{
try {
$token = $this->userService->updateUserPasswordResetToken($email);
} catch (NoResultException $nre) {
// aucun utilisateur trouvé tel que username = $email
// Recherche de l'utilisateur ayant pour *username* (login) l'email spécifié
$user = $this->userService->getUserMapper()->findOneByUsername($email);
if ($user === null) {
// Aucun utilisateur trouvé ayant l'email spécifié :
// on ne fait rien mais on ne le signale pas sinon le formulaire permettrait
// de tester si des emails potentiellement valides existent dans la base.
return;
}
if (! $user->isLocal()) {
// L'email spécifié appartient à un utilisateur non local : on signale l'impossibilité de changer le mdp.
throw new DomainException("Le changement de mot de passe n'est pas possible pour cet utilisateur.");
}
// génération/enregistrement d'un token
$token = $this->userService->updateUserPasswordResetToken($user);
// envoi du mail contenant le lien de changement de mdp
$app = $this->appInfos()->getNom();
......
......@@ -14,6 +14,9 @@ use Doctrine\ORM\Mapping as ORM;
*/
abstract class AbstractUser implements UserInterface, ProviderInterface
{
const PASSWORD_LDAP = 'ldap';
const PASSWORD_SHIB = 'shib';
/**
* @var int
* @ORM\Id
......@@ -246,6 +249,22 @@ abstract class AbstractUser implements UserInterface, ProviderInterface
$this->roles->add($role);
}
/**
* Retourne true si cet utilisateur est local.
*
* Un utilisateur est local s'il ne résulte pas d'une authentification LDAP ou Shibboleth.
* Son mot de passe est chiffré dans la table des utilisateurs.
*
* @return bool
*/
public function isLocal()
{
return ! in_array($this->getPassword(), [
AbstractUser::PASSWORD_LDAP,
AbstractUser::PASSWORD_SHIB,
]);
}
/**
*
* @return string
......
......@@ -2,6 +2,7 @@
namespace UnicaenAuth\Entity\Shibboleth;
use UnicaenAuth\Entity\Db\AbstractUser;
use ZfcUser\Entity\UserInterface;
class ShibUser implements UserInterface
......@@ -200,7 +201,7 @@ class ShibUser implements UserInterface
*/
public function getPassword()
{
return 'shib';
return AbstractUser::PASSWORD_SHIB;
}
/**
......
......@@ -3,11 +3,11 @@
namespace UnicaenAuth\Service;
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\Db\AbstractUser;
use UnicaenAuth\Entity\Shibboleth\ShibUser;
use UnicaenAuth\Event\UserAuthenticatedEvent;
use UnicaenAuth\Options\ModuleOptions;
......@@ -22,11 +22,11 @@ use Zend\Form\Form;
use Zend\InputFilter\Input;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorAwareTrait;
use Zend\Validator\EmailAddress;
use Zend\Validator\Identical;
use ZfcUser\Entity\UserInterface;
use ZfcUser\Options\AuthenticationOptionsInterface;
use ZfcUser\Options\ModuleOptions as ZfcUserModuleOptions;
use UnicaenAuth\Entity\Db\AbstractUser;
/**
* Service traitant des utilisateurs locaux de l'application.
......@@ -81,13 +81,13 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
case $userData instanceof People:
$username = $userData->getData($this->getOptions()->getLdapUsername());
$email = $userData->getMail();
$password = 'ldap';
$password = AbstractUser::PASSWORD_LDAP;
$state = in_array('deactivated', ldap_explode_dn($userData->getDn(), 1)) ? 0 : 1;
break;
case $userData instanceof ShibUser:
$username = $userData->getUsername();
$email = $userData->getEmail();
$password = 'shib';
$password = AbstractUser::PASSWORD_SHIB;
$state = 1;
break;
default:
......@@ -260,7 +260,11 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
$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));
$emailInput = new Input('email');
$emailInput->setRequired(true);
$emailInput->getValidatorChain()->attach(new EmailAddress());
$form->getInputFilter()->add($emailInput);
return $form;
}
......@@ -287,22 +291,13 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
}
/**
* 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.
* Génère puis enregistre le token permettant d'autoriser un utilisateur à changer son mot de passe.
*
* @param string $email Email de l'utilisateur qui doit être aussi son username
* @param AbstractUser $user Utilisateur concerné
* @return string|null Token généré
* @throws NoResultException Aucun utilisateur trouvé avec cet email
*/
public function updateUserPasswordResetToken($email)
public function updateUserPasswordResetToken(AbstractUser $user)
{
// 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();
......@@ -344,8 +339,14 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
*/
public function generatePasswordResetToken()
{
try {
$uuid = Uuid::uuid4();
} catch (\Exception $e) {
throw new RuntimeException("Erreur rencontrée lors de la génération du UUID.", null, $e);
}
// 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);
$token = $uuid->toString() . self::PASSWORD_RESET_TOKEN_SEP . date('YmdHis', time() + 3600 * 24);
// durée de vie = 24h
return $token;
......
......@@ -2,18 +2,32 @@
namespace UnicaenAuth\Service;
use UnicaenAuth\Entity\Db\User;
use UnicaenAuth\Entity\Db\AbstractUser;
use ZfcUserDoctrineORM\Mapper\User as ZfcUserDoctrineORMUserMapper;
class UserMapper extends ZfcUserDoctrineORMUserMapper
{
/**
* Recherche un utilisateur par son username (identifiant de connexion).
*
* @param string $username
* @return AbstractUser|null
*/
public function findOneByUsername($username)
{
/** @var AbstractUser $user */
$user = $this->em->getRepository($this->options->getUserEntityClass())->findOneBy(['username' => $username]);
return $user;
}
/**
* @param string $token
* @return User
* @return AbstractUser
*/
public function findOneByPasswordResetToken($token)
{
/** @var User $user */
/** @var AbstractUser $user */
$user = $this->em->getRepository($this->options->getUserEntityClass())->findOneBy(['passwordResetToken' => $token]);
return $user;
......
......@@ -15,8 +15,8 @@ use Zend\Form\Form;
</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.
Cette page permet d'initier le changement du mot de passe associé à votre compte local de connexion à l'application.<br>
<strong>NB:</strong> Cela n'est possible que pour les utilisateurs se connectant avec leur adresse électronique.<br>
</p>
<div class="col-sm-4">
......@@ -30,6 +30,7 @@ use Zend\Form\Form;
echo $this->formInput($email);
echo $this->formElementErrors($email, ['class' => 'text-danger']);
?>
<span class="help-block">Un lien à cliquer vous sera envoyé à cette adresse électronique.</span>
</p>
<?php echo $this->formInput($form->get('csrf')); ?>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment