From d975cfafd7dccbff676a3cc93d4abec583367552 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?ANDRE=20s=C3=A9bastien?= <sandre@afi-sa.fr>
Date: Wed, 8 Jul 2020 17:23:17 +0200
Subject: [PATCH] dev#110690 : Add CAS Identity provider

---
 VERSIONS_WIP/110690                           |   1 +
 .../opac/controllers/AuthController.php       |  29 +-
 .../opac/views/scripts/auth/boite-login.phtml |  10 +-
 .../opac/views/scripts/auth/login.phtml       |   9 +-
 library/Class/Auth/IdentityProvider.php       |  37 +-
 library/Class/IdentityProvider.php            |  46 +-
 library/Class/IdentityProvider/Cas2.php       |  34 +
 library/Class/IdentityProvider/Types.php      |   3 +-
 .../TableDescription/IdentityProviders.php    |  44 ++
 library/Class/Template.php                    |  27 +-
 library/Class/User/Identity.php               |  10 +
 library/Class/WebService/Acheteza.php         |  27 -
 library/Class/WebService/Cas2.php             |  96 +++
 library/Class/WebService/IdentityProvider.php |  23 +-
 library/Class/WebService/OpenId.php           |  17 +-
 library/ZendAfi/Controller/Action.php         |   5 +
 .../ZendAfi/Form/Admin/IdentityProvider.php   |  25 +-
 .../Abonne/AssociatedProvidersBoard.php       |  19 +-
 .../ZendAfi/View/Helper/Admin/HelpLink.php    |   3 +-
 .../ZendAfi/View/Helper/IdentityProviders.php |   7 +-
 library/ZendAfi/View/Helper/RenderLogin.php   |  34 +
 .../View/Wrapper/RichContent/Section.php      |  15 +-
 .../Wrapper/User/RichContent/Settings.php     | 132 ++--
 .../Widget/Accessibility/Definition.php       |   6 +
 .../Library/Widget/AdminTools/Definition.php  |   6 +
 .../Library/Widget/Breadcrumb/Definition.php  |   7 +
 .../Widget/Carousel/Agenda/Definition.php     |  11 +-
 .../Widget/Carousel/Article/Definition.php    |  10 +
 .../Widget/Carousel/Author/Definition.php     |   8 +
 .../Library/Widget/Carousel/Definition.php    | 109 +++-
 .../Widget/Carousel/Domain/Definition.php     |   9 +
 .../Widget/Carousel/Library/Definition.php    |   7 +
 .../Widget/Carousel/Newsletter/Definition.php |   8 +
 .../Widget/Carousel/Record/Definition.php     |   9 +
 .../Widget/Carousel/Review/Definition.php     |   5 +
 .../Widget/Carousel/Rss/Definition.php        |   8 +
 .../Library/Widget/Credits/Definition.php     |   6 +
 .../Library/Widget/Free/Definition.php        |   6 +
 .../Widget/IdentityProvider/Definition.php    |  42 ++
 .../Library/Widget/Image/Definition.php       |   6 +
 .../Library/Widget/Language/Definition.php    |   5 +
 .../Library/Widget/Login/Definition.php       |   6 +
 .../Intonation/Library/Widget/Login/View.php  |   3 +-
 .../Library/Widget/Menu/Definition.php        |   6 +
 .../Library/Widget/Nav/Definition.php         |   7 +
 .../Library/Widget/Notify/Definition.php      |   7 +
 .../Library/Widget/Scroll/Definition.php      |   6 +
 .../Library/Widget/Search/Definition.php      |  10 +-
 .../Library/Widget/Share/Definition.php       |   6 +
 .../Library/Widget/TemplatesAware.php         |  35 ++
 .../Library/Widget/TemplatesAwareNoHeader.php |  35 ++
 .../Intonation/Library/WidgetTemplates.php    | 473 +++++++++-----
 library/templates/Intonation/Template.php     |   4 +-
 .../Intonation/View/IdentityProviders.php     |  27 +
 .../templates/Intonation/View/RenderLogin.php |  49 ++
 .../Intonation/View/Widget/Login.php          |   1 +
 .../TEMPLATE_IDENTITY_PROVIDER.jpg            | Bin 0 -> 22202 bytes
 .../js/widget_templates/TEMPLATE_LANGUE.jpg   | Bin 0 -> 11804 bytes
 .../modules/AbstractControllerTestCase.php    |   1 +
 .../DriveCheckOutBookingTest.php              |   4 +-
 .../IdentityProviderAdminTest.php             |  71 +++
 .../IdentityProviderAuthenticationCasTest.php | 585 ++++++++++++++++++
 .../IdentityProviderAuthenticationTest.php    |  20 +-
 tests/scenarios/PnbDilicom/PnbDilicomTest.php |   2 +-
 .../RGPD/PatronDownloadDatasTest.php          |  15 +-
 .../Templates/TemplatesAddWidgetTest.php      |  79 +++
 .../Templates/TemplatesAuthLoginTest.php      |  73 +++
 .../Templates/TemplatesAuthorTest.php         |   8 +-
 .../TemplatesPatronConfigurationsTest.php     |  84 +++
 tests/scenarios/Templates/TemplatesTest.php   |  26 +-
 .../TemplatesWidgetIdentityProvidersTest.php  |  50 ++
 .../Templates/TemplatesWidgetTest.php         |  26 -
 72 files changed, 2213 insertions(+), 427 deletions(-)
 create mode 100644 VERSIONS_WIP/110690
 create mode 100644 library/Class/IdentityProvider/Cas2.php
 create mode 100644 library/Class/TableDescription/IdentityProviders.php
 create mode 100644 library/Class/WebService/Cas2.php
 create mode 100644 library/ZendAfi/View/Helper/RenderLogin.php
 create mode 100644 library/templates/Intonation/Library/Widget/IdentityProvider/Definition.php
 create mode 100644 library/templates/Intonation/Library/Widget/TemplatesAware.php
 create mode 100644 library/templates/Intonation/Library/Widget/TemplatesAwareNoHeader.php
 create mode 100644 library/templates/Intonation/View/IdentityProviders.php
 create mode 100644 library/templates/Intonation/View/RenderLogin.php
 create mode 100644 public/opac/js/widget_templates/TEMPLATE_IDENTITY_PROVIDER.jpg
 create mode 100644 public/opac/js/widget_templates/TEMPLATE_LANGUE.jpg
 create mode 100644 tests/scenarios/IdentityProvider/IdentityProviderAuthenticationCasTest.php
 create mode 100644 tests/scenarios/Templates/TemplatesAddWidgetTest.php
 create mode 100644 tests/scenarios/Templates/TemplatesAuthLoginTest.php
 create mode 100644 tests/scenarios/Templates/TemplatesPatronConfigurationsTest.php
 create mode 100644 tests/scenarios/Templates/TemplatesWidgetIdentityProvidersTest.php

diff --git a/VERSIONS_WIP/110690 b/VERSIONS_WIP/110690
new file mode 100644
index 00000000000..1adf4f022e4
--- /dev/null
+++ b/VERSIONS_WIP/110690
@@ -0,0 +1 @@
+ - ticket #110690 : Compte lecteur : Ajout de la prise en charge de serveurs d'identité CAS 2.0
\ No newline at end of file
diff --git a/application/modules/opac/controllers/AuthController.php b/application/modules/opac/controllers/AuthController.php
index c19f4034b14..dfc6a53165c 100644
--- a/application/modules/opac/controllers/AuthController.php
+++ b/application/modules/opac/controllers/AuthController.php
@@ -161,16 +161,12 @@ class AuthController extends ZendAfi_Controller_Action {
 
 
   public function loginAction() {
+    $preferences = Class_Template::current()
+      ->filterNotNamespaced($this->_loginPrefFromWidgetOrModule());
 
-    $this->view->preferences = $this->_loginPrefFromWidgetOrModule();
     $redirect = $this->_getParam('redirect', Class_Url::relative(['module' => 'opac',
                                                                   'action' => 'index',
                                                                   'controller' => 'index']));
-    $this->view->redirect = $redirect;
-    $service = $this->_getParam('service','');
-    $this->view->service = $service;
-    $this->view->titreAdd($this->view->_('Connexion'));
-
     $strategy = Class_Auth_Strategy::newFor($this);
     $strategy->setDefaultUrl($redirect);
     $strategy
@@ -179,11 +175,30 @@ class AuthController extends ZendAfi_Controller_Action {
                          $user->registerNotificationsOn($this->getHelper('notify')->bePopup());
                        });
 
+    $service = $this->_getParam('service', '');
+
+    $url_action = $this->view->url(($form_action = $strategy->getFormAction($this->view->id_module))
+                                   ? $form_action
+                                   : ['controller' => 'auth',
+                                      'action' => ('boite-login'),
+                                      'id_module' => $this->view->id_module]);
+
+    $datas = $this->_getInspector()
+                  ->addToParams(['redirect_url' => $redirect,
+                                 'service' => $service,
+                                 'id_notice' => $this->view->id_notice]);
+
+    $settings = array_merge(['Preferences' => $preferences],
+                            ['FormOptions' => ['data' => array_merge($preferences, $datas),
+                                               'action' => $url_action]]);
+
     $strategy->processLogin();
 
+    $this->view->titreAdd($this->_('Connexion'));
+    $this->view->preferences = $preferences;
     $this->view->title = $strategy->getPageTitle($this->view);
     $this->view->message = $strategy->getMessage($this->view);
-    $this->view->form_action = $this->view->url($strategy->getFormAction($this->view->id_module));
+    $this->view->settings = new Class_Entity($settings);
   }
 
 
diff --git a/application/modules/opac/views/scripts/auth/boite-login.phtml b/application/modules/opac/views/scripts/auth/boite-login.phtml
index d109856ab09..81450e47bc3 100644
--- a/application/modules/opac/views/scripts/auth/boite-login.phtml
+++ b/application/modules/opac/views/scripts/auth/boite-login.phtml
@@ -1,8 +1,10 @@
 <?php
-$url_action = $this->form_action ?
-              $this->form_action : $this->url(['controller' => 'auth',
-                                               'action' => ('boite-login'),
-                                               'id_module' => $this->id_module]);
+$url_action = $this->form_action
+  ? $this->form_action
+  : $this->url(['controller' => 'auth',
+                'action' => ('boite-login'),
+                'id_module' => $this->id_module]);
+
 $datas = ['redirect_url' => $this->redirect,
           'service' => $this->service,
           'id_notice' => $this->id_notice];
diff --git a/application/modules/opac/views/scripts/auth/login.phtml b/application/modules/opac/views/scripts/auth/login.phtml
index a8399533032..b310a0caccf 100644
--- a/application/modules/opac/views/scripts/auth/login.phtml
+++ b/application/modules/opac/views/scripts/auth/login.phtml
@@ -1,9 +1,2 @@
 <?php
