From e3e9cf986319067b34bebb14055491e6fc8e1550 Mon Sep 17 00:00:00 2001 From: Bertrand Gauthier <bertrand.gauthier@unicaen.fr> Date: Thu, 15 Mar 2018 16:53:53 +0100 Subject: [PATCH] Authentification Shibboleth --- composer.json | 2 +- config/module.config.php | 35 +++- config/unicaen-auth.local.php.dist | 6 + .../Adapter/AbstractFactory.php | 21 ++- .../Authentication/Adapter/Ldap.php | 87 +++++++-- .../Storage/ChainableStorage.php | 14 +- .../Authentication/Storage/Shib.php | 114 ++++++++++++ src/UnicaenAuth/Controller/AuthController.php | 48 +++++ .../Controller/AuthControllerFactory.php | 29 +++ src/UnicaenAuth/Entity/Db/AbstractUser.php | 2 +- .../Entity/Shibboleth/ShibUser.php | 171 ++++++++++++++++++ .../UserRoleSelectedEventAbstractListener.php | 1 - .../Event/UserAuthenticatedEvent.php | 55 ++++-- src/UnicaenAuth/Options/ModuleOptions.php | 56 +++--- src/UnicaenAuth/Service/ShibService.php | 112 ++++++++++++ .../Service/ShibServiceFactory.php | 20 ++ .../Service/Traits/ShibServiceAwareTrait.php | 21 +++ .../Service/Traits/UserServiceAwareTrait.php | 21 +++ src/UnicaenAuth/Service/User.php | 126 ++++++------- .../View/Helper/ShibConnectViewHelper.php | 47 +++++ .../Helper/ShibConnectViewHelperFactory.php | 24 +++ view/unicaen-auth/auth/shibboleth.phtml | 23 +++ view/zfc-user/user/login.phtml | 3 + 23 files changed, 903 insertions(+), 135 deletions(-) create mode 100644 src/UnicaenAuth/Authentication/Storage/Shib.php create mode 100644 src/UnicaenAuth/Controller/AuthController.php create mode 100644 src/UnicaenAuth/Controller/AuthControllerFactory.php create mode 100644 src/UnicaenAuth/Entity/Shibboleth/ShibUser.php create mode 100644 src/UnicaenAuth/Service/ShibService.php create mode 100644 src/UnicaenAuth/Service/ShibServiceFactory.php create mode 100644 src/UnicaenAuth/Service/Traits/ShibServiceAwareTrait.php create mode 100644 src/UnicaenAuth/Service/Traits/UserServiceAwareTrait.php create mode 100644 src/UnicaenAuth/View/Helper/ShibConnectViewHelper.php create mode 100644 src/UnicaenAuth/View/Helper/ShibConnectViewHelperFactory.php create mode 100644 view/unicaen-auth/auth/shibboleth.phtml diff --git a/composer.json b/composer.json index 6a8e036..c6bae3b 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "repositories": [ { "type": "composer", - "url": "https://dev.unicaen.fr/packagist" + "url": "https://gest.unicaen.fr/packagist" } ], "require": { diff --git a/config/module.config.php b/config/module.config.php index c0e9b8f..999594a 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -1,9 +1,11 @@ <?php -use UnicaenAuth\Provider\Privilege\Privileges; +use UnicaenAuth\Controller\AuthControllerFactory; +use UnicaenAuth\Service\ShibService; +use UnicaenAuth\Service\ShibServiceFactory; +use UnicaenAuth\View\Helper\ShibConnectViewHelperFactory; $settings = [ - /** * Fournisseurs d'identité. */ @@ -119,6 +121,8 @@ return [ ['controller' => 'UnicaenApp\Controller\Application', 'action' => 'informatique-et-libertes', 'roles' => []], ['controller' => 'UnicaenApp\Controller\Application', 'action' => 'refresh-session', 'roles' => []], ['controller' => 'UnicaenAuth\Controller\Utilisateur', 'action' => 'selectionner-profil', 'roles' => []], + + ['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'shibboleth', 'roles' => []], ], ], ], @@ -167,6 +171,27 @@ return [ ], 'router' => [ 'routes' => [ + 'auth' => [ + 'type' => 'Literal', + 'options' => [ + 'route' => '/auth', + 'defaults' => [ + 'controller' => 'UnicaenAuth\Controller\Auth', + ], + ], + 'may_terminate' => false, + 'child_routes' => [ + 'shibboleth' => [ + 'type' => 'Literal', + 'options' => [ + 'route' => '/shibboleth', + 'defaults' => [ + 'action' => 'shibboleth', + ], + ], + ], + ], + ], 'zfcuser' => [ 'type' => 'Literal', 'priority' => 1000, @@ -346,6 +371,7 @@ return [ 'invokables' => [ 'UnicaenAuth\Authentication\Storage\Db' => 'UnicaenAuth\Authentication\Storage\Db', 'UnicaenAuth\Authentication\Storage\Ldap' => 'UnicaenAuth\Authentication\Storage\Ldap', + 'UnicaenAuth\Authentication\Storage\Shib' => 'UnicaenAuth\Authentication\Storage\Shib', 'UnicaenAuth\View\RedirectionStrategy' => 'UnicaenAuth\View\RedirectionStrategy', 'UnicaenAuth\Service\UserContext' => 'UnicaenAuth\Service\UserContext', 'UnicaenAuth\Service\User' => 'UnicaenAuth\Service\User', @@ -369,6 +395,7 @@ return [ 'UnicaenAuth\Service\Privilege' => 'UnicaenAuth\Service\PrivilegeServiceFactory', 'BjyAuthorize\Service\Authorize' => 'UnicaenAuth\Service\AuthorizeServiceFactory', // substituion 'zfcuser_redirect_callback' => 'UnicaenAuth\Authentication\RedirectCallbackFactory', // substituion + ShibService::class => ShibServiceFactory::class, ], 'initializers' => [ 'UnicaenAuth\Service\UserAwareInitializer', @@ -380,6 +407,9 @@ return [ 'UnicaenAuth\Controller\Utilisateur' => 'UnicaenAuth\Controller\UtilisateurController', 'UnicaenAuth\Controller\Droits' => 'UnicaenAuth\Controller\DroitsController', ], + 'factories' => [ + 'UnicaenAuth\Controller\Auth' => AuthControllerFactory::class, + ], ], 'form_elements' => [ @@ -397,6 +427,7 @@ return [ 'userInfo' => 'UnicaenAuth\View\Helper\UserInfoFactory', 'userProfileSelect' => 'UnicaenAuth\View\Helper\UserProfileSelectFactory', 'userProfileSelectRadioItem' => 'UnicaenAuth\View\Helper\UserProfileSelectRadioItemFactory', + 'shibConnect' => ShibConnectViewHelperFactory::class, ], 'invokables' => [ 'appConnection' => 'UnicaenAuth\View\Helper\AppConnection', diff --git a/config/unicaen-auth.local.php.dist b/config/unicaen-auth.local.php.dist index ee3a26e..8cecd5d 100644 --- a/config/unicaen-auth.local.php.dist +++ b/config/unicaen-auth.local.php.dist @@ -6,6 +6,12 @@ * drop this config file in it and change the values as you wish. */ $settings = [ + /** + * Activation ou non de l'authentification Shibboleth. + */ + 'shibboleth' => [ + 'enable' => false, + ], /** * Paramètres de connexion au serveur CAS : * - pour désactiver l'authentification CAS, le tableau 'cas' doit être vide. diff --git a/src/UnicaenAuth/Authentication/Adapter/AbstractFactory.php b/src/UnicaenAuth/Authentication/Adapter/AbstractFactory.php index adb8ac1..d5ce373 100644 --- a/src/UnicaenAuth/Authentication/Adapter/AbstractFactory.php +++ b/src/UnicaenAuth/Authentication/Adapter/AbstractFactory.php @@ -1,10 +1,10 @@ <?php + namespace UnicaenAuth\Authentication\Adapter; -use UnicaenApp\Exception; -use UnicaenAuth\Authentication\Adapter\Cas; -use UnicaenAuth\Authentication\Adapter\Db; -use UnicaenAuth\Authentication\Adapter\Ldap; +use UnicaenApp\Exception\LogicException; +use Zend\EventManager\EventManager; +use Zend\EventManager\EventManagerAwareInterface; use Zend\ServiceManager\AbstractFactoryInterface; use Zend\ServiceManager\ServiceLocatorInterface; @@ -34,7 +34,7 @@ class AbstractFactory implements AbstractFactoryInterface * @param ServiceLocatorInterface $serviceLocator * @param $name * @param $requestedName - * @return mixed + * @return \ZfcUser\Authentication\Adapter\AbstractAdapter */ public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName) { @@ -48,12 +48,19 @@ class AbstractFactory implements AbstractFactoryInterface 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 Exception("Service demandé inattendu : '$requestedName'!"); + throw new LogicException("Service demandé inattendu : '$requestedName'!"); break; } - if ($adapter instanceof \Zend\EventManager\EventManagerAwareInterface) { + if ($adapter instanceof EventManagerAwareInterface) { + /** @var EventManager $eventManager */ $eventManager = $serviceLocator->get('event_manager'); $adapter->setEventManager($eventManager); $userService = $serviceLocator->get('unicaen-auth_user_service'); /* @var $userService \UnicaenAuth\Service\User */ diff --git a/src/UnicaenAuth/Authentication/Adapter/Ldap.php b/src/UnicaenAuth/Authentication/Adapter/Ldap.php index 9053dd4..f9875ef 100644 --- a/src/UnicaenAuth/Authentication/Adapter/Ldap.php +++ b/src/UnicaenAuth/Authentication/Adapter/Ldap.php @@ -1,10 +1,14 @@ <?php + namespace UnicaenAuth\Authentication\Adapter; +use UnicaenApp\Exception\RuntimeException; +use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; use UnicaenAuth\Options\ModuleOptions; -use Zend\Authentication\Exception\UnexpectedValueException; -use Zend\Authentication\Result as AuthenticationResult; +use UnicaenAuth\Service\User; use Zend\Authentication\Adapter\Ldap as LdapAuthAdapter; +use Zend\Authentication\Exception\ExceptionInterface; +use Zend\Authentication\Result as AuthenticationResult; use Zend\EventManager\EventManager; use Zend\EventManager\EventManagerAwareInterface; use Zend\EventManager\EventManagerInterface; @@ -38,6 +42,11 @@ class Ldap extends AbstractAdapter implements ServiceManagerAwareInterface, Even */ protected $ldapAuthAdapter; + /** + * @var LdapPeopleMapper + */ + protected $ldapPeopleMapper; + /** * @var ModuleOptions */ @@ -52,16 +61,21 @@ class Ldap extends AbstractAdapter implements ServiceManagerAwareInterface, Even * * @param AuthEvent $e * @return boolean - * @throws UnexpectedValueException + * @throws \Zend\Authentication\Adapter\Exception\ExceptionInterface + * @throws \Zend\Ldap\Exception\LdapException * @see ChainableAdapter */ public function authenticate(AuthEvent $e) { if ($this->isSatisfied()) { - $storage = $this->getStorage()->read(); + try { + $storage = $this->getStorage()->read(); + } catch (ExceptionInterface $e) { + throw new RuntimeException("Erreur de lecture du storage"); + } $e->setIdentity($storage['identity']) - ->setCode(AuthenticationResult::SUCCESS) - ->setMessages(['Authentication successful.']); + ->setCode(AuthenticationResult::SUCCESS) + ->setMessages(['Authentication successful.']); return; } @@ -73,20 +87,36 @@ class Ldap extends AbstractAdapter implements ServiceManagerAwareInterface, Even // Failure! if (! $success) { $e->setCode(AuthenticationResult::FAILURE) - ->setMessages(['LDAP bind failed.']); + ->setMessages(['LDAP bind failed.']); + $this->setSatisfied(false); + return false; + } + + // recherche de l'individu dans l'annuaire LDAP + $ldapPeople = $this->getLdapPeopleMapper()->findOneByUsername($username); + if (!$ldapPeople) { + $e + ->setCode(AuthenticationResult::FAILURE) + ->setMessages(['Authentication failed.']); $this->setSatisfied(false); return false; } $e->setIdentity($this->usernameUsurpe ?: $username); $this->setSatisfied(true); - $storage = $this->getStorage()->read(); - $storage['identity'] = $e->getIdentity(); - $this->getStorage()->write($storage); + try { + $storage = $this->getStorage()->read(); + $storage['identity'] = $e->getIdentity(); + $this->getStorage()->write($storage); + } catch (ExceptionInterface $e) { + throw new RuntimeException("Erreur de concernant le storage"); + } $e->setCode(AuthenticationResult::SUCCESS) - ->setMessages(['Authentication successful.']); + ->setMessages(['Authentication successful.']); - $this->getEventManager()->trigger('userAuthenticated', $e); + /* @var $userService User */ + $userService = $this->getServiceManager()->get('unicaen-auth_user_service'); + $userService->userAuthenticated($ldapPeople); } /** @@ -115,9 +145,11 @@ class Ldap extends AbstractAdapter implements ServiceManagerAwareInterface, Even /** * Authentifie l'identifiant et le mot de passe spécifiés. * - * @param string $username Identifiant de connexion + * @param string $username Identifiant de connexion * @param string $credential Mot de passe * @return boolean + * @throws \Zend\Authentication\Adapter\Exception\ExceptionInterface + * @throws \Zend\Ldap\Exception\LdapException */ public function authenticateUsername($username, $credential) { @@ -149,6 +181,31 @@ class Ldap extends AbstractAdapter implements ServiceManagerAwareInterface, Even return $success; } + /** + * get ldap people mapper + * + * @return LdapPeopleMapper + */ + public function getLdapPeopleMapper() + { + if (null === $this->ldapPeopleMapper) { + $this->ldapPeopleMapper = $this->getServiceManager()->get('ldap_people_mapper'); + } + return $this->ldapPeopleMapper; + } + + /** + * set ldap people mapper + * + * @param LdapPeopleMapper $mapper + * @return self + */ + public function setLdapPeopleMapper(LdapPeopleMapper $mapper) + { + $this->ldapPeopleMapper = $mapper; + return $this; + } + /** * @param ModuleOptions $options */ @@ -164,8 +221,8 @@ class Ldap extends AbstractAdapter implements ServiceManagerAwareInterface, Even { if (!$this->options instanceof ModuleOptions) { $options = array_merge( - $this->getServiceManager()->get('zfcuser_module_options')->toArray(), - $this->getServiceManager()->get('unicaen-auth_module_options')->toArray()); + $this->getServiceManager()->get('zfcuser_module_options')->toArray(), + $this->getServiceManager()->get('unicaen-auth_module_options')->toArray()); $this->setOptions(new ModuleOptions($options)); } return $this->options; diff --git a/src/UnicaenAuth/Authentication/Storage/ChainableStorage.php b/src/UnicaenAuth/Authentication/Storage/ChainableStorage.php index 8da4199..99f5f1b 100644 --- a/src/UnicaenAuth/Authentication/Storage/ChainableStorage.php +++ b/src/UnicaenAuth/Authentication/Storage/ChainableStorage.php @@ -2,8 +2,6 @@ namespace UnicaenAuth\Authentication\Storage; -use UnicaenAuth\Authentication\Storage\ChainEvent; - interface ChainableStorage { /** @@ -11,25 +9,21 @@ interface ChainableStorage * * Behavior is undefined when storage is empty. * - * @throws InvalidArgumentException If reading contents from storage is impossible - * @return People + * @param \UnicaenAuth\Authentication\Storage\ChainEvent $e */ public function read(ChainEvent $e); - + /** * Writes $contents to storage * - * @param mixed $contents - * @throws InvalidArgumentException If writing $contents to storage is impossible - * @return void + * @param \UnicaenAuth\Authentication\Storage\ChainEvent $e */ public function write(ChainEvent $e); /** * Clears contents from storage * - * @throws InvalidArgumentException If clearing contents from storage is impossible - * @return void + * @param \UnicaenAuth\Authentication\Storage\ChainEvent $e */ public function clear(ChainEvent $e); } \ No newline at end of file diff --git a/src/UnicaenAuth/Authentication/Storage/Shib.php b/src/UnicaenAuth/Authentication/Storage/Shib.php new file mode 100644 index 0000000..1bc25ef --- /dev/null +++ b/src/UnicaenAuth/Authentication/Storage/Shib.php @@ -0,0 +1,114 @@ +<?php + +namespace UnicaenAuth\Authentication\Storage; + +use UnicaenAuth\Entity\Shibboleth\ShibUser; +use UnicaenAuth\Options\ModuleOptions; +use UnicaenAuth\Service\ShibService; +use Zend\Authentication\Storage\Session; +use Zend\Authentication\Storage\StorageInterface; +use Zend\ServiceManager\ServiceLocatorAwareInterface; +use Zend\ServiceManager\ServiceLocatorAwareTrait; +use Zend\ServiceManager\ServiceManager; + +/** + * Shibboleth authentication storage. + * + * @author Unicaen + */ +class Shib implements ChainableStorage, ServiceLocatorAwareInterface +{ + use ServiceLocatorAwareTrait; + + /** + * @var StorageInterface + */ + protected $storage; + + /** + * @var ModuleOptions + */ + protected $options; + + /** + * @var ShibUser + */ + protected $resolvedIdentity; + + /** + * @var ServiceManager + */ + protected $serviceManager; + + /** + * Returns the contents of storage + * + * Behavior is undefined when storage is empty. + * + * @param ChainEvent $e + * @return ShibUser + * @throws \Zend\Authentication\Exception\ExceptionInterface + */ + public function read(ChainEvent $e) + { + /** @var ShibService $shib */ + $shib = $this->getServiceLocator()->get(ShibService::class); + $shibUser = $shib->getAuthenticatedUser(); + + $e->addContents('shib', $shibUser); + + return $shibUser; + } + + /** + * Writes $contents to storage + * + * @param ChainEvent $e + * @throws \Zend\Authentication\Exception\ExceptionInterface + */ + public function write(ChainEvent $e) + { + $contents = $e->getParam('contents'); + $this->resolvedIdentity = null; + $this->getStorage()->write($contents); + } + + /** + * Clears contents from storage + * + * @param ChainEvent $e + * @throws \Zend\Authentication\Exception\ExceptionInterface + */ + public function clear(ChainEvent $e) + { + $this->resolvedIdentity = null; + $this->getStorage()->clear(); + } + + /** + * getStorage + * + * @return StorageInterface + */ + public function getStorage() + { + if (null === $this->storage) { + $this->setStorage(new Session()); + } + + return $this->storage; + } + + /** + * setStorage + * + * @param StorageInterface $storage + * @return self + */ + public function setStorage(StorageInterface $storage) + { + $this->storage = $storage; + + return $this; + } +} diff --git a/src/UnicaenAuth/Controller/AuthController.php b/src/UnicaenAuth/Controller/AuthController.php new file mode 100644 index 0000000..3b61708 --- /dev/null +++ b/src/UnicaenAuth/Controller/AuthController.php @@ -0,0 +1,48 @@ +<?php + +namespace UnicaenAuth\Controller; + +use UnicaenApp\Exception\RuntimeException; +use UnicaenAuth\Service\Traits\ShibServiceAwareTrait; +use UnicaenAuth\Service\Traits\UserServiceAwareTrait; +use Zend\Authentication\AuthenticationService; +use Zend\Authentication\Exception\ExceptionInterface; +use Zend\Http\Response; +use Zend\Mvc\Controller\AbstractActionController; + +/** + * Classe ajoutée lors de l'implémentation de l'auth Shibboleth. + * + * @author Bertrand GAUTHIER <bertrand.gauthier at unicaen.fr> + */ +class AuthController extends AbstractActionController +{ + use ShibServiceAwareTrait; + use UserServiceAwareTrait; + + /** + * @return Response|array + */ + public function shibbolethAction() + { + $shibUser = $this->shibService->getAuthenticatedUser(); + + if ($shibUser === null) { + return []; // la page d'aide s'affiche + } + + /** @var AuthenticationService $authService */ + $authService = $this->getServiceLocator()->get('zfcuser_auth_service'); + try { + $authService->getStorage()->write($shibUser->getId()); + } catch (ExceptionInterface $e) { + throw new RuntimeException("Impossible d'écrire dans le storage"); + } + + $this->userService->userAuthenticated($shibUser); + + $redirectUrl = $this->params()->fromQuery('redirect', '/'); + + return $this->redirect()->toUrl($redirectUrl); + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Controller/AuthControllerFactory.php b/src/UnicaenAuth/Controller/AuthControllerFactory.php new file mode 100644 index 0000000..e771651 --- /dev/null +++ b/src/UnicaenAuth/Controller/AuthControllerFactory.php @@ -0,0 +1,29 @@ +<?php + +namespace UnicaenAuth\Controller; + +use UnicaenAuth\Service\ShibService; +use UnicaenAuth\Service\User as UserService; +use Zend\Mvc\Controller\ControllerManager; + +class AuthControllerFactory +{ + /** + * @param ControllerManager $cm + * @return AuthController + */ + public function __invoke(ControllerManager $cm) + { + /** @var ShibService $shibService */ + $shibService = $cm->getServiceLocator()->get(ShibService::class); + + /* @var $userService UserService */ + $userService = $cm->getServiceLocator()->get('unicaen-auth_user_service'); + + $controller = new AuthController(); + $controller->setShibService($shibService); + $controller->setUserService($userService); + + return $controller; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Entity/Db/AbstractUser.php b/src/UnicaenAuth/Entity/Db/AbstractUser.php index eb2918e..e19aba6 100644 --- a/src/UnicaenAuth/Entity/Db/AbstractUser.php +++ b/src/UnicaenAuth/Entity/Db/AbstractUser.php @@ -205,7 +205,7 @@ abstract class AbstractUser implements UserInterface, ProviderInterface /** * Get role. * - * @return RoleInterface[] + * @return Collection */ public function getRoles() { diff --git a/src/UnicaenAuth/Entity/Shibboleth/ShibUser.php b/src/UnicaenAuth/Entity/Shibboleth/ShibUser.php new file mode 100644 index 0000000..a51faa9 --- /dev/null +++ b/src/UnicaenAuth/Entity/Shibboleth/ShibUser.php @@ -0,0 +1,171 @@ +<?php + +namespace UnicaenAuth\Entity\Shibboleth; + +use ZfcUser\Entity\UserInterface; + +class ShibUser implements UserInterface +{ + /** + * @var string + */ + protected $id; + + /** + * @var string + */ + protected $username; + + /** + * @var string + */ + protected $email; + + /** + * @var string + */ + protected $displayName; + + /** + * @var int + */ + protected $state = 1; + + + /** + * @return string + */ + public function getEppn() + { + return $this->getUsername(); + } + + /** + * Get id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Set id. + * + * @param string $id + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * Get username. + * + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * Set username. + * + * @param string $username + */ + public function setUsername($username) + { + $this->username = $username; + } + + /** + * Get email. + * + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * Set email. + * + * @param string $email + */ + public function setEmail($email) + { + $this->email = $email; + } + + /** + * Get displayName. + * + * @return string + */ + public function getDisplayName() + { + return $this->displayName; + } + + /** + * Set displayName. + * + * @param string $displayName + */ + public function setDisplayName($displayName) + { + $this->displayName = $displayName; + } + + /** + * Get password. + * + * @return string + */ + public function getPassword() + { + return 'shib'; + } + + /** + * Set password. + * + * @param string $password + */ + public function setPassword($password) + { + + } + + /** + * Get state. + * + * @return int + */ + public function getState() + { + return $this->state; + } + + /** + * Set state. + * + * @param int $state + */ + public function setState($state) + { + $this->state = $state; + } + + /** + * + * @return string + */ + public function __toString() + { + return $this->getDisplayName(); + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Event/Listener/UserRoleSelectedEventAbstractListener.php b/src/UnicaenAuth/Event/Listener/UserRoleSelectedEventAbstractListener.php index 9d56da0..24410c2 100644 --- a/src/UnicaenAuth/Event/Listener/UserRoleSelectedEventAbstractListener.php +++ b/src/UnicaenAuth/Event/Listener/UserRoleSelectedEventAbstractListener.php @@ -28,7 +28,6 @@ abstract class UserRoleSelectedEventAbstractListener implements ListenerAggregat * Renseigne les relations 'intervenant' et 'personnel' avant que l'objet soit persisté. * * @param UserRoleSelectedEvent $e - * @return */ abstract public function postSelection(UserRoleSelectedEvent $e); diff --git a/src/UnicaenAuth/Event/UserAuthenticatedEvent.php b/src/UnicaenAuth/Event/UserAuthenticatedEvent.php index dfb893f..725bf30 100644 --- a/src/UnicaenAuth/Event/UserAuthenticatedEvent.php +++ b/src/UnicaenAuth/Event/UserAuthenticatedEvent.php @@ -2,6 +2,7 @@ namespace UnicaenAuth\Event; +use UnicaenAuth\Entity\Shibboleth\ShibUser; use UnicaenApp\Entity\Ldap\People; use Zend\EventManager\Event; use ZfcUser\Entity\UserInterface; @@ -14,12 +15,26 @@ use ZfcUser\Entity\UserInterface; class UserAuthenticatedEvent extends Event { const PRE_PERSIST = 'prePersist'; + const PARAM_DB_USER = 'db_user'; const PARAM_LDAP_USER = 'ldap_user'; - + const PARAM_SHIB_USER = 'shib_user'; + + /** + * Spécifie l'entité utilisateur issue de la base de données. + * + * @param UserInterface $dbUser + * @return UserAuthenticatedEvent + */ + public function setDbUser(UserInterface $dbUser) + { + $this->setParam(self::PARAM_DB_USER, $dbUser); + return $this; + } + /** * Retourne l'entité utilisateur issue de la base de données. - * + * * @return UserInterface */ public function getDbUser() @@ -27,9 +42,21 @@ class UserAuthenticatedEvent extends Event return $this->getParam(self::PARAM_DB_USER); } + /** + * Spécifie l'entité utilisateur issue de l'annuaire LDAP. + * + * @param People $ldapUser + * @return UserAuthenticatedEvent + */ + public function setLdapUser(People $ldapUser) + { + $this->setParam(self::PARAM_LDAP_USER, $ldapUser); + return $this; + } + /** * Retourne l'entité utilisateur issue de l'annuaire LDAP. - * + * * @return People */ public function getLdapUser() @@ -38,26 +65,24 @@ class UserAuthenticatedEvent extends Event } /** - * Spécifie l'entité utilisateur issue de la base de données. - * - * @param UserInterface $dbUser + * Spécifie l'entité utilisateur issue de l'authentification Shibboleth. + * + * @param ShibUser $shibUser * @return UserAuthenticatedEvent */ - public function setDbUser(UserInterface $dbUser) + public function setShibUser(ShibUser $shibUser) { - $this->setParam(self::PARAM_DB_USER, $dbUser); + $this->setParam(self::PARAM_SHIB_USER, $shibUser); return $this; } /** - * Spécifie l'entité utilisateur issue de l'annuaire LDAP. - * - * @param People $ldapUser - * @return UserAuthenticatedEvent + * Retourne l'entité utilisateur issue de l'authentification Shibboleth. + * + * @return ShibUser */ - public function setLdapUser(People $ldapUser) + public function getShibUser() { - $this->setParam(self::PARAM_LDAP_USER, $ldapUser); - return $this; + return $this->getParam(self::PARAM_SHIB_USER); } } \ No newline at end of file diff --git a/src/UnicaenAuth/Options/ModuleOptions.php b/src/UnicaenAuth/Options/ModuleOptions.php index c5f942a..4d3b5fb 100644 --- a/src/UnicaenAuth/Options/ModuleOptions.php +++ b/src/UnicaenAuth/Options/ModuleOptions.php @@ -1,4 +1,5 @@ <?php + namespace UnicaenAuth\Options; /** @@ -18,6 +19,11 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions */ protected $saveLdapUserInDatabase = false; + /** + * @var array + */ + protected $shibboleth = []; + /** * @var array */ @@ -27,9 +33,7 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions * @var string */ protected $entityManagerName = 'doctrine.entitymanager.orm_default'; - - - + /** * set usernames allowed to make usurpation * @@ -44,8 +48,6 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions return $this; } - - /** * get usernames allowed to make usurpation * @@ -56,8 +58,6 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions return $this->usurpationAllowedUsernames; } - - /** * Spécifie si l'utilisateur authentifié doit être enregistré dans la base * de données de l'appli @@ -72,9 +72,7 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions return $this; } - - - + /** * Retourne la valeur du flag spécifiant si l'utilisateur authentifié doit être * enregistré dans la base de données de l'appli @@ -85,9 +83,7 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions { return $this->saveLdapUserInDatabase; } - - - + /** * set cas connection params * @@ -101,9 +97,7 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions return $this; } - - - + /** * get cas connection params * @@ -113,9 +107,31 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions { return $this->cas; } + + /** + * set shibboleth connection params + * + * @param array $shibboleth + * + * @return ModuleOptions + */ + public function setShibboleth(array $shibboleth = []) + { + $this->shibboleth = $shibboleth; + return $this; + } - + /** + * get shibboleth connection params + * + * @return array + */ + public function getShibboleth() + { + return $this->shibboleth; + } + /** * @return string */ @@ -123,9 +139,7 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions { return $this->entityManagerName; } - - - + /** * @param string $entityManagerName * @@ -137,6 +151,4 @@ class ModuleOptions extends \ZfcUser\Options\ModuleOptions return $this; } - - } \ No newline at end of file diff --git a/src/UnicaenAuth/Service/ShibService.php b/src/UnicaenAuth/Service/ShibService.php new file mode 100644 index 0000000..f72de2f --- /dev/null +++ b/src/UnicaenAuth/Service/ShibService.php @@ -0,0 +1,112 @@ +<?php + +namespace UnicaenAuth\Service; + +use UnicaenApp\Exception\RuntimeException; +use UnicaenAuth\Entity\Shibboleth\ShibUser; +use UnicaenAuth\Options\ModuleOptions; + +/** + * Shibboleth service + * + * @author Unicaen + */ +class ShibService +{ + /** + * @var ModuleOptions + */ + protected $options; + + /** + * @var \UnicaenAuth\Entity\Shibboleth\ShibUser + */ + protected $authenticatedUser; + + /** + * @return string + */ + static public function apacheConfigSnippet() + { + $text = <<<EOS +<Location "/"> + AuthType Shibboleth + ShibRequestSetting requireSession false + Require shibboleth +</Location> +<Location "/auth/shibboleth"> + AuthType Shibboleth + ShibRequestSetting requireSession true + Require shibboleth +</Location> +EOS; + return $text; + } + + /** + * @return boolean + */ + public function isShibbolethEnable() + { + $options = $this->options->getShibboleth(); + + return array_key_exists('enable', $options) && (bool) $options['enable']; + } + + /** + * @return ShibUser|null + */ + public function getAuthenticatedUser() + { + if ($this->authenticatedUser === null) { + if (empty($_SERVER['REMOTE_USER'])) { + return null; + } + $this->authenticatedUser = $this->createShibUser(); + } + + return $this->authenticatedUser; + } + + /** + * @return ShibUser + */ + private function createShibUser() + { + $eppn = $_SERVER['REMOTE_USER']; + + if (isset($_SERVER['supannEtuId'])) { + $id = $_SERVER['supannEtuId']; + } elseif (isset($_SERVER['supannEmpId'])) { + $id = $_SERVER['supannEmpId']; + } else { + throw new RuntimeException('Un au moins des attributs suivants doivent exister dans $_SERVER : supannEtuId, supannEmpId.'); + } + + $mail = null; + if (isset($_SERVER['mail'])) { + $mail = $_SERVER['mail']; + } + + $displayName = null; + if (isset($_SERVER['displayName'])) { + $displayName = $_SERVER['displayName']; + } + + $shibUser = new ShibUser(); + $shibUser->setId($id); + $shibUser->setUsername($eppn); + $shibUser->setDisplayName($displayName); + $shibUser->setEmail($mail); + + return $shibUser; + } + + /** + * @param ModuleOptions $options + */ + public function setOptions(ModuleOptions $options) + { + $this->options = $options; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Service/ShibServiceFactory.php b/src/UnicaenAuth/Service/ShibServiceFactory.php new file mode 100644 index 0000000..418f717 --- /dev/null +++ b/src/UnicaenAuth/Service/ShibServiceFactory.php @@ -0,0 +1,20 @@ +<?php + +namespace UnicaenAuth\Service; + +use UnicaenAuth\Options\ModuleOptions; +use Zend\ServiceManager\ServiceLocatorInterface; + +class ShibServiceFactory +{ + public function __invoke(ServiceLocatorInterface $sl) + { + /** @var ModuleOptions $options */ + $options = $sl->get('unicaen-auth_module_options'); + + $service = new ShibService(); + $service->setOptions($options); + + return $service; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Service/Traits/ShibServiceAwareTrait.php b/src/UnicaenAuth/Service/Traits/ShibServiceAwareTrait.php new file mode 100644 index 0000000..825e898 --- /dev/null +++ b/src/UnicaenAuth/Service/Traits/ShibServiceAwareTrait.php @@ -0,0 +1,21 @@ +<?php + +namespace UnicaenAuth\Service\Traits; + +use UnicaenAuth\Service\ShibService; + +trait ShibServiceAwareTrait +{ + /** + * @var ShibService + */ + protected $shibService; + + /** + * @param ShibService $shibService + */ + public function setShibService(ShibService $shibService) + { + $this->shibService = $shibService; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Service/Traits/UserServiceAwareTrait.php b/src/UnicaenAuth/Service/Traits/UserServiceAwareTrait.php new file mode 100644 index 0000000..74a268f --- /dev/null +++ b/src/UnicaenAuth/Service/Traits/UserServiceAwareTrait.php @@ -0,0 +1,21 @@ +<?php + +namespace UnicaenAuth\Service\Traits; + +use UnicaenAuth\Service\User as UserService; + +trait UserServiceAwareTrait +{ + /** + * @var UserService + */ + protected $userService; + + /** + * @param UserService $userService + */ + public function setUserService(UserService $userService) + { + $this->userService = $userService; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/Service/User.php b/src/UnicaenAuth/Service/User.php index c1f2f67..81875d7 100644 --- a/src/UnicaenAuth/Service/User.php +++ b/src/UnicaenAuth/Service/User.php @@ -1,18 +1,19 @@ <?php + namespace UnicaenAuth\Service; +use UnicaenAuth\Event\UserAuthenticatedEvent; use PDOException; -use UnicaenApp\Exception; +use UnicaenApp\Entity\Ldap\People; +use UnicaenApp\Exception\RuntimeException; use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper; +use UnicaenAuth\Entity\Shibboleth\ShibUser; use UnicaenAuth\Options\ModuleOptions; -use UnicaenAuth\Event\UserAuthenticatedEvent; -use Zend\ServiceManager\ServiceLocatorAwareInterface; -use Zend\ServiceManager\ServiceLocatorAwareTrait; -use Zend\ServiceManager\ServiceManager; -use Zend\ServiceManager\ServiceManagerAwareInterface; use Zend\EventManager\EventManagerAwareInterface; use Zend\EventManager\EventManagerInterface; -use ZfcUser\Authentication\Adapter\AdapterChainEvent as AuthEvent; +use Zend\ServiceManager\ServiceLocatorAwareInterface; +use Zend\ServiceManager\ServiceLocatorAwareTrait; +use ZfcUser\Entity\UserInterface; use ZfcUser\Options\AuthenticationOptionsInterface; use ZfcUser\Options\ModuleOptions as ZfcUserModuleOptions; @@ -20,17 +21,13 @@ use ZfcUser\Options\ModuleOptions as ZfcUserModuleOptions; * Service d'enregistrement dans la table des utilisateurs de l'application * de l'utilisateur authentifié avec succès. * - * Est notifié via la méthode 'userAuthenticated()' lorsque l'authentification - * est terminée avec succès. - * * @see \UnicaenAuth\Authentication\Adapter\AbstractFactory - * @author Bertrand GAUTHIER <bertrand.gauthier at unicaen.fr> + * @author Unicaen */ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface { use ServiceLocatorAwareTrait; - const EVENT_USER_AUTHENTICATED_PRE_PERSIST = 'userAuthenticated.prePersist'; /** @@ -56,14 +53,35 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface /** * Save authenticated user in database from LDAP data. * + * @param UserInterface|People $userData * @return bool */ - public function userAuthenticated(AuthEvent $e) + public function userAuthenticated($userData) { if (!$this->getOptions()->getSaveLdapUserInDatabase()) { return false; } - if (!($username = $e->getIdentity())) { + + switch (true) { + case $userData instanceof People: + $username = $userData->getSupannAliasLogin(); + $email = $userData->getMail(); + $password = 'ldap'; + $state = in_array('deactivated', ldap_explode_dn($userData->getDn(), 1)) ? 0 : 1; + + break; + case $userData instanceof ShibUser: + $username = $userData->getUsername(); + $email = $userData->getEmail(); + $password = 'shib'; + $state = 1; + break; + default: + throw new RuntimeException("A implémenter!!"); + break; + } + + if (!$username) { return false; } @@ -73,18 +91,13 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface } if (!is_string($username)) { - throw new Exception("Identité rencontrée inattendue."); - } - - // recherche de l'individu dans l'annuaire LDAP - $ldapPeople = $this->getLdapPeopleMapper()->findOneByUsername($username); - if (!$ldapPeople) { - return false; + throw new RuntimeException("Identité rencontrée inattendue."); } // update/insert de l'utilisateur dans la table de l'appli $mapper = $this->getServiceLocator()->get('zfcuser_user_mapper'); /* @var $mapper \ZfcUserDoctrineORM\Mapper\User */ try { + /** @var UserInterface $entity */ $entity = $mapper->findByUsername($username); if (!$entity) { $entityClass = $this->getZfcUserOptions()->getUserEntityClass(); @@ -95,30 +108,44 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface else { $method = 'update'; } - $entity->setEmail($ldapPeople->getMail()); - $entity->setDisplayName($ldapPeople->getDisplayName()); - $entity->setPassword('ldap'); - $entity->setState(in_array('deactivated', ldap_explode_dn($ldapPeople->getDn(), 1)) ? 0 : 1); - - // déclenche l'événement donnant aux applications clientes l'opportunité de modifier l'entité - // utilisateur avant qu'elle ne soit persistée - $event = new UserAuthenticatedEvent(UserAuthenticatedEvent::PRE_PERSIST); - $event - ->setDbUser($entity) - ->setLdapUser($ldapPeople) - ->setTarget($this); - $this->getEventManager()->trigger($event); + $entity->setEmail($email); + $entity->setDisplayName($userData->getDisplayName()); + $entity->setPassword($password); + $entity->setState($state); + + // pre-persist + $this->triggerUserAuthenticatedEvent($entity, $userData); // persist $mapper->$method($entity); } catch (PDOException $pdoe) { - throw new Exception("Impossible d'enregistrer l'utilisateur authentifié dans la base de données.", null, $pdoe); + throw new RuntimeException("Impossible d'enregistrer l'utilisateur authentifié dans la base de données.", null, $pdoe); } return true; } + /** + * Déclenche l'événement donnant aux applications clientes l'opportunité de modifier l'entité + * utilisateur avant qu'elle ne soit persistée. + * + * @param mixed $entity + * @param People|ShibUser $userData + */ + private function triggerUserAuthenticatedEvent($entity, $userData) + { + $event = new UserAuthenticatedEvent(UserAuthenticatedEvent::PRE_PERSIST); + $event->setTarget($this); + $event->setDbUser($entity); + if ($userData instanceof People) { + $event->setLdapUser($userData); + } elseif ($userData instanceof ShibUser) { + $event->setShibUser($userData); + } + + $this->getEventManager()->trigger($event); + } /** * Retrieve the event manager @@ -136,7 +163,7 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface * Inject an EventManager instance * * @param EventManagerInterface $eventManager - * @return void + * @return self */ public function setEventManager(EventManagerInterface $eventManager) { @@ -148,33 +175,9 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface return $this; } - /** - * get ldap people mapper - * - * @return LdapPeopleMapper - */ - public function getLdapPeopleMapper() - { - if (null === $this->ldapPeopleMapper) { - $this->ldapPeopleMapper = $this->getServiceLocator()->get('ldap_people_mapper'); - } - return $this->ldapPeopleMapper; - } - - /** - * set ldap people mapper - * - * @param LdapPeopleMapper $mapper - * @return User - */ - public function setLdapPeopleMapper(LdapPeopleMapper $mapper) - { - $this->ldapPeopleMapper = $mapper; - return $this; - } - /** * @param ModuleOptions $options + * @return self */ public function setOptions(ModuleOptions $options) { @@ -195,6 +198,7 @@ class User implements ServiceLocatorAwareInterface, EventManagerAwareInterface /** * @param ZfcUserModuleOptions $options + * @return self */ public function setZfcUserOptions(ZfcUserModuleOptions $options) { diff --git a/src/UnicaenAuth/View/Helper/ShibConnectViewHelper.php b/src/UnicaenAuth/View/Helper/ShibConnectViewHelper.php new file mode 100644 index 0000000..72bde61 --- /dev/null +++ b/src/UnicaenAuth/View/Helper/ShibConnectViewHelper.php @@ -0,0 +1,47 @@ +<?php + +namespace UnicaenAuth\View\Helper; + +use UnicaenAuth\Service\Traits\ShibServiceAwareTrait; +use Zend\View\Helper\AbstractHelper; +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 ShibConnectViewHelper extends AbstractHelper +{ + 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() + { + if (! $this->shibService->isShibbolethEnable()) { + return ''; + } + + $shibUrl = $this->getView()->url('auth/shibboleth', [], ['query' => $this->getView()->queryParams()], true); + + return <<<EOS +<a href="$shibUrl" class="btn btn-info btn-lg">Se connecter via la fédération d'identité</a> +EOS; + } +} \ No newline at end of file diff --git a/src/UnicaenAuth/View/Helper/ShibConnectViewHelperFactory.php b/src/UnicaenAuth/View/Helper/ShibConnectViewHelperFactory.php new file mode 100644 index 0000000..f9828c0 --- /dev/null +++ b/src/UnicaenAuth/View/Helper/ShibConnectViewHelperFactory.php @@ -0,0 +1,24 @@ +<?php + +namespace UnicaenAuth\View\Helper; + +use UnicaenAuth\Service\ShibService; +use Zend\View\HelperPluginManager; + +class ShibConnectViewHelperFactory +{ + /** + * @param HelperPluginManager $hpm + * @return ShibConnectViewHelper + */ + public function __invoke(HelperPluginManager $hpm) + { + /** @var ShibService $shibService */ + $shibService = $hpm->getServiceLocator()->get(ShibService::class); + + $helper = new ShibConnectViewHelper(); + $helper->setShibService($shibService); + + return $helper; + } +} \ No newline at end of file diff --git a/view/unicaen-auth/auth/shibboleth.phtml b/view/unicaen-auth/auth/shibboleth.phtml new file mode 100644 index 0000000..63addc6 --- /dev/null +++ b/view/unicaen-auth/auth/shibboleth.phtml @@ -0,0 +1,23 @@ +<h1 class="page-header">Authentification Shibboleth</h1> + +<p> + Si vous arrivez sur cette page, c'est sans doute que vous cherchez à utiliser l'authentification Shibboleth, mais + qu'elle est mal configurée ! +</p> + +<p> + Vous devez activer l'authentification Shibboleth dans la config : +</p> +<pre> + 'unicaen-auth' => [ + ... + 'shibboleth' => [ + 'enable' => true + ], + ]; +</pre> + +<p> + Et voici ce que vous devez ajouter dans la configuration Apache de votre site : +</p> +<pre><?php echo htmlspecialchars(\UnicaenAuth\Service\ShibService::apacheConfigSnippet()) ?></pre> \ No newline at end of file diff --git a/view/zfc-user/user/login.phtml b/view/zfc-user/user/login.phtml index e52f5d7..fce5180 100644 --- a/view/zfc-user/user/login.phtml +++ b/view/zfc-user/user/login.phtml @@ -48,6 +48,9 @@ $form->setAttributes([ <?php echo $this->form()->closeTag() ?> + <!-- Bouton de connexion Shibboleth --> + <?php echo $this->shibConnect() ?> + <?php if ($this->enableRegistration) : ?> <?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> <?php endif; ?> -- GitLab