diff --git a/VERSIONS_HOTLINE/136551 b/VERSIONS_HOTLINE/136551
new file mode 100644
index 0000000000000000000000000000000000000000..643b802d686b7c29b3e4633b38d221d6e5c538a7
--- /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 118fc8c70c6b3f78c691def8aa2bf39ef1fb4e9f..ad3cabaea3cee287c32f7256b272e94dabe69f05 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 79f6275ca7f06508aff3cfb091494f5d214d1861..34f2e133733463d7b4e178052a0d3b3f8e0d64d0 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 0000000000000000000000000000000000000000..dd26d1bcdf4cf02931e9000bc66bc4c8ea7a6af0
--- /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 0000000000000000000000000000000000000000..eac5946e65c8447520821b30eba549926c87225a
--- /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 0000000000000000000000000000000000000000..eac5946e65c8447520821b30eba549926c87225a
--- /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 0000000000000000000000000000000000000000..f11b02989d56349d7fc4689d58e1f81d3a3aa1b3
--- /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 19742adbe606c08475db1e7d653058b8997bea80..103deb37a3bb95274cc8b8b9cd581f120198e79e 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 4aef13fd3e1f9f0e8ee400842ebbac4c272cec50..958231fda79e91c05fd4582c63a1ed728e807d77 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 0a19c451fb2170f08186d28ede0089f890498fa0..62afcf32242aeb15ecca4a3e0561ccde84b14fd0 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 0000000000000000000000000000000000000000..f06a5986d9f6cd2484a4f947621a3c164b5398cc
--- /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 305e608324061ae569d7ee667d3f3d36ca8aa604..f69b259739ec0212fbd3dd10d8a75befc8be9d87 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 a6087a7a694020bcbdf36b9733a380b176d672e0..390bd4abd58a682cbdf9237f78985befc5dbb487 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 45b6eeb76ea50bcab118b1a759a32071be37b2e3..c86b9d179b4dd2ae210576529f9c6445012c0aee 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 a8babf521242f11106a7675ceb2171adecda6646..bf9f32b1247194b9d0c8c10ae82305fecf0fe48f 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 1aec6e6d68b92924040eba148f8bcd494494f222..ed3e2f98e04f1b2db3daf270a8a353a1335b59f3 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 3c794c55520ff39f9683eb3d0f401c8cfbd168ae..84c0dc1a9cc89acefe96d6e3de3597a489cad71b 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 d4bbaec55b82b5b38cff48d2054a7c830214a0dd..8cec05bfc82cc658027b0328e35b43895dc57574 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 0000000000000000000000000000000000000000..82ce2720ba5611321f88f28240c73c9591ff83b4
--- /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 4142a963c6c0dac3ffb46aac0b7679707446cbc9..f0c26a28ddca38add480efd3b91d36b733a4e36e 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());
   }
 }