-$this->openBoite($this->title);
-
-if ($this->message)
-  echo $this->tag('p', $this->message);
-
-include('boite-login.phtml');
-$this->closeBoite();
-?>
+echo $this->renderLogin($this->settings, $this->title, $this->message);
diff --git a/library/Class/Auth/IdentityProvider.php b/library/Class/Auth/IdentityProvider.php
index 44c84158372..66aa661c040 100644
--- a/library/Class/Auth/IdentityProvider.php
+++ b/library/Class/Auth/IdentityProvider.php
@@ -52,13 +52,16 @@ class Class_Auth_IdentityProviderLogged extends Class_Auth_Logged {
 
 
   protected function _handleRedirect() {
-    if ((!$provider = $this->_getProvider())
-        || !$provider->associate(Class_Users::getIdentity()))
+    if (!$provider = $this->_getProvider())
       return parent::_handleRedirect();
 
-    $this->setRedirectUrl(($url = $provider->loginSuccessRedirectUrl())
-                          ? $url
-                          : $this->default_url);
+    if (!$provider->associate(Class_Users::getIdentity())
+        && ($message = $provider->getLastMessage()))
+      $this->controller->notify($message);
+
+    $this->redirect_url = ($url = $provider->loginSuccessRedirectUrl())
+        ? $url
+        : $this->default_url;
 
     return parent::_handleRedirect();
   }
@@ -130,16 +133,28 @@ class Class_Auth_IdentityProviderNotLogged extends Class_Auth_NotLogged {
 
 
   protected function _handleRedirect() {
-    if ((!$provider = $this->_getProvider()))
-      return parent::_handleRedirect();
+    $provider = $this->_getProvider();
+    $user = $provider ? $provider->getRemotelyLoggedUser() : null;
+    $message = $provider ? $provider->getLastMessage() : null;
+
+    if ($user || $message)
+      $this->_redirectToProviderSuccessOrDefaultUrl($provider);
 
-    if ($user = $provider->getRemotelyLoggedUser()) {
+    if ($user)
       ZendAfi_Auth::getInstance()->logUser($user);
-      $this->redirect_url = ($url = $provider->loginSuccessRedirectUrl())
+
+    if ($message)
+      $this->controller->notify($message);
+
+    return parent::_handleRedirect();
+  }
+
+
+  protected function _redirectToProviderSuccessOrDefaultUrl($provider) {
+    $this->redirect_url = ($url = $provider->loginSuccessRedirectUrl())
         ? $url
         : $this->default_url;
-    }
 
-    return parent::_handleRedirect();
+    return $this;
   }
 }
diff --git a/library/Class/IdentityProvider.php b/library/Class/IdentityProvider.php
index 6b9562dcbcc..d247c2f3db3 100644
--- a/library/Class/IdentityProvider.php
+++ b/library/Class/IdentityProvider.php
@@ -36,6 +36,8 @@ class IdentityProviderLoader extends Storm_Model_Loader {
 
 
 class Class_IdentityProvider extends Storm_Model_Abstract{
+  use Trait_Translator, Trait_LastMessage;
+
   protected
     $_table_name = 'identity_provider',
     $_loader_class = 'IdentityProviderLoader',
@@ -50,19 +52,29 @@ class Class_IdentityProvider extends Storm_Model_Abstract{
                                         'dependents' => 'delete'] ],
 
     $_config_fields = ['url',
-                       'url_api',
                        'client_id',
                        'client_secret',
                        'nonce',
                        'logout_url',
                        'button_login',
                        'button_logout',
-                       'prod_url' ],
+                       'prod_url',
+                       'associate_on_login'],
 
     $_config_as_array,
     $_context;
 
 
+  public function validate() {
+    $this->checkAttribute('client_id',
+                          $this->hasClientId() || $this->getType() == 'cas2',
+                          $this->_('Identifiant client est obligatoire'));
+    $this->checkAttribute('client_secret',
+                          $this->hasClientSecret() || $this->getType() == 'cas2',
+                          $this->_('Clé secrète est obligatoire'));
+  }
+
+
   public function setContext($context) {
     $this->_context = $context;
     return $this;
@@ -82,7 +94,6 @@ class Class_IdentityProvider extends Storm_Model_Abstract{
   public function isAttachable() {
     $user = Class_Users::getIdentity();
     return !($user && $this->isRemotelyLogged());
-//      && !$this->isAssociatedTo($user);
   }
 
 
@@ -119,11 +130,27 @@ class Class_IdentityProvider extends Storm_Model_Abstract{
 
 
   public function getRemotelyLoggedUser() {
-    return (($identifier = $this->getRemoteUserId())
+    $user =  (($identifier = $this->getRemoteUserId())
             && ($identity = Class_User_Identity::findFirstBy(['provider_id' => $this->getId(),
                                                               'identifier' => $identifier])))
       ? $identity->getUser()
       : null;
+
+    if ($user || !$this->getAssociateOnLogin())
+      return $user;
+
+    if (1 !== ($count = Class_Users::countBy(['login' => $identifier]))) {
+      $this->_error($this->_plural($count,
+                                   'Connection impossible, l\'identifiant n\'existe pas',
+                                   '',
+                                   'Connection impossible, l\'identifiant est dupliqué'));
+      return null;
+    }
+
+    $user = Class_Users::findFirstBy(['login' => $identifier]);
+
+    Class_User_Identity::findOrCreateFor($user, $this, $identifier);
+    return $user;
   }
 
 
@@ -134,6 +161,11 @@ class Class_IdentityProvider extends Storm_Model_Abstract{
     if (!$identifier = $this->getRemoteUserId())
       return false;
 
+    if (Class_User_Identity::isAssociatedToAnother($user, $this, $identifier)) {
+      $this->_error($this->_('L\'association a échoué, cette identité est déjà associée à un autre compte'));
+      return false;
+    }
+
     return Class_User_Identity::findOrCreateFor($user, $this, $identifier);
   }
 
@@ -145,8 +177,10 @@ class Class_IdentityProvider extends Storm_Model_Abstract{
 
   protected function _doWithValidatedType($closure) {
     $type = $this->getTypeClass();
-    $type->validate($this->_context);
-    return $closure($type);
+    try {
+      $type->validate($this->_context);
+      return $closure($type);
+    } catch (Exception $e) {}
   }
 
 
diff --git a/library/Class/IdentityProvider/Cas2.php b/library/Class/IdentityProvider/Cas2.php
new file mode 100644
index 00000000000..fd24fdf50a2
--- /dev/null
+++ b/library/Class/IdentityProvider/Cas2.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_IdentityProvider_Cas2 extends Class_IdentityProvider_Default {
+  protected $_service_class = 'Class_WebService_Cas2';
+
+  public function getType() {
+    return 'cas2';
+  }
+
+
+  public function setLoginSuccessRedirectUrl($url) {
+    return $this;
+  }
+}
diff --git a/library/Class/IdentityProvider/Types.php b/library/Class/IdentityProvider/Types.php
index db7c540de40..50704cb784f 100644
--- a/library/Class/IdentityProvider/Types.php
+++ b/library/Class/IdentityProvider/Types.php
@@ -34,7 +34,8 @@ class Class_IdentityProvider_Types {
     return ['default' => $this->_('OpenId Connect'),
             'google' => $this->_('Google (OpenId)'),
             'franceconnect' => $this->_('Franceconnect'),
-            'acheteza' => $this->_('Portail citoyen Acheteza.com')];
+            'acheteza' => $this->_('Portail citoyen Acheteza.com'),
+            'cas2' => $this->_('CAS 2.0')];
   }
 
 
diff --git a/library/Class/TableDescription/IdentityProviders.php b/library/Class/TableDescription/IdentityProviders.php
new file mode 100644
index 00000000000..8fb58f78274
--- /dev/null
+++ b/library/Class/TableDescription/IdentityProviders.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_TableDescription_IdentityProviders extends Class_TableDescription {
+  public function init() {
+    $this->addColumn($this->_('Nom du fournisseur d\'identité'),
+                     function($model)
+                     {
+                       return  $model->getProvider()
+                         ? $model->getProvider()->getLibelle()
+                         : null;
+                     })
+         ->addColumn($this->_('Action'),
+                     function($model, $attrib, $canvas)
+                     {
+                       return $canvas->getView()
+                                     ->tagAnchor(['controller' => 'abonne',
+                                                  'action' => 'dissociate-provider',
+                                                  'id' => $model->getId()
+                                                  ],
+                                                 $this->_('Dissocier votre compte'));
+                     });
+
+  }
+}
diff --git a/library/Class/Template.php b/library/Class/Template.php
index 7c31f7062e4..63ff1ce6799 100644
--- a/library/Class/Template.php
+++ b/library/Class/Template.php
@@ -34,7 +34,8 @@ class Class_Template {
     $_settings,
     $_control_key,
     $_patcher,
-    $_icons_cache;
+    $_icons_cache,
+    $_namespace;
 
 
   public function __call($name, $params) {
@@ -220,7 +221,29 @@ class Class_Template {
 
 
   public function withNameSpace($text) {
-    return Storm_Inflector::camelize($this->getId() . '_' . Storm_Inflector::underscorize($text));
+    return $this->_getNameSpace() . Storm_Inflector::camelize(Storm_Inflector::underscorize($text));
+  }
+
+
+  public function isNamespaced($name) {
+    return 0 === strpos($name, $this->_getNameSpace());
+  }
+
+
+  protected function _getNameSpace() {
+    return $this->_namespace
+      ? $this->_namespace
+      : $this->_namespace = Storm_Inflector::camelize($this->getId());
+  }
+
+
+  public function filterNotNamespaced($items) {
+    return array_filter($items,
+                        function($item)
+                        {
+                          return !$this->isNamespaced($item);
+                        },
+                        ARRAY_FILTER_USE_KEY);
   }
 
 
diff --git a/library/Class/User/Identity.php b/library/Class/User/Identity.php
index 85c8260893a..0f4bd4244e3 100644
--- a/library/Class/User/Identity.php
+++ b/library/Class/User/Identity.php
@@ -38,6 +38,16 @@ class Class_User_IdentityLoader extends Storm_Model_Loader {
   }
 
 
+  public function isAssociatedToAnother($user, $provider, $identifier) {
+    if (!$user || !$provider || !$identifier)
+      return false;
+
+    return 0 < Class_User_Identity::countBy(['provider_id' => (int) $provider->getId(),
+                                             'identifier' => $identifier,
+                                             'user_id not' => (int) $user->getId()]);
+  }
+
+
   public function findIdentitiesForActiveProviders($user) {
     return array_filter(array_map(function($provider) use ($user)
                                   {
diff --git a/library/Class/WebService/Acheteza.php b/library/Class/WebService/Acheteza.php
index c6b7acb7b74..f7b849f75e4 100644
--- a/library/Class/WebService/Acheteza.php
+++ b/library/Class/WebService/Acheteza.php
@@ -65,21 +65,6 @@ class Class_WebService_Acheteza extends Class_WebService_IdentityProvider {
   }
 
 
-  protected function _providerUrlTo($action) {
-    return $this->_trailingSlashUrl($this->_provider->getUrl()) . $action;
-  }
-
-
-  protected function _providerUrlApiTo($action) {
-    return $this->_trailingSlashUrl($this->_provider->getUrlApi()) . $action;
-  }
-
-
-  protected function _trailingSlashUrl($url) {
-    return $url . ('/' !== substr($url, -1) ? '/' : '');
-  }
-
-
   protected function _requestToken() {
     $response = $this->_postRequest('GetRequestToken',
                                     ['grant_type' => 'client_credentials']);
@@ -115,16 +100,4 @@ class Class_WebService_Acheteza extends Class_WebService_IdentityProvider {
                                       'application/x-www-form-urlencoded',
                                       ['headers' => $headers]);
   }
-
-
-  protected function _getRemoteId($token) {
-    $response = $this->getWebClient()->open_url($this->_providerUrlApiTo('me'),
-                                                ['headers' => ['Authorization: Bearer '. $token]]);
-
-    return ($response
-            && ($json = json_decode($response))
-            && isset($json->id))
-      ? $json->id
-      : null;
-  }
 }
diff --git a/library/Class/WebService/Cas2.php b/library/Class/WebService/Cas2.php
new file mode 100644
index 00000000000..bc54cda51a8
--- /dev/null
+++ b/library/Class/WebService/Cas2.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_WebService_Cas2 extends Class_WebService_IdentityProvider {
+
+  public function validate($context) {
+    if (!$ticket = $context->getParam('ticket'))
+      return $this;
+
+    if ($this->_isConsumed($ticket))
+      return $this;
+
+    $this->_consume($ticket);
+
+    $response = $this->getWebClient()
+                     ->getResponse($this->_providerUrlTo('serviceValidate')
+                                   . '?'
+                                   . http_build_query(['ticket' => $ticket,
+                                                       'service' => $this->_serviceUrl()]));
+
+    if ($response->isError()
+        || (!$body = $response->getBody())
+        || false === strpos($body, '<cas:authenticationSuccess>')
+        || (!$user = (string)array_filter((new SimpleXMLElement($body))->xpath('//cas:user'))[0])
+    ) {
+      $this->clearSession();
+      return $this;
+    }
+
+    $this->loginWith($user);
+    return $this;
+  }
+
+
+  protected function _consume($ticket) {
+    $tickets = isset($this->getSession()->tickets)
+      ? $this->getSession()->tickets
+      : [];
+
+    $tickets[] = $ticket;
+    $this->getSession()->tickets = array_unique($tickets);
+
+    return $this;
+  }
+
+
+  protected function _isConsumed($ticket) {
+    $tickets = isset($this->getSession()->tickets)
+      ? $this->getSession()->tickets
+      : [];
+
+    return in_array($ticket, $tickets);
+  }
+
+
+  public function getAuthorizeUrl() {
+    return $this->_providerUrlTo('login')
+      . '?'
+      . http_build_query(['service' => $this->_serviceUrl()]);
+  }
+
+
+  public function getLogoutUrl($redirect) {
+    return $this->_providerUrlTo('logout')
+      . '?'
+      . http_build_query(['service' => Class_Url::absolute([], null, true)]);
+  }
+
+
+  protected function _serviceUrl() {
+    return Class_Url::absolute(['controller' => 'auth',
+                                'action' => 'login',
+                                'provider' => $this->_provider->getId()],
+                               null,
+                               true);
+  }
+}
\ No newline at end of file
diff --git a/library/Class/WebService/IdentityProvider.php b/library/Class/WebService/IdentityProvider.php
index 868b2a3db46..ad3c3becefa 100644
--- a/library/Class/WebService/IdentityProvider.php
+++ b/library/Class/WebService/IdentityProvider.php
@@ -23,9 +23,9 @@
 abstract class Class_WebService_IdentityProvider {
   use Trait_SimpleWebClient, Trait_TimeSource;
 
-  /** @var array of ZendAfi_Session_Namespace */
-  protected static $_session_namespace = [],
-        $_expiration_delay = 300;
+  protected static
+    $_session_namespace = [], // @var array of ZendAfi_Session_Namespace
+    $_expiration_delay = 300;
 
   /** @var Class_IdentityProvider */
   protected $_provider;
@@ -33,7 +33,6 @@ abstract class Class_WebService_IdentityProvider {
 
   /** @return ZendAfi_Session_Namespace */
   public static function getSession() {
-
     $called_class  = get_called_class();
     if (isset(static::$_session_namespace[$called_class]))
       return static::$_session_namespace[$called_class];
@@ -130,8 +129,24 @@ abstract class Class_WebService_IdentityProvider {
     return $this;
   }
 
+
   /** @return string */
   public function getLogoutUrl($redirect) {
     return '';
   }
+
+
+  protected function _providerUrlTo($action) {
+    return $this->_trailingSlashUrl($this->_provider->getUrl()) . $action;
+  }
+
+
+  protected function _providerUrlApiTo($action) {
+    return $this->_trailingSlashUrl($this->_provider->getUrlApi()) . $action;
+  }
+
+
+  protected function _trailingSlashUrl($url) {
+    return $url . ('/' !== substr($url, -1) ? '/' : '');
+  }
 }
diff --git a/library/Class/WebService/OpenId.php b/library/Class/WebService/OpenId.php
index 99d35982061..5fbddbfd80c 100644
--- a/library/Class/WebService/OpenId.php
+++ b/library/Class/WebService/OpenId.php
@@ -65,7 +65,7 @@ class Class_WebService_OpenId extends Class_WebService_IdentityProvider {
 
     $this->_client_id = $provider->getClientId();
     $this->_client_secret = $provider->getClientSecret();
-    $this->_nonce = true;//$provider->getNonce();
+    $this->_nonce = true;
     $this->_url = $provider->getUrl();
     $this->_logout_url = $provider->getLogoutUrl()
       ? $provider->getLogoutUrl()
@@ -199,8 +199,8 @@ class Class_WebService_OpenId extends Class_WebService_IdentityProvider {
 
     $this->getSession()->access_token = $json->access_token;
     if (isset($json->expires_in)) {
-          $this->setExpirationDelay($json->expires_in);
-          $this->getSession()->expires_in = $json->expires_in;
+      $this->setExpirationDelay($json->expires_in);
+      $this->getSession()->expires_in = $json->expires_in;
     }
 
     $this->getSession()->token = $json->id_token;
@@ -268,7 +268,7 @@ class Class_WebService_OpenId extends Class_WebService_IdentityProvider {
   }
 
 
-  protected function _getRandomToken(){
+  protected function _getRandomToken() {
     return $this->getPhpCommand()->sha1(mt_rand(0, mt_getrandmax()));
   }
 
@@ -295,7 +295,6 @@ class Class_WebService_OpenId extends Class_WebService_IdentityProvider {
                'scope' => 'openid',
                'state' => $this->getState()];
 
-//    if ($this->_nonce)
     $params['nonce'] = $this->getNonce();
 
     return $this->_authorization_url . '?' . http_build_query($params);
@@ -303,8 +302,10 @@ class Class_WebService_OpenId extends Class_WebService_IdentityProvider {
 
 
   public function getLogoutUrl($redirect_url) {
-    return $this->_logout_url . '?' . http_build_query(['id_token_hint' => $this->_getToken(),
-                                                        'state' => $this->getState(),
-                                                        'post_logout_redirect_uri' => $this->_getBokehLogoutUrl()]);
+    return $this->_logout_url
+      . '?'
+      . http_build_query(['id_token_hint' => $this->_getToken(),
+                          'state' => $this->getState(),
+                          'post_logout_redirect_uri' => $this->_getBokehLogoutUrl()]);
   }
 }
diff --git a/library/ZendAfi/Controller/Action.php b/library/ZendAfi/Controller/Action.php
index 7f01e399844..ffb803b6019 100644
--- a/library/ZendAfi/Controller/Action.php
+++ b/library/ZendAfi/Controller/Action.php
@@ -401,4 +401,9 @@ class ZendAfi_Controller_Action_NullInspector {
   public function addButton($definition) {
     return $this;
   }
+
+
+  public function addToParams($datas) {
+    return $datas;
+  }
 }
diff --git a/library/ZendAfi/Form/Admin/IdentityProvider.php b/library/ZendAfi/Form/Admin/IdentityProvider.php
index 8282bdac1ec..e3fc75569c7 100644
--- a/library/ZendAfi/Form/Admin/IdentityProvider.php
+++ b/library/ZendAfi/Form/Admin/IdentityProvider.php
@@ -25,8 +25,11 @@ class ZendAfi_Form_Admin_IdentityProvider extends ZendAfi_Form {
     parent::init();
 
     Class_ScriptLoader::getInstance()
-      ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#url, #url_api, #button_logout, #logout_url").closest("tr"), ["default", "acheteza"]);')
-      ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#prod_url").closest("tr"), ["franceconnect"]);');
+      ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#button_logout, #logout_url").closest("tr"), ["default", "acheteza"]);')
+      ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#url").closest("tr"), ["cas2", "default", "acheteza"]);')
+      ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#prod_url").closest("tr"), ["franceconnect"]);')
+      ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#client_id, #client_secret").closest("tr"), ["default", "google", "franceconnect", "acheteza"]);')
+      ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#associate_on_login").closest("tr"), ["cas2"]);');
 
 
     $this
@@ -49,16 +52,12 @@ class ZendAfi_Form_Admin_IdentityProvider extends ZendAfi_Form {
       ->addElement('text',
                    'client_id',
                    ['label' => $this->_('Identifiant client'),
-                    'size' => 50,
-                    'required' => true,
-                    'allowEmpty' => false])
+                    'size' => 50])
 
       ->addElement('text',
                    'client_secret',
                    ['label' => $this->_('Clé secrète'),
-                    'size' => 50,
-                    'required' => true,
-                    'allowEmpty' => false])
+                    'size' => 50])
 
       ->addElement('url',
                    'url',
@@ -67,12 +66,6 @@ class ZendAfi_Form_Admin_IdentityProvider extends ZendAfi_Form {
                     'allowEmpty' => false,
                     'title' => $this->_('URL SSO du fournisseur d\'identité')])
 
-      ->addElement('url',
-                   'url_api',
-                   ['label' => $this->_('URL API'),
-                    'size' => 50,
-                    'title' => $this->_('URL API du fournisseur d\'identité')])
-
       ->addElement('textarea',
                    'button_logout',
                    ['label' => $this->_('Html de logout'),
@@ -89,6 +82,10 @@ class ZendAfi_Form_Admin_IdentityProvider extends ZendAfi_Form {
                    ['label' => $this->_('En production'),
                     'title' => $this->_('En production')])
 
+      ->addElement('checkbox',
+                   'associate_on_login',
+                   ['label' => $this->_('Association automatique sur l\'identifiant')])
+
       ->addUniqDisplayGroup('provider');
   }
 }
diff --git a/library/ZendAfi/View/Helper/Abonne/AssociatedProvidersBoard.php b/library/ZendAfi/View/Helper/Abonne/AssociatedProvidersBoard.php
index 0d247bc8b27..3ff91958103 100644
--- a/library/ZendAfi/View/Helper/Abonne/AssociatedProvidersBoard.php
+++ b/library/ZendAfi/View/Helper/Abonne/AssociatedProvidersBoard.php
@@ -42,24 +42,7 @@ class ZendAfi_View_Helper_Abonne_AssociatedProvidersBoard extends ZendAfi_View_H
     if (empty($this->_identities))
       return $this->_tag('p', $this->_('Votre compte n\'est pas associé à un fournisseur d\'identité'));
 
-    $description = (new Class_TableDescription('active_identities'))
-      ->addColumn($this->_('Nom du fournisseur d\'identité'),
-                  function($model)
-                  {
-                    return  $model->getProvider()
-                      ? $model->getProvider()->getLibelle()
-                      : null;
-                  })
-      ->addColumn($this->_('Dissocier'),
-                  function($model)
-                  {
-                    return $this->_tagAnchor(['controller' => 'abonne',
-                                             'action' => 'dissociate-provider',
-                                             'id' => $model->getId()
-                                             ],
-                                            $this->_('Dissocier votre compte'));
-                  });
-
+    $description = (new Class_TableDescription_IdentityProviders('active_identities'));
     return $this->view->renderTable($description, $this->_identities);
   }
 
diff --git a/library/ZendAfi/View/Helper/Admin/HelpLink.php b/library/ZendAfi/View/Helper/Admin/HelpLink.php
index fb1dd64c225..fc6a303cabc 100644
--- a/library/ZendAfi/View/Helper/Admin/HelpLink.php
+++ b/library/ZendAfi/View/Helper/Admin/HelpLink.php
@@ -129,7 +129,8 @@ class ZendAfi_View_Helper_Admin_HelpLinkBokehWiki {
      'emplacement'            => ['index' => 'Codification_des_genres,_emplacements,_annexes,_...'],
      'section'                => ['index' => 'Codification_des_genres,_emplacements,_annexes,_...'],
      'drive-checkout'         => ['index' => 'Gérer_les_listes_de_rendez-vous_en_mode_drive',
-                                  'plan' => 'Prise_de_rendez-vous_par_les_professionnels']
+                                  'plan' => 'Prise_de_rendez-vous_par_les_professionnels'],
+     'identity-providers'     => ['index' => 'Fournisseurs_d\'identités'],
     ];
 
 
diff --git a/library/ZendAfi/View/Helper/IdentityProviders.php b/library/ZendAfi/View/Helper/IdentityProviders.php
index 24f75f8de8f..e4b18531119 100644
--- a/library/ZendAfi/View/Helper/IdentityProviders.php
+++ b/library/ZendAfi/View/Helper/IdentityProviders.php
@@ -29,9 +29,14 @@ class ZendAfi_View_Helper_IdentityProviders extends ZendAfi_View_Helper_BaseHelp
                    array_map(function($provider)
                              {
                                if ($provider->isAttachable() || $provider->isLogged())
-                                 return $this->view->Button_IdentityProvider($provider);
+                                 return $this->_renderButton($provider);
                              },
                              Class_IdentityProvider::findAllActiveProviders()
                    ));
   }
+
+
+  protected function _renderButton($provider) {
+    return $this->view->Button_IdentityProvider($provider);
+  }
 }
diff --git a/library/ZendAfi/View/Helper/RenderLogin.php b/library/ZendAfi/View/Helper/RenderLogin.php
new file mode 100644
index 00000000000..4b100227c29
--- /dev/null
+++ b/library/ZendAfi/View/Helper/RenderLogin.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class ZendAfi_View_Helper_RenderLogin extends ZendAfi_View_Helper_BaseHelper {
+  public function renderLogin($settings, $title, $message) {
+    $html = $this->view->openBoiteContent($title);
+
+    if ($message)
+      $html .= $this->_tag('p', $message);
+
+    $html .= $this->view->widget_Login($settings);
+
+    return $html . $this->view->closeBoiteContent();
+  }
+}
diff --git a/library/templates/Intonation/Library/View/Wrapper/RichContent/Section.php b/library/templates/Intonation/Library/View/Wrapper/RichContent/Section.php
index 093d1d893eb..4687897f7e1 100644
--- a/library/templates/Intonation/Library/View/Wrapper/RichContent/Section.php
+++ b/library/templates/Intonation/Library/View/Wrapper/RichContent/Section.php
@@ -21,7 +21,6 @@
 
 
 abstract class Intonation_Library_View_Wrapper_RichContent_Section {
-
   use Trait_Translator;
 
 
@@ -151,6 +150,20 @@ abstract class Intonation_Library_View_Wrapper_RichContent_Section {
   }
 
 
+  protected function _div() {
+    return call_user_func_array([$this->_view, 'div'], func_get_args());
+  }
+
+
+  protected function _tag() {
+    return call_user_func_array([$this->_view, 'tag'], func_get_args());
+  }
+
+
+  protected function _grid() {
+    return call_user_func_array([$this->_view, 'grid'], func_get_args());
+  }
+
   abstract public function getTitle();
   abstract public function getContent();
   abstract public function getClass();
diff --git a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php
index 205f35de981..4c0f5d09f45 100644
--- a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php
+++ b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php
@@ -28,91 +28,81 @@ class Intonation_Library_View_Wrapper_User_RichContent_Settings extends Intonati
 
 
   public function getContent() {
+    $html = [];
+
+    $html [] = $this->_renderIdentityProviders();
+
     $cards = new Class_User_Cards($this->_model);
     $count_cards = count($cards);
-    $parent_cards = $this->_model->getParentCards();
-    $count_parent_cards = count($parent_cards);
-
-    $html = [$this->_view->div(['class' => 'col-12'],
-                               $this->_view->tag('h3', $this->_view->_plural($count_cards,
-                                                                             'Aucune carte d\'abonné',
-                                                                             'Ma carte d\'abonné',
-                                                                             'Mes cartes d\'abonnés',
-                                                                             $count_cards))),
-
-                               $this->_view->div(['class' => 'col-12'],
-                                                 $this->_renderCards($cards))];
-
-    if ($parent_cards) {
-      $html [] = $this->_view->div(['class' => 'col-12'],
-                                   $this->_view->tag('h3', $this->_view->_plural($count_parent_cards,
-                                                                                 '',
-                                                                                 'Il gère mes prêts et mes réservations',
-                                                                                 'Ils gèrent mes prêts et mes réservations',
-                                                                                 $count_parent_cards)));
-      $html [] = $this->_view->div(['class' => 'col-12'],
-                                   $this->_renderParentCards($parent_cards));
+    $parent_cards = new Storm_Collection($this->_model->getParentCards());
+    $count_parent_cards = $parent_cards->count();
+
+    $html [] = $this->_div(['class' => 'col-12'],
+                           $this->_tag('h3', $this->_plural($count_cards,
+                                                            'Aucune carte d\'abonné',
+                                                            'Ma carte d\'abonné',
+                                                            'Mes cartes d\'abonnés')));
+
+    $html [] = $this->_div(['class' => 'col-12'],
+                           $this->_renderCards($cards));
+
+    if ($count_parent_cards) {
+      $html [] = $this->_div(['class' => 'col-12'],
+                             $this->_tag('h3', $this->_plural($count_parent_cards,
+                                                              '',
+                                                              'Il gère mes prêts et mes réservations',
+                                                              'Ils gèrent mes prêts et mes réservations')));
+      $html [] = $this->_div(['class' => 'col-12'],
+                             $this->_renderParentCards($parent_cards));
     }
 
-    $html [] = $this->_view->div(['class' => 'col-12 mt-5'],
-                                 $this->_view->tag('h3', $this->_('Mes paramètres')));
+    $html [] = $this->_div(['class' => 'col-12 mt-5'],
+                           $this->_tag('h3', $this->_('Mes paramètres')));
 
-    $html [] = $this->_view->div(['class' => 'col-12'],
-                                 $this->_renderSettingsForm());
+    $html [] = $this->_div(['class' => 'col-12'],
+                           $this->_renderSettingsForm());
 
-    return $this->_view->grid(implode($html));
+    return $this->_grid(implode($html));
   }
 
 
   protected function _renderCards($cards) {
-    $html = [$this->_view->div(['class' => 'col-12'],
-                               $this->_view->tagAction(new Intonation_Library_Link(['Url' => $this->_view->url(['controller' => 'abonne',
-                                                                                                                'action' => 'add-card']),
-                                                                                    'Text' => $this->_('Ajouter une carte'),
-                                                                                    'Image' => Class_Template::current()->getIco($this->_view, 'add_user', 'utils'),
-                                                                                    'Class' => 'btn btn-sm btn-success',
-                                                                                    'InlineText' => 1,
-                                                                                    'Popup' => 1])))];
+    $link = new Intonation_Library_Link(['Url' => $this->_view->url(['controller' => 'abonne',
+                                                                     'action' => 'add-card']),
+                                         'Text' => $this->_('Ajouter une carte'),
+                                         'Image' => Class_Template::current()->getIco($this->_view, 'add_user', 'utils'),
+                                         'Class' => 'btn btn-sm btn-success',
+                                         'InlineText' => 1,
+                                         'Popup' => 1]);
+
+    $html = [$this->_div(['class' => 'col-12'],
+                         $this->_view->tagAction($link))];
 
     if (!$cards->isEmpty())
-      $html [] = $this->_view->div(['class' => 'mt-3 col-12'],
-                                    $this->_renderCardsCarousel($cards));
+      $html [] = $this->_div(['class' => 'mt-3 col-12'],
+                             $this->_renderCardsCarousel($cards,
+                                                         'Intonation_Library_View_Wrapper_Card'));
 
-    return $this->_view->grid(implode($html));
+    return $this->_grid(implode($html));
   }
 
 
   protected function _renderParentCards($cards) {
-    $html = $this->_view->div(['class' => 'col-12'],
-                              $this->_renderParentCardsCarousel(new Storm_Collection($cards)));
-
-    return $this->_view->grid($html);
-  }
-
-
-  protected function _renderCardsCarousel($cards) {
-    $cards = array_map(function($card)
-                          {
-                            return (new Intonation_Library_View_Wrapper_Card)
-                              ->setModel($card)
-                              ->setView($this->_view);
-                          }, $cards->getArrayCopy());
-
-    $callback = function($wrapped) {
-      return $this->_view->cardify($wrapped);
-    };
+    $html = $this->_div(['class' => 'col-12'],
+                        $this->_renderCardsCarousel($cards,
+                                                    'Intonation_Library_View_Wrapper_ParentCard'));
 
-    return $this->_view->renderMultipleCarousel(new Storm_Collection($cards), $callback);
+    return $this->_grid($html);
   }
 
 
-  protected function _renderParentCardsCarousel($cards) {
-    $cards = array_map(function($card)
-                          {
-                            return (new Intonation_Library_View_Wrapper_ParentCard)
-                              ->setModel($card)
-                              ->setView($this->_view);
-                          }, $cards->getArrayCopy());
+  protected function _renderCardsCarousel($cards, $wrapper_class) {
+    $cards = array_map(function($card) use($wrapper_class)
+                       {
+                         return (new $wrapper_class)
+                           ->setModel($card)
+                           ->setView($this->_view);
+                       }, $cards->getArrayCopy());
 
     $callback = function($wrapped) {
       return $this->_view->cardify($wrapped);
@@ -134,6 +124,20 @@ class Intonation_Library_View_Wrapper_User_RichContent_Settings extends Intonati
   }
 
 
+  protected function _renderIdentityProviders() {
+    if (!Class_AdminVar::isIdentityProvidersEnabled())
+      return '';
+
+    $identities = Class_User_Identity::findIdentitiesForActiveProviders($this->_model);
+    $description = new Class_TableDescription_IdentityProviders('active_identities');
+    $table = $this->_view->renderTable($description, $identities);
+
+    return $this->_div(['class' => 'col-12'],
+                       $this->_tag('h3', $this->_('Fournisseur(s) d\'identité associé(s)'))
+                       . $table);
+  }
+
+
   public function getClass() {
     return 'user_settings';
   }
diff --git a/library/templates/Intonation/Library/Widget/Accessibility/Definition.php b/library/templates/Intonation/Library/Widget/Accessibility/Definition.php
index cb491ba9db0..1664a166be9 100644
--- a/library/templates/Intonation/Library/Widget/Accessibility/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Accessibility/Definition.php
@@ -21,6 +21,7 @@
 
 
 class Intonation_Library_Widget_Accessibility_Definition extends Class_Systeme_ModulesAccueil_Null {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
 
   const
     CODE = 'ACCESSIBILITY',
@@ -41,4 +42,9 @@ class Intonation_Library_Widget_Accessibility_Definition extends Class_Systeme_M
                              'display_colors' => 1,
                              'display_font_size' => 1];
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Accessibilité');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/AdminTools/Definition.php b/library/templates/Intonation/Library/Widget/AdminTools/Definition.php
index b1b32d2bba3..a5d4bb3485a 100644
--- a/library/templates/Intonation/Library/Widget/AdminTools/Definition.php
+++ b/library/templates/Intonation/Library/Widget/AdminTools/Definition.php
@@ -21,6 +21,7 @@
 
 
 class Intonation_Library_Widget_AdminTools_Definition extends Class_Systeme_ModulesAccueil_Null {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
 
   const
     CODE = 'ADMIN_TOOLS',
@@ -40,4 +41,9 @@ class Intonation_Library_Widget_AdminTools_Definition extends Class_Systeme_Modu
     $this->_defaultValues = ['titre' => $this->_libelle,
                              'display_mode' => static::TOGGLE];
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Outils d\'administration');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Breadcrumb/Definition.php b/library/templates/Intonation/Library/Widget/Breadcrumb/Definition.php
index 3bbad070164..0f856404644 100644
--- a/library/templates/Intonation/Library/Widget/Breadcrumb/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Breadcrumb/Definition.php
@@ -21,6 +21,8 @@
 
 
 class Intonation_Library_Widget_Breadcrumb_Definition extends Class_Systeme_ModulesAccueil_Null {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
+
   const CODE = 'ARIANE';
 
   protected
@@ -35,4 +37,9 @@ class Intonation_Library_Widget_Breadcrumb_Definition extends Class_Systeme_Modu
                              'root' => 1,
                              'show_profile' => 1];
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Fil d\'Ariane');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Agenda/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Agenda/Definition.php
index 5fcff9df3f0..1995769d445 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/Agenda/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/Agenda/Definition.php
@@ -20,7 +20,8 @@
  */
 
 
-class Intonation_Library_Widget_Carousel_Agenda_Definition extends Intonation_Library_Widget_Carousel_Definition {
+class Intonation_Library_Widget_Carousel_Agenda_Definition
+  extends Intonation_Library_Widget_Carousel_Definition {
 
   const
     CODE = 'CALENDAR',
@@ -52,4 +53,12 @@ class Intonation_Library_Widget_Carousel_Agenda_Definition extends Intonation_Li
     return (new Class_Systeme_ModulesAccueil_Calendrier)
       ->getAvailableFilters();
   }
+
+
+  protected function _templateLayouts() {
+    return [static::CAROUSEL,
+            static::MULTIPLE_CAROUSEL,
+            static::LISTING,
+            static::WALL];
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Article/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Article/Definition.php
index a996c480632..49a216b9153 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/Article/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/Article/Definition.php
@@ -45,4 +45,14 @@ class Intonation_Library_Widget_Carousel_Article_Definition extends Intonation_L
                                         ['titre' => $this->_libelle,
                                          'order' => static::SORT_RANDOM]);
   }
+
+
+  protected function _templateLayouts() {
+    return [static::CAROUSEL,
+            static::MULTIPLE_CAROUSEL,
+            static::LISTING,
+            static::HORIZONTAL_LISTING,
+            static::WALL];
+  }
+
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Author/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Author/Definition.php
index 147c1e3e308..fda04dd2f6b 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/Author/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/Author/Definition.php
@@ -41,4 +41,12 @@ class Intonation_Library_Widget_Carousel_Author_Definition extends Intonation_Li
                                          'with_thumbnail' => true,
                                          'authors_selection_mode' => Class_Systeme_ModulesAccueil_Authors::AUTHORS_SELECTION_MODE_ALL]);
   }
+
+
+  protected function _templateLayouts() {
+    return [static::CAROUSEL,
+            static::MULTIPLE_CAROUSEL,
+            static::LISTING,
+            static::WALL];
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Definition.php
index 69d9fe8cd58..e951247187a 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/Definition.php
@@ -37,18 +37,99 @@ class Intonation_Library_Widget_Carousel_Definition extends Class_Systeme_Module
     HORIZONTAL_CARD = 'card-horizontal',
     CARD = 'card';
 
-    protected $_isPhone = false;
-
-
-    public function __construct() {
-      $this->_defaultValues = ['layout' => static::WALL,
-                               'rendering' => static::CARD_OVERLAY,
-                               'size' => 9,
-                               'rss' => false,
-                               'embeded_code' => false,
-                               'link_to_all' => false,
-                               'all_layout' => static::LISTING,
-                               'all_rendering' => static::HORIZONTAL_CARD
-      ];
-    }
+  protected $_isPhone = false;
+
+
+  public function __construct() {
+    $this->_defaultValues = ['layout' => static::WALL,
+                             'rendering' => static::CARD_OVERLAY,
+                             'size' => 9,
+                             'rss' => false,
+                             'embeded_code' => false,
+                             'link_to_all' => false,
+                             'all_layout' => static::LISTING,
+                             'all_rendering' => static::HORIZONTAL_CARD
+    ];
+  }
+
+
+
+  public function templates($styles_callback) {
+    $templates = [];
+    foreach($this->_templateLayouts() as $name)
+      $templates[] = $this->_templateFor(static::CODE, $name, $styles_callback);
+
+    return $templates;
+  }
+
+
+  protected function _templateLayouts() {
+    return [];
+  }
+
+
+  protected function _templateFor($type, $layout, $styles_callback) {
+    $definition = (new Intonation_Library_Widget_Carousel_DefinitionLayout)->newFor($layout);
+
+    return ['title' => $definition->title(),
+            'icon' => 'TEMPLATE_' . $type . '_' . $layout . '.jpg',
+            'type' => $type,
+            'configuration' => ['rendering' => $definition->rendering(),
+                                'layout' => $layout,
+                                'size' => $definition->size(),
+                                'boite' => $styles_callback()]];
+  }
+}
+
+
+
+
+class Intonation_Library_Widget_Carousel_DefinitionLayout {
+  use Trait_Translator;
+
+  protected
+    $_title,
+    $_rendering,
+    $_size;
+
+
+  public function __construct($title='', $rendering='', $size=9) {
+    $this->_title = $title;
+    $this->_rendering = $rendering;
+    $this->_size = $size;
+  }
+
+
+  public function title() {
+    return $this->_title;
+  }
+
+
+  public function rendering() {
+    return $this->_rendering;
+  }
+
+
+  public function size() {
+    return $this->_size;
+  }
+
+
+  public function newFor($name) {
+    $def = Intonation_Library_Widget_Carousel_Definition::class;
+    $map = [$def::WALL => new static($this->_('Mur'), $def::CARD_OVERLAY),
+            $def::CAROUSEL => new static($this->_('Carousel à une colonnne'), $def::CARD),
+            $def::MULTIPLE_CAROUSEL => new static($this->_('Carousel à plusieurs colonnes'),
+                                                  $def::CARD),
+            $def::LISTING => new static($this->_('Liste verticale'), $def::HORIZONTAL_CARD),
+            $def::HORIZONTAL_LISTING => new static($this->_('Liste horizontale'), $def::CARD, 5),
+            $def::LISTING_WITH_OPTIONS => new static($this->_('Liste verticale à interactions'),
+                                                     $def::HORIZONTAL_CARD)
+    ];
+
+    if (array_key_exists($name, $map))
+      return $map[$name];
+
+    return new static();
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Domain/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Domain/Definition.php
index 6410ffbd7fc..69fd97a48e6 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/Domain/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/Domain/Definition.php
@@ -38,4 +38,13 @@ class Intonation_Library_Widget_Carousel_Domain_Definition extends Intonation_Li
                                          'order' => Class_CriteresRecherche::SORT_PUBLICATION_DESC,
                                          'root_domain_id' => '']);
   }
+
+
+  protected function _templateLayouts() {
+    return [static::CAROUSEL,
+            static::MULTIPLE_CAROUSEL,
+            static::LISTING,
+            static::HORIZONTAL_LISTING,
+            static::WALL];
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Library/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Library/Definition.php
index a5ca1d9fac5..10a0f172566 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/Library/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/Library/Definition.php
@@ -42,4 +42,11 @@ class Intonation_Library_Widget_Carousel_Library_Definition extends Intonation_L
                                          'filters_position' => Class_Systeme_ModulesAccueil_Library::POSITION_RIGHT,
                                          'order' => Class_Systeme_ModulesAccueil_Library::ORDER_ALPHA]);
   }
+
+
+  protected function _templateLayouts() {
+    return [static::CAROUSEL,
+            static::MULTIPLE_CAROUSEL,
+            static::LISTING];
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Newsletter/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Newsletter/Definition.php
index cb3df967994..852f73b2820 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/Newsletter/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/Newsletter/Definition.php
@@ -38,4 +38,12 @@ class Intonation_Library_Widget_Carousel_Newsletter_Definition extends Intonatio
                                         ['titre' => $this->_libelle,
                                          'order' => static::SORT_TITLE_ASC]);
   }
+
+
+  protected function _templateLayouts() {
+    return [static::CAROUSEL,
+            static::MULTIPLE_CAROUSEL,
+            static::LISTING,
+            static::LISTING_WITH_OPTIONS];
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Record/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Record/Definition.php
index e6ef3556cd2..69a6b679876 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/Record/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/Record/Definition.php
@@ -38,4 +38,13 @@ class Intonation_Library_Widget_Carousel_Record_Definition extends Intonation_Li
                                         ['titre' => $this->_libelle,
                                          'order' => Class_CriteresRecherche::SORT_RANDOM]);
   }
+
+
+  protected function _templateLayouts() {
+    return [static::CAROUSEL,
+            static::MULTIPLE_CAROUSEL,
+            static::LISTING,
+            static::HORIZONTAL_LISTING,
+            static::WALL];
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Review/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Review/Definition.php
index e1e92d8377a..bf974e6cb76 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/Review/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/Review/Definition.php
@@ -44,4 +44,9 @@ class Intonation_Library_Widget_Carousel_Review_Definition extends Intonation_Li
                                         ['titre' => $this->_libelle,
                                          'order' => static::SORT_RANDOM]);
   }
+
+
+  protected function _templateLayouts() {
+    return [static::CAROUSEL, static::MULTIPLE_CAROUSEL];
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Rss/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Rss/Definition.php
index 0f6c2de7f0e..a0ba569d1f0 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/Rss/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/Rss/Definition.php
@@ -38,4 +38,12 @@ class Intonation_Library_Widget_Carousel_Rss_Definition extends Intonation_Libra
                                         ['titre' => $this->_libelle,
                                          'order' => static::SORT_TITLE_ASC]);
   }
+
+
+  protected function _templateLayouts() {
+    return [static::CAROUSEL,
+            static::LISTING,
+            static::HORIZONTAL_LISTING,
+            static::LISTING_WITH_OPTIONS];
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Credits/Definition.php b/library/templates/Intonation/Library/Widget/Credits/Definition.php
index 12ca1df4ad2..b268e0f3de4 100644
--- a/library/templates/Intonation/Library/Widget/Credits/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Credits/Definition.php
@@ -21,6 +21,7 @@
 
 
 class Intonation_Library_Widget_Credits_Definition extends Class_Systeme_ModulesAccueil_Null {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
 
   const CODE = 'CREDITS';
 
@@ -36,4 +37,9 @@ class Intonation_Library_Widget_Credits_Definition extends Class_Systeme_Modules
     $this->_view_helper = 'Intonation_Library_Widget_Credits_View';
     $this->_defaultValues = ['titre' => $this->_libelle];
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Crédits');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Free/Definition.php b/library/templates/Intonation/Library/Widget/Free/Definition.php
index 4a96ccb7e02..5c4270cbb01 100644
--- a/library/templates/Intonation/Library/Widget/Free/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Free/Definition.php
@@ -21,6 +21,7 @@
 
 
 class Intonation_Library_Widget_Free_Definition extends Class_Systeme_ModulesAccueil_Null {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
 
   const
     CODE = 'FREE';
@@ -36,4 +37,9 @@ class Intonation_Library_Widget_Free_Definition extends Class_Systeme_ModulesAcc
     $this->_view_helper = 'Intonation_Library_Widget_Free_View';
     $this->_defaultValues = ['titre' => $this->_libelle];
   }
+
+
+  public function _templateTitle() {
+    return $this->_('Contenu HTML');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/IdentityProvider/Definition.php b/library/templates/Intonation/Library/Widget/IdentityProvider/Definition.php
new file mode 100644
index 00000000000..edb4484c628
--- /dev/null
+++ b/library/templates/Intonation/Library/Widget/IdentityProvider/Definition.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Intonation_Library_Widget_IdentityProvider_Definition
+  extends Class_Systeme_ModulesAccueil_IdentityProvider {
+  use Intonation_Library_Widget_TemplatesAware {
+    Intonation_Library_Widget_TemplatesAware::templates as traitTemplates;
+  }
+
+  protected $_group = Class_Systeme_ModulesAccueil::GROUP_ABONNE;
+
+
+  public function templates($styles_callback) {
+    return Class_AdminVar::isIdentityProvidersEnabled()
+      ? $this->traitTemplates($styles_callback)
+      : [];
+  }
+
+
+  protected function _templateTitle() {
+    return $this->_('Se connecter avec');
+  }
+}
diff --git a/library/templates/Intonation/Library/Widget/Image/Definition.php b/library/templates/Intonation/Library/Widget/Image/Definition.php
index 1c250cd4e37..65bf3182558 100644
--- a/library/templates/Intonation/Library/Widget/Image/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Image/Definition.php
@@ -21,6 +21,7 @@
 
 
 class Intonation_Library_Widget_Image_Definition extends Class_Systeme_ModulesAccueil_Null {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
 
   const
     CODE = 'IMAGE';
@@ -40,4 +41,9 @@ class Intonation_Library_Widget_Image_Definition extends Class_Systeme_ModulesAc
                              'link' => '',
                              'link_title' => $this->_('Accéder à %s')];
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Image');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Language/Definition.php b/library/templates/Intonation/Library/Widget/Language/Definition.php
index 7f4ea5abda6..021ec50b8d3 100644
--- a/library/templates/Intonation/Library/Widget/Language/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Language/Definition.php
@@ -21,6 +21,11 @@
 
 
 class Intonation_Library_Widget_Language_Definition extends Class_Systeme_ModulesAccueil_Langue {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
 
   protected $_group = Class_Systeme_ModulesAccueil::GROUP_ABONNE;
+
+  protected function _templateTitle() {
+    return $this->_('Langues');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Login/Definition.php b/library/templates/Intonation/Library/Widget/Login/Definition.php
index b9b0be69c3b..ef5000d7b73 100644
--- a/library/templates/Intonation/Library/Widget/Login/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Login/Definition.php
@@ -21,6 +21,7 @@
 
 
 class Intonation_Library_Widget_Login_Definition extends Class_Systeme_ModulesAccueil_Login {
+  use Intonation_Library_Widget_TemplatesAware;
 
   protected
     $_group = Class_Systeme_ModulesAccueil::GROUP_ABONNE;
@@ -43,4 +44,9 @@ class Intonation_Library_Widget_Login_Definition extends Class_Systeme_ModulesAc
                                          'lien_deconnection' => $this->_('Se déconnecter'),
                                          Class_Template::current()->withNameSpace('form_style') => 'inline']);
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Formulaire de connexion');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Login/View.php b/library/templates/Intonation/Library/Widget/Login/View.php
index 4022e2242e6..4a3c0949e1f 100644
--- a/library/templates/Intonation/Library/Widget/Login/View.php
+++ b/library/templates/Intonation/Library/Widget/Login/View.php
@@ -105,8 +105,7 @@ abstract class IntonationLoginRenderAbstract {
                            'action' => 'login',
                            'redirect' => (isset($options['redirect_url']))
                            ? $options['redirect_url']
-                           : Class_Url::absolute()]
-                          , null, true);
+                           : Class_Url::absolute()]);
     $form
       ->setAction($action);
 
diff --git a/library/templates/Intonation/Library/Widget/Menu/Definition.php b/library/templates/Intonation/Library/Widget/Menu/Definition.php
index dc8a4973cd8..6358ec3534c 100644
--- a/library/templates/Intonation/Library/Widget/Menu/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Menu/Definition.php
@@ -21,6 +21,7 @@
 
 
 class Intonation_Library_Widget_Menu_Definition extends Class_Systeme_ModulesAccueil_Null {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
 
   const
     CODE = 'MENU',
@@ -40,4 +41,9 @@ class Intonation_Library_Widget_Menu_Definition extends Class_Systeme_ModulesAcc
                              'layout' => static::LAYOUT_HORIZONTAL,
                              'logo' => ''];
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Menu');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Nav/Definition.php b/library/templates/Intonation/Library/Widget/Nav/Definition.php
index ad436670fce..d0ec2a6673f 100644
--- a/library/templates/Intonation/Library/Widget/Nav/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Nav/Definition.php
@@ -21,6 +21,8 @@
 
 
 class Intonation_Library_Widget_Nav_Definition extends Class_Systeme_ModulesAccueil_Null {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
+
   const CODE = 'NAV';
 
   protected
@@ -35,4 +37,9 @@ class Intonation_Library_Widget_Nav_Definition extends Class_Systeme_ModulesAccu
                              'menu' => '1-H',
                              'logo' => ''];
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Menu principal');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Notify/Definition.php b/library/templates/Intonation/Library/Widget/Notify/Definition.php
index 2067a8b112e..c48dbf00a1d 100644
--- a/library/templates/Intonation/Library/Widget/Notify/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Notify/Definition.php
@@ -21,6 +21,8 @@
 
 
 class Intonation_Library_Widget_Notify_Definition extends Class_Systeme_ModulesAccueil_Null {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
+
   const CODE = 'NOTIFY';
 
   protected
@@ -34,4 +36,9 @@ class Intonation_Library_Widget_Notify_Definition extends Class_Systeme_ModulesA
     $this->_view_helper = 'Intonation_Library_Widget_Notify_View';
     $this->_defaultValues = ['titre' => $this->_libelle];
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Notifications');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Scroll/Definition.php b/library/templates/Intonation/Library/Widget/Scroll/Definition.php
index f4e17905713..4a38246ecd8 100644
--- a/library/templates/Intonation/Library/Widget/Scroll/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Scroll/Definition.php
@@ -21,6 +21,7 @@
 
 
 class Intonation_Library_Widget_Scroll_Definition extends Class_Systeme_ModulesAccueil_Null {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
 
   const
     CODE = 'SCROLL',
@@ -40,4 +41,9 @@ class Intonation_Library_Widget_Scroll_Definition extends Class_Systeme_ModulesA
     $this->_defaultValues = ['titre' => $this->_libelle,
                              'direction' => static::UP];
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Défiler vers le haut');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Search/Definition.php b/library/templates/Intonation/Library/Widget/Search/Definition.php
index cfa1134fb62..c500bdd5ba7 100644
--- a/library/templates/Intonation/Library/Widget/Search/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Search/Definition.php
@@ -20,7 +20,10 @@
  */
 
 
-class Intonation_Library_Widget_Search_Definition extends Class_Systeme_ModulesAccueil_RechercheSimple {
+class Intonation_Library_Widget_Search_Definition
+  extends Class_Systeme_ModulesAccueil_RechercheSimple {
+  use Intonation_Library_Widget_TemplatesAware;
+
   public function __construct() {
     parent::__construct();
     $this->_view_helper = 'Intonation_Library_Widget_Search_View';
@@ -38,4 +41,9 @@ class Intonation_Library_Widget_Search_Definition extends Class_Systeme_ModulesA
                                          'always_new_search' => 0
                                         ]);
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Rechercher');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Share/Definition.php b/library/templates/Intonation/Library/Widget/Share/Definition.php
index 19e8973053b..08370ed311d 100644
--- a/library/templates/Intonation/Library/Widget/Share/Definition.php
+++ b/library/templates/Intonation/Library/Widget/Share/Definition.php
@@ -21,6 +21,7 @@
 
 
 class Intonation_Library_Widget_Share_Definition extends Class_Systeme_ModulesAccueil_Null {
+  use Intonation_Library_Widget_TemplatesAwareNoHeader;
 
   const
     CODE = 'SHARE';
@@ -36,4 +37,9 @@ class Intonation_Library_Widget_Share_Definition extends Class_Systeme_ModulesAc
     $this->_view_helper = 'Intonation_Library_Widget_Share_View';
     $this->_defaultValues = ['titre' => $this->_libelle];
   }
+
+
+  protected function _templateTitle() {
+    return $this->_('Partager la page');
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/TemplatesAware.php b/library/templates/Intonation/Library/Widget/TemplatesAware.php
new file mode 100644
index 00000000000..16ee6ce1208
--- /dev/null
+++ b/library/templates/Intonation/Library/Widget/TemplatesAware.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+trait Intonation_Library_Widget_TemplatesAware {
+  public function templates($styles_callback) {
+    return [['title' => $this->_templateTitle(),
+             'icon' => 'TEMPLATE_' . static::CODE  . '.jpg',
+             'type' => static::CODE,
+             'configuration' => $this->_templateConfiguration($styles_callback)]];
+  }
+
+
+  protected function _templateConfiguration($styles_callback) {
+    return ['boite' => $styles_callback()];
+  }
+}
diff --git a/library/templates/Intonation/Library/Widget/TemplatesAwareNoHeader.php b/library/templates/Intonation/Library/Widget/TemplatesAwareNoHeader.php
new file mode 100644
index 00000000000..4b08a781c5d
--- /dev/null
+++ b/library/templates/Intonation/Library/Widget/TemplatesAwareNoHeader.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+trait Intonation_Library_Widget_TemplatesAwareNoHeader {
+  use Intonation_Library_Widget_TemplatesAware {
+    Intonation_Library_Widget_TemplatesAware::_templateConfiguration as _parentTemplateConfiguration;
+  }
+
+  protected function _templateConfiguration($styles_callback) {
+    $conf = $this->_parentTemplateConfiguration($styles_callback);
+    $show_header_key = Class_Template::current()->withNameSpace('show_header');
+    $conf[$show_header_key] = 0;
+
+    return $conf;
+  }
+}
diff --git a/library/templates/Intonation/Library/WidgetTemplates.php b/library/templates/Intonation/Library/WidgetTemplates.php
index 3abf83c87db..1c4db1744ea 100644
--- a/library/templates/Intonation/Library/WidgetTemplates.php
+++ b/library/templates/Intonation/Library/WidgetTemplates.php
@@ -21,175 +21,32 @@
 
 
 class Intonation_Library_WidgetTemplates {
-
   use Trait_Translator;
 
-
   public function getTemplates() {
-    $sections =
-      [$this->_('Agenda') => ['CALENDAR' => ['carousel',
-                                             'multiple_carousel',
-                                             'list',
-                                             'wall']],
-
-       $this->_('Articles') => ['NEWS' => ['carousel',
-                                           'multiple_carousel',
-                                           'list',
-                                           'horizontal_list',
-                                           'wall']],
-
-       $this->_('Auteurs') => ['AUTHORS' => ['carousel',
-                                             'multiple_carousel',
-                                             'horizontal_list',
-                                             'wall']],
-
-       $this->_('Avis') => ['CRITIQUES' => ['carousel',
-                                            'multiple_carousel']],
-
-       $this->_('Bibliothèques') => ['LIBRARY' => ['carousel',
-                                                   'multiple_carousel',
-                                                   'list']],
-
-       $this->_('Connexion') => ['LOGIN' => ['' => ['title' => $this->_('Formulaire de connexion')]]],
-
-       $this->_('Domaines') => ['DOMAIN_BROWSER' => ['carousel',
-                                                     'multiple_carousel',
-                                                     'list',
-                                                     'horizontal_list',
-                                                     'wall']],
-
-       $this->_('Flux RSS') => ['RSS' => ['carousel',
-                                          'list',
-                                          'horizontal_list',
-                                          'list_with_options']],
-
-       $this->_('Lettres d\'informations') => ['NEWSLETTERS' => ['carousel',
-                                                                 'multiple_carousel',
-                                                                 'list',
-                                                                 'list_with_options']],
-
-       $this->_('Navigation et menus') => ['ARIANE' => ['' => ['title' => $this->_('Fil d\'Ariane'),
-                                                               Class_Template::current()->withNameSpace('show_header') => 0]],
-                                           'MENU' => ['' => ['title' => $this->_('Menu'),
-                                                             Class_Template::current()->withNameSpace('show_header') => 0]],
-                                           'NAV' => ['' => ['title' => $this->_('Menu principal'),
-                                                            Class_Template::current()->withNameSpace('show_header') => 0]]],
-
-       $this->_('Notices') => ['KIOSQUE' => ['carousel',
-                                             'multiple_carousel',
-                                             'list',
-                                             'horizontal_list',
-                                             'wall']],
-
-       $this->_('Site') => ['ACCESSIBILITY' => ['' => ['title' => $this->_('Accessibilité'),
-                                                       Class_Template::current()->withNameSpace('show_header') => 0]],
-                            'FREE' => ['' => ['title' => $this->_('Contenu HTML'),
-                                              Class_Template::current()->withNameSpace('show_header') => 0]],
-                            'CREDITS' => ['' => ['title' => $this->_('Crédits'),
-                                                 Class_Template::current()->withNameSpace('show_header') => 0]],
-                            'SCROLL' => ['' => ['title' => $this->_('Défiler vers le haut'),
-                                                Class_Template::current()->withNameSpace('show_header') => 0]],
-                            'IMAGE' => ['' => ['title' => $this->_('Image'),
-                                               Class_Template::current()->withNameSpace('show_header') => 0]],
-                            'LANGUE' => ['' => ['title' => $this->_('Langues'),
-                                                Class_Template::current()->withNameSpace('show_header') => 0]],
-                            'NOTIFY' => ['' => ['title' => $this->_('Notifications')]],
-                            'ADMIN_TOOLS' => ['' => ['title' => $this->_('Outils d\'administration'),
-                                                     Class_Template::current()->withNameSpace('show_header') => 0]],
-                            'SHARE' => ['' => ['title' => $this->_('Partager la page'),
-                                               Class_Template::current()->withNameSpace('show_header') => 0]]],
-
-       $this->_('Recherche') => ['RECH_SIMPLE' => ['' => ['title' => $this->_('Rechercher')]]]
-      ];
-
-    $sections_content = [];
-    foreach ($sections as $title => $settings)
-      $sections_content [] = ['title' => $title,
-                              'templates' => $this->_createTemplatesFrom($settings)];
-
-    return ['sections' => $sections_content];
-  }
-
-
-  protected function _createTemplatesFrom($settings) {
-    $templates = [];
-    foreach ($settings as $type => $layouts)
-      $templates = $this->_createTemplateFor($type, $layouts, $templates);
-
-    return $templates;
-  }
-
-
-  protected function _createTemplateFor($type, $layouts, $templates) {
-    foreach($layouts as $layout)
-      $templates = $this->_createWidgetFor($type, $layout, $templates);
-
-    return $templates;
-  }
-
-
-  protected function _createWidgetFor($type, $layout, $templates) {
-    $title = '';
-    $rendering = '';
-    $size = 9;
-
-    if (is_array($layout)) {
-      $title = $layout['title'];
-
-      $boite = ((($type == 'NAV')
-                 || ($type == 'MENU'))
-                ? $this->_getMenuDefaultStyles()
-                : $this->_getDefaultStyles());
-
-      unset($layout['title']);
-      $templates [] = ['title' => $title,
-                       'icon' => 'TEMPLATE_' . $type  . '.jpg',
-                       'type' => $type,
-                       'configuration' => array_merge($layout,
-                                                      ['boite' => $boite])];
-      return $templates;
-    }
-
-    if ('wall' == $layout) {
-      $title = $this->_('Mur');
-      $rendering = 'card-overlay';
-    }
-
-    if ('list' == $layout) {
-      $title = $this->_('Liste verticale');
-      $rendering = 'card-horizontal';
-    }
-
-    if ('list_with_options' == $layout) {
-      $title = $this->_('Liste verticale à interactions');
-      $rendering = 'card-horizontal';
-    }
-
-    if ('horizontal_list' == $layout) {
-      $title = $this->_('Liste horizontale');
-      $rendering = 'card';
-      $size = 5;
-    }
-
-    if ('carousel' == $layout) {
-      $title = $this->_('Carousel à une colonnne');
-      $rendering = 'card';
-    }
-
-    if ('multiple_carousel' == $layout) {
-      $title = $this->_('Carousel à plusieurs colonnes');
-      $rendering = 'card';
-    }
+    $sections_content =
+      (new Storm_Collection([new Intonation_Library_WidgetTemplates_Agenda($this),
+                             new Intonation_Library_WidgetTemplates_News($this),
+                             new Intonation_Library_WidgetTemplates_Author($this),
+                             new Intonation_Library_WidgetTemplates_Review($this),
+                             new Intonation_Library_WidgetTemplates_Library($this),
+                             new Intonation_Library_WidgetTemplates_Login($this),
+                             new Intonation_Library_WidgetTemplates_Domain($this),
+                             new Intonation_Library_WidgetTemplates_Rss($this),
+                             new Intonation_Library_WidgetTemplates_Newsletter($this),
+                             new Intonation_Library_WidgetTemplates_Nav($this),
+                             new Intonation_Library_WidgetTemplates_Record($this),
+                             new Intonation_Library_WidgetTemplates_Site($this),
+                             new Intonation_Library_WidgetTemplates_Search($this),
+                             ]))
+      ->collect(function($section) { return $section->definition(); });
+
+    return ['sections' => (array)$sections_content];
+  }
 
-    $templates [] = ['title' => $title,
-                     'icon' => 'TEMPLATE_' . $type . '_' . $layout . '.jpg',
-                     'type' => $type,
-                     'configuration' => ['rendering' => $rendering,
-                                         'layout' => $layout,
-                                         'size' => $size,
-                                         'boite' => $this->_getDefaultStyles()]];
 
-    return $templates;
+  public function defaultStyles() {
+    return $this->_getDefaultStyles();
   }
 
 
@@ -203,6 +60,11 @@ class Intonation_Library_WidgetTemplates {
   }
 
 
+  public function defaultMenuStyles() {
+    return $this->_getMenuDefaultStyles();
+  }
+
+
   protected function _getMenuDefaultStyles() {
     return ['no_background',
             'no_border',
@@ -211,3 +73,286 @@ class Intonation_Library_WidgetTemplates {
             'black_and_white'];
   }
 }
+
+
+
+
+class Intonation_Library_WidgetTemplates_Section {
+  use Trait_Translator;
+
+  protected
+    $_widgets,
+    $_all;
+
+  public function __construct($all) {
+    $this->_widgets = new Storm_Collection;
+    $this->_all = $all;
+    $this->_init();
+  }
+
+
+  protected function _init() {
+  }
+
+
+  public function definition() {
+    return ['title' => $this->_title(),
+            'templates' => $this->_widgetsDefinition()];
+  }
+
+
+  protected function _widgetsDefinition() {
+    return $this->_widgets->injectInto([], [$this, 'widgetDefinition']);
+  }
+
+
+  public function widgetDefinition($value, $widget) {
+    $class_name = get_class($widget);
+    $code = $class_name::CODE;
+    foreach($widget->templates($this->_defaultStyles($code)) as $template)
+      $value[] = $template;
+
+    return $value;
+  }
+
+
+  protected function _defaultStyles($code) {
+    return [$this->_all, 'defaultStyles'];
+  }
+
+
+  protected function _title() {
+    return '';
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Agenda
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->append(new Intonation_Library_Widget_Carousel_Agenda_Definition);
+  }
+
+
+  protected function _title() {
+    return $this->_('Agenda');
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_News
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->append(new Intonation_Library_Widget_Carousel_Article_Definition);
+  }
+
+
+  protected function _title() {
+    return $this->_('Articles');
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Author
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->append(new Intonation_Library_Widget_Carousel_Author_Definition);
+  }
+
+
+  protected function _title() {
+    return $this->_('Auteurs');
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Review
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->append(new Intonation_Library_Widget_Carousel_Review_Definition);
+  }
+
+
+  protected function _title() {
+    return $this->_('Avis');
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Library
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->append(new Intonation_Library_Widget_Carousel_Library_Definition);
+  }
+
+
+  protected function _title() {
+    return $this->_('Bibliothèques');
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Login
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->addAll([new Intonation_Library_Widget_Login_Definition,
+                             new Intonation_Library_Widget_IdentityProvider_Definition]);
+  }
+
+
+  protected function _title() {
+    return $this->_('Connexion');
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Domain
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->append(new Intonation_Library_Widget_Carousel_Domain_Definition);
+  }
+
+
+  protected function _title() {
+    return $this->_('Domaines');
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Rss
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->append(new Intonation_Library_Widget_Carousel_Rss_Definition);
+  }
+
+
+  protected function _title() {
+    return $this->_('Flux RSS');
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Newsletter
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->append(new Intonation_Library_Widget_Carousel_Newsletter_Definition);
+  }
+
+
+  protected function _title() {
+    return $this->_('Lettres d\'informations');
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Nav
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->addAll([new Intonation_Library_Widget_Breadcrumb_Definition,
+                             new Intonation_Library_Widget_Menu_Definition,
+                             new Intonation_Library_Widget_Nav_Definition,
+                             ]);
+  }
+
+
+  protected function _title() {
+    return $this->_('Navigation et menus');
+  }
+
+
+  protected function _defaultStyles($code) {
+    $method = in_array($code, [Intonation_Library_Widget_Menu_Definition::CODE,
+                               Intonation_Library_Widget_Nav_Definition::CODE])
+      ? 'defaultMenuStyles'
+      : 'defaultStyles';
+
+    return [$this->_all, $method];
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Record
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->append(new Intonation_Library_Widget_Carousel_Record_Definition);
+  }
+
+
+  protected function _title() {
+    return $this->_('Notices');
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Site
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->addAll([new Intonation_Library_Widget_Accessibility_Definition,
+                             new Intonation_Library_Widget_Free_Definition,
+                             new Intonation_Library_Widget_Credits_Definition,
+                             new Intonation_Library_Widget_Scroll_Definition,
+                             new Intonation_Library_Widget_Image_Definition,
+                             new Intonation_Library_Widget_Language_Definition,
+                             new Intonation_Library_Widget_Notify_Definition,
+                             new Intonation_Library_Widget_AdminTools_Definition,
+                             new Intonation_Library_Widget_Share_Definition,
+                             ]);
+  }
+
+
+  protected function _title() {
+    return $this->_('Site');
+  }
+}
+
+
+
+
+class Intonation_Library_WidgetTemplates_Search
+  extends Intonation_Library_WidgetTemplates_Section {
+
+  public function _init() {
+    $this->_widgets->append(new Intonation_Library_Widget_Search_Definition);
+  }
+
+
+  protected function _title() {
+    return $this->_('Recherche');
+  }
+}
diff --git a/library/templates/Intonation/Template.php b/library/templates/Intonation/Template.php
index 8be15ce3888..0f4e67b1ae8 100644
--- a/library/templates/Intonation/Template.php
+++ b/library/templates/Intonation/Template.php
@@ -183,7 +183,9 @@ class Intonation_Template extends Class_Template {
 
        Intonation_Library_Widget_Menu_Definition::CODE => new Intonation_Library_Widget_Menu_Definition,
 
-       Intonation_Library_Widget_Carousel_Rss_Definition::CODE => new Intonation_Library_Widget_Carousel_Rss_Definition
+       Intonation_Library_Widget_Carousel_Rss_Definition::CODE => new Intonation_Library_Widget_Carousel_Rss_Definition,
+
+       Intonation_Library_Widget_IdentityProvider_Definition::CODE => new Intonation_Library_Widget_IdentityProvider_Definition
       ];
   }
 
diff --git a/library/templates/Intonation/View/IdentityProviders.php b/library/templates/Intonation/View/IdentityProviders.php
new file mode 100644
index 00000000000..fe964b0da99
--- /dev/null
+++ b/library/templates/Intonation/View/IdentityProviders.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+class Intonation_View_IdentityProviders extends ZendAfi_View_Helper_IdentityProviders {
+  protected function _renderButton($provider) {
+    return $this->_div(['class' => 'justify-content-center row no-gutters m-2'],
+                       parent::_renderButton($provider));
+  }
+}
diff --git a/library/templates/Intonation/View/RenderLogin.php b/library/templates/Intonation/View/RenderLogin.php
new file mode 100644
index 00000000000..f53196e79f2
--- /dev/null
+++ b/library/templates/Intonation/View/RenderLogin.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Intonation_View_RenderLogin extends ZendAfi_View_Helper_BaseHelper {
+
+  public function renderLogin($settings, $title, $message) {
+    return $this->_div(['class' => 'justify-content-center row no-gutters'],
+                       $this->_renderTitle($title)
+                       . $this->_renderMessage($message)
+                       . $this->view->widget_Login($settings));
+  }
+
+
+  protected function _renderTitle($title) {
+    return $this->_tag('h2',
+                       Class_Template::current()->getIco($this->view, 'login', 'utils')
+                       . ' ' . $title,
+                       ['class' => 'col-md-6 col-sm-12']);
+  }
+
+
+  protected function _renderMessage($message) {
+    if (!$message)
+      return '';
+
+    return $this->_div(['class' => 'container-fluid'],
+                       $this->_div(['class' => 'justify-content-center row'],
+                                   $this->_div(['class' => 'col-md-6 col-sm-12'], $message)));
+  }
+}
diff --git a/library/templates/Intonation/View/Widget/Login.php b/library/templates/Intonation/View/Widget/Login.php
index d80b4e79e5f..a42e889c663 100644
--- a/library/templates/Intonation/View/Widget/Login.php
+++ b/library/templates/Intonation/View/Widget/Login.php
@@ -32,6 +32,7 @@ class Intonation_View_Widget_Login extends ZendAfi_View_Helper_BaseHelper {
                                                                    :[])))
       ->setView($this->view);
     $login->getHtml();
+
     return $login->getContent();
   }
 }
\ No newline at end of file
diff --git a/public/opac/js/widget_templates/TEMPLATE_IDENTITY_PROVIDER.jpg b/public/opac/js/widget_templates/TEMPLATE_IDENTITY_PROVIDER.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..2165b710aeed7186e2632edc2d4335cbbdedb323
GIT binary patch
literal 22202
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

literal 0
HcmV?d00001

diff --git a/public/opac/js/widget_templates/TEMPLATE_LANGUE.jpg b/public/opac/js/widget_templates/TEMPLATE_LANGUE.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..b492dd1efc672416c82b35d63a5346c2df01cdf8
GIT binary patch
literal 11804
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
K&HpOTOaBYOIQ1$3

literal 0
HcmV?d00001

diff --git a/tests/application/modules/AbstractControllerTestCase.php b/tests/application/modules/AbstractControllerTestCase.php
index 8993a0aa427..ce353945d85 100644
--- a/tests/application/modules/AbstractControllerTestCase.php
+++ b/tests/application/modules/AbstractControllerTestCase.php
@@ -152,6 +152,7 @@ abstract class AbstractControllerTestCase extends Zend_Test_PHPUnit_ControllerTe
                                ->willDo(function($pass, $crypt) { return $pass; }));
     Class_WebService_BibNumerique_RessourceNumerique::setCommand($this->mock()->whenCalled('execTimedScript')->answers(''));
     Class_Notice_Thumbnail_ProviderCacheServer::doNotTrackSeen();
+    Class_CriteresRecherche::setMaxSearchResults('');
   }
 
 
diff --git a/tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php b/tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php
index ab30f8c7943..9a4dc956f3d 100644
--- a/tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php
+++ b/tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php
@@ -237,7 +237,7 @@ class DriveCheckOutUserNotificationsTest extends DriveCheckOutBookingTestCase {
                                 'controller' => 'drive-checkout',
                                 'action' => 'plan']);
     $this->assertEquals([$url => 'Planifier le retrait de mes documents'],
-                          $this->_actions[0]);
+                        $this->_actions[0]);
   }
 
 
