Commit 78f5db69 authored by efalcy's avatar efalcy
Browse files

hotline#136551 : add service verification on CAS 3 only

parent d4fcf5a5
- ticket #136551 : [Bibliothèque numérique]: Implémentation du serveur CAS V3 avec mise à jour de sècurité
\ No newline at end of file
......@@ -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
?>
......@@ -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));
}
}
......
<?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));
}
}
?>
<p><?php echo $this->_('Vous avez été déconnecté'); ?></p>
<p><?php echo $this->_('Vous avez été déconnecté'); ?></p>
<?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 {
}
?>
......@@ -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();
}
}
......@@ -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();
}
......
......@@ -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
}
<?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;
}
}
......@@ -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
}
......@@ -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);
}
......
......@@ -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
?>
......@@ -88,7 +88,7 @@ class SkilleosModulesControllerUserWithGroupWithRightTest
/** @test */
public function validateAuthWithInvalidTicketShouldAnswerError() {
$this->dispatch('/Skilleos_Plugin/auth/servicevalidate/service/blabla/ticket/666', true);