From cf1ec0476779ea7aaa1da9fddc454b1cecd3926a Mon Sep 17 00:00:00 2001 From: Bertrand GAUTHIER <bertrand.gauthier@unicaen.fr> Date: Fri, 4 Dec 2020 12:25:35 +0100 Subject: [PATCH] =?UTF-8?q?Poursuite=20du=20typage=20des=20authentificatio?= =?UTF-8?q?ns=20=20=20-=20Chaque=20adapter=20peut=20d=C3=A9sormais=20teste?= =?UTF-8?q?r=20s'il=20est=20comp=C3=A9tent=20pour=20traiter=20la=20requ?= =?UTF-8?q?=C3=AAte=20d'authentification.=20=20=20-=20Cr=C3=A9ation=20d'un?= =?UTF-8?q?=20adapter=20d'authentification=20comme=20les=20autres=20pour?= =?UTF-8?q?=20Shib.=20=20=20-=20Pages=20de=20connexion=20diff=C3=A9rentes?= =?UTF-8?q?=20selon=20le=20type=20d'authentification=20:=20shib=20;=20db?= =?UTF-8?q?=20ou=20ldap=20;=20cas.=20=20=20-=20Possibilit=C3=A9=20d'ordonn?= =?UTF-8?q?er=20les=20formulaires=20de=20connexion=20propos=C3=A9s=20(conf?= =?UTF-8?q?ig).=20=20=20-=20Possibilit=C3=A9=20d'ajouter=20une=20descripti?= =?UTF-8?q?on=20HTML=20=C3=A0=20chaque=20formulaire=20de=20connexion=20(co?= =?UTF-8?q?nfig).=20R=C3=A9paration=20du=20m=C3=A9canisme=20de=20redirecti?= =?UTF-8?q?on=20vers=20l'URL=20demand=C3=A9e=20avant=20connexion.=20Correc?= =?UTF-8?q?tion=20du=20bug=20de=20r=C3=A9manence=20de=20l'authentification?= =?UTF-8?q?=20shibboleth=20simul=C3=A9e.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 13 +- Module.php | 45 --- autoload_classmap.php | 88 ------ config/module.config.php | 173 ++++++++++-- config/unicaen-auth.global.php.dist | 41 --- config/unicaen-auth.local.php.dist | 91 +++++- doc/authentification.md | 47 +++- doc/configuration.md | 47 ++-- .../Adapter/AbstractAdapter.php | 82 ++++++ .../Adapter/AbstractFactory.php | 129 --------- .../Authentication/Adapter/AdapterChain.php | 79 ++++++ .../Adapter/AdapterChainServiceFactory.php | 70 +++++ .../Authentication/Adapter/Cas.php | 110 +++----- .../Adapter/CasAdapterFactory.php | 67 +++++ src/UnicaenAuth/Authentication/Adapter/Db.php | 213 +++++++++++++- .../Adapter/DbAdapterFactory.php | 33 +++ .../Authentication/Adapter/Ldap.php | 42 ++- .../Adapter/LdapAdapterFactory.php | 67 +++++ .../Authentication/Adapter/Shib.php | 179 ++++++++++++ .../Adapter/ShibAdapterFactory.php | 60 ++++ src/UnicaenAuth/Authentication/Storage/Db.php | 19 +- .../Authentication/Storage/DbFactory.php | 25 +- .../Authentication/Storage/Ldap.php | 52 ++-- .../Authentication/Storage/LdapFactory.php | 20 +- .../Authentication/Storage/Shib.php | 36 ++- .../Authentication/Storage/ShibFactory.php | 24 +- src/UnicaenAuth/Controller/AuthController.php | 253 +++++++++++++---- .../Controller/AuthControllerFactory.php | 32 +++ src/UnicaenAuth/Form/CasLoginForm.php | 58 ++++ src/UnicaenAuth/Form/CasLoginFormFactory.php | 16 ++ src/UnicaenAuth/Form/ShibLoginForm.php | 58 ++++ src/UnicaenAuth/Form/ShibLoginFormFactory.php | 16 ++ src/UnicaenAuth/Options/ModuleOptions.php | 82 +++++- .../Options/ModuleOptionsFactory.php | 32 +-- src/UnicaenAuth/Service/CasService.php | 264 ++++++++++++++++++ src/UnicaenAuth/Service/CasServiceFactory.php | 34 +++ src/UnicaenAuth/Service/ShibService.php | 12 +- .../Service/ShibServiceFactory.php | 2 +- .../Service/Traits/CasServiceAwareTrait.php | 21 ++ src/UnicaenAuth/Service/User.php | 1 - src/UnicaenAuth/Service/UserContext.php | 55 ++-- .../View/Helper/AbstractConnectViewHelper.php | 174 ++++++++++++ .../View/Helper/CasConnectViewHelper.php | 21 ++ .../Helper/CasConnectViewHelperFactory.php | 29 ++ .../View/Helper/ConnectViewHelper.php | 38 +++ .../View/Helper/DbConnectViewHelper.php | 21 ++ .../Helper/DbConnectViewHelperFactory.php | 30 ++ .../View/Helper/LdapConnectViewHelper.php | 66 +---- .../Helper/LdapConnectViewHelperFactory.php | 8 +- .../View/Helper/LocalConnectViewHelper.php | 62 +--- .../Helper/LocalConnectViewHelperFactory.php | 10 +- .../View/Helper/ShibConnectViewHelper.php | 35 +-- .../Helper/ShibConnectViewHelperFactory.php | 13 +- src/UnicaenAuth/View/Helper/UserCurrent.php | 8 +- .../View/Helper/partial/connect.phtml | 55 ++-- .../View/Helper/partial/form.phtml | 53 ++++ tests/config/autoload/unicaen-auth.local.php | 5 +- view/unicaen-auth/auth/login-tabs.phtml | 69 +++++ view/unicaen-auth/auth/login.phtml | 42 +++ view/unicaen-auth/auth/shibboleth.phtml | 4 +- view/zfc-user/user/login.phtml | 66 ----- 61 files changed, 2686 insertions(+), 911 deletions(-) delete mode 100644 autoload_classmap.php create mode 100644 src/UnicaenAuth/Authentication/Adapter/AbstractAdapter.php delete mode 100644 src/UnicaenAuth/Authentication/Adapter/AbstractFactory.php create mode 100644 src/UnicaenAuth/Authentication/Adapter/AdapterChain.php create mode 100644 src/UnicaenAuth/Authentication/Adapter/AdapterChainServiceFactory.php create mode 100644 src/UnicaenAuth/Authentication/Adapter/CasAdapterFactory.php create mode 100644 src/UnicaenAuth/Authentication/Adapter/DbAdapterFactory.php create mode 100644 src/UnicaenAuth/Authentication/Adapter/LdapAdapterFactory.php create mode 100644 src/UnicaenAuth/Authentication/Adapter/Shib.php create mode 100644 src/UnicaenAuth/Authentication/Adapter/ShibAdapterFactory.php create mode 100644 src/UnicaenAuth/Form/CasLoginForm.php create mode 100644 src/UnicaenAuth/Form/CasLoginFormFactory.php create mode 100644 src/UnicaenAuth/Form/ShibLoginForm.php create mode 100644 src/UnicaenAuth/Form/ShibLoginFormFactory.php create mode 100644 src/UnicaenAuth/Service/CasService.php create mode 100644 src/UnicaenAuth/Service/CasServiceFactory.php create mode 100644 src/UnicaenAuth/Service/Traits/CasServiceAwareTrait.php create mode 100644 src/UnicaenAuth/View/Helper/AbstractConnectViewHelper.php create mode 100644 src/UnicaenAuth/View/Helper/CasConnectViewHelper.php create mode 100644 src/UnicaenAuth/View/Helper/CasConnectViewHelperFactory.php create mode 100644 src/UnicaenAuth/View/Helper/ConnectViewHelper.php create mode 100644 src/UnicaenAuth/View/Helper/DbConnectViewHelper.php create mode 100644 src/UnicaenAuth/View/Helper/DbConnectViewHelperFactory.php create mode 100644 src/UnicaenAuth/View/Helper/partial/form.phtml create mode 100644 view/unicaen-auth/auth/login-tabs.phtml create mode 100644 view/unicaen-auth/auth/login.phtml delete mode 100644 view/zfc-user/user/login.phtml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f9fe88..3cbcd15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,4 +16,15 @@ Première version officielle sous ZF3. 3.0.12 (05/11/2020) ------------------- -- Ajout d'une méthode pour pouvoir purger la liste des rôles courante. \ No newline at end of file +- Ajout d'une méthode pour pouvoir purger la liste des rôles courante. + +3.1.0 +----- +- Poursuite du typage des authentifications + - Chaque adapter peut désormais tester s'il est compétent pour traiter la requête d'authentification. + - Création d'un adapter d'authentification comme les autres pour Shib. + - Pages de connexion différentes selon le type d'authentification : shib ; db ou ldap ; cas. + - Possibilité d'ordonner les formulaires de connexion proposés (config). + - Possibilité d'ajouter une description HTML à chaque formulaire de connexion (config). +- Réparation du mécanisme de redirection vers l'URL demandée avant connexion. +- Correction du bug de rémanence de l'authentification shibboleth simulée. diff --git a/Module.php b/Module.php index 30c0f05..9a44a43 100644 --- a/Module.php +++ b/Module.php @@ -2,16 +2,10 @@ namespace UnicaenAuth; -use UnicaenAuth\Authentication\Adapter\Cas as CasAdapter; -use UnicaenAuth\Options\ModuleOptions; -use UnicaenAuth\Service\ShibService; use Zend\EventManager\EventInterface; use Zend\ModuleManager\Feature\AutoloaderProviderInterface; use Zend\ModuleManager\Feature\ConfigProviderInterface; use Zend\ModuleManager\Feature\ServiceProviderInterface; -use Zend\ModuleManager\ModuleManager; -use Zend\ServiceManager\ServiceLocatorInterface; -use Zend\View\Helper\Navigation; use ZfcUser\Form\Login; use ZfcUser\Form\LoginFilter; @@ -22,11 +16,6 @@ use ZfcUser\Form\LoginFilter; */ class Module implements AutoloaderProviderInterface, ConfigProviderInterface, ServiceProviderInterface { - /** - * @var ModuleOptions - */ - private $options; - /** * @return array * @see ConfigProviderInterface @@ -64,41 +53,7 @@ class Module implements AutoloaderProviderInterface, ConfigProviderInterface, Se */ public function onBootstrap(EventInterface $e) { - /* @var \Zend\Mvc\MvcEvent $e */ - $application = $e->getApplication(); - /* @var $services \Zend\ServiceManager\ServiceManager */ - $services = $application->getServiceManager(); - - /* @var $options ModuleOptions */ - $this->options = $services->get('unicaen-auth_module_options'); - - $this->reconfigureRoutesForAuth($services); - } - - /** - * @param ServiceLocatorInterface $sl - */ - private function reconfigureRoutesForAuth(ServiceLocatorInterface $sl) - { - /* @var $router \Zend\Router\Http\TreeRouteStack */ - $router = $sl->get('router'); - - // si l'auth CAS est activée, modif de la route de connexion pour zapper le formulaire d'auth maison. - $isCasEnable = (bool) $this->options->getCas(); - if ($isCasEnable && php_sapi_name() !== 'cli') { - /** @var CasAdapter $casAdapter */ - $casAdapter = $sl->get('UnicaenAuth\Authentication\Adapter\Cas'); - $casAdapter->reconfigureRoutesForCasAuth($router); - } - // si l'auth Shibboleth est activée, modif de la route de déconnexion pour réaliser la déconnexion Shibboleth. - $shibOptions = $this->options->getShibboleth(); - $isShibEnable = array_key_exists('enable', $shibOptions) && (bool) $shibOptions['enable']; - if ($isShibEnable && php_sapi_name() !== 'cli') { - /** @var ShibService $shibService */ - $shibService = $sl->get(ShibService::class); - $shibService->reconfigureRoutesForShibAuth($router); - } } /** diff --git a/autoload_classmap.php b/autoload_classmap.php deleted file mode 100644 index 77c1f29..0000000 --- a/autoload_classmap.php +++ /dev/null @@ -1,88 +0,0 @@ -<?php -// Generated by ZF2's ./bin/classmap_generator.php -return array( - 'UnicaenAuth\Module' => __DIR__ . '/Module.php', - 'UnicaenAuth\Guard\PrivilegeController' => __DIR__ . '/src/UnicaenAuth/Guard/PrivilegeController.php', - 'UnicaenAuth\Options\AuthenticationOptionsInterface' => __DIR__ . '/src/UnicaenAuth/Options/AuthenticationOptionsInterface.php', - 'UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait' => __DIR__ . '/src/UnicaenAuth/Options/Traits/ModuleOptionsAwareTrait.php', - 'UnicaenAuth\Options\ModuleOptionsFactory' => __DIR__ . '/src/UnicaenAuth/Options/ModuleOptionsFactory.php', - 'UnicaenAuth\Options\ModuleOptions' => __DIR__ . '/src/UnicaenAuth/Options/ModuleOptions.php', - 'UnicaenAuth\Entity\Db\CategoriePrivilege' => __DIR__ . '/src/UnicaenAuth/Entity/Db/CategoriePrivilege.php', - 'UnicaenAuth\Entity\Db\User' => __DIR__ . '/src/UnicaenAuth/Entity/Db/User.php', - 'UnicaenAuth\Entity\Db\Privilege' => __DIR__ . '/src/UnicaenAuth/Entity/Db/Privilege.php', - 'UnicaenAuth\Entity\Db\Role' => __DIR__ . '/src/UnicaenAuth/Entity/Db/Role.php', - 'UnicaenAuth\Entity\Db\AbstractUser' => __DIR__ . '/src/UnicaenAuth/Entity/Db/AbstractUser.php', - 'UnicaenAuth\Entity\Ldap\People' => __DIR__ . '/src/UnicaenAuth/Entity/Ldap/People.php', - 'UnicaenAuth\Service\LdapUserAwareInterface' => __DIR__ . '/src/UnicaenAuth/Service/LdapUserAwareInterface.php', - 'UnicaenAuth\Service\AuthorizeService' => __DIR__ . '/src/UnicaenAuth/Service/AuthorizeService.php', - 'UnicaenAuth\Service\DbUserAwareInterface' => __DIR__ . '/src/UnicaenAuth/Service/DbUserAwareInterface.php', - 'UnicaenAuth\Service\User' => __DIR__ . '/src/UnicaenAuth/Service/User.php', - 'UnicaenAuth\Service\Traits\UserContextServiceAwareTrait' => __DIR__ . '/src/UnicaenAuth/Service/Traits/UserContextServiceAwareTrait.php', - 'UnicaenAuth\Service\Traits\RoleServiceAwareTrait' => __DIR__ . '/src/UnicaenAuth/Service/Traits/RoleServiceAwareTrait.php', - 'UnicaenAuth\Service\Traits\CategoriePrivilegeServiceAwareTrait' => __DIR__ . '/src/UnicaenAuth/Service/Traits/CategoriePrivilegeAwareTrait.php', - 'UnicaenAuth\Service\Traits\PrivilegeServiceAwareTrait' => __DIR__ . '/src/UnicaenAuth/Service/Traits/PrivilegeServiceAwareTrait.php', - 'UnicaenAuth\Service\UserAwareInitializer' => __DIR__ . '/src/UnicaenAuth/Service/UserAwareInitializer.php', - 'UnicaenAuth\Service\UserContext' => __DIR__ . '/src/UnicaenAuth/Service/UserContext.php', - 'UnicaenAuth\Service\PrivilegeService' => __DIR__ . '/src/UnicaenAuth/Service/PrivilegeService.php', - 'UnicaenAuth\Service\RoleService' => __DIR__ . '/src/UnicaenAuth/Service/RoleService.php', - 'UnicaenAuth\Service\CategoriePrivilegeService' => __DIR__ . '/src/UnicaenAuth/Service/CategoriePrivilegeService.php', - 'UnicaenAuth\Service\AbstractService' => __DIR__ . '/src/UnicaenAuth/Service/AbstractService.php', - 'UnicaenAuth\Service\AuthorizeServiceFactory' => __DIR__ . '/src/UnicaenAuth/Service/AuthorizeServiceFactory.php', - 'UnicaenAuth\Authentication\AuthenticationServiceFactory' => __DIR__ . '/src/UnicaenAuth/Authentication/AuthenticationServiceFactory.php', - 'UnicaenAuth\Authentication\Storage\Ldap' => __DIR__ . '/src/UnicaenAuth/Authentication/Storage/Ldap.php', - 'UnicaenAuth\Authentication\Storage\Db' => __DIR__ . '/src/UnicaenAuth/Authentication/Storage/Db.php', - 'UnicaenAuth\Authentication\Storage\Chain' => __DIR__ . '/src/UnicaenAuth/Authentication/Storage/Chain.php', - 'UnicaenAuth\Authentication\Storage\ChainableStorage' => __DIR__ . '/src/UnicaenAuth/Authentication/Storage/ChainableStorage.php', - 'UnicaenAuth\Authentication\Storage\ChainServiceFactory' => __DIR__ . '/src/UnicaenAuth/Authentication/Storage/ChainServiceFactory.php', - 'UnicaenAuth\Authentication\Storage\ChainEvent' => __DIR__ . '/src/UnicaenAuth/Authentication/Storage/ChainEvent.php', - 'UnicaenAuth\Authentication\Adapter\Ldap' => __DIR__ . '/src/UnicaenAuth/Authentication/Adapter/Ldap.php', - 'UnicaenAuth\Authentication\Adapter\Db' => __DIR__ . '/src/UnicaenAuth/Authentication/Adapter/Db.php', - 'UnicaenAuth\Authentication\Adapter\AbstractFactory' => __DIR__ . '/src/UnicaenAuth/Authentication/Adapter/AbstractFactory.php', - 'UnicaenAuth\Authentication\Adapter\Cas' => __DIR__ . '/src/UnicaenAuth/Authentication/Adapter/Cas.php', - 'UnicaenAuth\Assertion\AbstractAssertion' => __DIR__ . '/src/UnicaenAuth/Assertion/AbstractAssertion.php', - 'UnicaenAuth\Acl\NamedRole' => __DIR__ . '/src/UnicaenAuth/Acl/NamedRole.php', - 'UnicaenAuth\View\RedirectionStrategy' => __DIR__ . '/src/UnicaenAuth/View/RedirectionStrategy.php', - 'UnicaenAuth\View\Helper\AppConnection' => __DIR__ . '/src/UnicaenAuth/View/Helper/AppConnection.php', - 'UnicaenAuth\View\Helper\UserProfileSelectRadioItem' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserProfileSelectRadioItem.php', - 'UnicaenAuth\View\Helper\UserProfileFactory' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserProfileFactory.php', - 'UnicaenAuth\View\Helper\UserStatus' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserStatus.php', - 'UnicaenAuth\View\Helper\UserStatusFactory' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserStatusFactory.php', - 'UnicaenAuth\View\Helper\UserAbstract' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserAbstract.php', - 'UnicaenAuth\View\Helper\UserProfileSelect' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserProfileSelect.php', - 'UnicaenAuth\View\Helper\UserConnectionFactory' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserConnectionFactory.php', - 'UnicaenAuth\View\Helper\UserInfoFactory' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserInfoFactory.php', - 'UnicaenAuth\View\Helper\UserInfo' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserInfo.php', - 'UnicaenAuth\View\Helper\UserCurrentFactory' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserCurrentFactory.php', - 'UnicaenAuth\View\Helper\UserProfileSelectRadioItemFactory' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserProfileSelectRadioItemFactory.php', - 'UnicaenAuth\View\Helper\UserProfileSelectFactory' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserProfileSelectFactory.php', - 'UnicaenAuth\View\Helper\UserProfile' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserProfile.php', - 'UnicaenAuth\View\Helper\UserCurrent' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserCurrent.php', - 'UnicaenAuth\View\Helper\UserConnection' => __DIR__ . '/src/UnicaenAuth/View/Helper/UserConnection.php', - 'UnicaenAuth\Controller\DroitsController' => __DIR__ . '/src/UnicaenAuth/Controller/DroitsController.php', - 'UnicaenAuth\Controller\UtilisateurController' => __DIR__ . '/src/UnicaenAuth/Controller/UtilisateurController.php', - 'UnicaenAuth\Provider\Role\ConfigServiceFactory' => __DIR__ . '/src/UnicaenAuth/Provider/Role/ConfigServiceFactory.php', - 'UnicaenAuth\Provider\Role\Config' => __DIR__ . '/src/UnicaenAuth/Provider/Role/Config.php', - 'UnicaenAuth\Provider\Role\UsernameServiceFactory' => __DIR__ . '/src/UnicaenAuth/Provider/Role/UsernameServiceFactory.php', - 'UnicaenAuth\Provider\Role\DbRole' => __DIR__ . '/src/UnicaenAuth/Provider/Role/DbRole.php', - 'UnicaenAuth\Provider\Role\DbRoleServiceFactory' => __DIR__ . '/src/UnicaenAuth/Provider/Role/DbRoleServiceFactory.php', - 'UnicaenAuth\Provider\Role\Username' => __DIR__ . '/src/UnicaenAuth/Provider/Role/Username.php', - 'UnicaenAuth\Provider\Privilege\PrivilegeProviderAwareTrait' => __DIR__ . '/src/UnicaenAuth/Provider/Privilege/PrivilegeProviderAwareTrait.php', - 'UnicaenAuth\Provider\Privilege\PrivilegeProviderInterface' => __DIR__ . '/src/UnicaenAuth/Provider/Privilege/PrivilegeProviderInterface.php', - 'UnicaenAuth\Provider\Privilege\Privileges' => __DIR__ . '/src/UnicaenAuth/Provider/Privilege/Privileges.php', - 'UnicaenAuth\Provider\Rule\PrivilegeRuleProvider' => __DIR__ . '/src/UnicaenAuth/Provider/Rule/PrivilegeRuleProvider.php', - 'UnicaenAuth\Provider\Identity\DbServiceFactory' => __DIR__ . '/src/UnicaenAuth/Provider/Identity/DbServiceFactory.php', - 'UnicaenAuth\Provider\Identity\Basic' => __DIR__ . '/src/UnicaenAuth/Provider/Identity/Basic.php', - 'UnicaenAuth\Provider\Identity\Ldap' => __DIR__ . '/src/UnicaenAuth/Provider/Identity/Ldap.php', - 'UnicaenAuth\Provider\Identity\Db' => __DIR__ . '/src/UnicaenAuth/Provider/Identity/Db.php', - 'UnicaenAuth\Provider\Identity\Chain' => __DIR__ . '/src/UnicaenAuth/Provider/Identity/Chain.php', - 'UnicaenAuth\Provider\Identity\ChainableProvider' => __DIR__ . '/src/UnicaenAuth/Provider/Identity/ChainableProvider.php', - 'UnicaenAuth\Provider\Identity\LdapServiceFactory' => __DIR__ . '/src/UnicaenAuth/Provider/Identity/LdapServiceFactory.php', - 'UnicaenAuth\Provider\Identity\BasicServiceFactory' => __DIR__ . '/src/UnicaenAuth/Provider/Identity/BasicServiceFactory.php', - 'UnicaenAuth\Provider\Identity\ChainServiceFactory' => __DIR__ . '/src/UnicaenAuth/Provider/Identity/ChainServiceFactory.php', - 'UnicaenAuth\Provider\Identity\ChainEvent' => __DIR__ . '/src/UnicaenAuth/Provider/Identity/ChainEvent.php', - 'UnicaenAuth\Event\UserAuthenticatedEvent' => __DIR__ . '/src/UnicaenAuth/Event/UserAuthenticatedEvent.php', - 'UnicaenAuth\Event\Listener\AuthenticatedUserSavedAbstractListener' => __DIR__ . '/src/UnicaenAuth/Event/Listener/AuthenticatedUserSavedAbstractListener.php', - 'UnicaenAuth\Form\Droits\Traits\RoleFormAwareTrait' => __DIR__ . '/src/UnicaenAuth/Form/Droits/Traits/RoleFormAwareTrait.php', - 'UnicaenAuth\Form\Droits\RoleForm' => __DIR__ . '/src/UnicaenAuth/Form/Droits/RoleForm.php', - 'RoleFormHydrator' => __DIR__ . '/src/UnicaenAuth/Form/Droits/RoleForm.php', -); diff --git a/config/module.config.php b/config/module.config.php index faea79f..f8c8d10 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -1,21 +1,37 @@ <?php +use UnicaenAuth\Authentication\Adapter\AdapterChainServiceFactory; +use UnicaenAuth\Authentication\Adapter\CasAdapterFactory; +use UnicaenAuth\Authentication\Adapter\DbAdapterFactory; +use UnicaenAuth\Authentication\Adapter\LdapAdapterFactory; +use UnicaenAuth\Authentication\Adapter\ShibAdapterFactory; use UnicaenAuth\Authentication\Storage\DbFactory; use UnicaenAuth\Authentication\Storage\LdapFactory; use UnicaenAuth\Authentication\Storage\ShibFactory; use UnicaenAuth\Controller\AuthControllerFactory; use UnicaenAuth\Controller\DroitsControllerFactory; use UnicaenAuth\Controller\UtilisateurControllerFactory; +use UnicaenAuth\Form\CasLoginForm; +use UnicaenAuth\Form\CasLoginFormFactory; use UnicaenAuth\Form\Droits\RoleFormFactory; +use UnicaenAuth\Form\ShibLoginForm; +use UnicaenAuth\Form\ShibLoginFormFactory; use UnicaenAuth\Guard\PrivilegeControllerFactory; use UnicaenAuth\Guard\PrivilegeRouteFactory; 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\UserContextFactory; use UnicaenAuth\Service\UserFactory; use UnicaenAuth\Service\UserMapperFactory; +use UnicaenAuth\View\Helper\CasConnectViewHelper; +use UnicaenAuth\View\Helper\CasConnectViewHelperFactory; +use UnicaenAuth\View\Helper\ConnectViewHelper; +use UnicaenAuth\View\Helper\DbConnectViewHelper; +use UnicaenAuth\View\Helper\DbConnectViewHelperFactory; use UnicaenAuth\View\Helper\LdapConnectViewHelper; use UnicaenAuth\View\Helper\LdapConnectViewHelperFactory; use UnicaenAuth\View\Helper\LocalConnectViewHelper; @@ -42,25 +58,120 @@ use Zend\Authentication\AuthenticationService; use Zend\ServiceManager\Proxy\LazyServiceFactory; $settings = [ - /** - * Configuration de l'authentification locale. + * Configuration de l'authentification via la fédération d'identité (Shibboleth). */ - 'local' => [ + 'shib' => [ /** - * Possibilité ou non de s'authentifier à l'aide d'un compte local. + * Ordre d'affichage du formulaire de connexion. */ - 'enabled' => true, + 'order' => 1, + + /** + * Activation ou non de ce mode d'authentification. + */ + 'enabled' => false, + + /** + * Description facultative de ce mode d'authentification qui apparaîtra sur le formulaire de connexion. + */ + 'description' => "Cliquez sur le bouton ci-dessous pour accéder à l'authentification via la fédération d'identité.", + + /** + * URL de déconnexion. + */ + //'logout_url' => '/Shibboleth.sso/Logout?return=', // NB: '?return=' semble obligatoire! + + /* + 'simulate' => [ + 'eppn' => 'login@domain.fr', + 'supannEmpId' => '00012345', + ], + 'aliases' => [ + 'eppn' => 'HTTP_EPPN', + 'mail' => 'HTTP_MAIL', + 'eduPersonPrincipalName' => 'HTTP_EPPN', + 'supannEtuId' => 'HTTP_SUPANNETUID', + 'supannEmpId' => 'HTTP_SUPANNEMPID', + 'supannCivilite' => 'HTTP_SUPANNCIVILITE', + 'displayName' => 'HTTP_DISPLAYNAME', + 'sn' => 'HTTP_SN', + 'givenName' => 'HTTP_GIVENNAME', + ], + /* + 'required_attributes' => [ + 'eppn', + 'mail', + 'eduPersonPrincipalName', + 'supannCivilite', + 'displayName', + 'sn|surname', // i.e. 'sn' ou 'surname' + 'givenName', + 'supannEtuId|supannEmpId', + ], + */ ], /** - * Configuration de l'authentification LDAP. + * Configuration de l'authentification LDAP (compte établissement). */ 'ldap' => [ + 'order' => 2, + 'enabled' => true, + 'description' => "Utilisez ce formulaire pour vous connecter avec votre compte numérique établissement.", + /** - * Possibilité ou non de s'authentifier via l'annuaire LDAP. + * Type de substitution. + * Permet de "fusionner" les types d'authentification locale (db) et établissement (ldap) et donc leurs + * formulaires de connexion respectifs. */ - 'enabled' => true, + 'type' => 'local', + ], + + /** + * Configuration de l'authentification locale (compte propre à l'appli). + */ + 'db' => [ + 'order' => 3, + 'enabled' => false, + + /** + * Type de substitution. + * Permet de "grouper" les types d'authentification locale (db) et établissement (ldap) sous un même + * formulaire de connexion. + */ + 'type' => 'local', + + /** + * Description facultative de ce mode d'authentification qui apparaîtra sur le formulaire d'authentification. + * NB: si la valeur de 'order' pour le type 'db' est supérieure à celle pour le type 'ldap', + * c'est cette description qui sera visible. + */ + 'description' => "Utilisez ce formulaire si vous possédez un compte local propre à l'application.", + ], + + /** + * Configuration de l'authentification centralisée (CAS). + */ + 'cas' => [ + 'order' => 4, + 'enabled' => false, + 'description' => "Cliquez sur le bouton ci-dessous pour accéder à l'authentification centralisée.", + + /** + * Infos de connexion au serveur CAS. + */ + 'connection' => [ + 'default' => [ + 'params' => [ + 'hostname' => 'host.domain.fr', + 'port' => 443, + 'version' => "2.0", + 'uri' => "", + 'debug' => false, + ], + ], + ] ], /** @@ -129,9 +240,10 @@ return [ * Accepted values: array containing services that implement 'ZfcUser\Authentication\Adapter\ChainableAdapter' */ 'auth_adapters' => [ - 300 => 'UnicaenAuth\Authentication\Adapter\Ldap', // notifié en 1er - 200 => 'UnicaenAuth\Authentication\Adapter\Db', // ensuite (si échec d'authentification Ldap) - 100 => 'UnicaenAuth\Authentication\Adapter\Cas', // ensuite (si échec d'authentification Db) + 300 => 'UnicaenAuth\Authentication\Adapter\Ldap', + 200 => 'UnicaenAuth\Authentication\Adapter\Db', + 100 => 'UnicaenAuth\Authentication\Adapter\Cas', + 50 => 'UnicaenAuth\Authentication\Adapter\Shib', ], // telling ZfcUser to use our own class @@ -184,6 +296,9 @@ return [ ['controller' => 'UnicaenApp\Controller\Application', 'action' => 'refresh-session', 'roles' => 'guest'], ['controller' => 'UnicaenAuth\Controller\Utilisateur', 'action' => 'selectionner-profil', 'roles' => 'guest'], + ['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'login', 'roles' => 'guest'], + ['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'authenticate', 'roles' => 'guest'], + ['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'logout', 'roles' => 'guest'], ['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'shibboleth', 'roles' => 'guest'], ['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'requestPasswordReset', 'roles' => 'guest'], ['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'changePassword', 'roles' => 'guest'], @@ -287,21 +402,31 @@ return [ 'may_terminate' => true, 'child_routes' => [ 'login' => [ - 'type' => 'Literal', + 'type' => 'Segment', 'options' => [ - 'route' => '/connexion', + 'route' => '/connexion[/:type]', 'defaults' => [ - 'controller' => 'zfcuser', + 'controller' => 'UnicaenAuth\Controller\Auth', // remplace 'zfcuser' 'action' => 'login', ], ], ], + 'authenticate' => array( + 'type' => 'Segment', + 'options' => array( + 'route' => '/authenticate/:type', + 'defaults' => array( + 'controller' => 'UnicaenAuth\Controller\Auth', // remplace 'zfcuser' + 'action' => 'authenticate', + ), + ), + ), 'logout' => [ 'type' => 'Literal', 'options' => [ 'route' => '/deconnexion', 'defaults' => [ - 'controller' => 'zfcuser', + 'controller' => 'UnicaenAuth\Controller\Auth', // remplace 'zfcuser' 'action' => 'logout', ], ], @@ -462,9 +587,6 @@ return [ 'UnicaenAuth\View\RedirectionStrategy' => 'UnicaenAuth\View\RedirectionStrategy', 'UnicaenAuth\Service\CategoriePrivilege' => 'UnicaenAuth\Service\CategoriePrivilegeService', ], - 'abstract_factories' => [ - 'UnicaenAuth\Authentication\Adapter\AbstractFactory', - ], 'factories' => [ 'unicaen-auth_module_options' => 'UnicaenAuth\Options\ModuleOptionsFactory', 'zfcuser_auth_service' => 'UnicaenAuth\Authentication\AuthenticationServiceFactory', @@ -480,10 +602,15 @@ return [ 'UnicaenAuth\Service\Privilege' => 'UnicaenAuth\Service\PrivilegeServiceFactory', 'BjyAuthorize\Service\Authorize' => 'UnicaenAuth\Service\AuthorizeServiceFactory', // substituion 'zfcuser_redirect_callback' => 'UnicaenAuth\Authentication\RedirectCallbackFactory', // substituion + CasService::class => CasServiceFactory::class, ShibService::class => ShibServiceFactory::class, 'UnicaenAuth\Service\UserContext' => UserContextFactory::class, 'zfcuser_user_mapper' => UserMapperFactory::class, 'MouchardCompleterAuth' => 'UnicaenAuth\Mouchard\MouchardCompleterAuthFactory', + 'UnicaenAuth\Authentication\Adapter\Ldap' => LdapAdapterFactory::class, + 'UnicaenAuth\Authentication\Adapter\Db' => DbAdapterFactory::class, + 'UnicaenAuth\Authentication\Adapter\Cas' => CasAdapterFactory::class, + 'UnicaenAuth\Authentication\Adapter\Shib' => ShibAdapterFactory::class, 'UnicaenAuth\Authentication\Storage\Db' => DbFactory::class, 'UnicaenAuth\Authentication\Storage\Ldap' => LdapFactory::class, 'UnicaenAuth\Authentication\Storage\Shib' => ShibFactory::class, @@ -492,6 +619,10 @@ return [ 'UnicaenAuth\Guard\PrivilegeRoute' => PrivilegeRouteFactory::class, 'UnicaenAuth\Provider\Rule\PrivilegeRuleProvider' => PrivilegeRuleProviderFactory::class, + CasLoginForm::class => CasLoginFormFactory::class, + ShibLoginForm::class => ShibLoginFormFactory::class, + 'ZfcUser\Authentication\Adapter\AdapterChain' => AdapterChainServiceFactory::class, + 'UnicaenApp\HistoriqueListener' => HistoriqueListenerFactory::class, 'UnicaenAuth\HistoriqueListener' => HistoriqueListenerFactory::class, \UnicaenAuth\Event\EventManager::class => \UnicaenAuth\Event\EventManagerFactory::class @@ -543,9 +674,12 @@ return [ 'userProfileSelect' => UserProfileSelect::class, 'userProfileSelectRadioItem' => UserProfileSelectRadioItem::class, 'userUsurpation' => UserUsurpationHelper::class, + 'dbConnect' => DbConnectViewHelper::class, 'localConnect' => LocalConnectViewHelper::class, 'ldapConnect' => LdapConnectViewHelper::class, 'shibConnect' => ShibConnectViewHelper::class, + 'casConnect' => CasConnectViewHelper::class, + 'connect' => ConnectViewHelper::class, ], 'factories' => [ UserConnection::class => UserConnectionFactory::class, @@ -556,12 +690,15 @@ return [ UserProfileSelect::class => UserProfileSelectFactory::class, UserProfileSelectRadioItem::class => UserProfileSelectRadioItemFactory::class, UserUsurpationHelper::class => UserUsurpationHelperFactory::class, + DbConnectViewHelper::class => DbConnectViewHelperFactory::class, LocalConnectViewHelper::class => LocalConnectViewHelperFactory::class, LdapConnectViewHelper::class => LdapConnectViewHelperFactory::class, ShibConnectViewHelper::class => ShibConnectViewHelperFactory::class, + CasConnectViewHelper::class => CasConnectViewHelperFactory::class, ], 'invokables' => [ 'appConnection' => 'UnicaenAuth\View\Helper\AppConnection', + ConnectViewHelper::class, ], ], ]; \ No newline at end of file diff --git a/config/unicaen-auth.global.php.dist b/config/unicaen-auth.global.php.dist index 60918a2..ef59c94 100644 --- a/config/unicaen-auth.global.php.dist +++ b/config/unicaen-auth.global.php.dist @@ -1,49 +1,8 @@ <?php /** * UnicaenAuth Global Configuration - * - * If you have a ./config/autoload/ directory set up for your project, you can - * drop this config file in it and change the values as you wish. */ $settings = [ - - /** - * Configuration de l'authentification locale. - */ - 'local' => [ - /** - * Affichage ou non du formulaire d'authentification avec un compte local. - */ - 'enabled' => false, - ], - - /** - * Configuration de l'authentification LDAP. - */ - 'ldap' => [ - /** - * Affichage ou non du formulaire d'authentification via l'annuaire LDAP. - * NB: en réalité cela permet aussi l'authentification avec un compte local. - */ - 'enabled' => true, - ], - - /** - * Configuration de l'authentification Shibboleth. - */ - 'shibboleth' => [ - /** - * Affichage ou non du formulaire d'authentification via l'annuaire LDAP. - * NB: en réalité cela permet aussi l'authentification avec un compte local. - */ - 'enable' => false, - - /** - * URL de déconnexion. - */ - 'logout_url' => '/Shibboleth.sso/Logout?return=', // NB: '?return=' semble obligatoire! - ], - /** * Flag indiquant si l'utilisateur authenitifié avec succès via l'annuaire LDAP doit * être enregistré/mis à jour dans la table des utilisateurs de l'appli. diff --git a/config/unicaen-auth.local.php.dist b/config/unicaen-auth.local.php.dist index 39eadae..1a1cabf 100644 --- a/config/unicaen-auth.local.php.dist +++ b/config/unicaen-auth.local.php.dist @@ -1,17 +1,52 @@ <?php +use UnicaenAuth\Authentication\Adapter\Shib; +use UnicaenAuth\Authentication\Adapter\Cas; +use UnicaenAuth\Authentication\Adapter\Ldap; +use UnicaenAuth\Authentication\Adapter\Db; + return [ 'unicaen-auth' => [ - /** - * Configuration de l'authentification Shibboleth. + * Configuration de l'authentification via la fédération d'identité (Shibboleth). */ - 'shibboleth' => [ - 'enable' => false, + 'shib' => [ + /** + * Ordre d'affichage du formulaire de connexion. + */ + 'order' => 1, + + /** + * Activation ou non de ce mode d'authentification. + */ + 'enabled' => true, + + /** + * Description facultative de ce mode d'authentification qui apparaîtra sur le formulaire de connexion. + */ + 'description' => + "Cliquez sur le bouton ci-dessous pour accéder à l'authentification via la fédération d'identité. " . + "<strong>NB: Vous devrez utiliser votre compte " . + "« <a href='http://vie-etudiante.unicaen.fr/vie-numerique/etupass/'>etupass</a> » " . + "pour vous authentifier...</strong>", + + /** + * URL de déconnexion. + */ + 'logout_url' => '/Shibboleth.sso/Logout?return=', // NB: '?return=' semble obligatoire! + + /** + * Simulation d'authentification d'un utilisateur. + */ 'simulate' => [ 'eppn' => 'gauthierb@unicaen.fr', 'supannEmpId' => '00021237', ], + + /** + * Alias éventuels des clés renseignées par Shibboleth dans la variable superglobale $_SERVER + * une fois l'authentification réussie. + */ 'aliases' => [ 'eppn' => 'HTTP_EPPN', 'mail' => 'HTTP_MAIL', @@ -24,6 +59,10 @@ return [ 'givenName' => 'HTTP_GIVENNAME', ], /* + /** + * Clés dont la présence sera requise par l'application dans la variable superglobale $_SERVER + * une fois l'authentification réussie. + */ 'required_attributes' => [ 'eppn', 'mail', @@ -38,16 +77,49 @@ return [ ], /** - * Paramètres de connexion au serveur CAS : - * - pour désactiver l'authentification CAS, le tableau 'cas' doit être vide. - * - pour l'activer, renseigner les paramètres. + * Configuration de l'authentification LDAP (compte établissement). + */ + 'ldap' => [ + 'order' => 2, + 'enabled' => true, + + /** + * Type de substitution. + * Permet de "fusionner" les types d'authentification locale (db) et établissement (ldap) et donc leurs + * formulaires de connexion respectifs. + */ + 'type' => 'local', + ], + + /** + * Configuration de l'authentification locale (compte propre à l'appli). + */ + 'db' => [ + 'order' => 3, + 'enabled' => true, + 'type' => 'local', + + /** + * Description facultative de ce mode d'authentification qui apparaîtra sur le formulaire d'authentification. + * (NB: Si l'authentification LDAP est également activée, c'est cette description qui sera utilisée) + */ + 'description' => "Utilisez ce formulaire si vous possédez un compte local propre à l'application.", + ], + + /** + * Configuration de l'authentification centralisée (CAS). */ - /* 'cas' => [ + 'order' => 4, + 'enabled' => false, + + /** + * Infos de connexion au serveur CAS. + */ 'connection' => [ 'default' => [ 'params' => [ - 'hostname' => 'cas.unicaen.fr', + 'hostname' => 'host.domain.fr', 'port' => 443, 'version' => "2.0", 'uri' => "", @@ -56,7 +128,6 @@ return [ ], ], ], - */ /** * Identifiants de connexion LDAP autorisés à faire de l'usurpation d'identité. diff --git a/doc/authentification.md b/doc/authentification.md index 908b979..d497982 100644 --- a/doc/authentification.md +++ b/doc/authentification.md @@ -1,36 +1,53 @@ # Authentification -## Sources d'authentification +## Types d'authentification -Les 3 sources suivantes sont sollicitées successivement jusqu'à ce que l'une d'entre elles valide l'authentification de l'utilisateur. +Quatre types d'authentification sont activables dans la configuration du module. -1/ Annuaire LDAP +1/ Authentification via la fédération d'identité Renater (Shibboleth) -- La connexion à l'annuaire LDAP est requise pour authentifier (ldap_bind) et récupérer les infos concernant l'utilisateur (cf. configuration du module UnicaenApp). -- Il est possible d'enregistrer systématiquement l'utilisateur authentifié dans la base de données de l'application. +- Ce type d'authentification requiert l'installation d'un module Shibboleth sur le serveur d'application, configuré + pour se déclencher sur l'URL `/auth/shibboleth`, exemple : `https://sygal.univ.fr/auth/shibboleth`. +- Clé de configuration `shib`. -2/ Table des utilisateurs +2/ Avec un compte local établissement (LDAP) + +- La connexion à un annuaire LDAP est requise pour authentifier et récupérer les infos concernant l'utilisateur + (cf. configuration du module unicaen/app ou unicaen/ldap). +- Clé de configuration `ldap`. + +3/ Avec un compte local propre à l'appli (DB) - Il peut arriver qu'une appli ait besoin d'authentifier des personnes n'existant pas dans l'annuaire LDAP. - Pour donner accès à l'application à un nouvel utilisateur, 2 solutions : - - Un informaticien crée à la main l'utilisateur dans la table des utilisateurs ; le mot de passe doit être chiffré avec “Bcrypt” - (exemple en ligne de commande à la racine de votre projet : `php --run 'require "vendor/autoload.php"; $bcrypt = new Zend\Crypt\Password\Bcrypt(); var_dump($bcrypt->create("azerty"));'`). - - Si la fonctionnalité est activée (fournie par le module "zf-commons/zfc-user" dont dépend le module UnicaenAuth), l'utilisateur s'enregistre lui-même dans la table des utilisateurs via un formulaire de l'application (le lien figure sous le formulaire de connexion à l'appli). + - Un informaticien crée à la main l'utilisateur dans la table des utilisateurs ; le mot de passe doit être chiffré + avec “Bcrypt” (exemple en ligne de commande à la racine de votre projet : + `php --run 'require "vendor/autoload.php"; $bcrypt = new Zend\Crypt\Password\Bcrypt(); var_dump($bcrypt->create("azerty"));'`). + - Si la fonctionnalité est activée (fournie par le module "zf-commons/zfc-user" dont dépend le module unicaen/auth), + l'utilisateur s'enregistre lui-même dans la table des utilisateurs via un formulaire de l'application (le lien figure + sous le formulaire de connexion à l'appli). +- Clé de configuration `db`. -3/ Serveur CAS +4/ Via un serveur d'authentification centralisée (CAS) - L'authentification est déléguée au serveur CAS grâce au module jasig/phpcas (bibliothèque phpCAS). -- NB: La connexion à l'annuaire LDAP est tout de même requise pour récupérer les infos concernant l'utilisateur (cf. configuration du module UnicaenApp). +- NB: La connexion à l'annuaire LDAP est tout de même requise pour récupérer les infos concernant l'utilisateur + (cf. configuration du module unicaen/app). +- Clé de config `cas`. ## Événement UserAuthenticatedEvent -Si vous avez activé l'enregistrement automatique de l'utilisateur authentifié dans la base de données de votre application, la classe abstraite UnicaenAuth\Event\Listener\AuthenticatedUserSavedAbstractListener peut vous intéresser. +Si vous avez activé l'enregistrement automatique de l'utilisateur authentifié dans la base de données de votre +application, la classe abstraite UnicaenAuth\Event\Listener\AuthenticatedUserSavedAbstractListener peut vous intéresser. -Elle vous procure un moyen de “faire quelque chose” juste avant que l'entité utilisateur (fraîchement authentifié via LDAP) ne soit persistée. L'idée est d'écouter un événement particulier déclenché lors du processus d'authentification de l'utilisateur. +Elle vous procure un moyen de “faire quelque chose” juste avant que l'entité utilisateur (fraîchement authentifié +via LDAP) ne soit persistée. L'idée est d'écouter un événement particulier déclenché lors du processus d'authentification de l'utilisateur. -*Attention! Cet événement est déclenché par l'authentification LDAP, mais pas par l'authentification à partir d'une table locale en base de données.* +*Attention! Cet événement est déclenché par l'authentification LDAP, mais pas par l'authentification à partir d'une +table locale en base de données.* -*Si vous avez mis en place (en plus ou à la place de l'authentification LDAP) une authentification à partir d'une table locale, écoutez plutôt l'événement authentication.success déclenché par le module ZfcUser une fois que l'authentification a réussi. Exemple :* +*Si vous avez mis en place (en plus ou à la place de l'authentification LDAP) une authentification à partir d'une +table locale, écoutez plutôt l'événement authentication.success déclenché par le module ZfcUser une fois que l'authentification a réussi. Exemple :* Module.php diff --git a/doc/configuration.md b/doc/configuration.md index 82600be..451c136 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -1,8 +1,11 @@ -# Configuration +Configuration +============= Il s'agit ici d'adapter certaines options de configuration des modules. -## Configuration globale + +Configuration globale +--------------------- - Copier/coller/renommer le fichier config/unicaen-auth.global.php.dist du module vers config/autoload/unicaen-auth.global.php de votre projet. - Adapter les informations suivantes à votre contexte (sans modifier le nom des clés)… @@ -43,37 +46,27 @@ Le système de gestion des privilèges d'UnicaenAuth est associé à une interfa Un menu “Droits d'accès” est affiché par défaut dans votre barre de menu principale. Ceci peut bien entendu être modifié selon vos souhaits dans le fichier de configuration global d'UnicaenAuth placé dans votre projet. -## Configuration locale + +Configuration locale +--------------------- - Copier/coller/renommer le fichier config/unicaen-auth.local.php.dist du module vers config/autoload/unicaen-auth.local.php de votre projet. - Adapter les informations suivantes à votre contexte (sans modifier le nom des clés)… -### Authentification centralisée - -Clé 'cas' : décommenter pour activer l'authentification CAS, commenter pour la désactiver, exemple : - -unicaen-auth.local.php - - 'cas' => array( - 'connection' => array( - 'default' => array( - 'params' => array( - 'hostname' => 'cas.unicaen.fr', - 'port' => 443, - 'version' => "2.0", - 'uri' => "", - 'debug' => false, - ), - ), - ), - ), +### Authentification -### Usurpation d'identité +Cf. [Authentification](./authentification.md). -Clé 'usurpation_allowed_usernames' : liste des identifiants de connexion des utilisateurs (issus de l'annuaire LDAP) autorisés à se connecter à l'appli sous l'identité de n'importe quel utilisateur (issus de l'annuaire LDAP ou de la base de données d'authentification), exemple : +## Usurpation d'identité -unicaen-auth.local.php +Clé `usurpation_allowed_usernames` : liste des identifiants de connexion des utilisateurs +autorisés à se connecter à l'appli sous l'identité de n'importe quel utilisateur (issus de l'annuaire LDAP ou de la +base de données d'authentification), exemple : - 'usurpation_allowed_usernames' => array('gauthierb'), +`unicaen-auth.local.php` -D'après cet exemple, l'utilisateur “gauthierb” est habilité à saisir dans le formulaire de connexion à l'appli l'identifiant “gauthierb=fernagut” et son mot de passe habituel pour se faire passer pour l'utilisateur “fernagut”. + 'usurpation_allowed_usernames' => ['login'], + +D'après cet exemple, l'utilisateur dont l'identifiant de connexion est `login` est habilité à saisir dans le formulaire +de connexion l'identifiant `login=victime` et son mot de passe habituel pour se faire passer pour l'utilisateur dont +l'identifiant est `victime`. \ No newline at end of file diff --git a/src/UnicaenAuth/Authentication/Adapter/AbstractAdapter.php b/src/UnicaenAuth/Authentication/Adapter/AbstractAdapter.php new file mode 100644 index 0000000..9dc0eac --- /dev/null +++ b/src/UnicaenAuth/Authentication/Adapter/AbstractAdapter.php @@ -0,0 +1,82 @@ +<?php + +namespace UnicaenAuth\Authentication\Adapter; + +use Zend\Authentication\Storage; +use ZfcUser\Authentication\Adapter\ChainableAdapter; + +abstract class AbstractAdapter implements ChainableAdapter +{ + /** + * @var string + */ + protected $type; + + /** + * @var Storage\StorageInterface + */ + protected $storage; + + /** + * @param string $type + * @return self + */ + public function setType(string $type): self + { + $this->type = $type; + return $this; + } + + /** + * Returns the persistent storage handler + * + * Session storage is used by default unless a different storage adapter has been set. + * + * @return Storage\StorageInterface + */ + public function getStorage() + { + if (null === $this->storage) { + $this->setStorage(new Storage\Session(get_class($this))); + } + + return $this->storage; + } + + /** + * Sets the persistent storage handler + * + * @param Storage\StorageInterface $storage + * @return AbstractAdapter Provides a fluent interface + */ + public function setStorage(Storage\StorageInterface $storage) + { + $this->storage = $storage; + return $this; + } + + /** + * Check if this adapter is satisfied or not + * + * @return bool + */ + public function isSatisfied() + { + $storage = $this->getStorage()->read(); + return (isset($storage['is_satisfied']) && true === $storage['is_satisfied']); + } + + /** + * Set if this adapter is satisfied or not + * + * @param bool $bool + * @return AbstractAdapter + */ + public function setSatisfied($bool = true) + { + $storage = $this->getStorage()->read() ?: array(); + $storage['is_satisfied'] = $bool; + $this->getStorage()->write($storage); + return $this; + } +} diff --git a/src/UnicaenAuth/Authentication/Adapter/AbstractFactory.php b/src/UnicaenAuth/Authentication/Adapter/AbstractFactory.php deleted file mode 100644 index fa4e56d..0000000 --- a/src/UnicaenAuth/Authentication/Adapter/AbstractFactory.php +++ /dev/null @@ -1,129 +0,0 @@ -<?php - -namespace UnicaenAuth\Authentication\Adapter; - -use Interop\Container\ContainerInterface; -use UnicaenApp\Exception\LogicException; -use UnicaenAuth\Options\ModuleOptions; -use UnicaenAuth\Service\User; -use UnicaenAuth\Event\EventManager; -use Zend\EventManager\EventManagerAwareInterface; -use Zend\Router\Http\TreeRouteStack; -use Zend\ServiceManager\AbstractFactoryInterface; -use Zend\ServiceManager\ServiceLocatorInterface; -use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; - -/** - * Description of AbstractFactory - * - * @author Bertrand GAUTHIER <bertrand.gauthier at unicaen.fr> - */ -class AbstractFactory implements AbstractFactoryInterface -{ - public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName) - { - return $this->canCreate($serviceLocator, $requestedName); - } - - public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName) - { - return $this->__invoke($serviceLocator, $requestedName); - } - - public function canCreate(ContainerInterface $container, $requestedName) - { - return strpos($requestedName, __NAMESPACE__) === 0 && class_exists($requestedName); - } - - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) - { - switch ($requestedName) { - case __NAMESPACE__ . '\Ldap': - $adapter = new Ldap(); - break; - case __NAMESPACE__ . '\Db': - $adapter = new Db(); - break; - case __NAMESPACE__ . '\Cas': - $adapter = new Cas(); - break; - // - // NB: pour faire simple, la stratégie de créer un adapter pour l'auth Shibboleth n'a pas été retenue. - // - // case __NAMESPACE__ . '\Shib': - // $adapter = new Shib(); - // break; - default: - throw new LogicException("Service demandé inattendu : '$requestedName'!"); - break; - } - - $this->injectDependencies($adapter, $container); - - if ($adapter instanceof EventManagerAwareInterface) { - /** @var EventManager $eventManager */ - $eventManager = $container->get(EventManager::class); - $adapter->setEventManager($eventManager); - $userService = $container->get('unicaen-auth_user_service'); /* @var $userService \UnicaenAuth\Service\User */ - $eventManager->attach('userAuthenticated', [$userService, 'userAuthenticated'], 100); - $eventManager->attach('clear', function() use ($adapter){ - $adapter->getStorage()->clear(); - }); - } - - return $adapter; - } - - /** - * @param Ldap|Db|Cas $adapter - * @param ContainerInterface $container - */ - private function injectDependencies($adapter, ContainerInterface $container) - { - switch (true) { - - case $adapter instanceof Ldap: - /** @var User $userService */ - $userService = $container->get('unicaen-auth_user_service'); - $adapter->setUserService($userService); - - /** @var LdapPeopleMapper $ldapPeopleMapper */ - $ldapPeopleMapper = $container->get('ldap_people_mapper'); - $adapter->setLdapPeopleMapper($ldapPeopleMapper); - - $options = array_merge( - $container->get('zfcuser_module_options')->toArray(), - $container->get('unicaen-auth_module_options')->toArray()); - $adapter->setOptions(new ModuleOptions($options)); - - /** @var \UnicaenApp\Options\ModuleOptions $appModuleOptions */ - $appModuleOptions = $container->get('unicaen-app_module_options'); - $adapter->setAppModuleOptions($appModuleOptions); - - break; - - case $adapter instanceof Cas: - /** @var User $userService */ - $userService = $container->get('unicaen-auth_user_service'); - $adapter->setUserService($userService); - - /** @var mixed $router */ - $router = $container->get('router'); - $adapter->setRouter($router); - - $options = array_merge( - $container->get('zfcuser_module_options')->toArray(), - $container->get('unicaen-auth_module_options')->toArray()); - $adapter->setOptions(new ModuleOptions($options)); - - /** @var LdapPeopleMapper $ldapPeopleMapper */ - $ldapPeopleMapper = $container->get('ldap_people_mapper'); - $adapter->setLdapPeopleMapper($ldapPeopleMapper); - - break; - - default: - break; - } - } -} \ No newline at end of file diff --git a/src/UnicaenAuth/Authentication/Adapter/AdapterChain.php b/src/UnicaenAuth/Authentication/Adapter/AdapterChain.php new file mode 100644 index 0000000..b72a4aa --- /dev/null +++ b/src/UnicaenAuth/Authentication/Adapter/AdapterChain.php @@ -0,0 +1,79 @@ +<?php + +namespace UnicaenAuth\Authentication\Adapter; + +use Zend\Stdlib\RequestInterface as Request; +use Zend\Stdlib\ResponseInterface as Response; +use ZfcUser\Exception; + +class AdapterChain extends \ZfcUser\Authentication\Adapter\AdapterChain +{ + /** + * prepareForAuthentication + * + * @param Request $request + * @return Response|bool + * @throws Exception\AuthenticationEventException + */ + public function prepareForAuthentication(Request $request) + { + $e = $this->getEvent(); + $e->setRequest($request); + + $this->getEventManager()->trigger('authenticate.pre', $e); + + $result = $this->getEventManager()->triggerUntil(function ($test) { + return ($test instanceof Response); + }, 'authenticate', $e); + + if ($result->stopped()) { + if ($result->last() instanceof Response) { + return $result->last(); + } + + throw new Exception\AuthenticationEventException( + sprintf( + 'Auth event was stopped without a response. Got "%s" instead', + is_object($result->last()) ? get_class($result->last()) : gettype($result->last()) + ) + ); + } + + if ($e->getIdentity()) { + $this->getEventManager()->trigger('authenticate.success', $e); + return true; + } + + $this->getEventManager()->trigger('authenticate.fail', $e); + + return false; + } + + /** + * logoutAdapters + * + * @return Response|null + */ + public function logoutAdapters() + { + //Adapters might need to perform additional cleanup after logout + $responseCollection = $this->getEventManager()->triggerUntil(function ($test) { + return ($test instanceof Response); + }, 'logout', $this->getEvent()); + + if ($responseCollection->stopped()) { + if ($responseCollection->last() instanceof Response) { + return $responseCollection->last(); + } + + throw new Exception\AuthenticationEventException( + sprintf( + 'Auth event was stopped without a response. Got "%s" instead', + is_object($responseCollection->last()) ? get_class($responseCollection->last()) : gettype($responseCollection->last()) + ) + ); + } + + return null; + } +} diff --git a/src/UnicaenAuth/Authentication/Adapter/AdapterChainServiceFactory.php b/src/UnicaenAuth/Authentication/Adapter/AdapterChainServiceFactory.php new file mode 100644 index 0000000..0df001c --- /dev/null +++ b/src/UnicaenAuth/Authentication/Adapter/AdapterChainServiceFactory.php @@ -0,0 +1,70 @@ +<?php +namespace UnicaenAuth\Authentication\Adapter; + +use Interop\Container\ContainerInterface; +use ZfcUser\Authentication\Adapter\Exception\OptionsNotFoundException; +use ZfcUser\Options\ModuleOptions; + +class AdapterChainServiceFactory +{ + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + $chain = new AdapterChain(); + + $options = $this->getOptions($container); + + //iterate and attach multiple adapters and events if offered + foreach ($options->getAuthAdapters() as $priority => $adapterName) { + $adapter = $container->get($adapterName); + + if (is_callable(array($adapter, 'authenticate'))) { + $chain->getEventManager()->attach('authenticate', array($adapter, 'authenticate'), $priority); + } + + if (is_callable(array($adapter, 'logout'))) { + $chain->getEventManager()->attach('logout', array($adapter, 'logout'), $priority); + } + } + + return $chain; + } + + /** + * @var ModuleOptions + */ + protected $options; + + /** + * set options + * + * @param ModuleOptions $options + * @return AdapterChainServiceFactory + */ + public function setOptions(ModuleOptions $options) + { + $this->options = $options; + return $this; + } + + /** + * get options + * + * @param ContainerInterface|null $container (optional) Service Locator + * @return ModuleOptions $options + */ + public function getOptions(ContainerInterface $container = null) + { + if (!$this->options) { + if (!$container) { + throw new OptionsNotFoundException( + 'Options were tried to retrieve but not set ' . + 'and no service locator was provided' + ); + } + + $this->setOptions($container->get('zfcuser_module_options')); + } + + return $this->options; + } +} diff --git a/src/UnicaenAuth/Authentication/Adapter/Cas.php b/src/UnicaenAuth/Authentication/Adapter/Cas.php index 12cae0a..7d76936 100644 --- a/src/UnicaenAuth/Authentication/Adapter/Cas.php +++ b/src/UnicaenAuth/Authentication/Adapter/Cas.php @@ -5,37 +5,31 @@ namespace UnicaenAuth\Authentication\Adapter; use Exception; use phpCAS; use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; -use UnicaenAuth\Options\ModuleOptions; +use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait; use UnicaenAuth\Service\User; use Zend\Authentication\Exception\UnexpectedValueException; use Zend\Authentication\Result as AuthenticationResult; use Zend\EventManager\Event; use Zend\EventManager\EventInterface; -use Zend\EventManager\EventManager; -use Zend\EventManager\EventManagerAwareInterface; -use Zend\EventManager\EventManagerInterface; use Zend\Router\RouteInterface; use Zend\Router\RouteStackInterface; -use ZfcUser\Authentication\Adapter\AbstractAdapter; use ZfcUser\Authentication\Adapter\ChainableAdapter; -use Zend\Authentication\Storage\Session; /** * CAS authentication adpater * * @author Bertrand GAUTHIER <bertrand.gauthier@unicaen.fr> */ -class Cas extends AbstractAdapter implements EventManagerAwareInterface +class Cas extends AbstractAdapter { - /** - * @var EventManager - */ - protected $eventManager; + use ModuleOptionsAwareTrait; + + const TYPE = 'cas'; /** - * @var ModuleOptions + * @var string */ - protected $options; + protected $type = self::TYPE; /** * @var array @@ -92,11 +86,16 @@ class Cas extends AbstractAdapter implements EventManagerAwareInterface // Si un jour c'est un AdapterChainEvent qui est attendu, plus besoin de faire $e->getTarget(). $e = $e->getTarget(); + $type = $e->getRequest()->getPost()->get('type'); + if ($type !== $this->type) { + return; + } + // if ($e->getIdentity()) { // return; // } - /* DS : modification liée à une boucle infinie lors de l'authentification CAS */ - if ($this->isSatisfied()) { + /* DS : modification liée à une boucle infinie lors de l'authentification CAS */ + if ($this->isSatisfied()) { $storage = $this->getStorage()->read(); $e->setIdentity($storage['identity']) ->setCode(AuthenticationResult::SUCCESS) @@ -104,9 +103,8 @@ class Cas extends AbstractAdapter implements EventManagerAwareInterface return; } - $config = $this->getOptions()->getCas(); - if (!$config) { - return; // NB: l'authentification CAS est désactivée ssi le tableau des options est vide + if (! $this->isEnabled()) { + return; } error_reporting($oldErrorReporting = error_reporting() & ~E_NOTICE); @@ -135,6 +133,23 @@ class Cas extends AbstractAdapter implements EventManagerAwareInterface $this->userService->userAuthenticated($ldapPeople); } + /** + * @return bool + */ + protected function isEnabled() + { + $config = $this->moduleOptions->getCas(); + + if (! $config) { + return false; + } + if (isset($config['enabled'])) { + return (bool) $config['enabled']; + } + + return true; + } + /** * * @param Event $e @@ -142,8 +157,13 @@ class Cas extends AbstractAdapter implements EventManagerAwareInterface */ public function logout(Event $e) { - if (!$this->getOptions()->getCas()) { - return; // NB: l'authentification CAS est désactivée ssi le tableau des options est vide + if (! $this->isEnabled()) { + return; + } + + $storage = $this->getStorage()->read(); + if (! isset($storage['identity'])) { + return; } $returnUrl = $this->router->getRequestUri()->setPath($this->router->getBaseUrl())->toString(); @@ -168,7 +188,7 @@ class Cas extends AbstractAdapter implements EventManagerAwareInterface } if (null === $this->casOptions) { - $config = $this->getOptions()->getCas(); + $config = $this->moduleOptions->getCas(); if (!isset($config['connection']['default']['params']) || !$config['connection']['default']['params']) { throw new Exception("Les paramètres de connexion au serveur CAS sont invalides."); } @@ -201,28 +221,6 @@ class Cas extends AbstractAdapter implements EventManagerAwareInterface return $this; } - /** - * @param ModuleOptions $options - */ - public function setOptions(ModuleOptions $options) - { - $this->options = $options; - } - - /** - * @return ModuleOptions - */ - public function getOptions() - { -// if (!$this->options instanceof ModuleOptions) { -// $options = array_merge( -// $this->serviceLocator->get('zfcuser_module_options')->toArray(), -// $this->serviceLocator->get('unicaen-auth_module_options')->toArray()); -// $this->setOptions(new ModuleOptions($options)); -// } - return $this->options; - } - /** * get ldap people mapper * @@ -246,30 +244,14 @@ class Cas extends AbstractAdapter implements EventManagerAwareInterface return $this; } - /** - * Retrieve EventManager instance - * - * @return EventManagerInterface - */ - public function getEventManager() - { - return $this->eventManager; - } - - /** - * {@inheritdoc} - */ - public function setEventManager(EventManagerInterface $eventManager) - { - $this->eventManager = $eventManager; - return $this; - } - /** * @param RouteInterface $router */ public function reconfigureRoutesForCasAuth(RouteInterface $router) { + if (! $this->isEnabled()) { + return; + } if(!$router instanceof RouteStackInterface) { return; } @@ -289,9 +271,9 @@ class Cas extends AbstractAdapter implements EventManagerAwareInterface 'may_terminate' => true, 'child_routes' => [ 'login' => [ - 'type' => 'Literal', + 'type' => 'Segment', 'options' => [ - 'route' => '/connexion', + 'route' => '/connexion[/:type]', 'defaults' => [ 'controller' => 'zfcuser', 'action' => 'authenticate', // zappe l'action 'login' diff --git a/src/UnicaenAuth/Authentication/Adapter/CasAdapterFactory.php b/src/UnicaenAuth/Authentication/Adapter/CasAdapterFactory.php new file mode 100644 index 0000000..a08e24c --- /dev/null +++ b/src/UnicaenAuth/Authentication/Adapter/CasAdapterFactory.php @@ -0,0 +1,67 @@ +<?php + +namespace UnicaenAuth\Authentication\Adapter; + +use Interop\Container\ContainerInterface; +use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; +use UnicaenAuth\Options\ModuleOptions; +use UnicaenAuth\Service\User; +use Zend\Authentication\Storage\Session; + +class CasAdapterFactory +{ + /** + * @param ContainerInterface $container + * @param string $requestedName + * @param array|null $options + * @return Cas + */ + public function __invoke(ContainerInterface $container, string $requestedName, array $options = null) + { + $adapter = new Cas(); + $adapter->setStorage(new Session(Cas::class)); + + $this->injectDependencies($adapter, $container); + +// /** @var EventManager $eventManager */ +// $eventManager = $container->get(EventManager::class); +// $adapter->setEventManager($eventManager); +// $userService = $container->get('unicaen-auth_user_service'); /* @var $userService \UnicaenAuth\Service\User */ +// $eventManager->attach('userAuthenticated', [$userService, 'userAuthenticated'], 100); +// $eventManager->attach('clear', function() use ($adapter){ +// $adapter->getStorage()->clear(); +// }); + + return $adapter; + } + + /** + * @param Cas $adapter + * @param ContainerInterface $container + */ + private function injectDependencies(Cas $adapter, ContainerInterface $container) + { + /** @var User $userService */ + $userService = $container->get('unicaen-auth_user_service'); + $adapter->setUserService($userService); + + /** @var mixed $router */ + $router = $container->get('router'); + $adapter->setRouter($router); + + $options = array_merge( + $container->get('zfcuser_module_options')->toArray(), + $container->get('unicaen-auth_module_options')->toArray()); + $moduleOptions = new ModuleOptions($options); + $adapter->setModuleOptions($moduleOptions); + + $substitut = $moduleOptions->getCas()['type'] ?? null; + if ($substitut !== null) { + $adapter->setType($substitut); + } + + /** @var LdapPeopleMapper $ldapPeopleMapper */ + $ldapPeopleMapper = $container->get('ldap_people_mapper'); + $adapter->setLdapPeopleMapper($ldapPeopleMapper); + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Authentication/Adapter/Db.php b/src/UnicaenAuth/Authentication/Adapter/Db.php index 39b1c8d..f1e2bf5 100644 --- a/src/UnicaenAuth/Authentication/Adapter/Db.php +++ b/src/UnicaenAuth/Authentication/Adapter/Db.php @@ -6,9 +6,13 @@ use Interop\Container\ContainerInterface; use UnicaenApp\ServiceManager\ServiceLocatorAwareInterface; use UnicaenApp\ServiceManager\ServiceLocatorAwareTrait; use UnicaenAuth\Options\ModuleOptions; -use Zend\Authentication\Storage\Session; +use Zend\Authentication\Result as AuthenticationResult; +use Zend\Crypt\Password\Bcrypt; use Zend\EventManager\EventInterface; -use Zend\ServiceManager\Exception\ServiceNotFoundException; +use Zend\ServiceManager\ServiceManager; +use Zend\Session\Container as SessionContainer; +use ZfcUser\Entity\UserInterface; +use ZfcUser\Mapper\UserInterface as UserMapperInterface; /** * Adpater d'authentification à partir de la base de données. @@ -18,8 +22,35 @@ use Zend\ServiceManager\Exception\ServiceNotFoundException; * * @author Bertrand GAUTHIER <bertrand.gauthier@unicaen.fr> */ -class Db extends \ZfcUser\Authentication\Adapter\Db implements ServiceLocatorAwareInterface +class Db extends AbstractAdapter implements ServiceLocatorAwareInterface { + const TYPE = 'db'; + + /** + * @var string + */ + protected $type = self::TYPE; + + /** + * @var UserMapperInterface + */ + protected $mapper; + + /** + * @var callable + */ + protected $credentialPreprocessor; + + /** + * @var ServiceManager + */ + protected $serviceManager; + + /** + * @var \ZfcUser\Options\ModuleOptions + */ + protected $options; + use ServiceLocatorAwareTrait; /** @@ -37,6 +68,15 @@ class Db extends \ZfcUser\Authentication\Adapter\Db implements ServiceLocatorAwa return $this; } + /** + * Called when user id logged out + * @param EventInterface $e + */ + public function logout(EventInterface $e) + { + $this->getStorage()->clear(); + } + /** * Authentification. * @@ -45,21 +85,178 @@ class Db extends \ZfcUser\Authentication\Adapter\Db implements ServiceLocatorAwa */ public function authenticate(EventInterface $e) { + $type = $e->getTarget()->getRequest()->getPost()->get('type'); + if ($type !== $this->type) { + return; + } + // 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(). if ($e->getTarget()->getIdentity()) { return true; } - - try { - $result = parent::authenticate($e); + + $e = $e->getTarget(); + if ($this->isSatisfied()) { + $storage = $this->getStorage()->read(); + $e->setIdentity($storage['identity']) + ->setCode(AuthenticationResult::SUCCESS) + ->setMessages(array('Authentication successful.')); + return; + } + + $identity = $e->getRequest()->getPost()->get('identity'); + $credential = $e->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->getOptions()->getAuthIdentityFields(); + while (!is_object($userObject) && count($fields) > 0) { + $mode = array_shift($fields); + switch ($mode) { + case 'username': + $userObject = $this->getMapper()->findByUsername($identity); + break; + case 'email': + $userObject = $this->getMapper()->findByEmail($identity); + break; + } } - catch (ServiceNotFoundException $snfe) { + + if (!$userObject) { + $e->setCode(AuthenticationResult::FAILURE_IDENTITY_NOT_FOUND) + ->setMessages(array('A record with the supplied identity could not be found.')); + $this->setSatisfied(false); return false; } - return $result; + if ($this->getOptions()->getEnableUserState()) { + // Don't allow user to login if state is not in allowed list + if (!in_array($userObject->getState(), $this->getOptions()->getAllowedLoginStates())) { + $e->setCode(AuthenticationResult::FAILURE_UNCATEGORIZED) + ->setMessages(array('A record with the supplied identity is not active.')); + $this->setSatisfied(false); + return false; + } + } + + $bcrypt = new Bcrypt(); + $bcrypt->setCost($this->getOptions()->getPasswordCost()); + if (!$bcrypt->verify($credential, $userObject->getPassword())) { + // Password does not match + $e->setCode(AuthenticationResult::FAILURE_CREDENTIAL_INVALID) + ->setMessages(array('Supplied credential is invalid.')); + $this->setSatisfied(false); + return false; + } + + // regen the id + $session = new SessionContainer($this->getStorage()->getNameSpace()); + $session->getManager()->regenerateId(); + + // Success! + $e->setIdentity($userObject->getId()); + // 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'] = $e->getIdentity(); + $this->getStorage()->write($storage); + $e->setCode(AuthenticationResult::SUCCESS) + ->setMessages(array('Authentication successful.')); + } + + protected function updateUserPasswordHash(UserInterface $userObject, $password, Bcrypt $bcrypt) + { + $hash = explode('$', $userObject->getPassword()); + if ($hash[2] === $bcrypt->getCost()) { + return; + } + $userObject->setPassword($bcrypt->create($password)); + $this->getMapper()->update($userObject); + return $this; + } + + public function preProcessCredential($credential) + { + $processor = $this->getCredentialPreprocessor(); + if (is_callable($processor)) { + return $processor($credential); + } + + return $credential; + } + + /** + * getMapper + * + * @return UserMapperInterface + */ + public function getMapper() + { + if (null === $this->mapper) { + $this->mapper = $this->getServiceManager()->get('zfcuser_user_mapper'); + } + + return $this->mapper; + } + + /** + * setMapper + * + * @param UserMapperInterface $mapper + * @return \ZfcUser\Authentication\Adapter\Db + */ + public function setMapper(UserMapperInterface $mapper) + { + $this->mapper = $mapper; + + return $this; + } + + /** + * Get credentialPreprocessor. + * + * @return callable + */ + public function getCredentialPreprocessor() + { + return $this->credentialPreprocessor; + } + + /** + * Set credentialPreprocessor. + * + * @param callable $credentialPreprocessor + * @return $this + */ + public function setCredentialPreprocessor($credentialPreprocessor) + { + $this->credentialPreprocessor = $credentialPreprocessor; + return $this; + } + + /** + * Retrieve service manager instance + * + * @return ServiceManager + */ + public function getServiceManager() + { + return $this->serviceManager; + } + + /** + * Set service manager instance + * + * @param ContainerInterface $serviceManager + */ + public function setServiceManager(ContainerInterface $serviceManager) + { + $this->serviceManager = $serviceManager; } /** diff --git a/src/UnicaenAuth/Authentication/Adapter/DbAdapterFactory.php b/src/UnicaenAuth/Authentication/Adapter/DbAdapterFactory.php new file mode 100644 index 0000000..63abde5 --- /dev/null +++ b/src/UnicaenAuth/Authentication/Adapter/DbAdapterFactory.php @@ -0,0 +1,33 @@ +<?php + +namespace UnicaenAuth\Authentication\Adapter; + +use Interop\Container\ContainerInterface; +use UnicaenAuth\Options\ModuleOptions; +use Zend\Authentication\Storage\Session; +use Zend\EventManager\EventManagerAwareInterface; + +class DbAdapterFactory +{ + /** + * @param ContainerInterface $container + * @param string $requestedName + * @param array|null $options + * @return mixed|Cas|Db|Ldap|EventManagerAwareInterface + */ + public function __invoke(ContainerInterface $container, string $requestedName, array $options = null) + { + /** @var ModuleOptions $moduleOptions */ + $moduleOptions = $container->get('unicaen-auth_module_options'); + + $adapter = new Db(); + $adapter->setStorage(new Session(Db::class)); + + $substitut = $moduleOptions->getDb()['type'] ?? null; + if ($substitut !== null) { + $adapter->setType($substitut); + } + + return $adapter; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Authentication/Adapter/Ldap.php b/src/UnicaenAuth/Authentication/Adapter/Ldap.php index 711935d..ae7e624 100644 --- a/src/UnicaenAuth/Authentication/Adapter/Ldap.php +++ b/src/UnicaenAuth/Authentication/Adapter/Ldap.php @@ -5,6 +5,7 @@ namespace UnicaenAuth\Authentication\Adapter; use UnicaenApp\Exception\RuntimeException; use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; use UnicaenAuth\Options\ModuleOptions; +use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait; use UnicaenAuth\Service\User; use Zend\Authentication\Adapter\Ldap as LdapAuthAdapter; use Zend\Authentication\Exception\ExceptionInterface; @@ -14,9 +15,7 @@ use Zend\EventManager\EventInterface; use Zend\EventManager\EventManager; use Zend\EventManager\EventManagerAwareInterface; use Zend\EventManager\EventManagerInterface; -use ZfcUser\Authentication\Adapter\AbstractAdapter; use ZfcUser\Authentication\Adapter\ChainableAdapter; -use Zend\Authentication\Storage\Session; /** * LDAP authentication adpater @@ -25,8 +24,17 @@ use Zend\Authentication\Storage\Session; */ class Ldap extends AbstractAdapter implements EventManagerAwareInterface { + use ModuleOptionsAwareTrait; + + const TYPE = 'ldap'; + const USURPATION_USERNAMES_SEP = '='; + /** + * @var string + */ + protected $type = self::TYPE; + /** * @var EventManager */ @@ -42,11 +50,6 @@ class Ldap extends AbstractAdapter implements EventManagerAwareInterface */ protected $ldapPeopleMapper; - /** - * @var ModuleOptions - */ - protected $options; - /** * @var string */ @@ -93,6 +96,11 @@ class Ldap extends AbstractAdapter implements EventManagerAwareInterface // Si un jour c'est un AdapterChainEvent qui est attendu, plus besoin de faire $e->getTarget(). $e = $e->getTarget(); + $type = $e->getRequest()->getPost()->get('type'); + if ($type !== $this->type) { + return; + } + if ($this->isSatisfied()) { try { $storage = $this->getStorage()->read(); @@ -191,7 +199,7 @@ class Ldap extends AbstractAdapter implements EventManagerAwareInterface $usernames = self::extractUsernamesUsurpation($username); if (count($usernames) === 2) { list ($username, $this->usernameUsurpe) = $usernames; - if (!in_array($username, $this->getOptions()->getUsurpationAllowedUsernames())) { + if (!in_array($username, $this->moduleOptions->getUsurpationAllowedUsernames())) { $this->usernameUsurpe = null; } } @@ -215,7 +223,7 @@ class Ldap extends AbstractAdapter implements EventManagerAwareInterface // verif existence du login usurpé if ($this->usernameUsurpe) { // s'il nexiste pas, échec de l'authentification - if (!@$this->getLdapAuthAdapter()->getLdap()->searchEntries("(".$this->getOptions()->getLdapUsername()."=$this->usernameUsurpe)")) { + if (!@$this->getLdapAuthAdapter()->getLdap()->searchEntries("(".$this->moduleOptions->getLdapUsername()."=$this->usernameUsurpe)")) { $this->usernameUsurpe = null; $success = false; } @@ -246,22 +254,6 @@ class Ldap extends AbstractAdapter implements EventManagerAwareInterface return $this; } - /** - * @param ModuleOptions $options - */ - public function setOptions(ModuleOptions $options) - { - $this->options = $options; - } - - /** - * @return ModuleOptions - */ - public function getOptions() - { - return $this->options; - } - /** * @return \UnicaenApp\Options\ModuleOptions */ diff --git a/src/UnicaenAuth/Authentication/Adapter/LdapAdapterFactory.php b/src/UnicaenAuth/Authentication/Adapter/LdapAdapterFactory.php new file mode 100644 index 0000000..f70b66f --- /dev/null +++ b/src/UnicaenAuth/Authentication/Adapter/LdapAdapterFactory.php @@ -0,0 +1,67 @@ +<?php + +namespace UnicaenAuth\Authentication\Adapter; + +use Interop\Container\ContainerInterface; +use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; +use UnicaenAuth\Options\ModuleOptions; +use UnicaenAuth\Service\User; +use Zend\Authentication\Storage\Session; + +class LdapAdapterFactory +{ + /** + * @param ContainerInterface $container + * @param string $requestedName + * @param array|null $options + * @return Ldap + */ + public function __invoke(ContainerInterface $container, string $requestedName, array $options = null) + { + $adapter = new Ldap(); + $adapter->setStorage(new Session(Ldap::class)); + + $this->injectDependencies($adapter, $container); + +// /** @var EventManager $eventManager */ +// $eventManager = $container->get(EventManager::class); +// $adapter->setEventManager($eventManager); +// $userService = $container->get('unicaen-auth_user_service'); /* @var $userService \UnicaenAuth\Service\User */ +// $eventManager->attach('userAuthenticated', [$userService, 'userAuthenticated'], 100); +// $eventManager->attach('clear', function() use ($adapter){ +// $adapter->getStorage()->clear(); +// }); + + return $adapter; + } + + /** + * @param Ldap $adapter + * @param ContainerInterface $container + */ + private function injectDependencies(Ldap $adapter, ContainerInterface $container) + { + /** @var User $userService */ + $userService = $container->get('unicaen-auth_user_service'); + $adapter->setUserService($userService); + + /** @var LdapPeopleMapper $ldapPeopleMapper */ + $ldapPeopleMapper = $container->get('ldap_people_mapper'); + $adapter->setLdapPeopleMapper($ldapPeopleMapper); + + $options = array_merge( + $container->get('zfcuser_module_options')->toArray(), + $container->get('unicaen-auth_module_options')->toArray()); + $moduleOptions = new ModuleOptions($options); + $adapter->setModuleOptions($moduleOptions); + + $substitut = $moduleOptions->getLdap()['type'] ?? null; + if ($substitut !== null) { + $adapter->setType($substitut); + } + + /** @var \UnicaenApp\Options\ModuleOptions $appModuleOptions */ + $appModuleOptions = $container->get('unicaen-app_module_options'); + $adapter->setAppModuleOptions($appModuleOptions); + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Authentication/Adapter/Shib.php b/src/UnicaenAuth/Authentication/Adapter/Shib.php new file mode 100644 index 0000000..8222f89 --- /dev/null +++ b/src/UnicaenAuth/Authentication/Adapter/Shib.php @@ -0,0 +1,179 @@ +<?php + +namespace UnicaenAuth\Authentication\Adapter; + +use UnicaenAuth\Controller\AuthController; +use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait; +use UnicaenAuth\Service\Traits\ShibServiceAwareTrait; +use Zend\Authentication\AuthenticationService; +use Zend\Authentication\Exception\UnexpectedValueException; +use Zend\Authentication\Result as AuthenticationResult; +use Zend\EventManager\Event; +use Zend\EventManager\EventInterface; +use Zend\Http\Response; +use Zend\Router\RouteInterface; +use ZfcUser\Authentication\Adapter\ChainableAdapter; + +/** + * CAS authentication adpater + * + * @author Bertrand GAUTHIER <bertrand.gauthier@unicaen.fr> + */ +class Shib extends AbstractAdapter +{ + use ModuleOptionsAwareTrait; + use ShibServiceAwareTrait; + + const TYPE = 'shib'; + + /** + * @var string + */ + protected $type = self::TYPE; + + /** + * @var AuthenticationService + */ + protected $authenticationService; + + /** + * @param AuthenticationService $authenticationService + * @return self + */ + public function setAuthenticationService(AuthenticationService $authenticationService) + { + $this->authenticationService = $authenticationService; + return $this; + } + + /** + * @var RouteInterface + */ + private $router; + + /** + * @param RouteInterface $router + */ + public function setRouter(RouteInterface $router) + { + $this->router = $router; + } + + /** + * Réalise l'authentification. + * + * @param EventInterface $e + * @throws UnexpectedValueException + * @see ChainableAdapter + */ + public function authenticate(EventInterface $e) + { + // 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(). + $e = $e->getTarget(); + + $type = $e->getRequest()->getPost()->get('type'); + if ($type !== $this->type) { + return; + } + + /* DS : modification liée à une boucle infinie lors de l'authentification CAS */ + if ($this->isSatisfied()) { + $storage = $this->getStorage()->read(); + $e->setIdentity($storage['identity']) + ->setCode(AuthenticationResult::SUCCESS) + ->setMessages(['Authentication successful.']); + return; + } + + if (! $this->isEnabled()) { + return; + } + + $shibUser = $this->shibService->getAuthenticatedUser(); + + if ($shibUser === null) { + $redirectUrl = $this->router->assemble(['type' => 'shib'], [ + 'name' => 'zfcuser/authenticate', + 'query' => ['redirect' => $e->getRequest()->getQuery()->get('redirect')]] + ); + $shibbolethTriggerUrl = $this->router->assemble([], [ + 'name' => 'auth/shibboleth', + 'query' => ['redirect' => $redirectUrl]] + ); + $response = new Response(); + $response->getHeaders()->addHeaderLine('Location', $shibbolethTriggerUrl); + $response->setStatusCode(302); + + return $response; + } + + $identity = $shibUser->getEppn(); + + $e->setIdentity($identity); + $this->setSatisfied(true); + $storage = $this->getStorage()->read(); + $storage['identity'] = $e->getIdentity(); + $this->getStorage()->write($storage); + $e->setCode(AuthenticationResult::SUCCESS) + ->setMessages(['Authentication successful.']); + } + + /** + * @return bool + */ + protected function isEnabled() + { + $config = $this->moduleOptions->getShib(); + + if (isset($config['enabled'])) { + return (bool) $config['enabled']; + } + + return false; + } + + /** + * + * @param Event $e + * @see ChainableAdapter + */ + public function logout(Event $e) + { + if (! $this->isEnabled()) { + return; + } + + $storage = $this->getStorage()->read(); + if (! isset($storage['identity'])) { + return; + } + + $this->getStorage()->clear(); + + // désactivation de l'usurpation d'identité éventuelle + $this->shibService->deactivateUsurpation(); + + // URL vers laquelle on redirige après déconnexion + $returnUrl = $this->router->assemble([], [ + 'name' => 'zfcuser/logout', + 'force_canonical' => true, + ]); + $shibbolethLogoutUrl = $this->shibService->getLogoutUrl($returnUrl); + + $response = new Response(); + $response->getHeaders()->addHeaderLine('Location', $shibbolethLogoutUrl); + $response->setStatusCode(302); + + /** + * Problème : l'IDP Shibboleth ne redirige pas correctement vers l'URL demandée après déconnexion. + * Solution : pour l'instant, on fait le nécessaire ici (qui devrait être fait normalement dans + * {@see AuthController::logoutAction()} si l'IDP redonnait la main à l'appli. + * todo: supprimer cette verrue lorsque l'IDP redirigera correctement. + */ + $this->authenticationService->clearIdentity(); + + return $response; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Authentication/Adapter/ShibAdapterFactory.php b/src/UnicaenAuth/Authentication/Adapter/ShibAdapterFactory.php new file mode 100644 index 0000000..09cffd4 --- /dev/null +++ b/src/UnicaenAuth/Authentication/Adapter/ShibAdapterFactory.php @@ -0,0 +1,60 @@ +<?php + +namespace UnicaenAuth\Authentication\Adapter; + +use Interop\Container\ContainerInterface; +use UnicaenAuth\Options\ModuleOptions; +use UnicaenAuth\Service\ShibService; +use Zend\Authentication\AuthenticationService; +use Zend\Authentication\Storage\Session; +use Zend\Router\RouteInterface; + +class ShibAdapterFactory +{ + /** + * @param ContainerInterface $container + * @param string $requestedName + * @param array|null $options + * @return Shib + */ + public function __invoke(ContainerInterface $container, string $requestedName, array $options = null) + { + $adapter = new Shib(); + $adapter->setStorage(new Session(Shib::class)); + + $this->injectDependencies($adapter, $container); + + return $adapter; + } + + /** + * @param Shib $adapter + * @param ContainerInterface $container + */ + private function injectDependencies(Shib $adapter, ContainerInterface $container) + { + /** @var ShibService $shibService */ + $shibService = $container->get(ShibService::class); + $adapter->setShibService($shibService); + + /** @var AuthenticationService $authenticationService */ + $authenticationService = $container->get('zfcuser_auth_service'); + $adapter->setAuthenticationService($authenticationService); + + /** @var RouteInterface $router */ + $router = $container->get('router'); + $adapter->setRouter($router); + + $options = array_merge( + $container->get('zfcuser_module_options')->toArray(), + $container->get('unicaen-auth_module_options')->toArray()); + $moduleOptions = new ModuleOptions($options); + $adapter->setModuleOptions($moduleOptions); + + // type alias + $substitut = $moduleOptions->getShib()['type'] ?? null; + if ($substitut !== null) { + $adapter->setType($substitut); + } + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Authentication/Storage/Db.php b/src/UnicaenAuth/Authentication/Storage/Db.php index 7e2d968..0432bf4 100644 --- a/src/UnicaenAuth/Authentication/Storage/Db.php +++ b/src/UnicaenAuth/Authentication/Storage/Db.php @@ -3,6 +3,7 @@ namespace UnicaenAuth\Authentication\Storage; use Doctrine\DBAL\DBALException; +use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait; use Zend\Authentication\Storage\Session; use Zend\Authentication\Storage\StorageInterface; use Zend\ServiceManager\Exception\ServiceNotFoundException; @@ -15,6 +16,8 @@ use ZfcUser\Mapper\UserInterface as UserMapper; */ class Db implements ChainableStorage { + use ModuleOptionsAwareTrait; + /** * @var StorageInterface */ @@ -39,6 +42,10 @@ class Db implements ChainableStorage */ public function read(ChainEvent $e) { + if (! $this->isEnabled()) { + return; + } + if (!$this->resolvedIdentity) { $identity = $this->findIdentity(); if ($identity) { @@ -52,6 +59,16 @@ class Db implements ChainableStorage $e->addContents('db', $this->resolvedIdentity); } + /** + * @return bool + */ + protected function isEnabled() + { + $config = $this->moduleOptions->getDb(); + + return isset($config['enabled']) && (bool) $config['enabled']; + } + /** * Writes $contents to storage * @@ -60,7 +77,7 @@ class Db implements ChainableStorage public function write(ChainEvent $e) { $contents = $e->getParam('contents'); - + $this->resolvedIdentity = null; $this->getStorage()->write($contents); } diff --git a/src/UnicaenAuth/Authentication/Storage/DbFactory.php b/src/UnicaenAuth/Authentication/Storage/DbFactory.php index 6adb669..59f8f1a 100644 --- a/src/UnicaenAuth/Authentication/Storage/DbFactory.php +++ b/src/UnicaenAuth/Authentication/Storage/DbFactory.php @@ -3,25 +3,32 @@ namespace UnicaenAuth\Authentication\Storage; use Interop\Container\ContainerInterface; -use Zend\ServiceManager\FactoryInterface; -use Zend\ServiceManager\ServiceLocatorInterface; +use UnicaenAuth\Authentication\Adapter\Db as DbAdapter; +use UnicaenAuth\Options\ModuleOptions; +use Zend\Authentication\Storage\Session; use ZfcUser\Mapper\UserInterface as UserMapper; -class DbFactory implements FactoryInterface +class DbFactory { - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + /** + * @param ContainerInterface $container + * @param string $requestedName + * @param array|null $moduleOptions + * @return Db + */ + public function __invoke(ContainerInterface $container, string $requestedName, array $moduleOptions = null) { /** @var UserMapper $mapper */ $mapper = $container->get('zfcuser_user_mapper'); + /** @var ModuleOptions $moduleOptions */ + $moduleOptions = $container->get('unicaen-auth_module_options'); + $storage = new Db(); + $storage->setStorage(new Session(DbAdapter::class)); $storage->setMapper($mapper); + $storage->setModuleOptions($moduleOptions); return $storage; } - - public function createService(ServiceLocatorInterface $serviceLocator) - { - return $this->__invoke($serviceLocator, '?'); - } } \ No newline at end of file diff --git a/src/UnicaenAuth/Authentication/Storage/Ldap.php b/src/UnicaenAuth/Authentication/Storage/Ldap.php index aafe406..d9a27d7 100644 --- a/src/UnicaenAuth/Authentication/Storage/Ldap.php +++ b/src/UnicaenAuth/Authentication/Storage/Ldap.php @@ -4,7 +4,7 @@ namespace UnicaenAuth\Authentication\Storage; use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; use UnicaenAuth\Entity\Ldap\People; -use UnicaenAuth\Options\ModuleOptions; +use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait; use Zend\Authentication\Storage\Session; use Zend\Authentication\Storage\StorageInterface; @@ -15,6 +15,8 @@ use Zend\Authentication\Storage\StorageInterface; */ class Ldap implements ChainableStorage { + use ModuleOptionsAwareTrait; + /** * @var StorageInterface */ @@ -24,11 +26,6 @@ class Ldap implements ChainableStorage * @var LdapPeopleMapper */ protected $mapper; - - /** - * @var ModuleOptions - */ - protected $options; /** * @var People @@ -36,15 +33,15 @@ class Ldap implements ChainableStorage protected $resolvedIdentity; /** - * Returns the contents of storage - * - * Behavior is undefined when storage is empty. - * * @param ChainEvent $e - * @return People + * @return People|null */ public function read(ChainEvent $e) { + if (! $this->isEnabled()) { + return null; + } + $identity = $this->findIdentity(); $e->addContents('ldap', $identity); @@ -53,6 +50,21 @@ class Ldap implements ChainableStorage } /** + * @return bool + */ + protected function isEnabled() + { + $configLdap = $this->moduleOptions->getLdap(); + $configCas = $this->moduleOptions->getCas(); + + return + isset($configLdap['enabled']) && (bool) $configLdap['enabled'] || + isset($configCas['enabled']) && (bool) $configCas['enabled']; + } + + /** + * Recherche l'entité LDAP correspondant à l'identifiant trouvé en session. + * * @return People|null */ protected function findIdentity() @@ -155,22 +167,4 @@ class Ldap implements ChainableStorage $this->mapper = $mapper; return $this; } - - /** - * @param ModuleOptions $options - * @return Ldap - */ - public function setOptions(ModuleOptions $options = null) - { - $this->options = $options; - return $this; - } - - /** - * @return ModuleOptions - */ - public function getOptions() - { - return $this->options; - } } diff --git a/src/UnicaenAuth/Authentication/Storage/LdapFactory.php b/src/UnicaenAuth/Authentication/Storage/LdapFactory.php index 3c337b4..0530e12 100644 --- a/src/UnicaenAuth/Authentication/Storage/LdapFactory.php +++ b/src/UnicaenAuth/Authentication/Storage/LdapFactory.php @@ -4,29 +4,25 @@ namespace UnicaenAuth\Authentication\Storage; use Interop\Container\ContainerInterface; use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; +use UnicaenAuth\Authentication\Adapter\Ldap as LdapAdapter; use UnicaenAuth\Options\ModuleOptions; -use Zend\ServiceManager\FactoryInterface; -use Zend\ServiceManager\ServiceLocatorInterface; +use Zend\Authentication\Storage\Session; -class LdapFactory implements FactoryInterface +class LdapFactory { - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + public function __invoke(ContainerInterface $container, $requestedName, array $moduleOptions = null) { /** @var LdapPeopleMapper $mapper */ $mapper = $container->get('ldap_people_mapper'); - /** @var ModuleOptions $options */ - $options = $container->get('unicaen-auth_module_options'); + /** @var ModuleOptions $moduleOptions */ + $moduleOptions = $container->get('unicaen-auth_module_options'); $storage = new Ldap(); + $storage->setStorage(new Session(LdapAdapter::class)); $storage->setMapper($mapper); - $storage->setOptions($options); + $storage->setModuleOptions($moduleOptions); return $storage; } - - public function createService(ServiceLocatorInterface $serviceLocator) - { - return $this->__invoke($serviceLocator, '?'); - } } \ No newline at end of file diff --git a/src/UnicaenAuth/Authentication/Storage/Shib.php b/src/UnicaenAuth/Authentication/Storage/Shib.php index 76b7cc8..20d8b83 100644 --- a/src/UnicaenAuth/Authentication/Storage/Shib.php +++ b/src/UnicaenAuth/Authentication/Storage/Shib.php @@ -3,7 +3,9 @@ namespace UnicaenAuth\Authentication\Storage; use UnicaenAuth\Entity\Shibboleth\ShibUser; +use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait; use UnicaenAuth\Service\Traits\ShibServiceAwareTrait; +use Zend\Authentication\Exception\ExceptionInterface; use Zend\Authentication\Storage\Session; use Zend\Authentication\Storage\StorageInterface; use Zend\ServiceManager\ServiceManager; @@ -15,6 +17,7 @@ use Zend\ServiceManager\ServiceManager; */ class Shib implements ChainableStorage { + use ModuleOptionsAwareTrait; use ShibServiceAwareTrait; /** @@ -33,16 +36,15 @@ class Shib implements ChainableStorage protected $serviceManager; /** - * Returns the contents of storage - * - * Behavior is undefined when storage is empty. - * * @param ChainEvent $e - * @return ShibUser - * @throws \Zend\Authentication\Exception\ExceptionInterface + * @return ShibUser|null */ public function read(ChainEvent $e) { + if (! $this->isEnabled()) { + return null; + } + $shibUser = $this->getAuthenticatedUser(); $e->addContents('shib', $shibUser); @@ -50,6 +52,16 @@ class Shib implements ChainableStorage return $shibUser; } + /** + * @return bool + */ + protected function isEnabled() + { + $config = $this->moduleOptions->getShib(); + + return isset($config['enabled']) && (bool) $config['enabled']; + } + /** * @return null|ShibUser */ @@ -59,6 +71,14 @@ class Shib implements ChainableStorage return $this->resolvedIdentity; } + $identity = $this->getStorage()->read(); + + // L'identité en session doit ressembler à un EPPN. + $looksLikeEppn = strpos($identity, '@') !== false; + if (! $looksLikeEppn) { + return null; + } + $this->resolvedIdentity = $this->shibService->getAuthenticatedUser(); return $this->resolvedIdentity; @@ -68,7 +88,7 @@ class Shib implements ChainableStorage * Writes $contents to storage * * @param ChainEvent $e - * @throws \Zend\Authentication\Exception\ExceptionInterface + * @throws ExceptionInterface */ public function write(ChainEvent $e) { @@ -81,7 +101,7 @@ class Shib implements ChainableStorage * Clears contents from storage * * @param ChainEvent $e - * @throws \Zend\Authentication\Exception\ExceptionInterface + * @throws ExceptionInterface */ public function clear(ChainEvent $e) { diff --git a/src/UnicaenAuth/Authentication/Storage/ShibFactory.php b/src/UnicaenAuth/Authentication/Storage/ShibFactory.php index decd9e8..ae7b58c 100644 --- a/src/UnicaenAuth/Authentication/Storage/ShibFactory.php +++ b/src/UnicaenAuth/Authentication/Storage/ShibFactory.php @@ -3,25 +3,31 @@ namespace UnicaenAuth\Authentication\Storage; use Interop\Container\ContainerInterface; +use UnicaenAuth\Options\ModuleOptions; use UnicaenAuth\Service\ShibService; -use Zend\ServiceManager\FactoryInterface; -use Zend\ServiceManager\ServiceLocatorInterface; +use Zend\Authentication\Storage\Session; -class ShibFactory implements FactoryInterface +class ShibFactory { - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + /** + * @param ContainerInterface $container + * @param string $requestedName + * @param array|null $moduleOptions + * @return Shib + */ + public function __invoke(ContainerInterface $container, string $requestedName, array $moduleOptions = null) { /** @var ShibService $shibService */ $shibService = $container->get(ShibService::class); + /** @var ModuleOptions $moduleOptions */ + $moduleOptions = $container->get('unicaen-auth_module_options'); + $storage = new Shib(); + $storage->setStorage(new Session(\UnicaenAuth\Authentication\Adapter\Db::class)); $storage->setShibService($shibService); + $storage->setModuleOptions($moduleOptions); return $storage; } - - public function createService(ServiceLocatorInterface $serviceLocator) - { - return $this->__invoke($serviceLocator, '?'); - } } \ No newline at end of file diff --git a/src/UnicaenAuth/Controller/AuthController.php b/src/UnicaenAuth/Controller/AuthController.php index 2e90358..2558653 100644 --- a/src/UnicaenAuth/Controller/AuthController.php +++ b/src/UnicaenAuth/Controller/AuthController.php @@ -6,29 +6,58 @@ use DomainException; use UnicaenApp\Controller\Plugin\AppInfos; use UnicaenApp\Controller\Plugin\Mail; use UnicaenApp\Exception\RuntimeException; +use UnicaenAuth\Authentication\Adapter\Db; +use UnicaenAuth\Authentication\Adapter\Ldap; +use UnicaenAuth\Authentication\Adapter\Shib; +use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait; +use UnicaenAuth\Service\ShibService; +use UnicaenAuth\Service\Traits\CasServiceAwareTrait; use UnicaenAuth\Service\Traits\ShibServiceAwareTrait; use UnicaenAuth\Service\Traits\UserServiceAwareTrait; use Zend\Authentication\AuthenticationService; -use Zend\Authentication\Exception\ExceptionInterface; +use Zend\Form\FormInterface; use Zend\Http\Request; use Zend\Http\Response; use Zend\Mvc\Controller\AbstractActionController; +use Zend\Mvc\Plugin\FlashMessenger\FlashMessenger; +use Zend\Stdlib\ResponseInterface; 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() + * @method FlashMessenger flashMessenger() * * @author Bertrand GAUTHIER <bertrand.gauthier at unicaen.fr> */ class AuthController extends AbstractActionController { + use CasServiceAwareTrait; use ShibServiceAwareTrait; use UserServiceAwareTrait; + use ModuleOptionsAwareTrait; + + /** + * @var FormInterface[] [type => FormInterface] + */ + protected $loginFormForType; + + /** + * @var callable $redirectCallback + */ + protected $redirectCallback; + + /** + * @param callable $redirectCallback + * @return self + */ + public function setRedirectCallback(callable $redirectCallback): self + { + $this->redirectCallback = $redirectCallback; + return $this; + } /** * @var AuthenticationService @@ -44,82 +73,198 @@ class AuthController extends AbstractActionController } /** - * Cette action peut être appelée lorsque l'authentification Shibboleth est activée - * (unicaen-auth.shibboleth.enable === true). - * - * > Si la config Apache de Shibboleth est correcte, une requête à l'adresse correspondant à cette action - * (suite au clic sur le bouton "Authentification Shibboleth", typiquement) - * est détournée par Apache pour réaliser l'authentification Shibboleth. - * Ce n'est qu'une fois l'authentification réalisée avec succès que cette action est appelée. - * - * > Si la config Apache de Shibboleth est incorrecte ou absente (localhost par exemple), et que la simulation - * Shibboleth est activée dans la config (unicaen-auth.shibboleth.simulate), cette action est appelée et - * la simulation est enclenchée. - * - * @return Response|array + * @param string $type + * @return FormInterface */ - public function shibbolethAction() + public function getLoginFormForType(string $type) { - $operation = $this->params()->fromRoute('operation'); + if (! isset($this->loginFormForType[$type])) { + throw new RuntimeException("Pas de formulaire spécifié pour le type '$type'"); + } + + return $this->loginFormForType[$type]; + } - if ($operation === 'deconnexion') { - return $this->shibbolethLogout(); + /** + * @param FormInterface $loginForm + * @param string $type + * @return self + */ + public function setLoginFormForType(FormInterface $loginForm, string $type): self + { + $this->loginFormForType[$type] = $loginForm; + + return $this; + } + + /** + * @todo Make this dynamic / translation-friendly + * @var string + */ + protected $failedLoginMessage = "Identifiant ou mot de passe incorrect."; + + /** + * Login form + */ + public function loginAction() + { + if ($this->zfcUserAuthentication()->hasIdentity()) { + return $this->redirect()->toRoute($this->moduleOptions->getLoginRedirectRoute()); } - $redirectUrl = $this->params()->fromQuery('redirect', '/'); + $request = $this->getRequest(); + $type = $this->params('type'); + $types = $this->moduleOptions->getEnabledAuthTypes(); // types d'auth activés + + if ($type === null) { + // si aucun type n'est spécifié dans la requête, on prend le 1er type activé. + $type = key($types); + // si le type possède un subsitut (ex: 'local'), on l'utilise. + $type = $types[$type]['type'] ?? $type; + return $this->redirect()->toRoute(null, ['type' => $type], ['query' => $this->params()->fromQuery()], true); + } elseif (isset($types[$type]['type'])) { + // si le type spécifié possède un subsitut (ex: 'local'), on l'utilise. + $type = $types[$type]['type']; + return $this->redirect()->toRoute(null, ['type' => $type], ['query' => $this->params()->fromQuery()], true); + } - $shibUser = $this->shibService->getAuthenticatedUser(); - if ($shibUser === null) { - return []; // une page d'aide s'affichera si les données issues de Shibboleth attendues sont absentes + $form = $this->getLoginFormForType($type); + + // si le formulaire POSTé ne possède aucun champ identifiant, on va directement à authenticateAction() + if ($request->isPost() and ! $request->getPost()->get('identity')) { + return $this->redirect()->toRoute('zfcuser/authenticate', [], ['query' => $this->params()->fromQuery()], true); + } + + if ($this->moduleOptions->getUseRedirectParameterIfPresent() && $request->getQuery()->get('redirect')) { + $redirect = $request->getQuery()->get('redirect'); + } else { + $redirect = false; } - // arrivé ici, l'authentification shibboleth a été faite (réellement ou simulée) et a réussie. + $queryParams = ['query' => $redirect ? ['redirect' => $redirect] : []]; + $url = $this->url()->fromRoute(null, [], $queryParams, true); + $form->setAttribute('action', $url); + + if (!$request->isPost()) { + return array( + 'types' => $this->moduleOptions->getEnabledAuthTypes(), + 'type' => $type, + 'loginForm' => $form, + 'redirect' => $redirect, + 'enableRegistration' => $this->moduleOptions->getEnableRegistration(), + ); + } - $this->setStoredAuthenticatedUsername($shibUser->getUsername()); - $this->userService->userAuthenticated($shibUser); + $form->setData($request->getPost()); - return $this->redirect()->toUrl($redirectUrl); + if (!$form->isValid()) { + $this->flashMessenger()->setNamespace('zfcuser-login-form')->addMessage($this->failedLoginMessage); + return $this->redirect()->toUrl($url); + } + + // clear adapters + $this->zfcUserAuthentication()->getAuthAdapter()->resetAdapters(); + $this->zfcUserAuthentication()->getAuthService()->clearIdentity(); + + return $this->authenticateAction(); } /** - * Déconnexion Shibboleth. - * - * @return array|Response + * General-purpose authentication action */ - private function shibbolethLogout() + public function authenticateAction() { - // déconnexion applicative quoiqu'il arrive - $this->zfcUserAuthentication()->getAuthAdapter()->resetAdapters(); - $this->zfcUserAuthentication()->getAuthAdapter()->logoutAdapters(); - $this->zfcUserAuthentication()->getAuthService()->clearIdentity(); + if ($this->zfcUserAuthentication()->hasIdentity()) { +// return $this->redirect()->toRoute($this->moduleOptions->getLoginRedirectRoute()); + return $this->redirect()->toUrl('https://fr.wikipedia.org'); + } - // déconnexion Shibboleth le cas échéant - if ($this->shibService->isShibbolethEnabled()) { - // désactivation de l'usurpation d'identité éventuelle - $this->shibService->deactivateUsurpation(); + $type = $this->params('type'); + $adapter = $this->zfcUserAuthentication()->getAuthAdapter(); + $redirect = $this->params()->fromPost('redirect', $this->params()->fromQuery('redirect', false)); - // URL par défaut vers laquelle on redirige après déconnexion : accueil - $homeUrl = $this->url()->fromRoute('home', [], ['force_canonical' => true]); - $returnAbsoluteUrl = $this->params()->fromQuery('return', $homeUrl); + $this->getRequest()->getPost()->set('type', $type); + $result = $adapter->prepareForAuthentication($this->getRequest()); - return $this->redirect()->toUrl($this->shibService->getLogoutUrl($returnAbsoluteUrl)); - } else { - return []; // une page d'aide s'affichera + // Return early if an adapter returned a response + if ($result instanceof ResponseInterface) { + return $result; + } + + $auth = $this->zfcUserAuthentication()->getAuthService()->authenticate($adapter); + + if (!$auth->isValid()) { + $this->flashMessenger()->setNamespace('zfcuser-login-form')->addMessage($this->failedLoginMessage); + $adapter->resetAdapters(); + $url = $this->url()->fromRoute(null, [], ['query' => $redirect ? ['redirect' => $redirect] : []], true); + return $this->redirect()->toUrl($url); } + + $redirect = $this->redirectCallback; + + return $redirect(); } /** - * @param string $username + * Logout and clear the identity */ - private function setStoredAuthenticatedUsername($username) + public function logoutAction() { - /** @var AuthenticationService $authService */ - $authService = $this->authenticationService; - try { - $authService->getStorage()->write($username); - } catch (ExceptionInterface $e) { - throw new RuntimeException("Impossible d'écrire dans le storage"); + $chain = $this->zfcUserAuthentication()->getAuthAdapter(); + $service = $this->zfcUserAuthentication()->getAuthService(); + + $chain->resetAdapters(); + + /** + * @see Db::logout() + * @see Ldap::logout() + * @see Shib::logout() + */ + $result = $chain->logoutAdapters(); + + $service->clearIdentity(); + + if ($result instanceof ResponseInterface) { + return $result; } + + $redirect = $this->redirectCallback; + + return $redirect(); + } + + /** + * Cette action peut être appelée lorsque l'authentification Shibboleth est activée + * (unicaen-auth.shibboleth.enable === true). + * + * > Si la config Apache du module Shibboleth est correcte sur le serveur d'appli, une requête à l'adresse + * correspondant à cette action sera détournée par Apache pour réaliser l'authentification Shibboleth. + * Une fois l'authentification réalisée avec succès, le Apache renvoie une nouvelle requête + * à l'adresse correspondant à cette action, et l'utilisateur authentifié est disponible via + * {@see ShibService::getAuthenticatedUser()}. + * + * > Par contre, si la config Apache du module Shibboleth est incorrecte ou absente (sur votre machine de dev par + * exemple), alors : + * - si la simulation Shibboleth est activée dans la config du module unicaen/auth + * (unicaen-auth.shibboleth.simulate), c'est l'utilisateur configurée qui sera authentifié ; + * - sinon, une page d'aide s'affichera indiquant que la config Apache du module Shibboleth est sans doute + * erronée. + * + * @return Response|array + */ + public function shibbolethAction() + { + $shibUser = $this->shibService->getAuthenticatedUser(); + // NB: si la simulation d'authentification est activée (cf. config), $shibUser !== null. + + if ($shibUser === null) { + return []; // affichage d'une page d'aide + } + + // URL vers laquelle rediriger une fois l'authentification réussie + $redirectUrl = $this->params()->fromQuery('redirect', '/'); + + return $this->redirect()->toUrl($redirectUrl); } /** diff --git a/src/UnicaenAuth/Controller/AuthControllerFactory.php b/src/UnicaenAuth/Controller/AuthControllerFactory.php index 609c96f..6d953bf 100644 --- a/src/UnicaenAuth/Controller/AuthControllerFactory.php +++ b/src/UnicaenAuth/Controller/AuthControllerFactory.php @@ -3,9 +3,18 @@ namespace UnicaenAuth\Controller; use Interop\Container\ContainerInterface; +use UnicaenAuth\Authentication\Adapter\Cas; +use UnicaenAuth\Authentication\Adapter\Db; +use UnicaenAuth\Authentication\Adapter\Ldap; +use UnicaenAuth\Authentication\Adapter\Shib; +use UnicaenAuth\Form\CasLoginForm; +use UnicaenAuth\Form\ShibLoginForm; +use UnicaenAuth\Options\ModuleOptions; +use UnicaenAuth\Service\CasService; use UnicaenAuth\Service\ShibService; use UnicaenAuth\Service\User as UserService; use Zend\Authentication\AuthenticationService; +use ZfcUser\Controller\RedirectCallback; class AuthControllerFactory { @@ -15,6 +24,9 @@ class AuthControllerFactory */ public function __invoke(ContainerInterface $container) { + /** @var CasService $casService */ + $casService = $container->get(CasService::class); + /** @var ShibService $shibService */ $shibService = $container->get(ShibService::class); @@ -24,11 +36,31 @@ class AuthControllerFactory /** @var AuthenticationService $authService */ $authService = $container->get('zfcuser_auth_service'); + /** @var ModuleOptions $moduleOptions */ + $moduleOptions = $container->get('unicaen-auth_module_options'); + + /* @var RedirectCallback $redirectCallback */ + $redirectCallback = $container->get('zfcuser_redirect_callback'); + $controller = new AuthController(); + $controller->setCasService($casService); $controller->setShibService($shibService); $controller->setUserService($userService); $controller->setAuthenticationService($authService); + $controller->setModuleOptions($moduleOptions); + $controller->setRedirectCallback($redirectCallback); + + $this->injectLoginFormForTypes($container, $controller); return $controller; } + + private function injectLoginFormForTypes(ContainerInterface $container, AuthController $controller) + { + $controller->setLoginFormForType($container->get('zfcuser_login_form'), 'local'); + $controller->setLoginFormForType($container->get('zfcuser_login_form'), Db::TYPE); + $controller->setLoginFormForType($container->get('zfcuser_login_form'), Ldap::TYPE); + $controller->setLoginFormForType($container->get(CasLoginForm::class), Cas::TYPE); + $controller->setLoginFormForType($container->get(ShibLoginForm::class), Shib::TYPE); + } } \ No newline at end of file diff --git a/src/UnicaenAuth/Form/CasLoginForm.php b/src/UnicaenAuth/Form/CasLoginForm.php new file mode 100644 index 0000000..0c7ab92 --- /dev/null +++ b/src/UnicaenAuth/Form/CasLoginForm.php @@ -0,0 +1,58 @@ +<?php + +namespace UnicaenAuth\Form; + +use Zend\Form\Element; +use ZfcUser\Form\ProvidesEventsForm; +use ZfcUser\Options\AuthenticationOptionsInterface; + +class CasLoginForm extends ProvidesEventsForm +{ + /** + * @var AuthenticationOptionsInterface + */ + protected $authOptions; + + public function __construct($name, AuthenticationOptionsInterface $options) + { + $this->setAuthenticationOptions($options); + + parent::__construct($name); + + $submitElement = new Element\Button('submit'); + $submitElement + ->setLabel("Authentification centralisée") + ->setAttributes(array( + 'type' => 'submit', + )); + + $this->add($submitElement, array( + 'priority' => -100, + )); + + $this->getEventManager()->trigger('init', $this); + } + + /** + * Set Authentication-related Options + * + * @param AuthenticationOptionsInterface $authOptions + * @return self + */ + public function setAuthenticationOptions(AuthenticationOptionsInterface $authOptions) + { + $this->authOptions = $authOptions; + + return $this; + } + + /** + * Get Authentication-related Options + * + * @return AuthenticationOptionsInterface + */ + public function getAuthenticationOptions() + { + return $this->authOptions; + } +} diff --git a/src/UnicaenAuth/Form/CasLoginFormFactory.php b/src/UnicaenAuth/Form/CasLoginFormFactory.php new file mode 100644 index 0000000..f71626d --- /dev/null +++ b/src/UnicaenAuth/Form/CasLoginFormFactory.php @@ -0,0 +1,16 @@ +<?php + +namespace UnicaenAuth\Form; + +use Interop\Container\ContainerInterface; +use Zend\ServiceManager\Factory\FactoryInterface; + +class CasLoginFormFactory implements FactoryInterface +{ + public function __invoke(ContainerInterface $serviceManager, $requestedName, array $options = null) + { + $options = $serviceManager->get('zfcuser_module_options'); + + return new CasLoginForm(null, $options); + } +} diff --git a/src/UnicaenAuth/Form/ShibLoginForm.php b/src/UnicaenAuth/Form/ShibLoginForm.php new file mode 100644 index 0000000..5d0c0b3 --- /dev/null +++ b/src/UnicaenAuth/Form/ShibLoginForm.php @@ -0,0 +1,58 @@ +<?php + +namespace UnicaenAuth\Form; + +use Zend\Form\Element; +use ZfcUser\Form\ProvidesEventsForm; +use ZfcUser\Options\AuthenticationOptionsInterface; + +class ShibLoginForm extends ProvidesEventsForm +{ + /** + * @var AuthenticationOptionsInterface + */ + protected $authOptions; + + public function __construct($name, AuthenticationOptionsInterface $options) + { + $this->setAuthenticationOptions($options); + + parent::__construct($name); + + $submitElement = new Element\Button('submit'); + $submitElement + ->setLabel("Fédération d'identité") + ->setAttributes(array( + 'type' => 'submit', + )); + + $this->add($submitElement, array( + 'priority' => -100, + )); + + $this->getEventManager()->trigger('init', $this); + } + + /** + * Set Authentication-related Options + * + * @param AuthenticationOptionsInterface $authOptions + * @return self + */ + public function setAuthenticationOptions(AuthenticationOptionsInterface $authOptions) + { + $this->authOptions = $authOptions; + + return $this; + } + + /** + * Get Authentication-related Options + * + * @return AuthenticationOptionsInterface + */ + public function getAuthenticationOptions() + { + return $this->authOptions; + } +} diff --git a/src/UnicaenAuth/Form/ShibLoginFormFactory.php b/src/UnicaenAuth/Form/ShibLoginFormFactory.php new file mode 100644 index 0000000..6fb263d --- /dev/null +++ b/src/UnicaenAuth/Form/ShibLoginFormFactory.php @@ -0,0 +1,16 @@ +<?php + +namespace UnicaenAuth\Form; + +use Interop\Container\ContainerInterface; +use Zend\ServiceManager\Factory\FactoryInterface; + +class ShibLoginFormFactory implements FactoryInterface +{ + public function __invoke(ContainerInterface $serviceManager, $requestedName, array $options = null) + { + $options = $serviceManager->get('zfcuser_module_options'); + + return new ShibLoginForm(null, $options); + } +} diff --git a/src/UnicaenAuth/Options/ModuleOptions.php b/src/UnicaenAuth/Options/ModuleOptions.php index 7e76ac0..c2f0301 100644 --- a/src/UnicaenAuth/Options/ModuleOptions.php +++ b/src/UnicaenAuth/Options/ModuleOptions.php @@ -10,11 +10,11 @@ namespace UnicaenAuth\Options; class ModuleOptions extends \ZfcUser\Options\ModuleOptions { /** - * Paramètres concernant l'authentification locale. + * Paramètres concernant l'authentification locale propre à l'appli. * * @var array */ - protected $local = []; + protected $db = []; /** * Paramètres concernant l'authentification LDAP. @@ -41,7 +41,7 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions /** * @var array */ - protected $shibboleth = []; + protected $shib = []; /** * @var array @@ -55,20 +55,38 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions /** * @return array + * @deprecated Utiliser getDb() */ public function getLocal() { - return $this->local; + return $this->getDb(); } /** * @param array $local * @return self + * @deprecated Utiliser setDb() */ public function setLocal(array $local) { - $this->local = $local; + return $this->setDb($local); + } + /** + * @return array + */ + public function getDb() + { + return $this->db; + } + + /** + * @param array $db + * @return ModuleOptions + */ + public function setDb(array $db): ModuleOptions + { + $this->db = $db; return $this; } @@ -95,6 +113,30 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions return $this; } + /** + * @return array[] Ex: ['db' => ['enabled'=>true, 'type'=>'local'], 'shib' => ['enabled'=>true]] + */ + public function getEnabledAuthTypes() + { + $array = [ + 'db' => $this->getDb(), + 'ldap' => $this->getLdap(), + 'cas' => $this->getCas(), + 'shib' => $this->getShib(), + ]; + + $array = array_filter($array, function(array $config) { + return + isset($config['enabled']) and (bool) $config['enabled'] or + isset($config['enable']) and (bool) $config['enable']; + }); + uasort($array, function($a, $b) { + return ($a['order'] ?? 0) - ($b['order'] ?? 0); + }); + + return $array; + } + /** * set usernames allowed to make usurpation * @@ -193,12 +235,34 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions * set shibboleth connection params * * @param array $shibboleth - * * @return ModuleOptions + * @deprecated Utiliser setShib() */ public function setShibboleth(array $shibboleth = []) { - $this->shibboleth = $shibboleth; + return $this->setShib($shibboleth); + } + + /** + * get shibboleth connection params + * + * @return array + * @deprecated Utiliser getShib() + */ + public function getShibboleth() + { + return $this->getShib(); + } + + /** + * set shibboleth connection params + * + * @param array $shib + * @return ModuleOptions + */ + public function setShib(array $shib = []) + { + $this->shib = $shib; return $this; } @@ -208,9 +272,9 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions * * @return array */ - public function getShibboleth() + public function getShib() { - return $this->shibboleth; + return $this->shib; } /** diff --git a/src/UnicaenAuth/Options/ModuleOptionsFactory.php b/src/UnicaenAuth/Options/ModuleOptionsFactory.php index d054bfa..5d0cd00 100644 --- a/src/UnicaenAuth/Options/ModuleOptionsFactory.php +++ b/src/UnicaenAuth/Options/ModuleOptionsFactory.php @@ -34,32 +34,26 @@ class ModuleOptionsFactory */ private function validateConfig(array $config) { - $configKeyPath = ['unicaen-auth']; - // // Config shibboleth. + // 2 clés acceptées : 'shib' (nouvelle) et 'shibboleth' (ancienne). // - $parentKey = 'shibboleth'; - if (array_key_exists($parentKey, $config)) { - $shibConfig = $config[$parentKey]; - $configKeyPath[] = $parentKey; - + $shibConfig = []; + if (array_key_exists('shibboleth', $config)) { + $shibConfig = $config['shibboleth']; + } + if (array_key_exists('shib', $config)) { + $shibConfig = array_merge($shibConfig, $config['shib']); + } + if ($shibConfig) { try { Assertion::keyExists($shibConfig, $k = 'logout_url'); } catch (AssertionFailedException $e) { - throw new RuntimeException(sprintf( - "La clé de configuration '%s.$k' est absente (inspirez-vous du fichier de config " . - "unicaen-auth.global.php.dist du module unicaen/auth si besoin)", - join('.', $configKeyPath) - )); + throw new RuntimeException( + "Aucune des clés de configuration suivantes n'a été trouvée : 'unicaen-auth.shibboleth' ou 'unicaen-auth.shib'. " . + "Inspirez-vous des fichiers de config .dist du module unicaen/auth si besoin." + ); } - - array_pop($configKeyPath); } - - // - // Autres. - // - } } \ No newline at end of file diff --git a/src/UnicaenAuth/Service/CasService.php b/src/UnicaenAuth/Service/CasService.php new file mode 100644 index 0000000..8f3874b --- /dev/null +++ b/src/UnicaenAuth/Service/CasService.php @@ -0,0 +1,264 @@ +<?php + +namespace UnicaenAuth\Service; + +use Exception; +use phpCAS; +use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; +use UnicaenApp\Entity\Ldap\People as LdapPeople; +use UnicaenAuth\Options\ModuleOptions; +use Zend\Router\RouteInterface; +use Zend\Router\RouteStackInterface; +use ZfcUser\Authentication\Adapter\ChainableAdapter; + +class CasService +{ + /** + * @var ModuleOptions + */ + protected $options; + + /** + * @var array + */ + protected $casOptions; + + /** + * @var phpCAS + */ + protected $casClient; + + /** + * @var LdapPeopleMapper + */ + protected $ldapPeopleMapper; + + /** + * @var User + */ + private $userService; + + /** + * @param User $userService + */ + public function setUserService(User $userService) + { + $this->userService = $userService; + } + + /** + * @var RouteInterface + */ + private $router; + + /** + * @param RouteInterface $router + */ + public function setRouter(RouteInterface $router) + { + $this->router = $router; + } + + /** + * Réalise l'authentification. + * + * @return LdapPeople|null + */ + public function authenticate() + { + if (! $this->isCasEnabled()) { + return null; + } + + error_reporting($oldErrorReporting = error_reporting() & ~E_NOTICE); + + $this->getCasClient()->forceAuthentication(); + + // at this step, the user has been authenticated by the CAS server + // and the user's login name can be read with phpCAS::getUser(). + + $identity = $this->getCasClient(false)->getUser(); + + error_reporting($oldErrorReporting); + + // recherche de l'individu dans l'annuaire LDAP (il existe forcément puisque l'auth CAS a réussi) + $ldapPeople = $this->getLdapPeopleMapper()->findOneByUsername($identity); + + /* @var $userService User */ + $this->userService->userAuthenticated($ldapPeople); + + return $ldapPeople; + } + + /** + * @return bool + */ + public function isCasEnabled() + { + $config = $this->options->getCas(); + + if (! $config) { + return false; + } + if (isset($config['enabled'])) { + return (bool) $config['enabled']; + } + + return true; + } + + /** + * + * @see ChainableAdapter + */ + public function logout() + { + if (! $this->isCasEnabled()) { + return; + } + + $returnUrl = $this->router->getRequestUri()->setPath($this->router->getBaseUrl())->toString(); + $this->getCasClient()->logoutWithRedirectService($returnUrl); + } + + /** + * Retourne le client CAS. + * + * @param boolean $initClient + * @return phpCAS + * @throws Exception + */ + public function getCasClient($initClient = true) + { + if (null === $this->casClient) { + $this->casClient = new phpCAS(); + } + + if (!$initClient) { + return $this->casClient; + } + + if (null === $this->casOptions) { + $config = $this->getOptions()->getCas(); + if (!isset($config['connection']['default']['params']) || !$config['connection']['default']['params']) { + throw new Exception("Les paramètres de connexion au serveur CAS sont invalides."); + } + $this->casOptions = $config['connection']['default']['params']; + } + + $options = $this->casOptions; + + if (array_key_exists('debug', $options) && (bool) $options['debug']) { + $this->casClient->setDebug(); + } + + // initialize phpCAS + $this->casClient->client($options['version'], $options['hostname'], $options['port'], $options['uri'], true); + // no SSL validation for the CAS server + $this->casClient->setNoCasServerValidation(); + + return $this->casClient; + } + + /** + * Spécifie le client CAS. + * + * @param phpCAS $casClient + * @return self + */ + public function setCasClient(phpCAS $casClient) + { + $this->casClient = $casClient; + return $this; + } + + /** + * @param ModuleOptions $options + */ + public function setOptions(ModuleOptions $options) + { + $this->options = $options; + } + + /** + * @return ModuleOptions + */ + public function getOptions() + { + return $this->options; + } + + /** + * get ldap people mapper + * + * @return LdapPeopleMapper + */ + public function getLdapPeopleMapper() + { + return $this->ldapPeopleMapper; + } + + /** + * set ldap people mapper + * + * @param LdapPeopleMapper $mapper + * @return self + */ + public function setLdapPeopleMapper(LdapPeopleMapper $mapper) + { + $this->ldapPeopleMapper = $mapper; + + return $this; + } + + /** + * @param RouteInterface $router + */ + public function reconfigureRoutesForCasAuth(RouteInterface $router) + { + if (! $this->isCasEnabled()) { + return; + } + if (!$router instanceof RouteStackInterface) { + return; + } + + $router->addRoutes([ + // remplace les routes existantes (cf. config du module) + 'zfcuser' => [ + 'type' => 'Literal', + 'priority' => 1000, + 'options' => [ + 'route' => '/auth', + 'defaults' => [ + 'controller' => 'zfcuser', + 'action' => 'index', + ], + ], + 'may_terminate' => true, + 'child_routes' => [ + 'login' => [ + 'type' => 'Segment', + 'options' => [ + 'route' => '/connexion[/:type]', + 'defaults' => [ + 'controller' => 'zfcuser', + 'action' => 'authenticate', // zappe l'action 'login' + ], + ], + ], + 'logout' => [ + 'type' => 'Literal', + 'options' => [ + 'route' => '/deconnexion', + 'defaults' => [ + 'controller' => 'zfcuser', + 'action' => 'logout', + ], + ], + ], + ], + ], + ]); + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Service/CasServiceFactory.php b/src/UnicaenAuth/Service/CasServiceFactory.php new file mode 100644 index 0000000..88f70de --- /dev/null +++ b/src/UnicaenAuth/Service/CasServiceFactory.php @@ -0,0 +1,34 @@ +<?php + +namespace UnicaenAuth\Service; + +use Interop\Container\ContainerInterface; +use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; +use UnicaenAuth\Options\ModuleOptions; + +class CasServiceFactory +{ + public function __invoke(ContainerInterface $container) + { + $service = new CasService(); + + /** @var User $userService */ + $userService = $container->get('unicaen-auth_user_service'); + $service->setUserService($userService); + + /** @var mixed $router */ + $router = $container->get('router'); + $service->setRouter($router); + + $options = array_merge( + $container->get('zfcuser_module_options')->toArray(), + $container->get('unicaen-auth_module_options')->toArray()); + $service->setOptions(new ModuleOptions($options)); + + /** @var LdapPeopleMapper $ldapPeopleMapper */ + $ldapPeopleMapper = $container->get('ldap_people_mapper'); + $service->setLdapPeopleMapper($ldapPeopleMapper); + + return $service; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Service/ShibService.php b/src/UnicaenAuth/Service/ShibService.php index eee2d0f..3c07a8a 100644 --- a/src/UnicaenAuth/Service/ShibService.php +++ b/src/UnicaenAuth/Service/ShibService.php @@ -111,7 +111,9 @@ EOS; */ public function isShibbolethEnabled() { - return array_key_exists('enable', $this->shibbolethConfig) && (bool) $this->shibbolethConfig['enable']; + return + array_key_exists('enabled', $this->shibbolethConfig) && (bool) $this->shibbolethConfig['enabled'] || + array_key_exists('enable', $this->shibbolethConfig) && (bool) $this->shibbolethConfig['enable']; } /** @@ -451,6 +453,10 @@ EOS; */ public function reconfigureRoutesForShibAuth(TreeRouteStack $router) { + if (! $this->isShibbolethEnabled()) { + return; + } + $router->addRoutes([ // remplace les routes existantes (cf. config du module) 'zfcuser' => [ @@ -466,9 +472,9 @@ EOS; 'may_terminate' => true, 'child_routes' => [ 'login' => [ - 'type' => 'Literal', + 'type' => 'Segment', 'options' => [ - 'route' => '/connexion', + 'route' => '/connexion[/:type]', 'defaults' => [ 'controller' => 'zfcuser', // NB: lorsque l'auth Shibboleth est activée, la page propose 'action' => 'login', // 2 possibilités d'auth : LDAP et Shibboleth. diff --git a/src/UnicaenAuth/Service/ShibServiceFactory.php b/src/UnicaenAuth/Service/ShibServiceFactory.php index fd16d91..cbf84dc 100644 --- a/src/UnicaenAuth/Service/ShibServiceFactory.php +++ b/src/UnicaenAuth/Service/ShibServiceFactory.php @@ -13,7 +13,7 @@ class ShibServiceFactory $moduleOptions = $container->get('unicaen-auth_module_options'); $service = new ShibService(); - $service->setShibbolethConfig($moduleOptions->getShibboleth()); + $service->setShibbolethConfig($moduleOptions->getShib()); $service->setUsurpationAllowedUsernames($moduleOptions->getUsurpationAllowedUsernames()); return $service; diff --git a/src/UnicaenAuth/Service/Traits/CasServiceAwareTrait.php b/src/UnicaenAuth/Service/Traits/CasServiceAwareTrait.php new file mode 100644 index 0000000..9919a7c --- /dev/null +++ b/src/UnicaenAuth/Service/Traits/CasServiceAwareTrait.php @@ -0,0 +1,21 @@ +<?php + +namespace UnicaenAuth\Service\Traits; + +use UnicaenAuth\Service\CasService; + +trait CasServiceAwareTrait +{ + /** + * @var CasService + */ + protected $casService; + + /** + * @param CasService $casService + */ + public function setCasService(CasService $casService) + { + $this->casService = $casService; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Service/User.php b/src/UnicaenAuth/Service/User.php index 490793c..4112e74 100644 --- a/src/UnicaenAuth/Service/User.php +++ b/src/UnicaenAuth/Service/User.php @@ -28,7 +28,6 @@ use ZfcUser\Options\ModuleOptions as ZfcUserModuleOptions; /** * Service traitant des utilisateurs locaux de l'application. * - * @see \UnicaenAuth\Authentication\Adapter\AbstractFactory * @author Unicaen */ class User implements EventManagerAwareInterface diff --git a/src/UnicaenAuth/Service/UserContext.php b/src/UnicaenAuth/Service/UserContext.php index a0a18f0..65d2db4 100644 --- a/src/UnicaenAuth/Service/UserContext.php +++ b/src/UnicaenAuth/Service/UserContext.php @@ -212,6 +212,8 @@ class UserContext extends AbstractService implements EventManagerAwareInterface /** * Retourne parmi tous les rôles de l'utilisateur courant ceux qui peuvent être sélectionnés. * + * NB: si plus d'un rôle sont sélectionnables, on zappe le rôle "Authentifié". + * * @return array */ public function getSelectableIdentityRoles() @@ -221,6 +223,11 @@ class UserContext extends AbstractService implements EventManagerAwareInterface }; $roles = array_filter($this->getIdentityRoles(), $filter); + // si plus d'un rôle sont sélectionnables, on zappe le rôle "Authentifié" + if (count($roles) > 1 && isset($roles['user'])) { + unset($roles['user']); + } + return $roles; } @@ -228,38 +235,42 @@ class UserContext extends AbstractService implements EventManagerAwareInterface * Si un utilisateur est authentifié, retourne le rôle utilisateur sélectionné, * ou alors le premier sélectionnable si aucun n'a été sélectionné. * - * NB: Si un rôle est spécifié en session comme devant être le prochain rôle sélectionné, - * c'est lui qui est pris en compte. - * - * @return RoleInterface + * @return RoleInterface|null */ public function getSelectedIdentityRole() { - if ($this->getNextSelectedIdentityRole()) { - $this->getSessionContainer()->selectedIdentityRole = $this->getNextSelectedIdentityRole(); + // Si aucun utilisateur n'est authentifié, basta ! + if (! $this->getIdentity()) { + return null; } - if (null === $this->getSessionContainer()->selectedIdentityRole && $this->getIdentity()) { - $roles = $this->getSelectableIdentityRoles(); - $this->setSelectedIdentityRole(reset($roles)); + // NB: Si un rôle est spécifié en session comme devant être le prochain rôle sélectionné, + // c'est lui qui est pris en compte. + if ($next = $this->getNextSelectedIdentityRole()) { + $this->getSessionContainer()->selectedIdentityRole = $next; // écriture en session } - $roleId = $this->getSessionContainer()->selectedIdentityRole; - if ($roleId) { -// $roles = $this->getServiceAuthorize()->getRoles(); // Récupération de tous les rôles du provider - $roles = $this->getIdentityRoles(); - if (isset($roles[$roleId])) { - $role = $roles[$roleId]; - } else { - $role = null; - } + // Si en session aucun rôle n'est sélectionné ou si cette sélection n'est pas valide, + // on sélectionne le 1er rôle sélectionnable. + $selectedRoleId = $this->getSessionContainer()->selectedIdentityRole; + $selectableRoles = $this->getSelectableIdentityRoles(); + if (null === $selectedRoleId || ! isset($selectableRoles[$selectedRoleId])) { + $firstSelectableRoleId = reset($selectableRoles); + $this->setSelectedIdentityRole($firstSelectableRoleId); // écriture en session + } - if ($this->isRoleValid($role)) { - return $role; - } + // Rôle sélectionné en session. + $roleId = $this->getSessionContainer()->selectedIdentityRole; // lecture en session + if (! $roleId) { + return null; } - return null; + $role = $selectableRoles[$roleId]; + if (! $this->isRoleValid($role)) { + return null; + } + + return $role; } /** diff --git a/src/UnicaenAuth/View/Helper/AbstractConnectViewHelper.php b/src/UnicaenAuth/View/Helper/AbstractConnectViewHelper.php new file mode 100644 index 0000000..7239d1b --- /dev/null +++ b/src/UnicaenAuth/View/Helper/AbstractConnectViewHelper.php @@ -0,0 +1,174 @@ +<?php + +namespace UnicaenAuth\View\Helper; + +use Zend\Form\Form; +use Zend\View\Helper\AbstractHelper; +use Zend\View\Renderer\PhpRenderer; +use Zend\View\Resolver\TemplatePathStack; + +/** + * Aide de vue dessinant un formulaire d'authentification d'un type particulier, + * si l'authentification de ce type est activée. + * + * @method PhpRenderer getView() + * @author Unicaen + */ +class AbstractConnectViewHelper extends AbstractHelper +{ + /** + * @var string + */ + protected $type; + + /** + * @var string + */ + protected $title; + + /** + * @var string + */ + protected $description; + + /** + * @var bool + */ + protected $enabled = true; + + /** + * @var bool + */ + protected $passwordReset = false; + + /** + * @var Form + */ + protected $form; + + /** + * @param string $type + * @return AbstractConnectViewHelper + */ + public function setType(string $type): AbstractConnectViewHelper + { + $this->type = $type; + return $this; + } + + /** + * @param bool $enabled + * @return $this + */ + public function setEnabled($enabled = true) + { + $this->enabled = $enabled; + + return $this; + } + + /** + * @param string $title + * @return $this + */ + public function setTitle(string $title): AbstractConnectViewHelper + { + $this->title = $title; + return $this; + } + + /** + * @param string|null $description + * @return AbstractConnectViewHelper + */ + public function setDescription(?string $description): AbstractConnectViewHelper + { + $this->description = $description; + return $this; + } + + /** + * @param bool $passwordReset + * @return $this + */ + public function setPasswordReset(bool $passwordReset): AbstractConnectViewHelper + { + $this->passwordReset = $passwordReset; + return $this; + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @return string + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * @return bool + */ + public function isPasswordReset(): bool + { + return $this->passwordReset; + } + + /** + * @param Form $form + * @return $this + */ + public function __invoke(Form $form) + { + $this->form = $form; + + $this->getView()->resolver()->attach( + new TemplatePathStack(['script_paths' => [__DIR__ . "/partial"]]) + ); + + return $this; + } + + /** + * @return string + */ + public function __toString() + { + try { + return $this->getView()->render("connect", [ + 'type' => $this->type, + 'title' => $this->title, + 'description' => $this->description, + 'enabled' => $this->enabled, + 'form' => $this->form, + 'redirect' => null, + 'passwordReset' => $this->passwordReset, + ]); + } catch (\Exception $e) { + return '<p>' . $e->getMessage() . '</p><p>' . $e->getTraceAsString() . '</p>'; + } + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/View/Helper/CasConnectViewHelper.php b/src/UnicaenAuth/View/Helper/CasConnectViewHelper.php new file mode 100644 index 0000000..8cc460a --- /dev/null +++ b/src/UnicaenAuth/View/Helper/CasConnectViewHelper.php @@ -0,0 +1,21 @@ +<?php + +namespace UnicaenAuth\View\Helper; + +use Zend\View\Renderer\PhpRenderer; + +/** + * Aide de vue dessinant le bouton de connexion via Shibboleth, + * si l'authentification Shibboleth est activée. + * + * @method PhpRenderer getView() + * @author Unicaen + */ +class CasConnectViewHelper extends AbstractConnectViewHelper +{ + public function __construct() + { + $this->setType('cas'); + $this->setTitle("Authentification centralisée"); + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/View/Helper/CasConnectViewHelperFactory.php b/src/UnicaenAuth/View/Helper/CasConnectViewHelperFactory.php new file mode 100644 index 0000000..af671df --- /dev/null +++ b/src/UnicaenAuth/View/Helper/CasConnectViewHelperFactory.php @@ -0,0 +1,29 @@ +<?php + +namespace UnicaenAuth\View\Helper; + +use Interop\Container\ContainerInterface; +use UnicaenAuth\Options\ModuleOptions; + +class CasConnectViewHelperFactory +{ + /** + * @param ContainerInterface $container + * @return CasConnectViewHelper + */ + public function __invoke(ContainerInterface $container) + { + /** @var ModuleOptions $moduleOptions */ + $moduleOptions = $container->get('unicaen-auth_module_options'); + $config = $moduleOptions->getCas(); + + $enabled = isset($config['enabled']) && (bool) $config['enabled']; + $description = $config['description'] ?? null; + + $helper = new CasConnectViewHelper(); + $helper->setEnabled($enabled); + $helper->setDescription($description); + + return $helper; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/View/Helper/ConnectViewHelper.php b/src/UnicaenAuth/View/Helper/ConnectViewHelper.php new file mode 100644 index 0000000..b5eebed --- /dev/null +++ b/src/UnicaenAuth/View/Helper/ConnectViewHelper.php @@ -0,0 +1,38 @@ +<?php + +namespace UnicaenAuth\View\Helper; + +use UnicaenApp\Exception\RuntimeException; +use Zend\Form\Form; +use Zend\View\Helper\AbstractHelper; +use Zend\View\Renderer\PhpRenderer; + +/** + * Aide de vue dessinant le formulaire correspondant au type d'authentification spécifié. + * + * @method PhpRenderer getView() + * @author Unicaen + */ +class ConnectViewHelper extends AbstractHelper +{ + /** + * @param string $type + * @param Form $form + * @return AbstractConnectViewHelper + */ + public function __invoke(string $type, Form $form) + { + switch ($type) { + case 'shib': + return $this->view->shibConnect($form); + case 'cas': + return $this->view->casConnect($form); + case 'db': + return $this->view->dbConnect($form); + case 'ldap': + return $this->view->ldapConnect($form); + } + + throw new RuntimeException("Aucune aide de vue pour le type '$type'"); + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/View/Helper/DbConnectViewHelper.php b/src/UnicaenAuth/View/Helper/DbConnectViewHelper.php new file mode 100644 index 0000000..6b0ec68 --- /dev/null +++ b/src/UnicaenAuth/View/Helper/DbConnectViewHelper.php @@ -0,0 +1,21 @@ +<?php + +namespace UnicaenAuth\View\Helper; + +use Zend\View\Renderer\PhpRenderer; + +/** + * Aide de vue dessinant le formulaire d'authentification locale, + * si l'authentification locale est activée. + * + * @method PhpRenderer getView() + * @author Unicaen + */ +class DbConnectViewHelper extends AbstractConnectViewHelper +{ + public function __construct() + { + $this->setType('db'); + $this->setTitle("Avec un compte local"); + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/View/Helper/DbConnectViewHelperFactory.php b/src/UnicaenAuth/View/Helper/DbConnectViewHelperFactory.php new file mode 100644 index 0000000..eb4b8f4 --- /dev/null +++ b/src/UnicaenAuth/View/Helper/DbConnectViewHelperFactory.php @@ -0,0 +1,30 @@ +<?php + +namespace UnicaenAuth\View\Helper; + +use Interop\Container\ContainerInterface; +use UnicaenAuth\Options\ModuleOptions; + +class DbConnectViewHelperFactory +{ + /** + * @param ContainerInterface $container + * @return DbConnectViewHelper + */ + public function __invoke(ContainerInterface $container) + { + /** @var ModuleOptions $moduleOptions */ + $moduleOptions = $container->get('unicaen-auth_module_options'); + $config = $moduleOptions->getDb(); + + $enabled = isset($config['enabled']) && (bool) $config['enabled']; + $description = $config['description'] ?? null; + + $helper = new DbConnectViewHelper(); + $helper->setEnabled($enabled); + $helper->setDescription($description); + $helper->setPasswordReset(true); + + return $helper; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/View/Helper/LdapConnectViewHelper.php b/src/UnicaenAuth/View/Helper/LdapConnectViewHelper.php index dbccf4f..6cad097 100644 --- a/src/UnicaenAuth/View/Helper/LdapConnectViewHelper.php +++ b/src/UnicaenAuth/View/Helper/LdapConnectViewHelper.php @@ -2,75 +2,17 @@ namespace UnicaenAuth\View\Helper; -use Zend\Form\Form; -use Zend\View\Helper\AbstractHelper; -use Zend\View\Renderer\PhpRenderer; -use Zend\View\Resolver\TemplatePathStack; - /** * Aide de vue dessinant le formulaire d'authentification LDAP, * si l'authentification LDAP est activée. * - * @method PhpRenderer getView() * @author Unicaen */ -class LdapConnectViewHelper extends AbstractHelper +class LdapConnectViewHelper extends AbstractConnectViewHelper { - /** - * @var bool - */ - protected $enabled = true; - - /** - * @var Form - */ - protected $form; - - /** - * @param bool $enabled - * @return $this - */ - public function setEnabled($enabled = true) - { - $this->enabled = $enabled; - - return $this; - } - - /** - * @param Form $form - * @return $this - */ - public function __invoke(Form $form) + public function __construct() { - $this->form = $form; - - $this->getView()->resolver()->attach( - new TemplatePathStack(['script_paths' => [__DIR__ . "/partial"]]) - ); - - return $this; - } - - /** - * @return string - */ - public function __toString() - { - if (! $this->enabled) { - return ''; - } - - try { - return $this->getView()->render("connect", [ - 'title' => null, - 'enabled' => $this->enabled, - 'form' => $this->form, - 'redirect' => null, - 'passwordReset' => false, - ]); - } catch (\Exception $e) { - return '<p>' . $e->getMessage() . '</p><p>' . $e->getTraceAsString() . '</p>'; - } + $this->setType('ldap'); + $this->setTitle("Avec mon compte établissement"); } } \ No newline at end of file diff --git a/src/UnicaenAuth/View/Helper/LdapConnectViewHelperFactory.php b/src/UnicaenAuth/View/Helper/LdapConnectViewHelperFactory.php index 0568756..aaef569 100644 --- a/src/UnicaenAuth/View/Helper/LdapConnectViewHelperFactory.php +++ b/src/UnicaenAuth/View/Helper/LdapConnectViewHelperFactory.php @@ -15,12 +15,14 @@ class LdapConnectViewHelperFactory { /** @var ModuleOptions $moduleOptions */ $moduleOptions = $container->get('unicaen-auth_module_options'); - $ldapArrayConfig = $moduleOptions->getLdap(); + $config = $moduleOptions->getLdap(); - $ldapEnabled = isset($ldapArrayConfig['enabled']) && (bool) $ldapArrayConfig['enabled']; + $enabled = isset($config['enabled']) && (bool) $config['enabled']; + $description = $config['description'] ?? null; $helper = new LdapConnectViewHelper(); - $helper->setEnabled($ldapEnabled); + $helper->setEnabled($enabled); + $helper->setDescription($description); return $helper; } diff --git a/src/UnicaenAuth/View/Helper/LocalConnectViewHelper.php b/src/UnicaenAuth/View/Helper/LocalConnectViewHelper.php index b2475ed..189a803 100644 --- a/src/UnicaenAuth/View/Helper/LocalConnectViewHelper.php +++ b/src/UnicaenAuth/View/Helper/LocalConnectViewHelper.php @@ -2,10 +2,7 @@ namespace UnicaenAuth\View\Helper; -use Zend\Form\Form; -use Zend\View\Helper\AbstractHelper; use Zend\View\Renderer\PhpRenderer; -use Zend\View\Resolver\TemplatePathStack; /** * Aide de vue dessinant le formulaire d'authentification locale, @@ -13,64 +10,9 @@ use Zend\View\Resolver\TemplatePathStack; * * @method PhpRenderer getView() * @author Unicaen + * @deprecated Remplacé par DbConnectViewHelper */ -class LocalConnectViewHelper extends AbstractHelper +class LocalConnectViewHelper extends DbConnectViewHelper { - /** - * @var bool - */ - protected $enabled = true; - /** - * @var Form - */ - protected $form; - - /** - * @param bool $enabled - * @return $this - */ - public function setEnabled($enabled = true) - { - $this->enabled = $enabled; - - return $this; - } - - /** - * @param Form $form - * @return $this - */ - public function __invoke(Form $form) - { - $this->form = $form; - - $this->getView()->resolver()->attach( - new TemplatePathStack(['script_paths' => [__DIR__ . "/partial"]]) - ); - - return $this; - } - - /** - * @return string - */ - public function __toString() - { - if (! $this->enabled) { - return ''; - } - - try { - return $this->getView()->render("connect", [ - 'title' => "Avec un compte local", - 'enabled' => $this->enabled, - 'form' => $this->form, - 'redirect' => null, - 'passwordReset' => true, - ]); - } catch (\Exception $e) { - return '<p>' . $e->getMessage() . '</p><p>' . $e->getTraceAsString() . '</p>'; - } - } } \ No newline at end of file diff --git a/src/UnicaenAuth/View/Helper/LocalConnectViewHelperFactory.php b/src/UnicaenAuth/View/Helper/LocalConnectViewHelperFactory.php index 79a43f6..4524add 100644 --- a/src/UnicaenAuth/View/Helper/LocalConnectViewHelperFactory.php +++ b/src/UnicaenAuth/View/Helper/LocalConnectViewHelperFactory.php @@ -5,6 +5,11 @@ namespace UnicaenAuth\View\Helper; use Interop\Container\ContainerInterface; use UnicaenAuth\Options\ModuleOptions; +/** + * Class LocalConnectViewHelperFactory + * + * @deprecated + */ class LocalConnectViewHelperFactory { /** @@ -15,12 +20,15 @@ class LocalConnectViewHelperFactory { /** @var ModuleOptions $moduleOptions */ $moduleOptions = $container->get('unicaen-auth_module_options'); - $config = $moduleOptions->getLocal(); + $config = $moduleOptions->getDb(); $enabled = isset($config['enabled']) && (bool) $config['enabled']; + $description = $config['description'] ?? null; $helper = new LocalConnectViewHelper(); $helper->setEnabled($enabled); + $helper->setDescription($description); + $helper->setPasswordReset(true); return $helper; } diff --git a/src/UnicaenAuth/View/Helper/ShibConnectViewHelper.php b/src/UnicaenAuth/View/Helper/ShibConnectViewHelper.php index 44669aa..0130a57 100644 --- a/src/UnicaenAuth/View/Helper/ShibConnectViewHelper.php +++ b/src/UnicaenAuth/View/Helper/ShibConnectViewHelper.php @@ -2,8 +2,6 @@ namespace UnicaenAuth\View\Helper; -use UnicaenAuth\Service\Traits\ShibServiceAwareTrait; -use Zend\View\Helper\AbstractHelper; use Zend\View\Renderer\PhpRenderer; /** @@ -13,36 +11,11 @@ use Zend\View\Renderer\PhpRenderer; * @method PhpRenderer getView() * @author Unicaen */ -class ShibConnectViewHelper extends AbstractHelper +class ShibConnectViewHelper extends AbstractConnectViewHelper { - use ShibServiceAwareTrait; - - /** - * @return string - */ - public function __toString() - { - try { - return $this->render(); - } catch (\Exception $e) { - return '<p>' . $e->getMessage() . '</p><p>' . $e->getTraceAsString() . '</p>'; - } - } - - /** - * @return string - */ - private function render() + public function __construct() { - if (! $this->shibService->isShibbolethEnabled()) { - return ''; - } - - $shibUrl = $this->getView()->url('auth/shibboleth', [], ['query' => $this->getView()->queryParams()], true); - - return <<<EOS -<h3 class="connect-title">Via la fédération d'identité</h3> -<a href="$shibUrl" class="btn btn-success btn-lg">Fédération d'identité Renater</a> -EOS; + $this->setType('shib'); + $this->setTitle("Via la fédération d'identité"); } } \ No newline at end of file diff --git a/src/UnicaenAuth/View/Helper/ShibConnectViewHelperFactory.php b/src/UnicaenAuth/View/Helper/ShibConnectViewHelperFactory.php index 1d207a2..60311ca 100644 --- a/src/UnicaenAuth/View/Helper/ShibConnectViewHelperFactory.php +++ b/src/UnicaenAuth/View/Helper/ShibConnectViewHelperFactory.php @@ -3,7 +3,7 @@ namespace UnicaenAuth\View\Helper; use Interop\Container\ContainerInterface; -use UnicaenAuth\Service\ShibService; +use UnicaenAuth\Options\ModuleOptions; class ShibConnectViewHelperFactory { @@ -13,11 +13,16 @@ class ShibConnectViewHelperFactory */ public function __invoke(ContainerInterface $container) { - /** @var ShibService $shibService */ - $shibService = $container->get(ShibService::class); + /** @var ModuleOptions $moduleOptions */ + $moduleOptions = $container->get('unicaen-auth_module_options'); + $config = $moduleOptions->getShib(); + + $enabled = isset($config['enabled']) && (bool) $config['enabled']; + $description = $config['description'] ?? null; $helper = new ShibConnectViewHelper(); - $helper->setShibService($shibService); + $helper->setEnabled($enabled); + $helper->setDescription($description); return $helper; } diff --git a/src/UnicaenAuth/View/Helper/UserCurrent.php b/src/UnicaenAuth/View/Helper/UserCurrent.php index 3a1d51d..f5271a0 100644 --- a/src/UnicaenAuth/View/Helper/UserCurrent.php +++ b/src/UnicaenAuth/View/Helper/UserCurrent.php @@ -44,8 +44,12 @@ class UserCurrent extends UserAbstract if ($this->getIdentity()) { if ($userProfileSelectable) { - // DS : cas où aucun rôle n'est sélectionné, on affiche le rôle "user" - $role = $this->getUserContext()->getSelectedIdentityRole() ?: $this->getUserContext()->getIdentityRole('user'); + $role = $this->getUserContext()->getSelectedIdentityRole(); + // cas où aucun rôle n'est sélectionné : on affiche le 1er rôle sélectionnable ou sinon "user" + if ($role === null) { + $selectableRoles = $this->getUserContext()->getSelectableIdentityRoles(); + $role = current($selectableRoles) ?: $this->getUserContext()->getIdentityRole('user'); + } $status .= sprintf(", <small class='role-libelle'>%s</small>", !method_exists($role, '__toString') ? $role->getRoleId() : $role); } diff --git a/src/UnicaenAuth/View/Helper/partial/connect.phtml b/src/UnicaenAuth/View/Helper/partial/connect.phtml index 7d758b6..34aed63 100644 --- a/src/UnicaenAuth/View/Helper/partial/connect.phtml +++ b/src/UnicaenAuth/View/Helper/partial/connect.phtml @@ -1,11 +1,15 @@ <?php +use Application\View\Renderer\PhpRenderer; use Zend\Form\Form; /** + * @var PhpRenderer $this * @var bool $enabled * @var Form $form + * @var string $type * @var string $title + * @var string $description * @var string $redirect * @var bool $passwordReset */ @@ -16,35 +20,28 @@ use Zend\Form\Form; <?php echo $title ?> </h3> <?php endif ?> - -<?php echo $this->form()->openTag($form) ?> - -<?php if (($errors = $this->formErrors($form))): ?> - <p><?php echo $errors ?></p> -<?php endif ?> -<p class="connect-identity"> - <?php - $identity = $form->get($name = 'identity')->setAttributes(['id' => $name, 'class' => 'form-control']); - echo $this->formLabel($identity); - echo $this->formInput($identity); - ?> -</p> -<p class="connect-credentials"> - <?php - $identity = $form->get($name = 'credential')->setAttributes(['id' => $name, 'class' => 'form-control']); - echo $this->formLabel($identity); - echo $this->formInput($identity); - ?> - <?php if ($passwordReset): ?> - <a class="connect-credentials-lost" href="<?php echo $this->url('auth/requestPasswordReset') ?>">Mot de passe oublié</a> - <?php endif ?> -</p> -<?php if ($redirect): ?> - <input type="hidden" name="redirect" value="<?php echo $redirect ?>"/> +<?php if ($description): ?> + <div class="connect-description"> + <?php echo $description ?> + </div> <?php endif ?> -<p class="connect-submit"> - <?php echo $this->formButton($form->get('submit')->setAttribute('class', 'btn btn-primary')) ?> -</p> +<?php if ($messages = $this->flashMessenger('zfcuser-login-form')): ?> +<div class="messenger alert alert-danger "> + <button type="button" class="close" title="Fermer cette alerte" data-dismiss="alert">×</button> + <span class="glyphicon glyphicon-warning-sign"></span> + <?php foreach ($messages as $message): ?> + <?php echo $message ?> <br> + <?php endforeach ?> +</div> +<?php endif ?> -<?php echo $this->form()->closeTag() ?> +<?php //if ($type === 'shib'): ?> +<!-- --><?php //$shibUrl = $this->url('auth/shibboleth', [], ['query' => $this->queryParams()], true); ?> +<!-- <a href="--><?php //echo $shibUrl ?><!--" class="btn btn-success btn-lg">Fédération d'identité Renater</a>--> +<?php //elseif ($type === 'cas'): ?> +<!-- --><?php //$casUrl = $this->url('auth/cas', [], ['query' => $this->queryParams()], true); ?> +<!-- <a href="--><?php //echo $casUrl ?><!--" class="btn btn-success btn-lg">Authentification centralisée</a>--> +<?php //else: ?> + <?php echo $this->render('form.phtml', compact('form', 'type', 'redirect', 'passwordReset')) ?> +<?php //endif ?> diff --git a/src/UnicaenAuth/View/Helper/partial/form.phtml b/src/UnicaenAuth/View/Helper/partial/form.phtml new file mode 100644 index 0000000..2932e52 --- /dev/null +++ b/src/UnicaenAuth/View/Helper/partial/form.phtml @@ -0,0 +1,53 @@ +<?php + +use Application\View\Renderer\PhpRenderer; +use Zend\Form\Form; + +/** + * @var PhpRenderer $this + * @var Form $form + * @var string $type + * @var string $redirect + * @var bool $passwordReset + */ +?> + +<?php echo $this->form()->openTag($form) ?> + +<?php if (($errors = $this->formErrors($form))): ?> + <p><?php echo $errors ?></p> +<?php endif ?> + +<?php if ($form->has($name = 'identity')): ?> +<p class="connect-identity"> + <?php + $identity = $form->get($name = 'identity')->setAttributes(['id' => $name, 'class' => 'form-control']); + echo $this->formLabel($identity); + echo $this->formInput($identity); + ?> +</p> +<?php endif ?> +<?php if ($form->has($name = 'credential')): ?> +<p class="connect-credentials"> + <?php + $identity = $form->get($name = 'credential')->setAttributes(['id' => $name, 'class' => 'form-control']); + echo $this->formLabel($identity); + echo $this->formInput($identity); + ?> + <?php if ($passwordReset): ?> + <a class="connect-credentials-lost" href="<?php echo $this->url('auth/requestPasswordReset') ?>">Mot de passe oublié</a> + <?php endif ?> +</p> +<?php endif ?> + +<input type="hidden" name="type" value="<?php echo $type ?>"/> + +<?php if ($redirect): ?> + <input type="hidden" name="redirect" value="<?php echo $redirect ?>"/> +<?php endif ?> + +<p class="connect-submit"> + <?php echo $this->formButton($form->get('submit')->setAttribute('class', 'btn btn-primary')) ?> +</p> + +<?php echo $this->form()->closeTag() ?> diff --git a/tests/config/autoload/unicaen-auth.local.php b/tests/config/autoload/unicaen-auth.local.php index 9d29481..3ef8399 100644 --- a/tests/config/autoload/unicaen-auth.local.php +++ b/tests/config/autoload/unicaen-auth.local.php @@ -7,11 +7,10 @@ */ $settings = [ /** - * Paramètres de connexion au serveur CAS : - * - pour désactiver l'authentification CAS, le tableau 'cas' doit être vide. - * - pour l'activer, renseigner les paramètres. + * Paramètres de connexion au serveur CAS. */ 'cas' => [ +// 'enabled' => true, // 'connection' => array( // 'default' => array( // 'params' => array( diff --git a/view/unicaen-auth/auth/login-tabs.phtml b/view/unicaen-auth/auth/login-tabs.phtml new file mode 100644 index 0000000..ac4587f --- /dev/null +++ b/view/unicaen-auth/auth/login-tabs.phtml @@ -0,0 +1,69 @@ +<?php + +use UnicaenAuth\Authentication\Adapter; +use UnicaenAuth\View\Helper\AbstractConnectViewHelper; +use Zend\Form\Form; + +/** + * Génération des différents types de formulaire de connexion activés. + * + * @var array[] $types Types d'authentification activés, ex: ['db' => ['enabled'=>true, 'type'=>'local'], 'shib' => ['enabled'=>true]] + * @var string $type Type d'authentification dont il faut afficher le formulaire : ex: 'local', {@see Adapter\Shib::TYPE} + * @var Form $form Formulaire de connexion + * @var string $redirect URL demandée nécessitant authentification + * + * @method AbstractConnectViewHelper connect() + */ + +/** @var Form $form */ +$form->prepare(); +$form->setAttributes([ + 'class' => 'form-horizontal', + 'role' => 'form', +]); + +/** @var AbstractConnectViewHelper[] $helpers */ +$helpers = []; +foreach ($types as $t => $config) { + if (isset($config['type'])) { + if ($config['type'] === 'local') { + $substitut = $config['type']; + $helpers[$substitut] = $this->connect($t, $form); + $helpers[$substitut]->setTitle("Avec un compte local"); + } + } else { + $helpers[$t] = $this->connect($t, $form); + } +} +if ($type === null || ! array_key_exists($type, $helpers)) { + $type = key($helpers); +} +$activeHelper = null; +?> + +<ul class="nav nav-tabs nav-justified"> + <?php foreach ($helpers as $key => $helper): ?> + <?php + if ($key === $type) { + $activeHelper = $helper; + $activeClass = 'active'; + } else { + $activeClass = ''; + } + ?> + <li role="presentation" class="<?php echo $activeClass ?>"> + <?php $query = $redirect ? ['redirect' => $redirect] : [] ?> + <a href="<?php echo $this->url('zfcuser/login', ['type' => $key], ['query' => $query]) ?>"><?php echo $helper->getTitle() ?></a> + </li> + <?php endforeach ?> +</ul> + +<div class="tab-content"> + <div role="tabpanel" class="tab-pane active"> + <?php + $title = $activeHelper->getTitle(); + $activeHelper->setTitle(""); // pour éviter que le title soit répété + ?> + <?php echo (string)$activeHelper ?> + </div> +</div> diff --git a/view/unicaen-auth/auth/login.phtml b/view/unicaen-auth/auth/login.phtml new file mode 100644 index 0000000..e2624af --- /dev/null +++ b/view/unicaen-auth/auth/login.phtml @@ -0,0 +1,42 @@ +<?php +/** + * @var PhpRenderer $this + * @var array[] $types Types d'authentification activés, ex: ['db' => ['enabled'=>true, 'type'=>'local'], 'shib' => ['enabled'=>true]] + * @var string $type Type d'authentification courante : 'db', 'ldap', 'shib' + * @var Form $loginForm Formulaire de connexion + * @var string $redirect URL demandée nécessitant authentification + */ + +use Application\View\Renderer\PhpRenderer; +use Zend\Form\Form; + +$this->headTitle("Connexion") ?> + +<div class="div-connexion"> + <h1 class="page-header"><?php echo $this->translate("Connexion"); ?></h1> + <?php echo $this->partial('unicaen-auth/auth/login-tabs', [ + 'types' => $types, + 'type' => $type, + 'form' => $loginForm, + 'redirect' => $redirect, + ]); ?> +</div> + + +<!-- Création d'un compte local (si autorisée) --> +<?php if ($this->enableRegistration) : ?> +<div id="div-connexion" class="panel panel-primary"> + <?php echo $this->translate("Not registered?"); ?> <a href="<?php echo $this->url('zfcuser/register') . ($this->redirect ? '?redirect=' . $this->redirect : '') ?>"><?php echo $this->translate("Sign up!"); ?></a> +</div> +<?php endif; ?> + + +<script type="text/javascript"> + $(function() { + // focus sur le 1er champ vide visible + var $input = $("input").filter(function() { + return $(this).is(":visible") && this.value === ""; + }); + if ($input.length) $input.get(0).focus(); + }); +</script> \ No newline at end of file diff --git a/view/unicaen-auth/auth/shibboleth.phtml b/view/unicaen-auth/auth/shibboleth.phtml index 0543055..0b46698 100644 --- a/view/unicaen-auth/auth/shibboleth.phtml +++ b/view/unicaen-auth/auth/shibboleth.phtml @@ -11,7 +11,7 @@ <pre> 'unicaen-auth' => [ ... - 'shibboleth' => [ + 'shib' => [ 'enable' => true, ], ], @@ -30,7 +30,7 @@ <pre> 'unicaen-auth' => [ ... - 'shibboleth' => [ + 'shib' => [ 'enable' => true, 'simulate' => [ 'eppn' => $eppn = 'premierf@univ.fr', diff --git a/view/zfc-user/user/login.phtml b/view/zfc-user/user/login.phtml deleted file mode 100644 index 1f6e9a7..0000000 --- a/view/zfc-user/user/login.phtml +++ /dev/null @@ -1,66 +0,0 @@ -<?php -/** - * @var PhpRenderer $this - * - * @method LocalConnectViewHelper localConnect() - * @method LdapConnectViewHelper ldapConnect() - * @method ShibConnectViewHelper shibConnect() - */ - -use UnicaenAuth\View\Helper\LdapConnectViewHelper; -use UnicaenAuth\View\Helper\LocalConnectViewHelper; -use UnicaenAuth\View\Helper\ShibConnectViewHelper; -use Zend\Form\Form; -use Zend\View\Renderer\PhpRenderer; - -$this->headTitle("Connexion") ?> - -<?php -/** @var Form $form */ -$form = $this->loginForm; -$form->prepare(); -$form->setAttributes([ - 'action' => $this->url('zfcuser/login'), - 'method' => 'post', - 'class' => 'form-horizontal', - 'role' => 'form']); -?> - -<style> - .div-connexion { max-width: 350px; margin: auto auto 30px; } - .div-connexion h2 { margin: 0; } - .div-connexion .btn-primary { margin: 10px 0; } - .div-connexion .btn-success { margin: 10px 0; display: inline-block; } -</style> - -<div class="div-connexion panel panel-primary"> - - <div class="panel-heading"> - <h2><?php echo $this->translate("Connexion"); ?></h2> - </div> - - <div class="panel-body"> - <?php - $localAuthHtml = (string) $this->localConnect($form); - $ldapAuthHtml = (string) $this->ldapConnect($form); - $shibAuthHtml = (string) $this->shibConnect($form); - echo implode('<hr>', array_filter([$ldapAuthHtml, $shibAuthHtml, $localAuthHtml])); - ?> - </div> -</div> - - -<!-- Création d'un compte local (si autorisée) --> -<?php if ($this->enableRegistration) : ?> -<div id="div-connexion" class="panel panel-primary"> - <?php echo $this->translate("Not registered?"); ?> <a href="<?php echo $this->url('zfcuser/register') . ($this->redirect ? '?redirect=' . $this->redirect : '') ?>"><?php echo $this->translate("Sign up!"); ?></a> -</div> -<?php endif; ?> - - -<script type="text/javascript"> - // focus sur le 1er champ vide - $("input").filter(function() { - return this.value === ""; - }).get(0).focus(); -</script> \ No newline at end of file -- GitLab