@@ -957,7 +957,7 @@ class DriveCheckOutBookingPlanNotLoggedTest extends DriveCheckOutBookingTestCase
 
   /** @test */
   public function pageShouldContainLoginForm() {
-    $this->assertXPath('//div[@class="contenu"]//form[contains(@action, "/auth/login")]');
+    $this->assertXPath('//div[@data-action="drive-checkout_plan"]//form[contains(@action, "/auth/login")]');
   }
 }
 
diff --git a/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php b/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php
index 9c183714ead..6d1bbefedc0 100644
--- a/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php
+++ b/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php
@@ -125,6 +125,11 @@ class IdentityProviderAdminAddTest extends IdentityProviderAdminTestCase {
   }
 
 
+  /** @test */
+  public function typeSelectShouldContainCasOption() {
+    $this->assertXPath('//select[@name="type"]/option[@value="cas2"]');
+  }
+
   /** @test */
   public function formShouldContainsInputForClientId() {
     $this->assertXPath('//input[@name="client_id"]');
@@ -141,6 +146,12 @@ class IdentityProviderAdminAddTest extends IdentityProviderAdminTestCase {
   public function formShouldContainsUrlInput() {
     $this->assertXPath('//input[@name="url"]');
   }
+
+
+  /** @test */
+  public function formShouldContainsAssociateOnLogin() {
+    $this->assertXPath('//input[@name="associate_on_login"]');
+  }
 }
 
 
@@ -249,6 +260,66 @@ class IdentityProviderAdminPostAddTest extends IdentityProviderAdminTestCase {
 
 
 
+class IdentityProviderAdminPostAddInvalidGoogleTest extends IdentityProviderAdminTestCase {
+  protected $_provider;
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->postDispatch('/admin/identity-providers/add',
+                        ['label' => 'AFI Identities',
+                         'url' => 'https://www.afi-identities.fr',
+                         'type' => 'google']);
+
+    $this->_provider = Class_IdentityProvider::find(1);
+  }
+
+
+  /** @test */
+  public function providerShouldBeNull() {
+    $this->assertNull($this->_provider);
+  }
+
+
+  /** @test */
+  public function clientIdShouldBeMantory() {
+    $this->assertXPathContentContains('//ul[@class="errors"]//li', 'Identifiant client est obligatoire');
+  }
+
+
+  /** @test */
+  public function clientSecretShouldBeMantory() {
+    $this->assertXPathContentContains('//ul[@class="errors"]//li', 'Clé secrète est obligatoire');
+  }
+}
+
+
+
+
+class IdentityProviderAdminPostAddCasTest extends IdentityProviderAdminTestCase {
+  protected $_provider;
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->postDispatch('/admin/identity-providers/add',
+                        ['label' => 'AFI Identities',
+                         'url' => 'https://www.afi-identities.fr',
+                         'type' => 'cas2']);
+
+    $this->_provider = Class_IdentityProvider::find(1);
+  }
+
+
+  /** @test */
+  public function providerShouldBeNotnull() {
+    $this->assertNotNull($this->_provider);
+  }
+}
+
+
+
+
 class IdentityProviderAdminWidgetTest extends Admin_AbstractControllerTestCase {
   protected $_storm_default_to_volatile = true;
 
diff --git a/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationCasTest.php b/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationCasTest.php
new file mode 100644
index 00000000000..4088f3588cb
--- /dev/null
+++ b/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationCasTest.php
@@ -0,0 +1,585 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+require_once 'tests/fixtures/NanookFixtures.php';
+
+
+abstract class IdentityProviderAuthenticationCasTestCase extends AbstractControllerTestCase {
+  protected
+    $_storm_default_to_volatile = true,
+    $_provider;
+
+
+  public function setUp() {
+    parent::setUp();
+    Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 1);
+
+    $this->_provider = $this->fixture('Class_IdentityProvider',
+                                      ['id' => 1,
+                                       'label' => 'CAS 2.0',
+                                       'type' => 'cas2',
+                                       'url' => 'http://moncompte.server.com/',
+                                      ]);
+
+  }
+
+
+  public function tearDown() {
+    Class_WebService_Cas2::setWebClient(null);
+    Class_WebService_Cas2::clearSession();
+    parent::tearDown();
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasRedirectToLoginTest
+  extends IdentityProviderAuthenticationCasTestCase {
+
+  /** @test */
+  public function shouldRedirectToCasServerLogin() {
+    $this->dispatch('/identity-providers/authenticate/id/1');
+    $this->assertRedirectContains('http://moncompte.server.com/login?service=');
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasInvalidTicketNotLoggedNotAssociatedTest
+  extends IdentityProviderAuthenticationCasTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    ZendAfi_Auth::getInstance()->clearIdentity();
+    Class_WebService_Cas2::loginWith('mysuperid');
+
+    $response = $this->mock()
+                     ->whenCalled('isError')->answers(false)
+
+                     ->whenCalled('getBody')
+                     ->answers('<?xml version="1.0" encoding="UTF-8" ?>
+<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
+    <cas:authenticationFailure code="INVALID_TICKET">
+        Ticket testticket not recognized
+    </cas:authenticationFailure>
+</cas:serviceResponse>');
+
+    Class_WebService_Cas2::setWebClient($this->mock()
+                                        ->whenCalled('getResponse')
+                                        ->answers($response));
+
+    $this->dispatch('/auth/login/provider/1?ticket=testticket');
+  }
+
+
+  /** @test */
+  public function shouldClearRemotelyLogged() {
+    $this->assertFalse($this->_provider->isRemotelyLogged());
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasValidTicketNotLoggedNotAssociatedTest
+  extends IdentityProviderAuthenticationCasTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    ZendAfi_Auth::getInstance()->clearIdentity();
+
+    $response = $this->mock()
+                     ->whenCalled('isError')->answers(false)
+
+                     ->whenCalled('getBody')
+                     ->answers('<?xml version="1.0" encoding="UTF-8" ?>
+<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
+  <cas:authenticationSuccess>
+    <cas:user>mysuperid</cas:user>
+    <cas:proxyGrantingTicket>nimportenawak</cas:proxyGrantingTicket>
+  </cas:authenticationSuccess>
+</cas:serviceResponse>');
+
+    Class_WebService_Cas2::setWebClient($this->mock()
+                                        ->whenCalled('getResponse')
+                                        ->answers($response));
+
+    $this->dispatch('/auth/login/provider/1?ticket=testticket');
+  }
+
+
+  /** @test */
+  public function shouldNotValidateTicketTwice() {
+    $this->assertEquals(1, Class_WebService_Cas2::getWebClient()->methodCallCount('getResponse'));
+  }
+
+
+  /** @test */
+  public function shouldDisplayLoginForm() {
+    $this->assertXPath('//input[@name="username"]');
+  }
+
+
+  /** @test */
+  public function shouldBecomeRemotlyLoggedWithCasUserAsId() {
+    $this->assertTrue($this->_provider->isRemotelyLogged());
+    $this->assertEquals('mysuperid', $this->_provider->getRemoteUserId());
+  }
+
+
+  /** @test */
+  public function pageTitleShouldContainsBeAssocierVotreCompte() {
+    $this->assertXPathContentContains('//title', 'Associer votre compte');
+  }
+
+
+  /** @test */
+  public function pageShouldDisplayConnectedToCas2_0() {
+    $this->assertXPathContentContains('//p', 'Vous êtes connecté à CAS 2.0');
+  }
+
+
+  /** @test */
+  public function logoutBokehShouldLogoutCas2_0() {
+    $this->dispatch('/auth/logout');
+    $this->assertRedirect('/');
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasAuthLoginPostRemotelyLoggedTest
+  extends IdentityProviderAuthenticationCasTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    ZendAfi_Auth::getInstance()->clearIdentity();
+    Class_WebService_Cas2::loginWith('mysuperid');
+    NanookFixtures::connectNanook($this, 'name@server.tld', '1987');
+
+    $this->postDispatch('/auth/login/provider/1',
+                        ['username' => 'name@server.tld',
+                         'password' => '1987']);
+  }
+
+
+  /** @test */
+  public function identiyShouldBeCreated() {
+    $this->assertNotNull(Class_User_Identity::findFirstBy(['provider_id' => 1,
+                                                           'identifier' => 'mysuperid']));
+  }
+
+
+  /** @test */
+  public function shouldRedirect() {
+    $this->assertRedirect();
+  }
+
+
+  /** @test */
+  public function shouldNotifyAssociationComplete() {
+    $this->assertFlashMessengerContentContains('L\'association de votre compte à CAS 2.0 a réussi');
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasAuthLoginIdentityAssociatedToAnotherTest
+  extends IdentityProviderAuthenticationCasTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    Class_WebService_Cas2::loginWith('mysuperid');
+
+    $this->fixture('Class_User_Identity',
+                   ['id' => 803,
+                    'provider_id' => 1,
+                    'user_id' => 999,
+                    'identifier' => 'mysuperid']);
+
+    $response = $this->mock()
+                     ->whenCalled('isError')->answers(false)
+
+                     ->whenCalled('getBody')
+                     ->answers('<?xml version="1.0" encoding="UTF-8" ?>
+<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
+  <cas:authenticationSuccess>
+    <cas:user>mysuperid</cas:user>
+    <cas:proxyGrantingTicket>nimportenawak</cas:proxyGrantingTicket>
+  </cas:authenticationSuccess>
+</cas:serviceResponse>');
+
+    Class_WebService_Cas2::setWebClient($this->mock()
+                                        ->whenCalled('getResponse')
+                                        ->answers($response));
+
+    $this->dispatch('/auth/login/provider/1?ticket=testticket');
+  }
+
+
+  /** @test */
+  public function shouldRedirect() {
+    $this->assertRedirect();
+  }
+
+
+  /** @test */
+  public function shouldNotDuplicate() {
+    $this->assertEquals(1, Class_User_Identity::countBy(['identifier' => 'mysuperid',
+                                                         'provider_id' => 1]));
+  }
+
+
+  /** @test */
+  public function shouldNotifyAssociationNotComplete() {
+    $this->assertFlashMessengerContentContains('L\'association a échoué, cette identité est déjà associée à un autre compte');
+  }
+}
+
+
+
+abstract class IdentityProviderAuthenticationCasAutoAssociationTestCase
+  extends IdentityProviderAuthenticationCasTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    ZendAfi_Auth::getInstance()->clearIdentity();
+
+    $this->_provider->setAssociateOnLogin(1);
+
+    $response = $this->mock()
+                     ->whenCalled('isError')->answers(false)
+
+                     ->whenCalled('getBody')
+                     ->answers('<?xml version="1.0" encoding="UTF-8" ?>
+<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
+  <cas:authenticationSuccess>
+    <cas:user>mysuperid</cas:user>
+    <cas:proxyGrantingTicket>nimportenawak</cas:proxyGrantingTicket>
+  </cas:authenticationSuccess>
+</cas:serviceResponse>');
+
+    Class_WebService_Cas2::setWebClient($this->mock()
+                                        ->whenCalled('getResponse')
+                                        ->answers($response));
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasAuthLoginPostAutoAssociationTest
+  extends IdentityProviderAuthenticationCasAutoAssociationTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture('Class_Users', ['id' => 999,
+                                   'login' => 'mysuperid',
+                                   'password' => 'infopassword']);
+
+    $this->dispatch('/auth/login/provider/1?ticket=testticket');
+  }
+
+
+  /** @test */
+  public function shouldRedirect() {
+    $this->assertRedirect();
+  }
+
+
+  /** @test */
+  public function shouldAssociateOnLogin() {
+    $this->assertNotNull(Class_User_Identity::findFirstBy(['provider_id' => 1,
+                                                           'user_id' => 999,
+                                                           'identifier' => 'mysuperid']));
+  }
+
+
+  /** @test */
+  public function shouldBeConnected() {
+    $this->assertNotNull(Class_Users::getIdentity());
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasAuthLoginPostAutoAssociationWithDuplicateTest
+  extends IdentityProviderAuthenticationCasAutoAssociationTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture('Class_Users', ['id' => 999,
+                                   'login' => 'mysuperid',
+                                   'password' => 'infopassword']);
+
+    $this->fixture('Class_Users', ['id' => 1000,
+                                   'login' => 'mysuperid',
+                                   'password' => 'infopassword']);
+
+    $this->dispatch('/auth/login/provider/1?ticket=testticket');
+  }
+
+
+  /** @test */
+  public function shouldRedirect() {
+    $this->assertRedirect();
+  }
+
+
+  /** @test */
+  public function shouldNotAssociateOnLogin() {
+    $this->assertNull(Class_User_Identity::findFirstBy(['provider_id' => 1,
+                                                        'user_id' => 999,
+                                                        'identifier' => 'mysuperid']));
+  }
+
+
+  /** @test */
+  public function shouldNotifyDuplicateLogin() {
+    $this->assertFlashMessengerContentContains('Connection impossible, l\'identifiant est dupliqué');
+  }
+
+
+  /** @test */
+  public function shouldNotBeConnected() {
+    $this->assertNull(Class_Users::getIdentity());
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasAuthLoginPostAutoAssociationUnknownLoginTest
+  extends IdentityProviderAuthenticationCasAutoAssociationTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/auth/login/provider/1?ticket=testticket');
+  }
+
+
+  /** @test */
+  public function shouldRedirect() {
+    $this->assertRedirect();
+  }
+
+
+  /** @test */
+  public function shouldNotAssociateOnLogin() {
+    $this->assertNull(Class_User_Identity::findFirstBy(['provider_id' => 1,
+                                                        'user_id' => 999,
+                                                        'identifier' => 'mysuperid']));
+  }
+
+
+  /** @test */
+  public function shouldNotifyDuplicateLogin() {
+    $this->assertFlashMessengerContentContains('Connection impossible, l\'identifiant n\'existe pas');
+  }
+
+
+  /** @test */
+  public function shouldNotBeConnected() {
+    $this->assertNull(Class_Users::getIdentity());
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasAuthLoginPostNotRemotelyLoggedTest
+  extends IdentityProviderAuthenticationCasTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    ZendAfi_Auth::getInstance()->clearIdentity();
+    NanookFixtures::connectNanook($this, 'name@server.tld', '1987');
+
+    $this->postDispatch('/auth/login/provider/1',
+                        ['username' => 'name@server.tld',
+                         'password' => '1987']);
+  }
+
+
+  /** @test */
+  public function identiyShouldNotBeCreated() {
+    $this->assertNull(Class_User_Identity::findFirstBy(['provider_id' => 1]));
+  }
+
+
+  /** @test */
+  public function shouldRedirect() {
+    $this->assertRedirect();
+  }
+
+
+  /** @test */
+  public function shouldNotifyAssociationComplete() {
+    $this->assertFlashMessengerContentContains('L\'association de votre compte à CAS 2.0 a échoué');
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasAuthLoginRemotelyLoggedAlreadyAssociatedTest
+  extends IdentityProviderAuthenticationCasTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    ZendAfi_Auth::getInstance()->clearIdentity();
+
+    $this->fixture('Class_Users',
+                   ['id' => 33,
+                    'login' => 'name@server.tld',
+                    'password' => 'supersecret',
+                   ]);
+
+    $this->fixture('Class_User_Identity',
+                   ['id' => 1,
+                    'provider_id' => 1,
+                    'user_id' => 33,
+                    'identifier' => 'mysuperid']);
+
+    $response = $this->mock()
+                     ->whenCalled('isError')->answers(false)
+
+                     ->whenCalled('getBody')
+                     ->answers('<?xml version="1.0" encoding="UTF-8" ?>
+<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
+  <cas:authenticationSuccess>
+    <cas:user>mysuperid</cas:user>
+    <cas:proxyGrantingTicket>teststicket</cas:proxyGrantingTicket>
+  </cas:authenticationSuccess>
+</cas:serviceResponse>');
+
+    Class_WebService_Cas2::setWebClient($this->mock()
+                                        ->whenCalled('getResponse')
+                                        ->answers($response));
+
+    $this->dispatch('/auth/login/provider/1?ticket=testticket');
+  }
+
+
+  /** @test */
+  public function userShouldBeLoggedIn() {
+    $this->assertEquals('name@server.tld', Class_Users::getIdentity()->getLogin());
+  }
+
+
+  /** @test */
+  public function shouldRedirect() {
+    $this->assertRedirect();
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasAuthLogoutRemotelyLoggedAlreadyAssociatedTest
+  extends IdentityProviderAuthenticationCasTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $user = $this->fixture('Class_Users',
+                           ['id' => 33,
+                            'login' => 'name@server.tld',
+                            'password' => 'supersecret',
+                           ]);
+
+    ZendAfi_Auth::getInstance()->logUser($user);
+
+    $this->fixture('Class_User_Identity',
+                   ['id' => 1,
+                    'provider_id' => 1,
+                    'user_id' => 33,
+                    'identifier' => 'mysuperid']);
+
+    Class_WebService_Cas2::loginWith('mysuperid');
+    Class_WebService_Cas2::setWebClient($this->mock());
+
+    $this->dispatch('/auth/logout/provider/1');
+  }
+
+
+  /** @test */
+  public function shouldBeLoggedOut() {
+    $this->assertNull(Class_Users::getIdentity());
+  }
+
+
+  /** @test */
+  public function shouldBeNoLongerRemotelyLogged() {
+    $this->assertFalse($this->_provider->isRemotelyLogged());
+  }
+
+
+  /** @test */
+  public function shouldRedirectToCasLogout() {
+    $this->assertRedirectContains('http://moncompte.server.com/logout?service=http');
+  }
+}
+
+
+
+
+class IdentityProviderAuthenticationCasNetworkErrorTest
+  extends IdentityProviderAuthenticationCasTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    ZendAfi_Auth::getInstance()->clearIdentity();
+
+    Class_WebService_Cas2::setWebClient($this->mock()
+                                        ->whenCalled('getResponse')
+                                        ->willDo(function()
+                                                 {
+                                                   throw new RuntimeException("Erreur Réseau");
+                                                 }));
+
+    $this->dispatch('/auth/login/provider/1?ticket=testticket');
+  }
+
+
+  /** @test */
+  public function shouldDisplayLoginForm() {
+    $this->assertXPath('//input[@name="username"]');
+  }
+
+
+  /** @test */
+  public function shouldNotBecomeRemotlyLogged() {
+    $this->assertFalse($this->_provider->isRemotelyLogged());
+  }
+}
\ No newline at end of file
diff --git a/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php b/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php
index c620f99960a..8c7efe11ffd 100644
--- a/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php
+++ b/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php
@@ -115,6 +115,13 @@ class IdentityProviderAuthenticationBoxDisplayTest extends AbstractControllerTes
                                              'client_secret' => '9876'])
                    ]);
 
