Commit 4d43239a authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Modifications/améliorations pour faciliter le support d'autres modes...

Modifications/améliorations pour faciliter le support d'autres modes d'authentification (ex: unicaen/auth-token).
parent 5958fd68
Pipeline #10054 passed with stage
in 1 minute and 4 seconds
......@@ -3,8 +3,10 @@ CHANGELOG
3.2.1
-----
- Modifications/améliorations pour faciliter le support d'autres modes d'authentification (ex: unicaen/auth-token).
- Le type d'authentification souhaité (local, shib ou cas) peut être spécifié dans l'URL de redirection via le
query param 'authtype'
- Ajout de la colonne CREATED_AT dans les scripts SQL de création de la table USER (non mappée dans l'entité).
- [FIX] Usurpation d'un compte local (db) depuis une authentification shib
- [FIX] Une chaîne vide doit être considérée comme null dans ShibService::extractShibUserIdValueForDomainFromShibData()
- [FIX] Nécessité de clés littérales dans la config par domaine de 'shib_user_id_extractor' sinon doublons lors de la
......
<?php
namespace UnicaenAuth;
use UnicaenAuth\Authentication\Adapter\AdapterChainServiceFactory;
use UnicaenAuth\Authentication\Adapter\Cas;
use UnicaenAuth\Authentication\Adapter\CasAdapterFactory;
......@@ -18,6 +20,7 @@ use UnicaenAuth\Authentication\Storage\LdapFactory;
use UnicaenAuth\Authentication\Storage\ShibFactory;
use UnicaenAuth\Authentication\Storage\Usurpation;
use UnicaenAuth\Authentication\Storage\UsurpationFactory;
use UnicaenAuth\Controller\AuthController;
use UnicaenAuth\Controller\AuthControllerFactory;
use UnicaenAuth\Controller\DroitsControllerFactory;
use UnicaenAuth\Controller\UtilisateurControllerFactory;
......@@ -30,12 +33,14 @@ use UnicaenAuth\Form\ShibLoginForm;
use UnicaenAuth\Form\ShibLoginFormFactory;
use UnicaenAuth\Guard\PrivilegeControllerFactory;
use UnicaenAuth\Guard\PrivilegeRouteFactory;
use UnicaenAuth\Options\ModuleOptions;
use UnicaenAuth\ORM\Event\Listeners\HistoriqueListenerFactory;
use UnicaenAuth\Provider\Rule\PrivilegeRuleProviderFactory;
use UnicaenAuth\Service\CasService;
use UnicaenAuth\Service\CasServiceFactory;
use UnicaenAuth\Service\ShibService;
use UnicaenAuth\Service\ShibServiceFactory;
use UnicaenAuth\Service\UserContext;
use UnicaenAuth\Service\UserContextFactory;
use UnicaenAuth\Service\UserFactory;
use UnicaenAuth\Service\UserMapperFactory;
......@@ -70,6 +75,15 @@ use Zend\Authentication\AuthenticationService;
use Zend\ServiceManager\Proxy\LazyServiceFactory;
$settings = [
/**
* Tous les types d'authentification supportés par le module unicaen/auth.
*/
'auth_types' => [
'local', // càd 'ldap' et 'db'
'cas',
'shib',
],
/**
* Configuration de l'authentification centralisée (CAS).
*/
......@@ -629,21 +643,22 @@ return [
// in /var/www/sygal/module/Application/src/Application/Controller/UtilisateurController.php on line 34
'service_manager' => [
'aliases' => [
'unicaen-auth_module_options' => ModuleOptions::class,
'zfcuser_login_form' => LoginForm::class,
'Zend\Authentication\AuthenticationService' => 'zfcuser_auth_service',
'UnicaenAuth\Privilege\PrivilegeProvider' => 'UnicaenAuth\Service\Privilege',
'\UnicaenAuth\Guard\PrivilegeController' => 'UnicaenAuth\Guard\PrivilegeController',
'unicaen-auth_user_service' => 'UnicaenAuth\Service\User', // pour la compatibilité
'authUserContext' => 'UnicaenAuth\Service\UserContext', // pour la compatibilité
'AuthUserContext' => 'UnicaenAuth\Service\UserContext', // pour la compatibilité
'authUserContext' => UserContext::class, // pour la compatibilité
'AuthUserContext' => UserContext::class, // pour la compatibilité
],
'invokables' => [
'UnicaenAuth\View\RedirectionStrategy' => 'UnicaenAuth\View\RedirectionStrategy',
'UnicaenAuth\Service\CategoriePrivilege' => 'UnicaenAuth\Service\CategoriePrivilegeService',
],
'factories' => [
'unicaen-auth_module_options' => 'UnicaenAuth\Options\ModuleOptionsFactory',
ModuleOptions::class => 'UnicaenAuth\Options\ModuleOptionsFactory',
'zfcuser_auth_service' => 'UnicaenAuth\Authentication\AuthenticationServiceFactory',
'UnicaenAuth\Authentication\Storage\Chain' => 'UnicaenAuth\Authentication\Storage\ChainServiceFactory',
'UnicaenAuth\Provider\Identity\Chain' => 'UnicaenAuth\Provider\Identity\ChainServiceFactory',
......@@ -659,7 +674,7 @@ return [
'zfcuser_redirect_callback' => 'UnicaenAuth\Authentication\RedirectCallbackFactory', // substituion
CasService::class => CasServiceFactory::class,
ShibService::class => ShibServiceFactory::class,
'UnicaenAuth\Service\UserContext' => UserContextFactory::class,
UserContext::class => UserContextFactory::class,
'zfcuser_user_mapper' => UserMapperFactory::class,
'MouchardCompleterAuth' => 'UnicaenAuth\Mouchard\MouchardCompleterAuthFactory',
LocalAdapter::class => LocalAdapterFactory::class,
......@@ -708,10 +723,11 @@ return [
],
'controllers' => [
'invokables' => [
'aliases' => [
'UnicaenAuth\Controller\Auth' => AuthController::class,
],
'factories' => [
'UnicaenAuth\Controller\Auth' => AuthControllerFactory::class,
AuthController::class => AuthControllerFactory::class,
'UnicaenAuth\Controller\Utilisateur' => UtilisateurControllerFactory::class,
'UnicaenAuth\Controller\Droits' => DroitsControllerFactory::class,
],
......
<?php
namespace UnicaenAuth\Authentication\Adapter;
use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait;
use Zend\Authentication\Result as AuthenticationResult;
use Zend\EventManager\EventInterface;
use Zend\Session\Container as SessionContainer;
use ZfcUser\Authentication\Adapter\AdapterChainEvent;
use ZfcUser\Entity\UserInterface;
use ZfcUser\Mapper\UserInterface as UserMapperInterface;
/**
* Classe abstraite des adpater d'authentification à partir de la base de données.
*
* Ajout par rapport à la classe mère : si aucune base de données ou table n'existe,
* l'authentification ne plante pas (i.e. renvoit false).
*
* @author Bertrand GAUTHIER <bertrand.gauthier@unicaen.fr>
*/
abstract class AbstractDb extends AbstractAdapter
{
use ModuleOptionsAwareTrait;
/**
* @var string
*/
protected $type;
/**
* @var AdapterChainEvent
*/
protected $event;
/**
* @var UserMapperInterface
*/
protected $mapper;
/**
* @inheritDoc
*/
public function authenticate(EventInterface $e): bool
{
// NB: Dans la version 3.0.0 de zf-commons/zfc-user, cette méthode prend un EventInterface.
// Mais dans la branche 3.x, c'est un AdapterChainEvent !
// Si un jour c'est un AdapterChainEvent qui est attendu, plus besoin de faire $e->getTarget().
$this->event = $e->getTarget();
if ($this->event->getIdentity()) {
return true;
}
if ($this->isSatisfied()) {
$storage = $this->getStorage()->read();
$this->event
->setIdentity($storage['identity'])
->setCode(AuthenticationResult::SUCCESS)
->setMessages(array('Authentication successful.'));
return true;
}
$userObject = $this->fetchUserObject();
if ($userObject === null) {
return false;
}
if ($this->moduleOptions->getEnableUserState()) {
// Don't allow user to login if state is not in allowed list
if (!in_array($userObject->getState(), $this->moduleOptions->getAllowedLoginStates())) {
$this->event
->setCode(AuthenticationResult::FAILURE_UNCATEGORIZED)
->setMessages(["Ce compte utilisateur a été désactivé"]);
$this->setSatisfied(false);
return false;
}
}
$result = $this->authenticateUserObject($userObject);
if ($result === false) {
return false;
}
// regen the id
$session = new SessionContainer($this->getStorage()->getNamespace());
$session->getManager()->regenerateId();
// Success!
$identity = $this->createSessionIdentity($userObject->getUsername());
$this->event->setIdentity($identity);
$this->setSatisfied(true);
$storage = $this->getStorage()->read();
$storage['identity'] = $this->event->getIdentity();
$this->getStorage()->write($storage);
$this->event
->setCode(AuthenticationResult::SUCCESS)
->setMessages(array('Authentication successful.'));
return true;
}
/**
* @return \ZfcUser\Entity\UserInterface|null
*/
abstract protected function fetchUserObject(): ?UserInterface;
/**
* @param \ZfcUser\Entity\UserInterface $userObject
* @return bool
*/
abstract protected function authenticateUserObject(UserInterface $userObject): bool;
/**
* setMapper
*
* @param UserMapperInterface $mapper
* @return self
*/
public function setMapper(UserMapperInterface $mapper): self
{
$this->mapper = $mapper;
return $this;
}
}
\ No newline at end of file
<?php
namespace UnicaenAuth\Authentication\Adapter;
use Interop\Container\ContainerInterface;
......@@ -7,19 +8,19 @@ use ZfcUser\Options\ModuleOptions;
class AdapterChainServiceFactory
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
public function __invoke(ContainerInterface $container, $requestedName, array $options = null): AdapterChain
{
$chain = new AdapterChain();
$options = $this->getOptions($container);
$enabledTypes = array_keys($options->getEnabledAuthTypes()); // types d'auth activés
//iterate and attach multiple adapters and events if offered
// on attache chaque adapter uniquement s'il est activé
foreach ($options->getAuthAdapters() as $priority => $adapterName) {
/** @var AbstractAdapter $adapter */
$adapter = $container->get($adapterName);
if (in_array($adapter->getType(), $enabledTypes)) {
$adapter->attach($chain->getEventManager());
$adapter->attach($chain->getEventManager(), $priority);
}
}
......@@ -35,9 +36,9 @@ class AdapterChainServiceFactory
* set options
*
* @param ModuleOptions $options
* @return AdapterChainServiceFactory
* @return self
*/
public function setOptions(ModuleOptions $options)
public function setOptions(ModuleOptions $options): self
{
$this->options = $options;
return $this;
......@@ -49,7 +50,7 @@ class AdapterChainServiceFactory
* @param ContainerInterface|null $container (optional) Service Locator
* @return ModuleOptions $options
*/
public function getOptions(ContainerInterface $container = null)
public function getOptions(ContainerInterface $container = null): ModuleOptions
{
if (!$this->options) {
if (!$container) {
......
......@@ -98,10 +98,6 @@ class Cas extends AbstractAdapter
return true;
}
if (! $this->isEnabled()) {
return false;
}
error_reporting($oldErrorReporting = error_reporting() & ~E_NOTICE);
$this->getCasClient()->forceAuthentication();
......@@ -131,23 +127,6 @@ class Cas extends AbstractAdapter
return true;
}
/**
* @return bool
*/
protected function isEnabled(): bool
{
$config = $this->moduleOptions->getCas();
if (! $config) {
return false;
}
if (isset($config['enabled'])) {
return (bool) $config['enabled'];
}
return true;
}
/**
* @inheritDoc
*/
......@@ -155,10 +134,6 @@ class Cas extends AbstractAdapter
{
parent::logout($e);
if (! $this->isEnabled()) {
return;
}
$storage = $this->getStorage()->read();
if (! isset($storage['identity'])) {
return;
......@@ -247,9 +222,6 @@ class Cas extends AbstractAdapter
*/
public function reconfigureRoutesForCasAuth(RouteInterface $router)
{
if (! $this->isEnabled()) {
return;
}
if(!$router instanceof RouteStackInterface) {
return;
}
......
......@@ -2,15 +2,9 @@
namespace UnicaenAuth\Authentication\Adapter;
use UnicaenAuth\Options\ModuleOptions;
use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait;
use Zend\Authentication\Result as AuthenticationResult;
use Zend\Crypt\Password\Bcrypt;
use Zend\EventManager\EventInterface;
use Zend\Session\Container as SessionContainer;
use ZfcUser\Authentication\Adapter\AdapterChainEvent;
use ZfcUser\Entity\UserInterface;
use ZfcUser\Mapper\UserInterface as UserMapperInterface;
/**
* Adpater d'authentification à partir de la base de données.
......@@ -20,67 +14,29 @@ use ZfcUser\Mapper\UserInterface as UserMapperInterface;
*
* @author Bertrand GAUTHIER <bertrand.gauthier@unicaen.fr>
*/
class Db extends AbstractAdapter
class Db extends AbstractDb
{
use ModuleOptionsAwareTrait;
const TYPE = 'db';
/**
* @var string
*/
protected $type = self::TYPE;
/**
* @var UserMapperInterface
*/
protected $mapper;
/**
* @var callable
*/
protected $credentialPreprocessor;
/**
* @var ModuleOptions
*/
protected $options;
/**
* @inheritDoc
* @return \ZfcUser\Entity\UserInterface|null
*/
public function authenticate(EventInterface $e): bool
protected function fetchUserObject(): ?UserInterface
{
// NB: Dans la version 3.0.0 de zf-commons/zfc-user, cette méthode prend un EventInterface.
// Mais dans la branche 3.x, c'est un AdapterChainEvent !
// Si un jour c'est un AdapterChainEvent qui est attendu, plus besoin de faire $e->getTarget().
$event = $e->getTarget(); /* @var $event AdapterChainEvent */
if ($event->getIdentity()) {
return true;
}
if ($this->isSatisfied()) {
$storage = $this->getStorage()->read();
$event
->setIdentity($storage['identity'])
->setCode(AuthenticationResult::SUCCESS)
->setMessages(array('Authentication successful.'));
return true;
}
if (! $this->isEnabled()) {
return false;
}
$identity = $this->event->getRequest()->getPost()->get('identity');
$identity = $event->getRequest()->getPost()->get('identity');
$credential = $event->getRequest()->getPost()->get('credential');
$credential = $this->preProcessCredential($credential);
/** @var UserInterface|null $userObject */
$userObject = null;
// Cycle through the configured identity sources and test each
$fields = $this->options->getAuthIdentityFields();
$fields = $this->moduleOptions->getAuthIdentityFields();
while (!is_object($userObject) && count($fields) > 0) {
$mode = array_shift($fields);
switch ($mode) {
......@@ -94,69 +50,45 @@ class Db extends AbstractAdapter
}
if (!$userObject) {
$event
$this->event
->setCode(AuthenticationResult::FAILURE_IDENTITY_NOT_FOUND)
->setMessages(array('A record with the supplied identity could not be found.'));
->setMessages([]); // NB: ne pas préciser la cause
$this->setSatisfied(false);
return false;
}
if ($this->options->getEnableUserState()) {
// Don't allow user to login if state is not in allowed list
if (!in_array($userObject->getState(), $this->options->getAllowedLoginStates())) {
$event
->setCode(AuthenticationResult::FAILURE_UNCATEGORIZED)
->setMessages(array('A record with the supplied identity is not active.'));
$this->setSatisfied(false);
return false;
}
return null;
}
return $userObject;
}
/**
* @param \ZfcUser\Entity\UserInterface $userObject
* @return bool
*/
protected function authenticateUserObject(UserInterface $userObject): bool
{
$credential = $this->event->getRequest()->getPost()->get('credential');
$credential = $this->preProcessCredential($credential);
$bcrypt = new Bcrypt();
$bcrypt->setCost($this->options->getPasswordCost());
if (!$bcrypt->verify($credential, $userObject->getPassword())) {
// Password does not match
$event
$bcrypt->setCost($this->moduleOptions->getPasswordCost());
$ok = $bcrypt->verify($credential, $userObject->getPassword());
if (!$ok) {
$this->event
->setCode(AuthenticationResult::FAILURE_CREDENTIAL_INVALID)
->setMessages(array('Supplied credential is invalid.'));
->setMessages([]); // NB: ne pas préciser la cause
$this->setSatisfied(false);
return false;
}
// regen the id
$session = new SessionContainer($this->getStorage()->getNamespace());
$session->getManager()->regenerateId();
// Success!
$identity = $this->createSessionIdentity($userObject->getUsername());
$event->setIdentity($identity);
// Update user's password hash if the cost parameter has changed
$this->updateUserPasswordHash($userObject, $credential, $bcrypt);
$this->setSatisfied(true);
$storage = $this->getStorage()->read();
$storage['identity'] = $event->getIdentity();
$this->getStorage()->write($storage);
$event
->setCode(AuthenticationResult::SUCCESS)
->setMessages(array('Authentication successful.'));
return true;
}
/**
* @return bool
*/
protected function isEnabled(): bool
{
$config = $this->moduleOptions->getDb();
if (isset($config['enabled'])) {
return (bool) $config['enabled'];
}
return false;
}
protected function updateUserPasswordHash(UserInterface $userObject, $password, Bcrypt $bcrypt): self
{
$hash = explode('$', $userObject->getPassword());
......@@ -179,19 +111,6 @@ class Db extends AbstractAdapter
return $credential;
}
/**
* setMapper
*
* @param UserMapperInterface $mapper
* @return self
*/
public function setMapper(UserMapperInterface $mapper): self
{
$this->mapper = $mapper;
return $this;
}
/**
* Get credentialPreprocessor.
*
......@@ -213,22 +132,4 @@ class Db extends AbstractAdapter
$this->credentialPreprocessor = $credentialPreprocessor;
return $this;
}
/**
* @param \ZfcUser\Options\ModuleOptions $options
* @return self
*/
public function setOptions(\ZfcUser\Options\ModuleOptions $options): self
{
$this->options = $options;
return $this;
}
/**
* @return ModuleOptions
*/
protected function getOptions(): ModuleOptions
{
return $this->options;
}
}
\ No newline at end of file
......@@ -5,18 +5,15 @@ namespace UnicaenAuth\Authentication\Adapter;
use Interop\Container\ContainerInterface;
use UnicaenAuth\Options\ModuleOptions;
use Zend\Authentication\Storage\Session;
use Zend\EventManager\EventManagerAwareInterface;
use ZfcUser\Mapper\UserInterface as UserMapperInterface;
class DbAdapterFactory
{
/**
* @param ContainerInterface $container
* @param string $requestedName
* @param array|null $options
* @return mixed|Cas|Db|Ldap|EventManagerAwareInterface
* @return Db
*/
public function __invoke(ContainerInterface $container, string $requestedName, array $options = null)
public function __invoke(ContainerInterface $container): Db
{
/** @var UserMapperInterface $userMapper */
$userMapper = $container->get('zfcuser_user_mapper');
......@@ -30,7 +27,6 @@ class DbAdapterFactory
$container->get('unicaen-auth_module_options')->toArray());
$moduleOptions = new ModuleOptions($options);
$adapter->setModuleOptions($moduleOptions);
$adapter->setOptions($moduleOptions);
$substitut = $moduleOptions->getDb()['type'] ?? null;
if ($substitut !== null) {
......
......@@ -103,10 +103,6 @@ class Ldap extends AbstractAdapter implements EventManagerAwareInterface
return true;
}
if (! $this->isEnabled()) {
return false;
}
$username = $event->getRequest()->getPost()->get('identity');
$credential = $event->getRequest()->getPost()->get('credential');
......@@ -122,7 +118,7 @@ class Ldap extends AbstractAdapter implements EventManagerAwareInterface
if (! $success) {
$event
->setCode(AuthenticationResult::FAILURE)
->setMessages(['LDAP bind failed.']);
->setMessages([/*'LDAP bind failed.'*/]);
$this->setSatisfied(false);
return false;