Commit 76e25e7e authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Merge branch 'release-3.2.1'

parents f3344ee8 4d43239a
Pipeline #10056 passed with stage
in 17 seconds
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
fusion des configs
3.2.0
-----
- Configuration de la stratégie d'extraction d'un identifiant utile parmi les données d'authentification shibboleth
......
<?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).
*/
......@@ -197,19 +211,19 @@ $settings = [
/*
// domaine (ex: 'unicaen.fr') de l'EPPN (ex: hochonp@unicaen.fr')
'unicaen.fr' => [
[
'supannRefId' => [
// nom du 1er attribut recherché
'name' => 'supannRefId', // ex: '{OCTOPUS:ID}1234;{ISO15693}044D1AZE7A5P80'
// pattern éventuel pour extraire la partie intéressante
'preg_match_pattern' => '|\{OCTOPUS:ID\}(\d+)|', // ex: permet d'extraire '1234'
],
[
'supannEmpId' => [
// nom du 2e attribut recherché, si le 1er est introuvable
'name' => 'supannEmpId',
// pas de pattern donc valeur brute utilisée
'preg_match_pattern' => null,
],
[
'supannEtuId' => [
// nom du 3e attribut recherché, si le 2e est introuvable
'name' => 'supannEtuId',
],
......@@ -217,10 +231,10 @@ $settings = [
*/
// config de repli pour tous les autres domaines
'default' => [
[
'supannEmpId' => [
'name' => 'supannEmpId',
],
[
'supannEtuId' => [
'name' => 'supannEtuId',
],
],
......@@ -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,
],
......
......@@ -124,29 +124,29 @@ return [
'shib_user_id_extractor' => [
// domaine (ex: 'unicaen.fr') de l'EPPN (ex: hochonp@unicaen.fr')
// 'unicaen.fr' => [
// [
// 'supannRefId' => [
// // nom du 1er attribut recherché
// 'name' => 'supannRefId', // ex: '{OCTOPUS:ID}1234;{ISO15693}044D1AZE7A5P80'
// // pattern éventuel pour extraire la partie intéressante
// 'preg_match_pattern' => '|\{OCTOPUS:ID\}(\d+)|', // ex: permet d'extraire '1234'
// ],
// [
// 'supannEmpId' => [
// // nom du 2e attribut recherché
// 'name' => 'supannEmpId',
// // pas de pattern donc valeur brute utilisée
// 'preg_match_pattern' => null,
// ],
// [
// 'supannEtuId' => [
// // nom du 3e attribut recherché
// 'name' => 'supannEtuId',
// ],
// ],
// config de repli pour tous les autres domaines
'default' => [
[
'supannEmpId' => [
'name' => 'supannEmpId',
],
[
'supannEtuId' => [
'name' => 'supannEtuId',
],
],
......
......@@ -6,7 +6,8 @@ CREATE TABLE IF NOT EXISTS user
display_name VARCHAR(64) DEFAULT NULL,
password VARCHAR(128) NOT NULL,
state SMALLINT default 1,
last_role_id INTEGER default null
last_role_id INTEGER default null,
created_on DATE default current_date not null
);
ALTER TABLE user ADD PASSWORD_RESET_TOKEN varchar(256) DEFAULT NULL;
CREATE UNIQUE INDEX user_unique_username ON user(username);
......
......@@ -6,6 +6,7 @@ CREATE TABLE user (
password VARCHAR(128) NOT NULL,
state SMALLINT default 1,
last_role_id INT(11) default null,
created_on DATE default now() not null,
PRIMARY KEY (id),
UNIQUE INDEX unique_username (username ASC)
) ENGINE=InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_unicode_ci;
......
......@@ -6,6 +6,7 @@ CREATE TABLE "USER"
"PASSWORD" VARCHAR2(128 CHAR) NOT NULL ENABLE,
"STATE" SMALLINT DEFAULT 1 NOT NULL ENABLE,
"last_role_id" NUMBER(*,0),
created_on DATE default sysdate not null,
CONSTRAINT "USER_PK" PRIMARY KEY ("ID"),
CONSTRAINT "USER_USERNAME_UN" UNIQUE ("USERNAME"),
CONSTRAINT "USER_LAST_ROLE_FK" FOREIGN KEY ("last_role_id") REFERENCES USER_ROLE ("ID") ENABLE
......@@ -64,6 +65,24 @@ INSERT INTO USER_ROLE_LINKER(user_id, role_id)
SELECT u.id, r.id FROM "USER" u, user_role r WHERE u.username = 'demo' and r.role_id = 'Standard';
CREATE TABLE USER_TOKEN
(
ID NUMBER(*, 0) NOT NULL,
USER_ID NUMBER(*,0) NOT NULL ENABLE,
TOKEN VARCHAR2(256) NOT NULL,
ACTION VARCHAR2(256) NOT NULL,
NB_ACTIONS smallint default 0 NOT NULL,
NB_ACTIONS_MAX smallint default 1 NOT NULL,
created_on DATE default sysdate not null,
expired_on DATE not null,
last_used_on DATE,
CONSTRAINT USER_TOKEN_PK PRIMARY KEY (ID),
CONSTRAINT USER_TOKEN_USER_FK UNIQUE (USER_ID)
);
CREATE INDEX USER_TOKEN_USER_IDX ON USER_TOKEN (USER_ID);
CREATE SEQUENCE USER_TOKEN_ID_SEQ;
/**
* Privilèges
......
......@@ -6,6 +6,7 @@ CREATE TABLE "user" (
password VARCHAR(128) NOT NULL,
state SMALLINT default 1,
last_role_id SMALLINT,
created_on DATE default current_timestamp not null,
FOREIGN KEY (last_role_id) REFERENCES user_role (id) ON DELETE SET NULL
) ;
CREATE UNIQUE INDEX user_username_unique ON "user" (username);
......
<?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) {