+    $this->fixture('Class_IdentityProvider',
+                   ['id' => 6,
+                    'label' => 'CAS 2.0',
+                    'type' => 'cas2',
+                    'config' => json_encode(['url' => 'https://moncas.server.com'])
+                   ]);
+
     Class_WebService_OpenId::setWebClient($this->mock()->whenCalled('open_url')->answers(''));
 
     $this->dispatch('/');
@@ -154,6 +161,13 @@ class IdentityProviderAuthenticationBoxDisplayTest extends AbstractControllerTes
   }
 
 
+  /** @test */
+  public function pageShouldContainsButtonForCas() {
+    $this->assertXPath('//button[contains(@onclick, "/identity-providers/authenticate/id/6")]',
+                       $this->_response->getBody());
+  }
+
+
   /** @test */
   public function pageShouldContainsButtonOpenIdConnectForBiblibre() {
     $this->assertXPathContentContains('//button[contains(@onclick, "/identity-providers/authenticate/id/2")]',
@@ -539,7 +553,9 @@ class IdentityProviderAuthenticationCallbackAuthenticationFirstTime
     $this->fixture('Class_IdentityProvider',
                    ['id' => 12,
                     'label' => 'FranceConnect',
-                    'type' => 'franceconnect']);
+                    'type' => 'franceconnect',
+                    'client_id' => 'myclientid',
+                    'client_secret' => 'supersecret']);
 
     $this->dispatch('/auth/login/provider/12?code=1233&state='.$this->_state);
   }
