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 @@ ...@@ -11,7 +11,8 @@
"unicaen/app": "^1.3", "unicaen/app": "^1.3",
"zf-commons/zfc-user-doctrine-orm": ">=0.1", "zf-commons/zfc-user-doctrine-orm": ">=0.1",
"jasig/phpcas": ">=1.3.3", "jasig/phpcas": ">=1.3.3",
"bjyoungblood/bjy-authorize": ">=1.4" "bjyoungblood/bjy-authorize": ">=1.4",
"ramsey/uuid": "^3.8"
}, },
"require-dev": { "require-dev": {
"phpunit/PHPUnit": ">=3.7" "phpunit/PHPUnit": ">=3.7"
...@@ -24,5 +25,8 @@ ...@@ -24,5 +25,8 @@
"classmap": [ "classmap": [
"./Module.php" "./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; ...@@ -5,12 +5,24 @@ use UnicaenAuth\Controller\AuthControllerFactory;
use UnicaenAuth\Service\ShibService; use UnicaenAuth\Service\ShibService;
use UnicaenAuth\Service\ShibServiceFactory; use UnicaenAuth\Service\ShibServiceFactory;
use UnicaenAuth\Service\UserContextFactory; use UnicaenAuth\Service\UserContextFactory;
use UnicaenAuth\Service\UserMapperFactory;
use UnicaenAuth\View\Helper\LdapConnectViewHelperFactory; use UnicaenAuth\View\Helper\LdapConnectViewHelperFactory;
use UnicaenAuth\View\Helper\LocalConnectViewHelperFactory;
use UnicaenAuth\View\Helper\ShibConnectViewHelperFactory; use UnicaenAuth\View\Helper\ShibConnectViewHelperFactory;
use UnicaenAuth\View\Helper\UserUsurpationHelperFactory; use UnicaenAuth\View\Helper\UserUsurpationHelperFactory;
$settings = [ $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. * Configuration de l'authentification LDAP.
*/ */
...@@ -143,6 +155,8 @@ return [ ...@@ -143,6 +155,8 @@ return [
['controller' => 'UnicaenAuth\Controller\Utilisateur', 'action' => 'selectionner-profil', 'roles' => []], ['controller' => 'UnicaenAuth\Controller\Utilisateur', 'action' => 'selectionner-profil', 'roles' => []],
['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'shibboleth', '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 [ ...@@ -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' => [ 'zfcuser' => [
...@@ -417,6 +449,7 @@ return [ ...@@ -417,6 +449,7 @@ return [
'zfcuser_redirect_callback' => 'UnicaenAuth\Authentication\RedirectCallbackFactory', // substituion 'zfcuser_redirect_callback' => 'UnicaenAuth\Authentication\RedirectCallbackFactory', // substituion
ShibService::class => ShibServiceFactory::class, ShibService::class => ShibServiceFactory::class,
'UnicaenAuth\Service\UserContext' => UserContextFactory::class, 'UnicaenAuth\Service\UserContext' => UserContextFactory::class,
'zfcuser_user_mapper' => UserMapperFactory::class,
'MouchardCompleterAuth' => 'UnicaenAuth\Mouchard\MouchardCompleterAuthFactory', 'MouchardCompleterAuth' => 'UnicaenAuth\Mouchard\MouchardCompleterAuthFactory',
], ],
'shared' => [ 'shared' => [
...@@ -453,6 +486,7 @@ return [ ...@@ -453,6 +486,7 @@ return [
'userProfileSelect' => 'UnicaenAuth\View\Helper\UserProfileSelectFactory', 'userProfileSelect' => 'UnicaenAuth\View\Helper\UserProfileSelectFactory',
'userProfileSelectRadioItem' => 'UnicaenAuth\View\Helper\UserProfileSelectRadioItemFactory', 'userProfileSelectRadioItem' => 'UnicaenAuth\View\Helper\UserProfileSelectRadioItemFactory',
'userUsurpation' => UserUsurpationHelperFactory::class, 'userUsurpation' => UserUsurpationHelperFactory::class,
'localConnect' => LocalConnectViewHelperFactory::class,
'ldapConnect' => LdapConnectViewHelperFactory::class, 'ldapConnect' => LdapConnectViewHelperFactory::class,
'shibConnect' => ShibConnectViewHelperFactory::class, 'shibConnect' => ShibConnectViewHelperFactory::class,
], ],
......
...@@ -7,6 +7,16 @@ ...@@ -7,6 +7,16 @@
*/ */
$settings = [ $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. * Configuration de l'authentification LDAP.
*/ */
......
...@@ -9,6 +9,10 @@ CREATE TABLE user ( ...@@ -9,6 +9,10 @@ CREATE TABLE user (
UNIQUE INDEX `unique_username` (`username` ASC) UNIQUE INDEX `unique_username` (`username` ASC)
) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_unicode_ci; ) 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` ( CREATE TABLE IF NOT EXISTS `user_role` (
`id` INT(11) NOT NULL AUTO_INCREMENT, `id` INT(11) NOT NULL AUTO_INCREMENT,
......
...@@ -10,6 +10,10 @@ CREATE TABLE "USER" ...@@ -10,6 +10,10 @@ CREATE TABLE "USER"
); );
CREATE SEQUENCE "USER_ID_SEQ" ; 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 CREATE TABLE USER_ROLE
( "ID" NUMBER(*,0) NOT NULL ENABLE, ( "ID" NUMBER(*,0) NOT NULL ENABLE,
......
...@@ -8,6 +8,10 @@ CREATE TABLE "user" ( ...@@ -8,6 +8,10 @@ CREATE TABLE "user" (
) ; ) ;
CREATE UNIQUE INDEX user_username_unique ON "user" (username); 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 ( CREATE TABLE user_role (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
role_id VARCHAR(64) NOT NULL, role_id VARCHAR(64) NOT NULL,
......
...@@ -2,19 +2,27 @@ ...@@ -2,19 +2,27 @@
namespace UnicaenAuth\Controller; namespace UnicaenAuth\Controller;
use Doctrine\ORM\NoResultException;
use UnicaenApp\Controller\Plugin\AppInfos;
use UnicaenApp\Controller\Plugin\Mail;
use UnicaenApp\Exception\RuntimeException; use UnicaenApp\Exception\RuntimeException;
use UnicaenAuth\Service\Traits\ShibServiceAwareTrait; use UnicaenAuth\Service\Traits\ShibServiceAwareTrait;
use UnicaenAuth\Service\Traits\UserServiceAwareTrait; use UnicaenAuth\Service\Traits\UserServiceAwareTrait;
use Zend\Authentication\AuthenticationService; use Zend\Authentication\AuthenticationService;
use Zend\Authentication\Exception\ExceptionInterface; use Zend\Authentication\Exception\ExceptionInterface;
use Zend\Http\Request;
use Zend\Http\Response; use Zend\Http\Response;
use Zend\Mvc\Controller\AbstractActionController; use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use ZfcUser\Controller\Plugin\ZfcUserAuthentication; use ZfcUser\Controller\Plugin\ZfcUserAuthentication;
/** /**
* Classe ajoutée lors de l'implémentation de l'auth Shibboleth. * Classe ajoutée lors de l'implémentation de l'auth Shibboleth.
* *
* @method ZfcUserAuthentication zfcUserAuthentication() * @method ZfcUserAuthentication zfcUserAuthentication()
* @method AppInfos appInfos()
* @method Mail mail()
*
* @author Bertrand GAUTHIER <bertrand.gauthier at unicaen.fr> * @author Bertrand GAUTHIER <bertrand.gauthier at unicaen.fr>
*/ */
class AuthController extends AbstractActionController class AuthController extends AbstractActionController
...@@ -107,4 +115,114 @@ class AuthController extends AbstractActionController ...@@ -107,4 +115,114 @@ class AuthController extends AbstractActionController
throw new RuntimeException("Impossible d'écrire dans le storage"); 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 ...@@ -52,6 +52,12 @@ abstract class AbstractUser implements UserInterface, ProviderInterface
*/ */
protected $state; protected $state;
/**
* @var string
* @ORM\Column(type="string", length=256)
*/
protected $passwordResetToken;
/** /**
* @var Collection * @var Collection
* @ORM\ManyToMany(targetEntity="UnicaenAuth\Entity\Db\Role") * @ORM\ManyToMany(targetEntity="UnicaenAuth\Entity\Db\Role")
...@@ -202,6 +208,22 @@ abstract class AbstractUser implements UserInterface, ProviderInterface ...@@ -202,6 +208,22 @@ abstract class AbstractUser implements UserInterface, ProviderInterface
$this->state = $state; $this->state = $state;
} }
/**
* @return string
*/
public function getPasswordResetToken()
{
return $this->passwordResetToken;
}
/**
* @param string $passwordResetToken
*/
public function setPasswordResetToken($passwordResetToken = null)
{
$this->passwordResetToken = $passwordResetToken;
}
/** /**
* Get role. * Get role.
* *
......
...@@ -9,6 +9,13 @@ namespace UnicaenAuth\Options; ...@@ -9,6 +9,13 @@ namespace UnicaenAuth\Options;
*/ */
class ModuleOptions extends \ZfcUser\Options\ModuleOptions class ModuleOptions extends \ZfcUser\Options\ModuleOptions
{ {
/**
* Paramètres concernant l'authentification locale.
*
* @var array
*/
protected $local = [];
/** /**
* Paramètres concernant l'authentification LDAP. * Paramètres concernant l'authentification LDAP.
* *
...@@ -46,6 +53,25 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions ...@@ -46,6 +53,25 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions
*/ */
protected $entityManagerName = 'doctrine.entitymanager.orm_default'; 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. * Retourne les paramètres concernant l'authentification LDAP.
* *
......
...@@ -327,6 +327,9 @@ EOS; ...@@ -327,6 +327,9 @@ EOS;
if ($this->getShibbolethSimulate()) { if ($this->getShibbolethSimulate()) {
return '/'; return '/';
} }
if ($this->getAuthenticatedUser() === null) {
return '/';
}
$logoutRelativeUrl = '/Shibboleth.sso/Logout?return='; // NB: '?return=' semble obligatoire! $logoutRelativeUrl = '/Shibboleth.sso/Logout?return='; // NB: '?return=' semble obligatoire!
......
...@@ -2,24 +2,34 @@ ...@@ -2,24 +2,34 @@
namespace UnicaenAuth\Service; namespace UnicaenAuth\Service;
use UnicaenAuth\Event\UserAuthenticatedEvent; use DateTime;
use PDOException; use Doctrine\ORM\NoResultException;
use Ramsey\Uuid\Uuid;
use UnicaenApp\Entity\Ldap\People; use UnicaenApp\Entity\Ldap\People;
use UnicaenApp\Exception\RuntimeException; use UnicaenApp\Exception\RuntimeException;
use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper;
use UnicaenAuth\Entity\Shibboleth\ShibUser; use UnicaenAuth\Entity\Shibboleth\ShibUser;
use UnicaenAuth\Event\UserAuthenticatedEvent;
use UnicaenAuth\Options\ModuleOptions; use UnicaenAuth\Options\ModuleOptions;
use Zend\Crypt\Password\Bcrypt;
use Zend\EventManager\EventManagerAwareInterface; use Zend\EventManager\EventManagerAwareInterface;
use Zend\EventManager\EventManagerInterface; 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\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorAwareTrait; use Zend\ServiceManager\ServiceLocatorAwareTrait;
use Zend\Validator\Identical;
use ZfcUser\Entity\UserInterface; use ZfcUser\Entity\UserInterface;
use ZfcUser\Options\AuthenticationOptionsInterface; use ZfcUser\Options\AuthenticationOptionsInterface;
use ZfcUser\Options\ModuleOptions as ZfcUserModuleOptions; use ZfcUser\Options\ModuleOptions as ZfcUserModuleOptions;
use UnicaenAuth\Entity\Db\AbstractUser;
/** /**
* Service d'enregistrement dans la table des utilisateurs de l'application * Service traitant des utilisateurs locaux de l'application.
* de l'utilisateur authentifié avec succès.
* *
* @see \UnicaenAuth\Authentication\Adapter\AbstractFactory * @see \UnicaenAuth\Authentication\Adapter\AbstractFactory
* @author Unicaen * @author Unicaen
...@@ -45,15 +55,20 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface ...@@ -45,15 +55,20 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
*/ */
protected $zfcUserOptions; protected $zfcUserOptions;
/**
* @var UserMapper
*/
protected $userMapper;
/** /**
* @var LdapPeopleMapper * @var LdapPeopleMapper
*/ */
protected $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 * @return bool
*/ */
public function userAuthenticated($userData) public function userAuthenticated($userData)
...@@ -68,7 +83,6 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface ...@@ -68,7 +83,6 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
$email = $userData->getMail(); $email = $userData->getMail();
$password = 'ldap'; $password = 'ldap';
$state = in_array('deactivated', ldap_explode_dn($userData->getDn(), 1)) ? 0 : 1; $state = in_array('deactivated', ldap_explode_dn($userData->getDn(), 1)) ? 0 : 1;
break; break;
case $userData instanceof ShibUser: case $userData instanceof ShibUser:
$username = $userData->getUsername(); $username = $userData->getUsername();
...@@ -94,46 +108,42 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface ...@@ -94,46 +108,42 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface
throw new RuntimeException("Identité rencontrée inattendue."); throw new RuntimeException("Identité rencontrée inattendue.");
} }
// update/insert de l'utilisateur dans la table de l'appli $mapper = $this->getUserMapper();
$mapper = $this->getServiceLocator()->get('zfcuser_user_mapper'); /* @var $mapper \ZfcUserDoctrineORM\Mapper\User */
try {