From 78f5db6949cb3a359820f5e349d552423cc050d7 Mon Sep 17 00:00:00 2001 From: efalcy <efalcy@afi-sa.fr> Date: Fri, 23 Jul 2021 16:10:14 +0200 Subject: [PATCH] hotline#136551 : add service verification on CAS 3 only --- VERSIONS_HOTLINE/136551 | 1 + .../opac/controllers/CasServerController.php | 99 ++++---- .../controllers/CasServerV10Controller.php | 2 +- .../controllers/CasServerV3Controller.php | 49 ++++ .../views/scripts/cas-server-v3/logout.phtml | 1 + .../views/scripts/cas-server/logout.phtml | 1 + .../controllers/CasServerV3Controller.php | 27 +++ library/Class/Auth/Cas.php | 7 +- library/Class/Auth/Strategy.php | 23 +- library/Class/CasTicket.php | 54 ++++- library/Class/CasTicketV3.php | 119 +++++++++ library/Class/DigitalResource/Controller.php | 2 +- library/Trait/TimeSource.php | 5 +- .../Action/Helper/CasFailureResponse.php | 32 ++- .../Skilleos/tests/SkilleosTest.php | 2 +- .../Syracuse/tests/SyracuseTest.php | 4 +- library/startup.php | 6 + .../controllers/CasServerControllerTest.php | 69 +++--- .../controllers/CasServerControllerV3Test.php | 229 ++++++++++++++++++ .../controllers/CasServerControllerTest.php | 34 ++- 20 files changed, 645 insertions(+), 121 deletions(-) create mode 100644 VERSIONS_HOTLINE/136551 create mode 100644 application/modules/opac/controllers/CasServerV3Controller.php create mode 100644 application/modules/opac/views/scripts/cas-server-v3/logout.phtml create mode 100644 application/modules/opac/views/scripts/cas-server/logout.phtml create mode 100644 application/modules/telephone/controllers/CasServerV3Controller.php create mode 100644 library/Class/CasTicketV3.php create mode 100644 tests/application/modules/opac/controllers/CasServerControllerV3Test.php diff --git a/VERSIONS_HOTLINE/136551 b/VERSIONS_HOTLINE/136551 new file mode 100644 index 00000000000..643b802d686 --- /dev/null +++ b/VERSIONS_HOTLINE/136551 @@ -0,0 +1 @@ + - ticket #136551 : [Bibliothèque numérique]: Implémentation du serveur CAS V3 avec mise à jour de sècurité \ No newline at end of file diff --git a/application/modules/opac/controllers/CasServerController.php b/application/modules/opac/controllers/CasServerController.php index 118fc8c70c6..ad3cabaea3c 100644 --- a/application/modules/opac/controllers/CasServerController.php +++ b/application/modules/opac/controllers/CasServerController.php @@ -29,18 +29,6 @@ class CasServerController extends ZendAfi_Controller_Action { } - public function returnValidTicketResponse($user, $ticket) { - $this->getResponse()->setHeader('Content-Type', 'application/xml;charset=utf-8'); - $this->getResponse()->setBody("<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> - <cas:authenticationSuccess> - <cas:user>".$user->getId()."</cas:user> - <cas:proxyGrantingTicket>".$ticket." - </cas:proxyGrantingTicket> - </cas:authenticationSuccess> - </cas:serviceResponse>"); - } - - public function returnValidMusicmeResponse($user) { $this->getResponse()->setHeader('Content-Type', 'application/xml;charset=utf-8'); $this->getResponse()->setBody( '<User> @@ -55,75 +43,94 @@ class CasServerController extends ZendAfi_Controller_Action { } - public function returnMusicmeFailureTicketResponse($error,$ticket=null) { - $this->getResponse()->setHeader('Content-Type', 'application/xml;charset=utf-8'); - $xml="<User />"; - $this->getResponse()->setBody($xml); + protected function _getAttributes($user) { + return []; } - public function returnFailureTicketResponse($error,$ticket=null) { - $this->_helper->casFailureResponse($error,$ticket); + public function returnMusicmeFailureTicketResponse($error, $ticket = null) { + $this->getResponse()->setHeader('Content-Type', 'application/xml;charset=utf-8'); + $xml = "<User />"; + $this->getResponse()->setBody($xml); } - public function servicevalidateAction() { - $this->_forward('validate'); + public function returnFailureTicketResponse($error, $ticket = null, $service = null) { + $this->_helper->casFailureResponse($error, $ticket, $service); } - public function validateMusicmeAction() { + protected function _validate() { $this->getHelper('ViewRenderer')->setNoRender(); - $bibid=$this->_request->getParam('MediaLibraryID'); - $ticket=$this->_request->getParam('ticket'); + $service = $this->_getParam('service'); + $ticket = $this->_getParam('ticket'); - if (strlen($ticket)<1 || strlen($bibid)<1) { - return $this->returnMusicmeFailureTicketResponse('INVALID_REQUEST'); + if (!$ticket || !$service) { + return $this->returnFailureTicketResponse(Class_CasTicket::CODE_INVALID_REQUEST); } - if ($user = (new Class_CasTicket())->userForTicket($ticket)) - return $this->returnValidMusicmeResponse($user); + $cas_ticket = $this->_getCasTicket($service); + return ($user = $cas_ticket->userForTicket($ticket)) + ? $this->returnValidTicketResponse($user, $ticket) + : $this->returnFailureTicketResponse($cas_ticket->getErrorCode(), $ticket, $cas_ticket->getService()); + } - return $this->returnMusicmeFailureTicketResponse('INVALID_TICKET',$ticket); + protected function _getCasTicket($service) { + return (new Class_CasTicket()); } -/* To be implemented : INVALID_SERVICE - the ticket provided was valid, but the service specified did not match the service associated with the ticket. CAS MUST invalidate the ticket and disallow future validation of that same ticket. */ -/* INTERNAL_ERROR - an internal error occurred during ticket validation */ + public function returnValidTicketResponse($user, $ticket) { + return $this->_helper->casValidResponse($user, $ticket, $this->_getAttributes($user)); + } - public function validateAction() { + public function servicevalidateAction() { + return $this->_validate(); + } + + + public function validateMusicmeAction() { $this->getHelper('ViewRenderer')->setNoRender(); - $service=$this->_request->getParam('service'); - $ticket=$this->_request->getParam('ticket'); - if (strlen($ticket)<1 || strlen($service)<1) { - return $this->returnFailureTicketResponse('INVALID_REQUEST'); + $bibid = $this->_getParam('MediaLibraryID'); + $ticket = $this->_getParam('ticket'); + + if (!$ticket || !$bibid) { + return $this->returnMusicmeFailureTicketResponse('INVALID_REQUEST'); } + $cas_ticket = $this->_getCasTicket(null); + return ($user = $cas_ticket->userForTicket($ticket)) + ? $this->returnValidMusicmeResponse($user) + : $this->returnMusicmeFailureTicketResponse('INVALID_TICKET', $ticket); + } - if ($user = (new Class_CasTicket())->userForTicket($ticket)) - return $this->returnValidTicketResponse($user, $ticket); - return $this->returnFailureTicketResponse('INVALID_TICKET',$ticket); + /* To be implemented : INVALID_SERVICE - the ticket provided was valid, but the service specified did not match the service associated with the ticket. CAS MUST invalidate the ticket and disallow future validation of that same ticket. */ + /* INTERNAL_ERROR - an internal error occurred during ticket validation */ + + + public function validateAction() { + return $this->_validate(); } -/* Cas server methods not used for now : - function proxyAction() { - } - function proxyValidateAction() { - } -*/ + /* Cas server methods not used for now : + function proxyAction() { + } + function proxyValidateAction() { + } + */ public function loginAction() { $this->_forward('login', 'auth'); } + public function logoutAction() { ZendAfi_Auth::getInstance()->clearIdentity(); if ($url_redirect = $this->_getParam('url')) $this->_redirect($url_redirect); } - } -?> \ No newline at end of file +?> diff --git a/application/modules/opac/controllers/CasServerV10Controller.php b/application/modules/opac/controllers/CasServerV10Controller.php index 79f6275ca7f..34f2e133733 100644 --- a/application/modules/opac/controllers/CasServerV10Controller.php +++ b/application/modules/opac/controllers/CasServerV10Controller.php @@ -32,7 +32,7 @@ class CasServerV10Controller extends CasServerController { } - public function returnFailureTicketResponse($error,$ticket=null) { + public function returnFailureTicketResponse($error, $ticket=null, $service=null) { $this->getResponse()->setBody('no'.chr(10)); } } diff --git a/application/modules/opac/controllers/CasServerV3Controller.php b/application/modules/opac/controllers/CasServerV3Controller.php new file mode 100644 index 00000000000..dd26d1bcdf4 --- /dev/null +++ b/application/modules/opac/controllers/CasServerV3Controller.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright (c) 2012-2021, 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 CasServerV3Controller extends CasServerController { + + + protected function _getAttributes($user) { + + return ['lastname' => $user->getNom(), + 'firstname' => $user->getPrenom(), + 'mail' => $user->getMail(), + 'expire_at' => $user->getValidSubscriptionEndDate(), + 'card_number' => $user->getIdabon(), + 'site_code' => $user->getLibraryId(), + 'site_label' => $user->getLibelleBib(), + 'birth_date' => $user->getNaissance(), + 'postal_code' => $user->getCodePostal(), + 'city' => $user->getVille(), + 'affiliation' => array_map( + function($group) { + return $group->getLibelle(); + }, + $user->getUserGroups()) + ]; + } + + + protected function _getCasTicket($service) { + return (new Class_CasTicketV3($service)); + } +} +?> diff --git a/application/modules/opac/views/scripts/cas-server-v3/logout.phtml b/application/modules/opac/views/scripts/cas-server-v3/logout.phtml new file mode 100644 index 00000000000..eac5946e65c --- /dev/null +++ b/application/modules/opac/views/scripts/cas-server-v3/logout.phtml @@ -0,0 +1 @@ +<p><?php echo $this->_('Vous avez été déconnecté'); ?></p> diff --git a/application/modules/opac/views/scripts/cas-server/logout.phtml b/application/modules/opac/views/scripts/cas-server/logout.phtml new file mode 100644 index 00000000000..eac5946e65c --- /dev/null +++ b/application/modules/opac/views/scripts/cas-server/logout.phtml @@ -0,0 +1 @@ +<p><?php echo $this->_('Vous avez été déconnecté'); ?></p> diff --git a/application/modules/telephone/controllers/CasServerV3Controller.php b/application/modules/telephone/controllers/CasServerV3Controller.php new file mode 100644 index 00000000000..f11b02989d5 --- /dev/null +++ b/application/modules/telephone/controllers/CasServerV3Controller.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright (c) 2012, 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 + */ + +require_once ROOT_PATH.'application/modules/opac/controllers/CasServerV3Controller.php'; + +class Telephone_CasServerV3Controller extends CasServerV3Controller { +} + +?> diff --git a/library/Class/Auth/Cas.php b/library/Class/Auth/Cas.php index 19742adbe60..103deb37a3b 100644 --- a/library/Class/Auth/Cas.php +++ b/library/Class/Auth/Cas.php @@ -49,7 +49,7 @@ trait Trait_Auth_CasAware { if ($url_musicme = $this->_redirectMusicme()) return $url_musicme; - $ticket = (new Class_CasTicket())->getTicketForCurrentUser(); + $ticket = $this->_getCasTicket()->getTicketForCurrentUser(); $queries = []; $url_cas = array_merge(['query'=> '', 'path' => ''], parse_url($this->_getServerUrl())); @@ -77,6 +77,9 @@ class Class_Auth_CasLogged extends Class_Auth_Logged { use Trait_Auth_CasAware; public function prepareLogin() { + + $this->_getCasTicket()->save(); + $this->redirect_url = stristr($this->_getServerUrl(), 'deconnexion=ok') ? '/opac' : $this->_urlServiceCas(); @@ -90,6 +93,8 @@ class Class_Auth_CasNotLogged extends Class_Auth_NotLogged { use Trait_Auth_CasAware; protected function _doOnLoginSuccess() { + $this->_getCasTicket()->save(); + $this->redirect_url = $this->_urlServiceCas(); } } diff --git a/library/Class/Auth/Strategy.php b/library/Class/Auth/Strategy.php index 4aef13fd3e1..958231fda79 100644 --- a/library/Class/Auth/Strategy.php +++ b/library/Class/Auth/Strategy.php @@ -27,8 +27,8 @@ abstract class Class_Auth_Strategy { $redirect_url = '', $disable_redirect = false, $on_login_success_callback, - $on_login_fail_callback; - + $on_login_fail_callback, + $_cas_ticket = null; /** * @param $controller Zend_Controller_Action @@ -45,11 +45,24 @@ abstract class Class_Auth_Strategy { } + public function __construct($controller) { + $this->controller = $controller; + $this->default_url = $this->controller->getRedirectDefaultUrl(); + } + + protected static function isLogged() { return Class_Users::getIdentity(); } + protected function _getCasTicket() { + return $this->cas_ticket = ($this->_cas_ticket + ? $this->_cas_ticket + : (Class_CasTicket::newFor($this->controller->getRequest()))); + } + + protected function _getProfilRedirectUrl() { $profil = Class_Profil::getCurrentProfil(); $preferences = $profil->getModuleAccueilPreferencesByType('LOGIN'); @@ -99,12 +112,6 @@ abstract class Class_Auth_Strategy { } - public function __construct($controller) { - $this->controller = $controller; - $this->default_url = $this->controller->getRedirectDefaultUrl(); - } - - public function getRequest(){ return $this->controller->getRequest(); } diff --git a/library/Class/CasTicket.php b/library/Class/CasTicket.php index 0a19c451fb2..62afcf32242 100644 --- a/library/Class/CasTicket.php +++ b/library/Class/CasTicket.php @@ -19,8 +19,24 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ class Class_CasTicket { + const CODE_INVALID_TICKET = 'INVALID_TICKET', + CODE_INVALID_SERVICE = 'INVALID_SERVICE', + CODE_INVALID_REQUEST = 'INVALID_REQUEST'; const PREFIX = 'ST-'; + protected $_error_code; + + public static function newFor($request) { + return ($service = $request->getParam('service')) + && ($request->getParam('controller') == 'cas-server-v3') + ? new Class_CasTicketV3($service) + : (new Class_CasTicket()); + } + + + public function getService() { + return; + } public function getTicketForCurrentUser() { @@ -30,15 +46,32 @@ class Class_CasTicket { } + protected function _setErrorCode($code) { + $this->_error_code = $code; + return $this; + } + + + public function getErrorCode() { + return $this->_error_code; + } + + public function getTicketForUser($user) { return self::PREFIX.md5(Zend_Session::getId() . $user->getId()); } - public function save() { - if ($user = Class_Users::getIdentity()) - (new Storm_Cache())->save((string) $user->getId(), - $this->withoutPrefix($this->getTicketForCurrentUser())); + public function save($user = null) { + $user = ($user + ? $user + : Class_Users::getIdentity()); + + if (!$user) + return $this; + (new Storm_Cache())->save((string) $user->getId(), + $this->withoutPrefix($this->getTicketForCurrentUser())); + return $this; } @@ -47,15 +80,18 @@ class Class_CasTicket { } - public function clear() { - if ($ticket = $this->getTicketForCurrentUser()) + public function clear($ticket = null) { + if ($ticket = $this->getTicketForCurrentUser()) (new Storm_Cache())->remove($this->withoutPrefix($ticket)); + return $this; } public function userForTicket($ticket) { - if ($id = (int) (new Storm_Cache())->load($this->withoutPrefix($ticket))) + if (($id = (new Storm_Cache())->load($this->withoutPrefix($ticket))) + && !is_array($id)) return Class_Users::find($id); - return null; + $this->_setErrorCode(static::CODE_INVALID_TICKET); + return; } -} \ No newline at end of file +} diff --git a/library/Class/CasTicketV3.php b/library/Class/CasTicketV3.php new file mode 100644 index 00000000000..f06a5986d9f --- /dev/null +++ b/library/Class/CasTicketV3.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright (c) 2021, 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 Class_CasTicketV3 extends Class_CasTicket{ + use Trait_TimeSource; + + const CODE_INVALID_TICKET_EXPIRED = 'INVALID_TICKET_EXPIRED', + EXPIRED_INTERVAL_SECONDS = 350; + + protected $_service; + + public function __construct($service) { + $this->_service = $service; + } + + + public function getService() { + return $this->_service; + } + + + public function getTicketForUser($user) { + return self::PREFIX.md5(Zend_Session::getId() . $user->getId() . $this->_service); + } + + + protected function _expiredAt() { + return $this->getCurrentTime() + self::EXPIRED_INTERVAL_SECONDS; + } + + + public function save($user = null) { + $user = ($user ? $user : Class_Users::getIdentity()); + if ($user) + (new Storm_Cache())->save( + new CasTicketV3Cache((string) $user->getId(), $this->_service, $this->_expiredAt()), + $this->withoutPrefix($this->getTicketForUser($user))); + return $this; + } + + + public function userForTicket($ticket) { + + if (!$cas_cache = (new Storm_Cache())->load($this->withoutPrefix($ticket))) { + $this->_setErrorCode(static::CODE_INVALID_TICKET); + $this->clear($ticket); + return; + } + + if ($cas_cache->getService() != $this->_service) { + $this->_setErrorCode(static::CODE_INVALID_SERVICE); + $this->clear($ticket); + return; + } + + if ($cas_cache->isExpired($this->getCurrentTime())) { + $this->_setErrorCode(static::CODE_INVALID_TICKET_EXPIRED); + $this->clear($ticket); + return; + } + + $this->clear($ticket); + return Class_Users::find($cas_cache->getId()); + } +} + + + + +class CasTicketV3Cache { + + protected $_id, + $_service, + $_expired_at; + + public function __construct($id, $service, $expired_at) { + $this->_id = $id; + $this->_service = $service; + $this->_expired_at = $expired_at; + } + + + public function getService() { + return $this->_service; + } + + + public function getId() { + return $this->_id; + } + + + public function getExpiredAt() { + return $this->_expired_at; + } + + + public function isExpired($date) { + xdebug_break(); + return $this->_expired_at < $date; + } +} diff --git a/library/Class/DigitalResource/Controller.php b/library/Class/DigitalResource/Controller.php index 305e6083240..f69b259739e 100644 --- a/library/Class/DigitalResource/Controller.php +++ b/library/Class/DigitalResource/Controller.php @@ -92,4 +92,4 @@ class Class_DigitalResource_Controller extends ZendAfi_Controller_Action { $this->_redirect($this->view->absoluteUrl([], null, true)); } -} \ No newline at end of file +} diff --git a/library/Trait/TimeSource.php b/library/Trait/TimeSource.php index a6087a7a694..390bd4abd58 100644 --- a/library/Trait/TimeSource.php +++ b/library/Trait/TimeSource.php @@ -23,7 +23,6 @@ trait Trait_TimeSource { /** @var Class_TimeSource */ protected static $_time_source; - /** * @category testing * @return int @@ -60,8 +59,8 @@ trait Trait_TimeSource { public static function addIntervalToDate($interval, $date){ $date= $date - ? strtotime($date) - : static::getTimeSource()->time(); + ? strtotime($date) + : static::getTimeSource()->time(); return strtotime($interval, $date); } diff --git a/library/ZendAfi/Controller/Action/Helper/CasFailureResponse.php b/library/ZendAfi/Controller/Action/Helper/CasFailureResponse.php index 45b6eeb76ea..c86b9d179b4 100644 --- a/library/ZendAfi/Controller/Action/Helper/CasFailureResponse.php +++ b/library/ZendAfi/Controller/Action/Helper/CasFailureResponse.php @@ -22,18 +22,30 @@ class ZendAfi_Controller_Action_Helper_CasFailureResponse extends Zend_Controller_Action_Helper_Abstract { - public function direct($error,$ticket=null) { + public function direct($error, $ticket = null, $service = null) { + $xml = new Class_Xml_Builder(); + $body = []; + + $code = $error; + $msg = ''; + + if ($error == Class_CasTicket::CODE_INVALID_SERVICE && isset($service)) + $msg = ' Service '.$service.' not recognized'; + + if ($error == Class_CasTicket::CODE_INVALID_TICKET && isset($ticket)) + $msg = ' Ticket '.$ticket.' not recognized'; + + if ($error == Class_CasTicketV3::CODE_INVALID_TICKET_EXPIRED && isset($ticket)) + $msg = ' Ticket '.$ticket.' expired'; + + if ($error == Class_CasTicketV3::CODE_INVALID_TICKET_EXPIRED) + $code = Class_CasTicket::CODE_INVALID_TICKET; + + $body[] = $xml->_xmlString('cas:authenticationFailure', $msg ? $xml->cdata($msg): '', sprintf(' code="%s"', $code)); $this->getActionController()->getHelper('ViewRenderer')->setNoRender(); $this->getResponse()->setHeader('Content-Type', 'application/xml;charset=utf-8'); - $xml='<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">'; - $xml.='<cas:authenticationFailure code="'.$error.'">'; - if (isset($ticket)) - $xml.=' Ticket '.$ticket.' not recognized'; - - $xml.='</cas:authenticationFailure>'; - $xml.='</cas:serviceResponse>'; + $xml = $xml->_xmlString('cas:serviceResponse', implode('\n', $body), ' xmlns:cas="http://www.yale.edu/tp/cas"'); $this->getResponse()->setBody($xml); } - } -?> \ No newline at end of file +?> diff --git a/library/digital_resources/Skilleos/tests/SkilleosTest.php b/library/digital_resources/Skilleos/tests/SkilleosTest.php index a8babf52124..bf9f32b1247 100644 --- a/library/digital_resources/Skilleos/tests/SkilleosTest.php +++ b/library/digital_resources/Skilleos/tests/SkilleosTest.php @@ -88,7 +88,7 @@ class SkilleosModulesControllerUserWithGroupWithRightTest /** @test */ public function validateAuthWithInvalidTicketShouldAnswerError() { $this->dispatch('/Skilleos_Plugin/auth/servicevalidate/service/blabla/ticket/666', true); - $this->assertContains('<cas:authenticationFailure code="INVALID_TICKET"> Ticket 666 not recognized</cas:authenticationFailure>',$this->_response->getBody()); + $this->assertContains('<cas:authenticationFailure code="INVALID_TICKET"><![CDATA[ Ticket 666 not recognized]]></cas:authenticationFailure>',$this->_response->getBody()); } diff --git a/library/digital_resources/Syracuse/tests/SyracuseTest.php b/library/digital_resources/Syracuse/tests/SyracuseTest.php index 1aec6e6d68b..ed3e2f98e04 100644 --- a/library/digital_resources/Syracuse/tests/SyracuseTest.php +++ b/library/digital_resources/Syracuse/tests/SyracuseTest.php @@ -116,7 +116,7 @@ class SyracuseModulesControllerTest extends SyracuseModulesControllerTestCase { /** @test */ public function validateAuthWithInvalidTicketShouldAnswerError() { $this->dispatch('/Syracuse_Plugin/auth/servicevalidate/service/blabla/ticket/666'); - $this->assertContains('<cas:authenticationFailure code="INVALID_TICKET"> Ticket 666 not recognized</cas:authenticationFailure>', + $this->assertContains('<cas:authenticationFailure code="INVALID_TICKET"><![CDATA[ Ticket 666 not recognized]]></cas:authenticationFailure>', $this->_response->getBody()); } @@ -259,4 +259,4 @@ class SyracuseModulesControllerWithRightsTest extends SyracuseModulesControllerT $this->assertContains('<pre>http://localhost' . Class_Url::baseUrl() . '/Syracuse_Plugin/auth/serviceValidate?service=bokeh-test&ticket=' . $ticket . '</pre>', $this->_response->getBody()); } -} \ No newline at end of file +} diff --git a/library/startup.php b/library/startup.php index 42206f8ea75..7e3e72edaef 100644 --- a/library/startup.php +++ b/library/startup.php @@ -380,6 +380,12 @@ class Bokeh_Engine { ['module' => 'telephone', 'controller' => 'index', 'action' => 'index'])) + ->addRoute('casV3', + new Zend_Controller_Router_Route('cas-server-v3/p3/serviceValidate', + ['module' => 'opac', + 'controller' => 'cas-server-v3', + 'action' => 'serviceValidate'])) + ->addRoute('sitemap', new Zend_Controller_Router_Route_Static('sitemap.xml', ['module' => 'opac', diff --git a/tests/application/modules/opac/controllers/CasServerControllerTest.php b/tests/application/modules/opac/controllers/CasServerControllerTest.php index d4bbaec55b8..8cec05bfc82 100644 --- a/tests/application/modules/opac/controllers/CasServerControllerTest.php +++ b/tests/application/modules/opac/controllers/CasServerControllerTest.php @@ -29,6 +29,7 @@ class CasServerControllerValidateActionTest extends AbstractControllerTestCase { Storm_Cache::beVolatile(); $user = new StdClass(); $user->ID_USER=300; + $time_source = new TimeSourceForTest('2021-08-01'); Class_Users::newInstanceWithId(300, ['login' => '87364', 'pseudo' => 'georges']); @@ -37,47 +38,52 @@ class CasServerControllerValidateActionTest extends AbstractControllerTestCase { } + /** @test */ + public function requestWithNoServiceShouldRespondinvalidRequestFailureXML() { + $this->dispatch('/opac/cas-server/validate?ticket=myticket'); + + $this->assertContains('<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"><cas:authenticationFailure code="INVALID_REQUEST"></cas:authenticationFailure></cas:serviceResponse>', $this->_response->getBody()); + } + + /** @test */ public function requestWithNoTicketShouldRespondinvalidRequestFailureXML() { - $this->dispatch('/opac/cas-server/validate?service=http://test.com'); - $this->assertContains('<cas:authenticationFailure code="INVALID_REQUEST">',$this->_response->getBody()); + $this->dispatch('/opac/cas-server/validate?service='.urlencode('http://test.com')); + $this->assertContains('<cas:authenticationFailure code="INVALID_REQUEST">', $this->_response->getBody()); } /** @test */ public function requestWithInvalidTicketShouldRespondInvalidTicketFailureXML() { - $this->dispatch('/opac/cas-server/validate?ticket=STmarchepo&service=http://test.com',true); - $this->assertContains('<cas:authenticationFailure code="INVALID_TICKET"> Ticket STmarchepo not recognized</cas:authenticationFailure>',$this->_response->getBody()); + $this->dispatch('/opac/cas-server/validate?ticket=STmarchepo&service=http://test.com', true); + $this->assertContains('<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"><cas:authenticationFailure code="INVALID_TICKET"><![CDATA[ Ticket STmarchepo not recognized]]></cas:authenticationFailure></cas:serviceResponse>', $this->_response->getBody()); } /** @test */ public function requestWithInvalidTicketOnAuthShouldRespondInvalidTicketFailureXML() { - $this->dispatch('/opac/auth/validate?ticket=STmarchepo&service=http://test.com',true); - $this->assertContains('<cas:authenticationFailure code="INVALID_TICKET"> Ticket STmarchepo not recognized</cas:authenticationFailure>',$this->_response->getBody()); + $this->dispatch('/opac/auth/validate?ticket=STmarchepo&service=http://test.com', true); + $this->assertContains('<cas:authenticationFailure code="INVALID_TICKET"><![CDATA[ Ticket STmarchepo not recognized]]></cas:authenticationFailure>', $this->_response->getBody()); } /** @test */ public function requestWithValidTicketShouldRespondValidXML() { - $this->dispatch('/opac/cas-server/validate?ticket='.md5(Zend_Session::getId().'300').'&service=http://test.com'); - $this->assertContains('<cas:user>300</cas:user>',$this->_response->getBody()); - $this->assertContains('<cas:proxyGrantingTicket>',$this->_response->getBody()); - } - - /** @test */ - public function requestWithValidTicketPrefixedBySTShouldRespondValidXML() { - $this->dispatch('/opac/cas-server/validate?ticket=ST-'.md5(Zend_Session::getId().'300').'&service=http://test.com'); - $this->assertContains('<cas:user>300</cas:user>',$this->_response->getBody()); - $this->assertContains('<cas:proxyGrantingTicket>',$this->_response->getBody()); + $ticket = md5(Zend_Session::getId().'300'); + $this->dispatch(sprintf('/opac/cas-server/validate?ticket=%s&service=%s', + $ticket, + urlencode('http://test.com'))); + $this->assertContains("<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>\n <cas:authenticationSuccess>\n <cas:user>300</cas:user>\n <cas:proxyGrantingTicket>".$ticket."</cas:proxyGrantingTicket>\n </cas:authenticationSuccess>\n</cas:serviceResponse>", $this->_response->getBody()); } /** @test */ - public function requestWithValidTicketPrefixedBySTOnAuthenticateControllerShouldRespondValidXML() { - $this->dispatch('/opac/auth/validate?ticket=ST-'.md5(Zend_Session::getId().'300').'&service=http://test.com'); - $this->assertContains('<cas:user>300</cas:user>',$this->_response->getBody()); - $this->assertContains('<cas:proxyGrantingTicket>',$this->_response->getBody()); + public function requestWithValidTicketPrefixedBySTShouldRespondValidXML() { + $this->dispatch(sprintf('/opac/cas-server/validate?ticket=ST-%s&service=%s', + md5(Zend_Session::getId().'300'), + urlencode('http://test.com'))); + $this->assertContains('<cas:user>300</cas:user>', $this->_response->getBody()); + $this->assertContains('<cas:proxyGrantingTicket>', $this->_response->getBody()); } @@ -86,12 +92,15 @@ class CasServerControllerValidateActionTest extends AbstractControllerTestCase { * @test */ public function validateOnCasOneZeroWithValidTicketShouldAnswerYesLFUsernameLogin() { - $this->dispatch( - '/opac/cas-server-v10/validate?ticket=ST-'.md5(Zend_Session::getId().'300').'&service=http://test.com', - true); - $this->assertEquals('yes'.chr(10).'georges|87364||'.chr(10), $this->_response->getBody()); + + $this->dispatch(sprintf('/opac/cas-server-v10/validate?ticket=ST-%s&service=%s', + md5(Zend_Session::getId().'300'), + urlencode('http://test.com'))); + + $this->assertEquals('yes'.chr(10).'georges|87364||'.chr(10), $this->_response->getBody(), $this->_response->getBody()); } + /** * @test */ @@ -100,8 +109,8 @@ class CasServerControllerValidateActionTest extends AbstractControllerTestCase { ->setMail('georges@souris.fr') ->setNaissance('1978-02-17'); $this->dispatch( - '/opac/cas-server-v10/validate?ticket=ST-'.md5(Zend_Session::getId().'300').'&service=http://test.com', - true); + '/opac/cas-server-v10/validate?ticket=ST-'.md5(Zend_Session::getId().'300').'&service=http://test.com', + true); $this->assertEquals('yes'.chr(10) .'georges|87364|georges@souris.fr|1978/02/17'.chr(10), $this->_response->getBody()); @@ -111,8 +120,8 @@ class CasServerControllerValidateActionTest extends AbstractControllerTestCase { /** @test */ public function validateOnCasOneZeroWithInValidTicketShouldAnswerNoLF() { $this->dispatch( - '/opac/cas-server-v10/validate?ticket=zork&service=http://test.com', - true); + '/opac/cas-server-v10/validate?ticket=zork&service=http://test.com', + true); $this->assertEquals('no'.chr(10), $this->_response->getBody()); } @@ -121,8 +130,8 @@ class CasServerControllerValidateActionTest extends AbstractControllerTestCase { public function loginOnCasOneZeroShouldRedirectToServiceWithTicket() { $this->dispatch('/opac/cas-server-v10/login?service=http://test.com', true); $this->assertRedirectTo( - 'http://test.com?ticket='.(new Class_CasTicket())->getTicketForCurrentUser(), - $this->getResponseLocation()); + 'http://test.com?ticket='.(new Class_CasTicket('http://test.com'))->getTicketForCurrentUser(), + $this->getResponseLocation()); } diff --git a/tests/application/modules/opac/controllers/CasServerControllerV3Test.php b/tests/application/modules/opac/controllers/CasServerControllerV3Test.php new file mode 100644 index 00000000000..82ce2720ba5 --- /dev/null +++ b/tests/application/modules/opac/controllers/CasServerControllerV3Test.php @@ -0,0 +1,229 @@ +<?php +/** + * Copyright (c) 2021, 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 CasServerControllerV3ValidateActionTest extends AbstractControllerTestCase { + protected $session_file_contents_logged; + protected $session_file_contents_nologin, + $_ticket_v3; + + public function setUp() { + parent::setUp(); + Storm_Cache::beVolatile(); + $user = new StdClass(); + $user->ID_USER=300; + $time_source = new TimeSourceForTest('2020-08-01 14:00:00'); + Class_CasTicketV3::setTimeSource($time_source); + $user = Class_Users::newInstanceWithId(300, + ['login' => '87364', + 'pseudo' => 'georges']); + $cas = (new Class_CasTicketV3('http://test.com')); + + $this->_ticket_v3 = $cas->getTicketForUser($user); + $cas->save($user); + } + + + /** @test */ + public function requestWithBadServiceShouldRespondinvalidServiceFailureXML() { + $this->dispatch(sprintf('/opac/cas-server-v3/validate?ticket=%s&service=%s', + md5(Zend_Session::getId().'300'.'http://test.com'), + urlencode('http://fakeservice.com'))); + $this->assertContains('<cas:authenticationFailure code="INVALID_SERVICE"><![CDATA[ Service http://fakeservice', $this->_response->getBody()); + } + + + /** @test */ + public function requestWithExpiredMoreThanFiveMinutesTicketShouldRespondinvalidTicketFailureXML() { + $time_source = new TimeSourceForTest('2020-08-01 14:06:00'); + Class_CasTicketV3::setTimeSource($time_source); + + $this->dispatch(sprintf('/opac/cas-server-v3/validate?ticket=%s&service=%s', + $this->_ticket_v3, + urlencode('http://test.com'))); + $this->assertContains('<cas:authenticationFailure code="INVALID_TICKET"><![CDATA[ Ticket ' + .$this->_ticket_v3 + .' expired]]></cas:authenticationFailure>', $this->_response->getBody()); + } + + + /** @test */ + public function requestWithNoTicketShouldRespondinvalidRequestFailureXML() { + $this->dispatch('/opac/cas-server-v3/validate?service='.urlencode('http://test.com')); + $this->assertContains('<cas:authenticationFailure code="INVALID_REQUEST">', $this->_response->getBody()); + } + + + /** @test */ + public function requestWithInvalidTicketShouldRespondInvalidTicketFailureXML() { + $this->dispatch('/opac/cas-server-v3/validate?ticket=STmarchepo&service=http://test.com', true); + $this->assertContains('<cas:authenticationFailure code="INVALID_TICKET"><![CDATA[ Ticket STmarchepo not recognized]]></cas:authenticationFailure>', $this->_response->getBody()); + } + + + /** @test */ + public function requestWithInvalidTicketOnAuthShouldRespondInvalidTicketFailureXML() { + $this->dispatch('/opac/auth/validate?ticket=STmarchepo&service=http://test.com', true); + $this->assertContains('<cas:authenticationFailure code="INVALID_TICKET"><![CDATA[ Ticket STmarchepo not recognized]]></cas:authenticationFailure>', $this->_response->getBody()); + } + + + /** @test */ + public function requestWithValidTicketShouldRespondValidXML() { + $this->dispatch(sprintf('/opac/cas-server-v3/validate?ticket=%s&service=%s', + $this->_ticket_v3, + urlencode('http://test.com'))); + $this->assertContains('<cas:user>300</cas:user>', $this->_response->getBody()); + $this->assertContains('<cas:proxyGrantingTicket>', $this->_response->getBody()); + } + + + /** @test */ + public function requestWithValidTicketPrefixedBySTShouldRespondValidXML() { + $this->dispatch(sprintf('/opac/cas-server-v3/validate?ticket=%s&service=%s', + $this->_ticket_v3, + urlencode('http://test.com'))); + $this->assertContains('<cas:user>300</cas:user>', $this->_response->getBody()); + $this->assertContains('<cas:proxyGrantingTicket>', $this->_response->getBody()); + } + + + /** @test */ + public function loginShouldRedirectToServiceWithTicket() { + ZendAfi_Auth::getInstance()->logUser(Class_Users::find(300)); + $this->dispatch('/opac/cas-server-v3/login?service=http://test.com', true); + $this->assertRedirectTo( + 'http://test.com?ticket='.$this->_ticket_v3, + $this->getResponseLocation()); + } + + + /** @test */ + public function loginWithoutOpenedSessionShouldDisplayLoginForm() { + ZendAfi_Auth::getInstance()->clearIdentity(); + $this->dispatch('/opac/cas-server-v3/login?service=http://test.com', true); + $this->assertXPath('//form//input[@name="password"]'); + } + + + /** @test */ + public function logoutShouldClearIdentityAndDisplayThatYouHaveBeenDisconnected() { + $this->dispatch('/opac/cas-server-v3/logout', true); + $this->assertXPathContentContains('//p', 'Vous avez été déconnecté'); + $this->assertEmpty(ZendAfi_Auth::getInstance()->getIdentity()); + } + + + /** @test */ + public function logoutWithUrlParamShouldRedirectToIt() { + $this->dispatch('/opac/cas-server-v3/logout?url=http://go-out.com', true); + $this->assertRedirectTo('http://go-out.com'); + } + + + /** @test */ + public function loginOnCasThreeShouldRedirectToServiceWithTicket() { + ZendAfi_Auth::getInstance()->logUser(Class_Users::find(300)); + $this->dispatch('/opac/cas-server-v3/login?service=http://test.com', true); + $this->assertRedirectTo( + 'http://test.com?ticket='.$this->_ticket_v3, + $this->getResponseLocation()); + } + + + /** @test */ + public function validateOnCasThreeWithInValidTicketShouldAnswerNotRecognized() { + $this->dispatch( '/cas-server-v3/p3/serviceValidate?ticket=zork&service=http://test.com'); + $this->assertEquals('<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"><cas:authenticationFailure code="INVALID_TICKET"><![CDATA[ Ticket zork not recognized]]></cas:authenticationFailure></cas:serviceResponse>', $this->_response->getBody()); + } +} + + + + +class CasServerV3ControllerWithValidTicketTest extends AbstractControllerTestCase { + protected $_storm_default_to_volatile = true; + protected $session_file_contents_logged; + protected $session_file_contents_nologin, + $_ticket; + + public function setUp() { + parent::setUp(); + Storm_Cache::beVolatile(); + $user = $this->fixture('Class_Users', + ['id' => 300, + 'login' => '87364', + 'prenom' => 'georges', + 'nom' => 'souris', + 'pseudo' => 'georges', + 'mail' => 'georges@souris.fr', + 'idabon' => '777', + 'code_postal' => '74000', + 'ville' => 'Annecy', + 'password' => 'go', + 'naissance' => '1978-02-17']); + $time_source = new TimeSourceForTest('2021-08-01'); + Class_CasTicketV3::setTimeSource($time_source); + $cas = (new Class_CasTicketV3('http://test.com')); + $cas->save($user); + $this->_ticket = $cas->getTicketForUser($user); + + $this->dispatch(sprintf('/cas-server-v3/p3/serviceValidate?ticket=%s&service=http://test.com', + $this->_ticket)); + } + + + public function getCasAttribs() { + return [ + ['lastname', 'souris'], + ['firstname','georges'], + ['mail' , 'georges@souris.fr'], + ['birth_date', '1978-02-17'], + ['card_number','777'], + ['postal_code','74000'], + ['city','Annecy'], + + ]; + } + + + /** + * @test + * @dataProvider getCasAttribs + */ + public function validateShouldContains($key, $value) { + $this->assertContains(sprintf('<cas:%s>%s</cas:%s>', + $key, + $value, + $key) , $this->_response->getBody()); + } + + + /** @test */ + public function afterValidationTicketShouldExpired() { + $this->assertFalse((new Storm_Cache())->load($this->_ticket)); + } + + + /** @test */ + public function ticketShouldExpiredAfter5minutes() { + } +} diff --git a/tests/application/modules/telephone/controllers/CasServerControllerTest.php b/tests/application/modules/telephone/controllers/CasServerControllerTest.php index 4142a963c6c..f0c26a28ddc 100644 --- a/tests/application/modules/telephone/controllers/CasServerControllerTest.php +++ b/tests/application/modules/telephone/controllers/CasServerControllerTest.php @@ -20,30 +20,45 @@ */ require_once 'TelephoneAbstractControllerTestCase.php'; - class Telephone_CasServerControllerLoggedTest extends TelephoneAbstractControllerTestCase { + protected $_ticket, + $_ticket_v3; + public function setUp() { parent::setUp(); Storm_Cache::beVolatile(); $user = new StdClass(); $user->ID_USER=300; - Class_Users::newInstanceWithId(300, + $user = Class_Users::newInstanceWithId(300, ['login' => '87364', 'pseudo' => 'georges']); + $this->_ticket = md5(Zend_Session::getId().'300'); + $cas = new Class_CasTicketV3('http://test.com'); + + $this->_ticket_v3 = $cas->getTicketForUser($user); + $cas->save($user); (new Storm_Cache())->save('300', - md5(Zend_Session::getId().'300')); + $this->_ticket); } + /** @test */ public function requestWithValidTicketResponseShouldContainsValidXML() { - $this->dispatch('/telephone/cas-server/validate?ticket='.md5(Zend_Session::getId().'300').'&service=http://test.com', true); + $this->dispatch('/telephone/cas-server/validate?ticket='.$this->_ticket.'&service=http://test.com'); + $this->assertContains("<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>\n <cas:authenticationSuccess>\n <cas:user>300</cas:user>\n <cas:proxyGrantingTicket>".$this->_ticket."</cas:proxyGrantingTicket>\n </cas:authenticationSuccess>\n</cas:serviceResponse>", $this->_response->getBody()); + } + + + /** @test */ + public function requestWithValidCasV3TicketResponseShouldContainsValidXML() { + $this->dispatch('/telephone/cas-server-v3/servicevalidate?ticket='.$this->_ticket_v3.'&service=http://test.com'); $this->assertContains('<cas:user>300</cas:user>', $this->_response->getBody()); } /** @test */ public function requestOnV10WithValidTicketResponseShouldContainsGeorges87364() { - $this->dispatch('/telephone/cas-server-v10/validate?ticket='.md5(Zend_Session::getId().'300').'&service=http://test.com', true); + $this->dispatch('/telephone/cas-server-v10/validate?ticket='.$this->_ticket.'&service=http://test.com', true); $this->assertContains('georges|87364', $this->_response->getBody()); } @@ -52,13 +67,14 @@ class Telephone_CasServerControllerLoggedTest extends TelephoneAbstractControlle public function loginOnCasOneZeroShouldRedirectToServiceWithTicket() { $this->dispatch('/telephone/cas-server-v10/login?service=http://test.com', true); $this->assertRedirectTo( - 'http://test.com?ticket='.(new Class_CasTicket())->getTicketForCurrentUser(), - $this->getResponseLocation()); + 'http://test.com?ticket='.(new Class_CasTicket('http://test.com'))->getTicketForCurrentUser(), + $this->getResponseLocation()); } } + class Telephone_CasServerControllerNotLoggedTest extends TelephoneAbstractControllerTestCase { public function setUp() { parent::setUp(); @@ -68,8 +84,8 @@ class Telephone_CasServerControllerNotLoggedTest extends TelephoneAbstractContro /** @test */ public function pageAuthLoginWithServiceShouldIncludeHiddenService() { - $this->dispatch('/telephone/auth/login?service=http://monurlservice',true); - $this->assertXPath('//input[@name="service"][@type="hidden"][@value="http://monurlservice"]',$this->_response->getBody()); + $this->dispatch('/telephone/auth/login?service=http://monurlservice', true); + $this->assertXPath('//input[@name="service"][@type="hidden"][@value="http://monurlservice"]', $this->_response->getBody()); } } -- GitLab