@@ -656,6 +672,8 @@ class IdentityProviderAuthenticationCallbackAuthentication
                    ['id' => 12,
                     'label' => 'FranceConnect',
                     'type' => 'franceconnect',
+                    'client_id' => 'myclientid',
+                    'client_secret' => 'supersecret'
                    ]);
   }
 
diff --git a/tests/scenarios/PnbDilicom/PnbDilicomTest.php b/tests/scenarios/PnbDilicom/PnbDilicomTest.php
index b089e263bf1..20a9a6d1d2b 100644
--- a/tests/scenarios/PnbDilicom/PnbDilicomTest.php
+++ b/tests/scenarios/PnbDilicom/PnbDilicomTest.php
@@ -3936,6 +3936,7 @@ class PnbDilicomAdminIndexControllerTest extends AbstractControllerTestCase {
                                    'login' => 'super admin',
                                    'password' => 'pwd',
                                    'role_level' => ZendAfi_Acl_AdminControllerRoles::SUPER_ADMIN]);
+
     ZendAfi_Auth::getInstance()->logUser($super_admin);
 
     Class_WebService_BibNumerique_Dilicom_Hub::setDefaultHttpClient($this
@@ -3949,7 +3950,6 @@ class PnbDilicomAdminIndexControllerTest extends AbstractControllerTestCase {
     Class_AdminVar::set('DILICOM_PNB_PWD_COLLECTIVITE', 'secretPassword');
     Class_AdminVar::set('DILICOM_PNB_SERVER_URL', 'https://pnb-test.centprod.com');
     Class_AdminVar::set('DILICOM_PNB_GLN_CONTRACTOR', 123456789);
-    Class_AdminVar::set('DILICOM_PNB_IP_ADRESSES', '127.0.0.1');
 
 
     $this->dispatch('/admin/index/index', true);
diff --git a/tests/scenarios/RGPD/PatronDownloadDatasTest.php b/tests/scenarios/RGPD/PatronDownloadDatasTest.php
index 33794b50fa6..e95c7a512fa 100644
--- a/tests/scenarios/RGPD/PatronDownloadDatasTest.php
+++ b/tests/scenarios/RGPD/PatronDownloadDatasTest.php
@@ -90,6 +90,13 @@ class RGPD_PatronDowloadDatasTest extends AbstractControllerTestCase {
     $this->_createMultimediaHold();
   }
 
+
+  public function tearDown() {
+    Class_WebService_BibNumerique_Dilicom_Hub::setHttpClient(null);
+    parent::tearDown();
+  }
+
+
   protected function _createArticle() {
     $this->fixture('Class_Article',
                    ['id' => 10,
@@ -245,14 +252,6 @@ class RGPD_PatronDowloadDatasTest extends AbstractControllerTestCase {
                     'record_origin_id' => $book->getIdOrigine(),
                     'order_line_id' => 'x321',
                     'expected_return_date' => '2022-06-01 20:10:00']);
-
-
-  }
-
-
-  public function tearDown() {
-    Class_WebService_BibNumerique_Dilicom_Hub::setDefaultHttpClient(null);
-    parent::tearDown();
   }
 
 
diff --git a/tests/scenarios/Templates/TemplatesAddWidgetTest.php b/tests/scenarios/Templates/TemplatesAddWidgetTest.php
new file mode 100644
index 00000000000..fa9b59dd04e
--- /dev/null
+++ b/tests/scenarios/Templates/TemplatesAddWidgetTest.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+require_once 'TemplatesTest.php';
+
+
+class TemplatesAddWidgetSimpleTest extends TemplatesIntonationTestCase {
+  /** @test */
+  public function addWidgetShouldRenderFreeWidget() {
+    $this->dispatch('/admin/widget/add-from-template/id_profil/72');
+    $this->assertXPathContentContains('//div', 'Contenu HTML');
+  }
+
+
+  /** @test */
+  public function shouldContainsAdminToolsJpg() {
+    $this->dispatch('/admin/widget/add-from-template/id_profil/72');
+    $this->assertXPath('//div//img[contains(@src, "/TEMPLATE_ADMIN_TOOLS.jpg")]');
+  }
+
+
+  /** @test */
+  public function addWidgetWithConfShouldRedirect() {
+    $this->dispatch('/admin/widget/add/after/20/division/2/id_profil/72/template/0/template_no/1');
+    $this->assertRedirect();
+  }
+}
+
+
+
+
+class TemplatesAddWidgetIdentityProviderEnabledTest extends TemplatesIntonationTestCase {
+  public function setUp() {
+    parent::setUp();
+    Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 1);
+  }
+
+
+  /** @test */
+  public function shouldDisplayIdentityProviderWidgetChoice() {
+    $this->dispatch('/admin/widget/add-from-template/id_profil/72');
+    $this->assertXPathContentContains('//div', 'Se connecter avec');
+  }
+}
+
+
+
+
+class TemplatesAddWidgetIdentityProviderDisabledTest extends TemplatesIntonationTestCase {
+  public function setUp() {
+    parent::setUp();
+    Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 0);
+  }
+
+
+  /** @test */
+  public function shouldDisplayIdentityProviderWidgetChoice() {
+    $this->dispatch('/admin/widget/add-from-template/id_profil/72');
+    $this->assertNotXPathContentContains('//div', 'Se connecter avec');
+  }
+}
\ No newline at end of file
diff --git a/tests/scenarios/Templates/TemplatesAuthLoginTest.php b/tests/scenarios/Templates/TemplatesAuthLoginTest.php
new file mode 100644
index 00000000000..fb6e4ed1114
--- /dev/null
+++ b/tests/scenarios/Templates/TemplatesAuthLoginTest.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+require_once 'TemplatesTest.php';
+
+
+class TemplatesAuthLoginWithIdentityProviderTest extends TemplatesIntonationTestCase {
+  /** @test */
+  public function formActionShouldContainProvider() {
+    Zendafi_Auth::getInstance()->clearIdentity();
+    $this->dispatch('/opac/auth/login/id_profil/72/provider/1?ST-P2908128716');
+    $this->assertXPath('//form[contains(@action, "/provider/1")]');
+  }
+}
+
+
+
+
+class TemplatesAuthLoginWidgetTest extends TemplatesIntonationTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    Zendafi_Auth::getInstance()->clearIdentity();
+
+    $cfg_accueil = Class_Profil::find(72)->getCfgAccueilAsArray();
+    $cfg_accueil['modules'][10]['preferences']['IntonationFormStyle'] = 'toggle';
+    Class_Profil::find(72)->setCfgAccueil($cfg_accueil);
+
+    $this->dispatch('/opac/auth/login/id_profil/72');
+  }
+
+
+  /** @test */
+  public function widgetShouldHaveDefaultDisplayOption() {
+    $this->assertNotXPath('//div[@data-action="auth_login"]//div[@class="dropdown"]');
+  }
+
+
+  /** @test */
+  public function shouldContainBooststrapContainer() {
+    $this->assertXPath('//div[@data-action="auth_login"]//div[@class="justify-content-center row no-gutters"]');
+  }
+
+
+  /** @test */
+  public function shouldContainUsernameInput() {
+    $this->assertXPath('//div[@data-action="auth_login"]//input[@name="username"]');
+  }
+
+
+  /** @test */
+  public function shouldContainTitleMonCompte() {
+    $this->assertXPathContentContains('//div[@data-action="auth_login"]//h2', 'Se connecter');
+  }
+}
\ No newline at end of file
diff --git a/tests/scenarios/Templates/TemplatesAuthorTest.php b/tests/scenarios/Templates/TemplatesAuthorTest.php
index c68e499b475..43a7f4b673e 100644
--- a/tests/scenarios/Templates/TemplatesAuthorTest.php
+++ b/tests/scenarios/Templates/TemplatesAuthorTest.php
@@ -22,7 +22,7 @@
 require_once('TemplatesTest.php');
 
 
