diff --git a/VERSIONS_WIP/110690 b/VERSIONS_WIP/110690
new file mode 100644
index 0000000000000000000000000000000000000000..1adf4f022e49667e6370b514e715b20d06083e35
--- /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 c19f4034b1498ac937e12c2a71de8ec3516122a8..dfc6a53165cd710fa9fb1c674a199a341cc8c376 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 d109856ab096c98bcdb83f5fa82ad8e3a7d7fa5a..81450e47bc3e1fff31264e8a84b9f7f03664d697 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 a8399533032b9d83017734fed64a5fecb631820e..b310a0caccfed6a26ac8aa430628820769ef6ed9 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 44c8415837261c9e9e7eba8cadff250bc5828440..66aa661c04069af790ebb8b054146d21236addbe 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 6b9562dcbcc5b8d5dbedd19f23493b6ef7eb891c..d247c2f3db3298248caa9cab1b6fa59a91ffa08b 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 0000000000000000000000000000000000000000..fd24fdf50a2baefccc913bce2f7c3492e7273dbc
--- /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 db7c540de40b4d7fdd2b93ee4e4ea149168df581..50704cb784fdd070f4bd39cfd88c46002ce9655c 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 0000000000000000000000000000000000000000..8fb58f78274e506d396368947fbfddd6ec1df73f
--- /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 7c31f7062e460acfb82599f6360534eb15e78455..63ff1ce679915654230f651af79069036e2ca946 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 85c8260893a25b4b0da22b8496c31eb00ee67d82..0f4bd4244e3653efe00c1d730dc9120bb3490ef6 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 c6b7acb7b74863889173681da2a7844add6e4b12..f7b849f75e4cfd831fa9f23a27df107d7927cc8a 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 0000000000000000000000000000000000000000..bc54cda51a8d389c77f61bc4c4b9a9e1948c4bdc
--- /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 868b2a3db46e47eb0999e5edb16999a1b28dcbc6..ad3c3becefab0deaa00b8205568cbd5380f3b9a6 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 99d35982061c465d62b4b0b81730a4ae5433b5a6..5fbddbfd80cf8733fda739584e1733fd7600ec31 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 7f01e39984456049e72bde4d39e29baa2df55375..ffb803b6019e0c65aa5a38249f8e5c043705136b 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 8282bdac1ec425174f8ad7e2721353e3c3716df2..e3fc75569c7eb9d0273ed4ec31046986d7e523da 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 0d247bc8b276ef49936281e402a6fa07f6933f9a..3ff919581030110dfe1d4780842fdda40832265d 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 fb1dd64c22576dc0da1b1e71eef19f45b863099f..fc6a303cabcb932a72d2f952f204a0a31981663b 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 24f75f8de8f9ce95b5c4c7e62758870008307a85..e4b18531119fa8e89442bed2d9c51579ef06e951 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 0000000000000000000000000000000000000000..4b100227c299ee753acfb51ab1d8b74fc9719623
--- /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 093d1d893eb20d388cc3ba479fd8eb32c5fa2298..4687897f7e1f59c9e6eb3560c4a112696e2cedbf 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 205f35de981d0f1d874f07f2ea4e2dd49c60f3a8..4c0f5d09f45c8d4fb052c877693be38e01c50507 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 cb491ba9db0a90fb5273fc5a2a49045c2de4dc84..1664a166be93dae46c7769f5a5c91d4c200625af 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 b1b32d2bba350d7ddec5761e267e31adcb8c3c48..a5d4bb3485a65e9f15b0661b27e236ef4df9045c 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 3bbad070164d81806ff76a4596ba9b50077880f9..0f856404644e025258f32f155fb85fe958371018 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 5fcff9df3f0b0a2e1c5e60b326400605392b21be..1995769d4453fa15ffe80e7b1209c9d04e0b42cb 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 a996c48063262d237380a6eab54e120703acec83..49a216b9153ba83ef8cf0e345138d954553d90bf 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 147c1e3e308b22df49a9ba4893cafed5b5bc3821..fda04dd2f6b0a8f456a5866721914c716a172d4e 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 69d9fe8cd58bbb4c331d82c57bc892edee214551..e951247187a7e5633ea83ff66c666fd6ac6ce77b 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 6410ffbd7fcf6f7ede9bd2c9d98d780dcce6629c..69fd97a48e6d945857f6567a49456de2e2150459 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 a5ca1d9fac5d3300710e46ce8c01c371a3b3a2bb..10a0f17256693f557c456a55becf243c1efc6a34 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 cb3df967994dea8e07e213cf78a55f260a1a2bba..852f73b28209a0dd1bf246b6fd9c313475171151 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 e6ef3556cd2d047b04a2def3c0c7b38c801c44d1..69a6b67987618b47029c89afb07440c187050df6 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 e1e92d8377a7d038f303342f4ff1b3997717dd18..bf974e6cb769b9b9d0ee8874afa4c0371e3c09dc 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 0f6c2de7f0e69f62fe70ce6245a53a8861403d74..a0ba569d1f02803733fafd1545cd683bcd6bc4ec 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 12ca1df4ad2f7e9236ea4789f8fe32aa7df9b4a1..b268e0f3de4fbe166930fe9c2999a1b98dcd5460 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 4a96ccb7e02ed810990e4e2b918bb3e0c3004f6d..5c4270cbb0173de7b5bd7ae130f63d7aecf51491 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 0000000000000000000000000000000000000000..edb4484c628174dea3b8a0d451dd75879b7cdcfd
--- /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 1c250cd4e37c4b2603b653d2e1d6843c8affdba5..65bf31825581dfdad71c3c9759e76fe790a36291 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 7f4ea5abda60829d47da87b66007970f4bba8789..021ec50b8d30bc888fea1103352b956119f4cde0 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 b9b0be69c3b33d51559a02c6726c1d701810aedc..ef5000d7b73e67eda28df2fe548ce90236e41a28 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 4022e2242e648cb21b1dca454a1b9fc625f82706..4a3c0949e1fc29f90b9297392911f8cfe14271fa 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 dc8a4973cd859e98431626f0c1faa481000885a2..6358ec3534cb3579ff3b2a4a42ed4f8a06922317 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 ad436670fcee4e524a1833dc2049b00aefbcf87c..d0ec2a6673fb9f86f04fdcaf97d4de4d918d3c4b 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 2067a8b112e1c04f214a4935849ff1d7d37fa79c..c48dbf00a1dbedf461bdc7f9c30e5255267f0da0 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 f4e1790571328d2391e3d9771fd94f7e45292f08..4a38246ecd8026189107b45790268903ddac1789 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 cfa1134fb622457dd56405b3ebd79aac7fcbca83..c500bdd5ba7c20910f580868eabc5b50f9992f47 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 19e8973053bda989e0771a1a4ac2cebf504646a8..08370ed311d40e0a4aae246e59359db618f13a23 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 0000000000000000000000000000000000000000..16ee6ce1208e1d5092fe6a498033faf5fe506feb
--- /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 0000000000000000000000000000000000000000..4b08a781c5dcc0e1abeb9bc974d687273a4dcbe4
--- /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 3abf83c87db0c7e098b39441e7359ff9af76dc9d..1c4db1744eaf7126244c5dd57991657d49626b75 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 8be15ce38885e6f68d3ff8783f08d7dcf18b78e6..0f4e67b1ae87f070316b16d1952cf65fe9695554 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 0000000000000000000000000000000000000000..fe964b0da99f7588ac3c34c71dd6a14dee192fe3
--- /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 0000000000000000000000000000000000000000..f53196e79f2b905ffafe0b0f40c123a81f28fc92
--- /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 d80b4e79e5ff33de18b99fe427b401e1c6dfd37e..a42e889c663d712afeeaae59e67e36645ba7185a 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
Binary files /dev/null and b/public/opac/js/widget_templates/TEMPLATE_IDENTITY_PROVIDER.jpg differ
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
Binary files /dev/null and b/public/opac/js/widget_templates/TEMPLATE_LANGUE.jpg differ
diff --git a/tests/application/modules/AbstractControllerTestCase.php b/tests/application/modules/AbstractControllerTestCase.php
index 8993a0aa427149bb40931d1916fdede814a2457c..ce353945d85769b39d1166693dd8c54a6e392502 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 ab30f8c794347965a2952954478c5f07e03c24f3..9a4dc956f3d0680035399a27038c669aca1ceba7 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 9c183714ead12c04291b584a0b3d61cbdbe20ab7..6d1bbefedc0b2cde3ca78ee2a85188aaf402989c 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 0000000000000000000000000000000000000000..4088f3588cb6eda7d6bc1968fec559ee936e8003
--- /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 c620f99960a02665ca510f7ae0fc429711d7f4d7..8c7efe11ffdfef55402604042fdbae5f0ce0e7bd 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 b089e263bf12dd3685ab53b91c80667631e0c85d..20a9a6d1d2b51a847fb9130670544c865ef106e0 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 33794b50fa61de5c86474bc21d3cdd512a5caf09..e95c7a512fac1c8c2448f586962d83ec20d700b2 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 0000000000000000000000000000000000000000..fa9b59dd04e763e1269d2af2e17d6626164bf900
--- /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 0000000000000000000000000000000000000000..fb6e4ed1114d6948972e9367f1c0ad6fd598ac70
--- /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 c68e499b475db5e4d968d9f09e22c1687a27b684..43a7f4b673e6613240c2097a2bc228953b9c8a08 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 0000000000000000000000000000000000000000..65580bb80900e5d4866961d85d6f979781cea27c
--- /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 b458e54cb4adf53439ee54f7c71e3e6d926495df..e886c6bb0c134804893c0073f125605fafe6c5e1 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 0000000000000000000000000000000000000000..ade229f98e9b64740f4ffd602ac9fc21e0fe2046
--- /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 70616a0b1ba2f149e84f7cad8252e431489b604f..48e5008d3e9ac9d67986be17e35c97fe9fde9a44 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 */