From 9349c23151c6c72bfe905fce219d2dedbeb14170 Mon Sep 17 00:00:00 2001 From: Patrick Barroca <pbarroca@afi-sa.fr> Date: Tue, 11 Feb 2020 14:51:51 +0100 Subject: [PATCH] dev #79874 : identity providers with types --- .../scripts/identity-providers/index.phtml | 22 +++- .../opac/controllers/AbonneController.php | 15 ++- .../opac/controllers/AuthController.php | 10 -- .../IdentityProvidersController.php | 5 +- .../scripts/abonne/associated-providers.phtml | 4 +- library/Class/Auth/IdentityProvider.php | 12 +- library/Class/Auth/NotLogged.php | 11 +- library/Class/IdentityProvider.php | 39 ++++-- library/Class/IdentityProvider/Default.php | 7 +- library/Class/IdentityProvider/Types.php | 43 ++++++- library/Class/User/Identity.php | 14 ++ library/Class/WebService/Acheteza.php | 79 ++---------- library/Class/WebService/IdentityProvider.php | 113 ++++++++++++++++ library/Class/{ => WebService}/OpenId.php | 111 +++++++--------- library/Trait/TimeSource.php | 4 +- library/ZendAfi/Session/Namespace.php | 56 ++++++++ .../IdentityProviderAuthenticationTest.php | 121 +++++++++++------- 17 files changed, 446 insertions(+), 220 deletions(-) create mode 100644 library/Class/WebService/IdentityProvider.php rename library/Class/{ => WebService}/OpenId.php (72%) create mode 100644 library/ZendAfi/Session/Namespace.php diff --git a/application/modules/admin/views/scripts/identity-providers/index.phtml b/application/modules/admin/views/scripts/identity-providers/index.phtml index b321c499fe6..887301a9cfb 100644 --- a/application/modules/admin/views/scripts/identity-providers/index.phtml +++ b/application/modules/admin/views/scripts/identity-providers/index.phtml @@ -1,10 +1,18 @@ <?php -echo $this->Button_New((new Class_Entity())->setText($this->_('Ajouter un fournisseur d\'identité'))); +echo $this->Button_New((new Class_Entity()) + ->setText($this->_('Ajouter un fournisseur d\'identité'))); -echo $this->renderTable( - (new Class_TableDescription('providers')) - ->addColumn($this->_('Libellé'), ['attribute' => 'label']) - ->addColumn($this->_('Url de retour'), ['attribute' => 'callback_url']) - ->addRowAction(function($model) { return $this->renderPluginsActions($model); }), - $this->providers); +echo $this + ->renderTable((new Class_TableDescription('providers')) + ->addColumn($this->_('Libellé'), ['attribute' => 'label']) + ->addColumn($this->_('Url de retour'), ['attribute' => 'callback_url']) + ->addColumn($this->_('Actif ?'), + function($model) + { + return $model->getActive() + ? $this->_('Oui') + : $this->_('Non'); + }) + ->addRowAction(function($model) { return $this->renderPluginsActions($model); }), + $this->providers); diff --git a/application/modules/opac/controllers/AbonneController.php b/application/modules/opac/controllers/AbonneController.php index 7114e856004..d22b11d7ccd 100644 --- a/application/modules/opac/controllers/AbonneController.php +++ b/application/modules/opac/controllers/AbonneController.php @@ -1223,7 +1223,20 @@ class AbonneController extends ZendAfi_Controller_Action { public function associatedProvidersAction() { $this->view->titre = $this->_('Mes comptes associés'); $this->view->identities = Class_User_Identity::findIdentitiesForActiveProviders($this->_user); - $this->view->providers = Class_IdentityProvider::findAllActiveProviders(); + } + + + public function dissociateProviderAction() { + if ((!$identity = Class_User_Identity::find((int)$this->_getParam('id'))) + || $this->_user->getId() != $identity->getUserId()) { + $this->_helper->notify($this->_('Impossible de dissocier une identité inconnue')); + return $this->_redirectToUrlOrReferer('/'); + } + + $identity->dissociate(); + $this->_helper->notify($this->_('Votre compte a été dissocié de "%s"', + $identity->getProviderLabel())); + $this->_redirectToUrlOrReferer('/'); } diff --git a/application/modules/opac/controllers/AuthController.php b/application/modules/opac/controllers/AuthController.php index 073aac4c787..9f1acd6dd22 100644 --- a/application/modules/opac/controllers/AuthController.php +++ b/application/modules/opac/controllers/AuthController.php @@ -184,16 +184,6 @@ class AuthController extends ZendAfi_Controller_Action { } - public function dissociateAction() { - if (!$identity = Class_User_Identity::find((int)$this->_getParam('id'))) - return $this->_redirectToUrlOrReferer('/'); - - $identity->dissociate(); - $this->_helper->notify($this->_('Votre compte a été dissocié.')); - $this->_redirectToUrlOrReferer('/'); - } - - public function ajaxLoginAction(){ $redirect = urldecode($this->_getParam('redirect')); $location = urldecode($this->_getParam('location')); diff --git a/application/modules/opac/controllers/IdentityProvidersController.php b/application/modules/opac/controllers/IdentityProvidersController.php index f7920b208ec..0af9c1af02e 100644 --- a/application/modules/opac/controllers/IdentityProvidersController.php +++ b/application/modules/opac/controllers/IdentityProvidersController.php @@ -41,9 +41,8 @@ class IdentityProvidersController extends ZendAfi_Controller_Action { public function logoutAction() { if (!$provider = Class_IdentityProvider::find((int)$this->_getParam('id'))) - return; + return $this->_redirectToReferer(); - return $this->_redirect((new Class_OpenId($provider)) - ->getLogoutUrl()); + $this->_redirect($provider->logoutUrl()); } } diff --git a/application/modules/opac/views/scripts/abonne/associated-providers.phtml b/application/modules/opac/views/scripts/abonne/associated-providers.phtml index 7f80030e7b5..ff1849dd502 100644 --- a/application/modules/opac/views/scripts/abonne/associated-providers.phtml +++ b/application/modules/opac/views/scripts/abonne/associated-providers.phtml @@ -16,8 +16,8 @@ $description = (new Class_TableDescription('active_identities')) ->addColumn($this->_('Dissocier'), function($model) { - return $this->tagAnchor(['controller' => 'auth', - 'action' => 'dissociate', + return $this->tagAnchor(['controller' => 'abonne', + 'action' => 'dissociate-provider', 'id' => $model->getId() ], $this->_('Dissocier votre compte')); diff --git a/library/Class/Auth/IdentityProvider.php b/library/Class/Auth/IdentityProvider.php index b58c70be1ad..609721779fa 100644 --- a/library/Class/Auth/IdentityProvider.php +++ b/library/Class/Auth/IdentityProvider.php @@ -92,16 +92,19 @@ class Class_Auth_IdentityProviderNotLogged extends Class_Auth_NotLogged { protected function _doOnLoginSuccess() { if (!$provider = $this->_getProvider()) { - $this->controller->notify($this->_('Association de compte impossible : fournisseur inconnu')); + $this->controller->notify($this->_('Impossible d\'associer votre compte à un fournisseur inconnu')); return parent::_doOnLoginSuccess(); } if (!$provider->associate(Class_Users::getIdentity())) { - $this->controller->notify($this->_('L\'association de compte vers "%s" a échoué', + $this->controller->notify($this->_('L\'association de votre compte à "%s" a échoué', $provider->getLabel())); return parent::_doOnLoginSuccess(); } + $this->controller->notify($this->_('L\'association de votre compte à "%s" a réussi', + $provider->getLabel())); + if ($url = $provider->loginSuccessRedirectUrl()) $this->setRedirectUrl($url); @@ -109,6 +112,11 @@ class Class_Auth_IdentityProviderNotLogged extends Class_Auth_NotLogged { } + protected function _clearRemoteLogins() { + // do not clear as we are meant to keep remote login + } + + protected function _doOnLoginFail() { $this->redirect_url = ''; } diff --git a/library/Class/Auth/NotLogged.php b/library/Class/Auth/NotLogged.php index dc91f2e0bca..80f1c821950 100644 --- a/library/Class/Auth/NotLogged.php +++ b/library/Class/Auth/NotLogged.php @@ -25,11 +25,20 @@ class Class_Auth_NotLogged extends Class_Auth_Strategy { $this->redirect_url = $this->default_url; if (!$error = $this->controller->_authenticate()) - return function() { return $this->_doOnLoginSuccess(); }; + return function() { + // login without associating remote identity should clear to prevent messing with remote ids afterwards + $this->_clearRemoteLogins(); + return $this->_doOnLoginSuccess(); + }; return function() use($error) { $this->controller->notify($error); return $this->_doOnLoginFail(); }; } + + + protected function _clearRemoteLogins() { + (new Class_IdentityProvider_Types)->clearRemoteLogin(); + } } diff --git a/library/Class/IdentityProvider.php b/library/Class/IdentityProvider.php index a871910cc5c..d3e888743ff 100644 --- a/library/Class/IdentityProvider.php +++ b/library/Class/IdentityProvider.php @@ -20,27 +20,35 @@ */ - class IdentityProviderLoader extends Storm_Model_Loader { + protected $_actives; + public function findAllActiveProviders() { return Class_IdentityProvider::findAllBy(['active' => 1]); + + return isset($this->_actives) + ? $this->_actives + : $this->_actives = Class_IdentityProvider::findAllBy(['active' => 1]); } } + class Class_IdentityProvider extends Storm_Model_Abstract{ protected $_table_name = 'identity_provider', - $_table_primary = 'id', $_loader_class = 'IdentityProviderLoader', $_default_attribute_values = ['label' => '', 'config' => '', 'active' => true, 'type' => '', - 'protocol' => 'openid_connect_1' - ], - $_identifier = '', + 'protocol' => 'openid_connect_1'], + + $_has_many = ['user_identities' => ['model' => 'Class_User_Identity', + 'role' => 'provider', + 'dependents' => 'delete'] ], + $_config_fields = ['url', 'url_api', 'client_id', @@ -50,9 +58,7 @@ class Class_IdentityProvider extends Storm_Model_Abstract{ 'button_login', 'button_logout'], - $_context, - $_type_class - ; + $_context; public function setContext($context) { @@ -66,8 +72,23 @@ class Class_IdentityProvider extends Storm_Model_Abstract{ } + public function getServiceClass() { + return $this->getTypeClass()->getServiceClass(); + } + + public function isAttachable() { - return !(Class_Users::getIdentity() && $this->isRemotelyLogged()); + $user = Class_Users::getIdentity(); + return !($user && $this->isRemotelyLogged()) + && !$this->isAssociatedTo($user); + } + + + public function isAssociatedTo($user) { + return ($user && !$this->isNew()) + ? 0 < Class_User_Identity::countBy(['provider_id' => $this->getId(), + 'user_id' => $user->getId()]) + : false; } diff --git a/library/Class/IdentityProvider/Default.php b/library/Class/IdentityProvider/Default.php index a21d549bff4..c29385d748c 100644 --- a/library/Class/IdentityProvider/Default.php +++ b/library/Class/IdentityProvider/Default.php @@ -24,7 +24,7 @@ class Class_IdentityProvider_Default { protected $_config = [], $_service, - $_service_class = 'Class_OpenId', + $_service_class = 'Class_WebService_OpenId', $_provider; public function __construct($provider) { @@ -99,6 +99,11 @@ class Class_IdentityProvider_Default { } + public function getServiceClass() { + return $this->_service_class; + } + + protected function _service() { $class_name = $this->_service_class; diff --git a/library/Class/IdentityProvider/Types.php b/library/Class/IdentityProvider/Types.php index 5cf8804907d..db7c540de40 100644 --- a/library/Class/IdentityProvider/Types.php +++ b/library/Class/IdentityProvider/Types.php @@ -23,6 +23,12 @@ class Class_IdentityProvider_Types { use Trait_Translator; + protected $_services; + + public function __construct() { + $this->_services = new Storm_Collection(['Class_WebService_OpenId', 'Class_WebService_Acheteza']); + } + public function asMultiOptions() { return ['default' => $this->_('OpenId Connect'), @@ -42,17 +48,40 @@ class Class_IdentityProvider_Types { public function isLogged() { - foreach(['Class_OpenId', 'Class_WebService_Acheteza'] as $class_name) - if (call_user_func([$class_name, 'isLogged'])) - return true; - - return false; + return null !== $this->_services + ->detect(function($each) { return call_user_func([$each, 'isLogged']); }); } public function clearRemoteLogin() { - foreach(['Class_OpenId', 'Class_WebService_Acheteza'] as $class_name) - call_user_func([$class_name, 'clearSession']); + $this->_services + ->eachDo(function($each) { call_user_func([$each, 'clearSession']); }); + + return $this; + } + + + public function clearUnusedRemoteIds() { + if (!$user = Class_Users::getIdentity()) + return $this->clearRemoteLogin(); + + if (!$identities = $user->getUserIdentities()) + return $this->clearRemoteLogin(); + + $map_by_service_class = []; + foreach($identities as $identity) { + if (!$service = $identity->getServiceClass()) + continue; + + if (!array_key_exists($service, $map_by_service_class)) + $map_by_service_class[$service] = []; + + $map_by_service_class[$service][] = $identity->getIdentifier(); + } + + foreach($map_by_service_class as $service_class => $ids) + call_user_func([$service_class, 'clearSessionIfNotIn'], $ids); + return $this; } diff --git a/library/Class/User/Identity.php b/library/Class/User/Identity.php index 96bea376723..85c8260893a 100644 --- a/library/Class/User/Identity.php +++ b/library/Class/User/Identity.php @@ -67,4 +67,18 @@ class Class_User_Identity extends Storm_Model_Abstract { $this->delete(); return $this; } + + + public function getProviderLabel() { + return ($provider = $this->getProvider()) + ? $provider->getLabel() + : ''; + } + + + public function getServiceClass() { + return ($provider = $this->getProvider()) + ? $provider->getServiceClass() + : ''; + } } diff --git a/library/Class/WebService/Acheteza.php b/library/Class/WebService/Acheteza.php index dfd35691fbe..81c13dce5e3 100644 --- a/library/Class/WebService/Acheteza.php +++ b/library/Class/WebService/Acheteza.php @@ -20,82 +20,29 @@ */ -class Class_WebService_Acheteza { - use Trait_SimpleWebClient; - - const SESSION_NAMESPACE = 'Class_WebService_Acheteza'; - - /** @var Zend_Session_Namespace */ - protected static $_session_namespace; - - /** @var Class_IdentityProvider */ - protected $_provider; - - - public static function getSession() { - return static::$_session_namespace - ? static::$_session_namespace - : static::$_session_namespace = new Zend_Session_Namespace(md5(BASE_URL.static::SESSION_NAMESPACE)); - } - - - public static function isLogged() { - return isset(static::getSession()->id); - } - - - public static function loginWith($id) { - static::getSession()->id = $id; - } - - - public static function clearSession() { - static::getSession()->unsetAll(); - } - - - public static function logout() { - static::clearSession(); - } - - - public function __construct($provider) { - $this->_provider = $provider; - } - - +class Class_WebService_Acheteza extends Class_WebService_IdentityProvider { public function validate($context) { + if (!$context) + return $this; + if ((!$token = $context->getParam('access_token')) || (!$pan = $context->getParam('pan'))) return $this; - if (!$this->_checkAccessToken($token, $pan)) + /* + if (!$this->_checkAccessToken($token, $pan)) return $this; + */ - if ($id = $this->_getRemoteId($token)) - $this->loginWith($id); - + $this->loginWith($pan); return $this; - } - - - public function getUserId() { - return $this->getSession()->id; - } - - - public function remoteName() { - return ''; - } - - - public function loginFormAction($params, $context) { - return $params; - } + /* + if ($id = $this->_getRemoteId($token)) + $this->loginWith($id); + */ - public function getLoginSuccessRedirectUrl() { - return ''; + return $this; } diff --git a/library/Class/WebService/IdentityProvider.php b/library/Class/WebService/IdentityProvider.php new file mode 100644 index 00000000000..a44eb7cc8bd --- /dev/null +++ b/library/Class/WebService/IdentityProvider.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +abstract class Class_WebService_IdentityProvider { + use Trait_SimpleWebClient, Trait_TimeSource; + + /** @var ZendAfi_Session_Namespace */ + protected static $_session_namespace; + + /** @var Class_IdentityProvider */ + protected $_provider; + + /** @return ZendAfi_Session_Namespace */ + public static function getSession() { + if (static::$_session_namespace) + return static::$_session_namespace; + + static::$_session_namespace = (new ZendAfi_Session_Namespace(md5(BASE_URL . get_called_class()))) + ->setExpirationDelay(300); + + return static::$_session_namespace; + } + + + public static function clearSession() { + static::getSession()->unsetAll(); + } + + + public static function clearSessionIfNotIn($ids) { + if (!$ids || !in_array(static::getUserId(), $ids)) + static::clearSession(); + } + + + public static function getUserId() { + return static::getSession()->id; + } + + + public static function isLogged() { + return isset(static::getSession()->id); + } + + + public static function loginWith($id) { + static::getSession()->id = $id; + } + + + public function __construct($provider) { + $this->_provider = $provider; + } + + + /** @param $context Zend_Controller_Request_Abstract */ + public function validate($context) { + return $this; + } + + + /** + * @param $params array + * @param $context Zend_Controller_Request_Abstract + * @return array + */ + public function loginFormAction($params, $context) { + return $params; + } + + + /** @return string */ + public function remoteName() { + return ''; + } + + + /** @return string */ + public function getAuthorizeUrl() { + return ''; + } + + + /** @return string */ + public function getLoginSuccessRedirectUrl() { + return ''; + } + + + /** @return string */ + public function getLogoutUrl($redirect) { + return ''; + } +} diff --git a/library/Class/OpenId.php b/library/Class/WebService/OpenId.php similarity index 72% rename from library/Class/OpenId.php rename to library/Class/WebService/OpenId.php index 52e288915b4..f6676d125ff 100644 --- a/library/Class/OpenId.php +++ b/library/Class/WebService/OpenId.php @@ -18,14 +18,13 @@ * along with BOKEH; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -require_once('library/php-jwt/autoload.php'); -require_once('library/phpseclib/autoload.php'); -class Class_OpenId { - use Trait_SimpleWebClient; +require_once 'library/php-jwt/autoload.php'; +require_once 'library/phpseclib/autoload.php'; + +class Class_WebService_OpenId extends Class_WebService_IdentityProvider { protected - $_provider, $_client_id, $_client_secret = '' , $_url='', @@ -40,29 +39,30 @@ class Class_OpenId { - public static function clearSession() { - Zend_Registry::get('session')->openId=[]; + public static function isLogged() { + return isset(static::getSession()->userinfo); } - public static function getSession() { - $session = Zend_Registry::get('session'); - if (!$session->openId) - $session->openId = []; + public static function getUserId() { + if (!static::isLogged()) + return null; - return $session; - } + if (!$user_info = static::getSession()->userinfo) + return null; + if (isset($user_info->openid)) + return $user_info->openid; - public static function isLogged() { - return isset(static::getSession()->openId['userinfo']); + return isset($user_info->sub) + ? $user_info->sub + : null; } - public function __construct($provider, $redirect_url='') { - $this->_provider = $provider; - if ($redirect_url) - $this->_redirect_url = $redirect_url; + public function __construct($provider) { + parent::__construct($provider); + $this->_client_id = $provider->getClientId(); $this->_client_secret = $provider->getClientSecret(); $this->_nonce = true;//$provider->getNonce(); @@ -70,6 +70,7 @@ class Class_OpenId { $this->_logout_url = $provider->getLogoutUrl() ? $provider->getLogoutUrl() : $this->_url.'/logout'; + if (!$this->getConfiguration()) $this->_setDefaultConfiguration(); } @@ -85,6 +86,7 @@ class Class_OpenId { return $this; $this->getUserInfos($code, $state); + return $this; } @@ -100,8 +102,8 @@ class Class_OpenId { public function remoteName() { - return isset($this->getSession()->openId['userinfo']) - ? $this->getSession()->openId['userinfo']->name + return isset($this->getSession()->userinfo) + ? $this->getSession()->userinfo->name : ''; } @@ -133,8 +135,8 @@ class Class_OpenId { public function getUserInfos($code, $state) { - if (isset($this->getSession()->openId['userinfo'])) - return $this->getSession()->openId['userinfo']; + if (isset($this->getSession()->userinfo)) + return $this->getSession()->userinfo; if ($state != $this->getState()) return false; @@ -142,35 +144,19 @@ class Class_OpenId { if (!$access_token = $this->_getAndCheckAccessToken($code)) return []; - $this->getSession()->openId['userinfo'] = $this->_getInfoFromFI($access_token); + $this->getSession()->userinfo = $this->_getInfoFromFI($access_token); - return $this->getSession()->openId['userinfo']; + return $this->getSession()->userinfo; } protected function _getToken() { - return isset($this->getSession()->openId['token']) ? - $this->getSession()->openId['token'] + return isset($this->getSession()->token) + ? $this->getSession()->token : ''; } - public function getUserId() { - if (!isset($this->getSession()->openId['userinfo'])) - return null; - - if (!$user_info = $this->getSession()->openId['userinfo']) - return null; - - if (isset($user_info->openid)) - return $user_info->openid; - - return isset($user_info->sub) - ? $user_info->sub - : null; - } - - protected function _getTokenUrl() { return $this->_token_url; } @@ -187,8 +173,8 @@ class Class_OpenId { protected function _getAndCheckAccessToken($code) { - if (isset($this->getSession()->openId['access_token'])) - return $this->getSession()->openId['access_token']; + if (isset($this->getSession()->access_token)) + return $this->getSession()->access_token; $http_client = $this->getWebClient(); $post_data = [ @@ -206,7 +192,7 @@ class Class_OpenId { if (!isset($json->access_token)|| !isset($json->id_token)) return ; - $this->getSession()->openId['access_token']=$json->access_token; + $this->getSession()->access_token = $json->access_token; if (!$this->_nonce) return $json->access_token; @@ -224,7 +210,7 @@ class Class_OpenId { if (!$this->_checkNonce($response->nonce)) return ; - $this->getSession()->openId['token'] = $json->id_token; + $this->getSession()->token = $json->id_token; return $json->access_token; } @@ -250,36 +236,37 @@ class Class_OpenId { public function getState() { - if (isset($this->getSession()->openId['state']) && - ($state = $this->getSession()->openId['state'])) + if (isset($this->getSession()->state) + && ($state = $this->getSession()->state)) return $state; - return $this->getSession()->openId['state'] = $this->_redirect_url ? base64_encode($this->_redirect_url) : $this->_getRandomToken(); + return $this->getSession()->state = $this->_redirect_url + ? base64_encode($this->_redirect_url) + : $this->_getRandomToken(); } public function getNonce() { - if (isset($this->getSession()->openId['nonce']) && - ($nonce = $this->getSession()->openId['nonce'])) + if (isset($this->getSession()->nonce) + && ($nonce = $this->getSession()->nonce)) return $nonce; - return $this->getSession()->openId['nonce'] = $this->_getRandomToken(); + return $this->getSession()->nonce = $this->_getRandomToken(); } protected function _getRandomToken(){ - return sha1(mt_rand(0,mt_getrandmax())); + return sha1(mt_rand(0, mt_getrandmax())); } public function getRedirectLoginUrl() { - return (new Class_Url)->absoluteUrl(['module' => 'opac', - 'controller' => 'auth', - 'action' => 'login', - 'provider' => $this->_provider->getId(), - ], - null, - true); + return Class_Url::absolute(['module' => 'opac', + 'controller' => 'auth', + 'action' => 'login', + 'provider' => $this->_provider->getId(), + ], + null, true); } @@ -297,7 +284,7 @@ class Class_OpenId { protected function _getBokehLogoutUrl() { - return (new Class_Url)->absoluteUrl([], null,true); + return Class_Url::absolute([], null, true); } diff --git a/library/Trait/TimeSource.php b/library/Trait/TimeSource.php index 973d3e39e27..f8530a64fcf 100644 --- a/library/Trait/TimeSource.php +++ b/library/Trait/TimeSource.php @@ -47,6 +47,7 @@ trait Trait_TimeSource { return date('Y-m-d H:i:s',self::getTimeSource()->time()); } + public static function substractYearsToCurrentDate($days) { return date('Y-m-d',strtotime('-'.(string)$days.' year', self::getTimeSource()->time())); } @@ -57,6 +58,7 @@ trait Trait_TimeSource { return $now; } + /** @return Class_TimeSource */ public static function getTimeSource() { if (null == self::$_time_source) @@ -70,5 +72,3 @@ trait Trait_TimeSource { self::$_time_source = $time_source; } } - -?> \ No newline at end of file diff --git a/library/ZendAfi/Session/Namespace.php b/library/ZendAfi/Session/Namespace.php new file mode 100644 index 00000000000..851dad44001 --- /dev/null +++ b/library/ZendAfi/Session/Namespace.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright (c) 2012-2019, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +class ZendAfi_Session_Namespace extends Zend_Session_Namespace { + use Trait_TimeSource; + + const EXPIRATION_VAR_NAME = 'expires_at'; + + protected $_expiration_delay; + + public function __construct($namespace = 'Default', $singleInstance = false) { + parent::__construct($namespace, $singleInstance); + if ($this->_isExpired()) + $this->unsetAll(); + } + + + public function __set($name, $value) { + if ($this->_expiration_delay) + parent::__set(static::EXPIRATION_VAR_NAME, + $this->getTimeSource()->time() + $this->_expiration_delay); + + return parent::__set($name, $value); + } + + + public function setExpirationDelay($seconds) { + $this->_expiration_delay = (int)$seconds; + return $this; + } + + + protected function _isExpired() { + $expires_at = $this->__get(static::EXPIRATION_VAR_NAME); + return isset($expires_at) && $this->getTimeSource()->time() > $expires_at; + } +} diff --git a/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php b/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php index 1649057d446..ad488051508 100644 --- a/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php +++ b/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php @@ -115,7 +115,7 @@ class IdentityProviderAuthenticationBoxDisplayTest extends AbstractControllerTes 'client_secret' => '9876']) ]); - Class_OpenId::setWebClient($this->mock()->whenCalled('open_url')->answers('')); + Class_WebService_OpenId::setWebClient($this->mock()->whenCalled('open_url')->answers('')); $this->dispatch('/'); } @@ -300,7 +300,7 @@ abstract class IdentityProviderAuthenticationCallbackAuthenticationTestCase ->whenCalled('postRawData') ->answers($response_token); - Class_OpenId::setWebClient($web_client); + Class_WebService_OpenId::setWebClient($web_client); $this->fixture('Class_Users', ['id' => 33, @@ -320,8 +320,8 @@ abstract class IdentityProviderAuthenticationCallbackAuthenticationTestCase public function tearDown() { - Class_OpenId::setWebClient(null); - Zend_Registry::get('session')->unsetAll(); + Class_WebService_OpenId::setWebClient(null); + Class_WebService_OpenId::clearSession(); parent::tearDown(); } } @@ -335,9 +335,7 @@ class IdentityProviderAuthenticationConnectedOpenidUserTest public function setUp() { parent::setUp(); ZendAfi_Auth::getInstance()->logUser(Class_Users::find(33)); - - $session = Zend_Registry::get('session'); - $session->openId['userinfo'] = (object)['sub' => '1232']; + Class_WebService_OpenId::getSession()->userinfo = (object)['sub' => '1232']; $simple_widgets = ['modules' => ['1' => ['division' => '1', 'type_module' => 'IDENTITY_PROVIDER', @@ -384,7 +382,6 @@ class IdentityProviderAuthenticationLoggedUserTest extends IdentityProviderAuthe public function setUp() { parent::setUp(); ZendAfi_Auth::getInstance()->logUser(Class_Users::find(33)); - Zend_Registry::get('session')->openId = []; $simple_widgets = ['modules' => ['1' => ['division' => '1', 'type_module' => 'IDENTITY_PROVIDER', 'preferences' => []], @@ -421,8 +418,9 @@ class IdentityProviderAuthenticationShouldDisplayButtonWithFCUrl public function authenticationToFranceConnectShouldRedirectToFranceConnectAuthorize() { $_SERVER['HTTP_REFERER'] = 'referer'; $this->_state = base64_encode('http://bokeh.org/mycurrenturl'); - Zend_Registry::get('session')->openId = ['state' => $this->_state, - 'nonce' => $this->_nonce]; + $session = Class_WebService_OpenId::getSession();; + $session->state = $this->_state; + $session->nonce = $this->_nonce; $this->dispatch('/identity-providers/authenticate/id/1/redirect/'.urlencode('http://bokeh.org/mycurrenturl'));; @@ -447,9 +445,10 @@ class IdentityProviderAuthenticationCallbackAuthenticationFirstTime ZendAfi_Auth::getInstance()->clearIdentity(); $this->_state = base64_encode('http://bokeh.org/mycurrenturl'); - Zend_Registry::get('session')->openId = ['state' => $this->_state, - 'nonce' => $this->_nonce, - 'access_token' => '1111']; + $session = Class_WebService_OpenId::getSession();; + $session->state = $this->_state; + $session->nonce = $this->_nonce; + $session->access_token = 1111; $this->fixture('Class_IdentityProvider', ['id' => 12, @@ -484,13 +483,15 @@ class IdentityProviderAuthenticationCallbackAuthenticationFirstTime -class IdentityProviderAuthenticationConnectedUser extends IdentityProviderAuthenticationCallbackAuthenticationTestCase { +class IdentityProviderAuthenticationConnectedUser + extends IdentityProviderAuthenticationCallbackAuthenticationTestCase { + public function setUp() { parent::setUp(); - Zend_Registry::get('session')->openId = ['state' => $this->_state, - 'nonce' => $this->_nonce]; - Zend_Registry::get('session')->openId['userinfo'] = (object)['sub' => '9763', - 'name' => 'Albator']; + $session = Class_WebService_OpenId::getSession(); + $session->state = $this->_state; + $session->nonce = $this->_nonce; + $session->userinfo = (object)['sub' => '9763', 'name' => 'Albator']; $this->fixture('Class_User_Identity', ['id' => 1, 'provider_id' => 1, @@ -503,7 +504,7 @@ class IdentityProviderAuthenticationConnectedUser extends IdentityProviderAuthen /** @test */ public function ifUserExistsWithOpenidThenShouldBeConnected() { - $this->assertEquals('Albator',Class_Users::getIdentity()->getLogin()); + $this->assertEquals('Albator', Class_Users::getIdentity()->getLogin()); $this->assertRedirectTo('http://bokeh.org/mycurrenturl'); } } @@ -524,11 +525,9 @@ class IdentityProviderAuthenticationDissociateUserTest 'identifier' => '9763']); ZendAfi_Auth::getInstance()->logUser(Class_Users::find(33)); + Class_WebService_OpenId::getSession()->userinfo = (object)['sub' => '9763']; - $session = Zend_Registry::get('session'); - $session->openId['userinfo'] = (object)['sub' => '9763']; - - $this->dispatch('/auth/dissociate/id/1'); + $this->dispatch('/abonne/dissociate-provider/id/1'); } @@ -546,8 +545,7 @@ class IdentityProviderAuthenticationDissociateUserTest /** @test */ public function sessionShouldBeCleared() { - $session = Zend_Registry::get('session'); - $this->assertEmpty($session->openId); + $this->assertEmpty(Class_WebService_OpenId::getSession()->getIterator()); } } @@ -559,9 +557,10 @@ class IdentityProviderAuthenticationCallbackAuthentication public function setUp() { parent::setup(); - Zend_Registry::get('session')->openId = ['state' => $this->_state, - 'nonce' => $this->_nonce, - 'access_token' => 1111]; + $session = Class_WebService_OpenId::getSession(); + $session->state = $this->_state; + $session->nonce = $this->_nonce; + $session->access_token = 1111; $this->fixture('Class_IdentityProvider', ['id' => 12, @@ -615,23 +614,24 @@ class IdentityProviderAuthenticationCallbackAuthentication /** @test */ public function logoutShouldRemoveOpenIdInSession() { - Zend_Registry::get('session')->openId = ['userinfo' => '999']; + Class_WebService_OpenId::getSession()->userinfo = '999'; ZendAfi_Auth::getInstance()->logUser(Class_Users::find(33)); $this->dispatch('/auth/logout'); - $this->assertEquals([], Zend_Registry::get('session')->openId); + $this->assertEmpty(Class_WebService_OpenId::getSession()->getIterator()); } /** @test */ public function logoutProviderShouldRedirectToProviderLogout() { - Zend_Registry::get('session')->openId = ['userinfo' => '999', - 'state' => '6', - 'token' => 'mytoken']; + $session = Class_WebService_OpenId::getSession(); + $session->state = '6'; + $session->userinfo = '999'; + $session->token = 'mytoken'; ZendAfi_Auth::getInstance()->logUser(Class_Users::find(33)); $this->dispatch('/auth/logout/provider/1'); - $this->assertEquals([], Zend_Registry::get('session')->openId); + $this->assertEmpty(Class_WebService_OpenId::getSession()->getIterator()); $this->assertRedirectTo('https://fcp.integ01.dev-franceconnect.fr/api/v1/logout?id_token_hint=mytoken&state=6&post_logout_redirect_uri='.urlencode(Class_Url::absolute([], null, true))); $this->assertEmpty(Class_Users::getIdentity()); } @@ -658,7 +658,7 @@ class IdentityProviderAuthenticationAbonneListTest ]); ZendAfi_Auth::getInstance()->logUser(Class_Users::find(33)); - Class_OpenId::getWebClient() + Class_WebService_OpenId::getWebClient() ->whenCalled('open_url') ->with('http://afi.fr/.well-known/openid-configuration') ->answers(''); @@ -676,33 +676,34 @@ class IdentityProviderAuthenticationAbonneListTest public function withAssociatefranceConnectShouldBeDisplayed() { $this->_associateUser(); $this->dispatch('/abonne/associated-providers'); - $this->assertXPathContentContains('//table//td','FranceConnect', $this->_response->getBody()); + $this->assertXPathContentContains('//table//td', 'FranceConnect', $this->_response->getBody()); } /** @test */ - public function withConnectedAndAssociateFCAfiShouldNotBeDisplayed() { + public function withAssociateDissociateLinkForFranceConnectShouldBeDisplayed() { $this->_associateUser(); - $session = Zend_Registry::get('session'); - $session->openId['userinfo'] = (object)['sub' => '1232']; $this->dispatch('/abonne/associated-providers'); - $this->assertNotXPathContentContains('//button','Afi', $this->_response->getBody()); + $this->assertXPathContentContains('//a[contains(@href, "/abonne/dissociate-provider/id/1")]', + 'Dissocier', + $this->_response->getBody()); } /** @test */ - public function withNoConnectionAfiShouldNotBeDisplayed() { + public function withConnectedAndAssociateFCAfiShouldNotBeDisplayed() { $this->_associateUser(); + Class_WebService_OpenId::getSession()->userinfo = (object)['sub' => '1232']; $this->dispatch('/abonne/associated-providers'); - $this->assertXPathContentContains('//button','Afi', $this->_response->getBody()); + $this->assertNotXPathContentContains('//button','Afi', $this->_response->getBody()); } /** @test */ - public function withAssociateDissociateLinkForFranceConnectShouldBeDisplayed() { + public function withNoConnectionAfiShouldBeDisplayed() { $this->_associateUser(); $this->dispatch('/abonne/associated-providers'); - $this->assertXPathContentContains('//a','Dissocier', $this->_response->getBody()); + $this->assertXPathContentContains('//button','Afi', $this->_response->getBody()); } } @@ -749,7 +750,7 @@ abstract class IdentityProviderAuthenticationAchetezaTestCase public function tearDown() { Class_WebService_Acheteza::setWebClient(null); - Class_WebService_Acheteza::logout(); + Class_WebService_Acheteza::clearSession(); parent::tearDown(); } } @@ -864,10 +865,12 @@ class IdentityProviderAuthenticationAuthLoginWithPanAchetezaTest ->whenCalled('isError')->answers(false) ->whenCalled('getBody')->answers('{"success":"true"}')) + /* ->whenCalled('open_url') ->with('http://api.crm.server.com/api/v2.3/me', ['headers' => ['Authorization: Bearer MYACCESSTOKEN']]) ->answers(json_encode(['id' => '95fbcb97-6461-48de-8923-ef4f1de30409'])) + */ ; $this->dispatch('/auth/login/provider/5?access_token=MYACCESSTOKEN&pan=8839'); @@ -875,8 +878,9 @@ class IdentityProviderAuthenticationAuthLoginWithPanAchetezaTest /** @test */ - public function shouldBecomeRemotelyLogged() { + public function shouldBecomeRemotelyLoggedWithPanAsId() { $this->assertTrue(Class_IdentityProvider::find(5)->isRemotelyLogged()); + $this->assertEquals('8839', Class_IdentityProvider::find(5)->getRemoteUserId()); } @@ -921,6 +925,12 @@ class IdentityProviderAuthenticationAuthLoginPostRemotelyLoggedAchetezaTest public function shouldRedirect() { $this->assertRedirect(); } + + + /** @test */ + public function shouldNotifyAssociationComplete() { + $this->assertFlashMessengerContentContains('L\'association de votre compte à "Acheteza" a réussi'); + } } @@ -974,7 +984,7 @@ class IdentityProviderAuthenticationDissociateAchetezaTest ZendAfi_Auth::getInstance()->logUser(Class_Users::find(33)); Class_WebService_Acheteza::loginWith(9763); - $this->dispatch('/auth/dissociate/id/1'); + $this->dispatch('/abonne/dissociate-provider/id/1'); } @@ -1081,4 +1091,21 @@ class IdentityProviderAuthenticationAuthLoginSigbAchetezaTest $this->assertRedirectTo('http://moncompte.server.com/ServiceLogout?' . http_build_query($params)); } +} + + + + +class IdentityProviderAuthenticationExpiringSessionAchetezaTest + extends IdentityProviderAuthenticationAchetezaTestCase { + + /** @test */ + public function notLoggedRemoteIdShouldBeCleared() { + ZendAfi_Session_Namespace::setTimeSource(new TimeSourceForTest('2020-02-17 10:20:38')); + Class_WebService_Acheteza::loginWith(9763); + // jump to future past expiration time of 300s + ZendAfi_Session_Namespace::setTimeSource(new TimeSourceForTest('2020-02-17 10:55:38')); + $this->dispatch('/opac/index/index'); + $this->assertNull(Class_IdentityProvider::find(5)->getRemoteUserId()); + } } \ No newline at end of file -- GitLab