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

Merge branch 'local-auth'

parents ca921e67 d84d6b2d
Pipeline #3343 failed with stages
in 2 minutes and 2 seconds
<?php
/**
* Affichage d'un avertissement concernant la mise à jour nécessaire du scema de BDD
* depuis l'ajout de la fonctionnalité 'mot de passe oublié'.
*/
//require '../../../autoload.php';
$message = <<<EOS
/*******************************************************************
* ! ATTENTION !
*
* Si vous installez cette version d'unicaen/auth, vous devez
* vous assurer que la colonne PASSWORD_RESET_TOKEN existe bien
* dans votre table utilisateur (nommee USER par défaut).
* Si ce n'est pas le cas, utilisez ceci pour l'ajouter :
*
* alter table "USER" add PASSWORD_RESET_TOKEN varchar2(256);
* create unique index USER_PASSWORD_RESET_TOKEN_UN on "USER"(PASSWORD_RESET_TOKEN);
*
******************************************************************/
EOS;
echo $message;
readline("Appuyez sur entrée pour pousuivre le processus 'composer' ou CTRL-C pour abandonner... ");
echo PHP_EOL;
......@@ -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"
......@@ -24,5 +25,8 @@
"classmap": [
"./Module.php"
]
},
"scripts": {
"pre-update-cmd": "@php bin/password-reset-requires-schema-update-warning.php"
}
}
\ No newline at end of file
......@@ -5,12 +5,24 @@ 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;
use UnicaenAuth\View\Helper\UserUsurpationHelperFactory;
$settings = [
/**
* Configuration de l'authentification locale.
*/
'local' => [
/**
* Possibilité ou non de s'authentifier à l'aide d'un compte local.
*/
'enabled' => true,
],
/**
* Configuration de l'authentification LDAP.
*/
......@@ -143,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' => []],
],
],
],
......@@ -210,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' => [
......@@ -417,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' => [
......@@ -453,6 +486,7 @@ return [
'userProfileSelect' => 'UnicaenAuth\View\Helper\UserProfileSelectFactory',
'userProfileSelectRadioItem' => 'UnicaenAuth\View\Helper\UserProfileSelectRadioItemFactory',
'userUsurpation' => UserUsurpationHelperFactory::class,
'localConnect' => LocalConnectViewHelperFactory::class,
'ldapConnect' => LdapConnectViewHelperFactory::class,
'shibConnect' => ShibConnectViewHelperFactory::class,
],
......
......@@ -7,6 +7,16 @@
*/
$settings = [
/**
* Configuration de l'authentification locale.
*/
'local' => [
/**
* Possibilité ou non de s'authentifier à l'aide d'un compte local.
*/
'enabled' => true,
],
/**
* Configuration de l'authentification LDAP.
*/
......
......@@ -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,
......
......@@ -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,
......
......@@ -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,
......
......@@ -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
......@@ -107,4 +115,114 @@ class AuthController extends AbstractActionController
throw new RuntimeException("Impossible d'écrire dans le storage");
}
}
/**
* @return Response|ViewModel
*/
public function requestPasswordResetAction()
{
$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');
}
}
return $view;
}
private function processPasswordResetRequest($email)
{
try {
$token = $this->userService->updateUserPasswordResetToken($email);
} catch (NoResultException $nre) {
// aucun utilisateur trouvé tel que username = $email
return;
}
// 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()
{
$token = $this->params()->fromRoute('token');
$view = new ViewModel();
// 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;
}
$view->setVariable('form', $form);
$view->setTemplate('unicaen-auth/auth/change-password-form');
return $view;
}
}
\ No newline at end of file
......@@ -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.
*
......
......@@ -9,6 +9,13 @@ namespace UnicaenAuth\Options;
*/
class ModuleOptions extends \ZfcUser\Options\ModuleOptions
{
/**
* Paramètres concernant l'authentification locale.
*
* @var array
*/
protected $local = [];
/**
* Paramètres concernant l'authentification LDAP.
*
......@@ -46,6 +53,25 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions
*/
protected $entityManagerName = 'doctrine.entitymanager.orm_default';
/**
* @return array
*/
public function getLocal()
{
return $this->local;
}
/**
* @param array $local
* @return self
*/
public function setLocal(array $local)
{
$this->local = $local;
return $this;
}
/**
* Retourne les paramètres concernant l'authentification LDAP.
*
......
......@@ -327,6 +327,9 @@ EOS;
if ($this->getShibbolethSimulate()) {
return '/';
}
if ($this->getAuthenticatedUser() === null) {
return '/';
}
$logoutRelativeUrl = '/Shibboleth.sso/Logout?return='; // NB: '?return=' semble obligatoire!
......
......@@ -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;
}