-class TemplatesDispatchEditAuthorWidgetTest extends TemplatesIntonationTestCase {
+class TemplatesAuthorEditWidgetTest extends TemplatesIntonationTestCase {
   public function setUp() {
     parent::setUp();
     $this->dispatch('/admin/widget/edit-widget/id/23/id_profil/72', true);
@@ -38,7 +38,7 @@ class TemplatesDispatchEditAuthorWidgetTest extends TemplatesIntonationTestCase
 
 
 
-class TemplatesDispatchIntonationWithAuthorWidgetTest extends TemplatesIntonationTestCase {
+class TemplatesAuthorWidgetTest extends TemplatesIntonationTestCase {
 
   /** @test */
   public function rahanAuthorShouldBePresent() {
@@ -114,7 +114,7 @@ abstract class TemplatesIntonationWithAuthorTest extends TemplatesIntonationTest
 
 
 
-class TemplatesDispatchAuthorActionsTest extends TemplatesIntonationWithAuthorTest {
+class TemplatesAuthorActionsTest extends TemplatesIntonationWithAuthorTest {
   public function setUp() {
     parent::setUp();
 
@@ -206,7 +206,7 @@ class TemplatesDispatchAuthorActionsTest extends TemplatesIntonationWithAuthorTe
 
 
 
-class TemplatesDispatchAuthorDisabledBiographyTest extends TemplatesIntonationWithAuthorTest {
+class TemplatesAuthorDisabledBiographyTest extends TemplatesIntonationWithAuthorTest {
   public function setUp() {
     parent::setUp();
 
diff --git a/tests/scenarios/Templates/TemplatesPatronConfigurationsTest.php b/tests/scenarios/Templates/TemplatesPatronConfigurationsTest.php
new file mode 100644
index 00000000000..65580bb8090
--- /dev/null
+++ b/tests/scenarios/Templates/TemplatesPatronConfigurationsTest.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+require_once 'TemplatesTest.php';
+
+class TemplatesPatronConfigurationsIdentityProvidersNoneAssociatedTest
+  extends TemplatesIntonationAccountTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 1);
+    $this->dispatch('/opac/abonne/configurations/id_profil/72');
+  }
+
+
+  /** @test */
+  public function pageShouldContainIdentityProvidersSection() {
+    $this->assertXPathContentContains('//h3', 'Fournisseur(s) d\'identité associé(s)');
+  }
+
+
+  /** @test */
+  public function pageShouldContainIdentitiesTable() {
+    $this->assertXPath('//table[@id="active_identities"]');
+  }
+}
+
+
+
+
+class TemplatesPatronConfigurationsIdentityProvidersOneAssociatedTest
+  extends TemplatesIntonationAccountTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 1);
+
+    $this->fixture('Class_User_Identity',
+                   ['id' => 1,
+                    'provider_id' => 1,
+                    'user_id' => Class_Users::getIdentity()->getId(),
+                    'identifier' => 'mysuperid']);
+
+    $this->fixture('Class_IdentityProvider',
+                   ['id' => 1,
+                    'label' => 'CAS 2.0',
+                    'type' => 'cas2',
+                    'url' => 'http://moncompte.server.com/',
+                   ]);
+
+    $this->dispatch('/opac/abonne/configurations/id_profil/72');
+  }
+
+
+  /** @test */
+  public function pageShouldContainIdentityProvidersSection() {
+    $this->assertXPathContentContains('//h3', 'Fournisseur(s) d\'identité associé(s)');
+  }
+
+
+  /** @test */
+  public function pageShouldContainIdentityCas2() {
+    $this->assertXPathContentContains('//table[@id="active_identities"]//td', 'CAS 2.0');
+  }
+}
diff --git a/tests/scenarios/Templates/TemplatesTest.php b/tests/scenarios/Templates/TemplatesTest.php
index b458e54cb4a..e886c6bb0c1 100644
--- a/tests/scenarios/Templates/TemplatesTest.php
+++ b/tests/scenarios/Templates/TemplatesTest.php
@@ -920,30 +920,6 @@ class TemplatesDispatchIntonationSearchTest extends TemplatesIntonationTestCase
 
 
 
-class TemplatesMultipleDispatchTest extends TemplatesEnabledTestCase {
-  protected $_storm_default_to_volatile = true;
-
-  public function setUp() {
-    parent::setUp();
-    $this->fixture('Class_Profil',
-                   ['id' => 5,
-                    'template' => 'INTONATION']);
-    $this->dispatch('/opac/index/index/id_profil/5', true);
-    $this->dispatch('/opac/recherche/simple/pomme', true);
-    $this->dispatch('/opac/index/index/id_profil/2', true);
-    $this->dispatch('/opac/index/index/id_profil/5', true);
-  }
-
-
-  /** @test */
-  public function currentProfilTemplateShouldBeIntonation() {
-    $this->assertEquals('INTONATION', Class_Profil::getCurrentProfil()->getTemplate());
-  }
-}
-
-
-
-
 class TemplatesEditTest extends TemplatesEnabledTestCase {
   protected $_storm_default_to_volatile = true;
 
@@ -3556,7 +3532,7 @@ class TemplatesDispatchIntonationAuthLoginWithRedirectTest extends TemplatesInto
 
   /** @test */
   public function formActionShouldContainsRedirectUrlMyBib() {
-    $this->assertXPath('//form[contains(@action, "/auth/login/redirect/'.urlencode("http://mybib/modules/skilleos").'")]', $this->_response->getBody());
+    $this->assertXPath('//form[contains(@action, "/auth/login/id_profil/72/redirect/'.urlencode("http://mybib/modules/skilleos").'")]', $this->_response->getBody());
   }
 
 
diff --git a/tests/scenarios/Templates/TemplatesWidgetIdentityProvidersTest.php b/tests/scenarios/Templates/TemplatesWidgetIdentityProvidersTest.php
new file mode 100644
index 00000000000..ade229f98e9
--- /dev/null
+++ b/tests/scenarios/Templates/TemplatesWidgetIdentityProvidersTest.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+require_once 'TemplatesTest.php';
+
+
+class TemplatesWidgetIdentityProvidersTest extends TemplatesIntonationTestCase {
+  /** @test */
+  public function shouldContainBootstrapedButton() {
+    Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 1);
+
+    $this->fixture('Class_IdentityProvider',
+                   ['id' => 1,
+                    'label' => 'CAS 2.0',
+                    'type' => 'cas2',
+                    'url' => 'http://moncompte.server.com/',
+                   ]);
+
+    Zendafi_Auth::getInstance()->clearIdentity();
+
+    $cfg_accueil = Class_Profil::find(72)->getCfgAccueilAsArray();
+    $cfg_accueil['modules'][] = ['division' => 4,
+                                 'type_module' => 'IDENTITY_PROVIDER',
+                                 'preferences' => []];
+    Class_Profil::find(72)->setCfgAccueil($cfg_accueil);
+
+    $this->dispatch('/opac/index/index/id_profil/72');
+
+    $this->assertXPathContentContains('//div[contains(@class, "justify-content-center")]//button',
+                                      'CAS 2.0');
+  }
+}
diff --git a/tests/scenarios/Templates/TemplatesWidgetTest.php b/tests/scenarios/Templates/TemplatesWidgetTest.php
index 70616a0b1ba..48e5008d3e9 100644
--- a/tests/scenarios/Templates/TemplatesWidgetTest.php
+++ b/tests/scenarios/Templates/TemplatesWidgetTest.php
@@ -60,32 +60,6 @@ class TemplatesDispatchNewsletterWidgetTest extends TemplatesIntonationTestCase
 
 
 
-class TemplatesDispatchWidgetAddWidgetTest extends TemplatesIntonationTestCase {
-
-  /** @test */
-  public function addWidgetShouldRenderFreeWidget() {
-    $this->dispatch('/admin/widget/add-from-template/id_profil/72');
-    $this->assertXPathContentContains('//div', 'Contenu HTML');
-  }
-
-
-  /** @test */
-  public function shouldContainsAdminToolsJpg() {
-    $this->dispatch('/admin/widget/add-from-template/id_profil/72');
-    $this->assertXPath('//div//img[contains(@src, "/TEMPLATE_ADMIN_TOOLS.jpg")]');
-  }
-
-
-  /** @test */
-  public function addWidgetWithConfShouldRedirect() {
-    $this->dispatch('/admin/widget/add/after/20/division/2/id_profil/72/template/0/template_no/1');
-    $this->assertRedirect();
-  }
-}
-
-
-
-
 class TemplatesDispatchMenuWidgetTest extends TemplatesIntonationTestCase {
 
   /** @test */
-- 
GitLab