diff --git a/FEATURES/143970 b/FEATURES/143970
new file mode 100644
index 0000000000000000000000000000000000000000..d0aaee65f40ef1bcd82be7c64bf9d0cd665b5ee3
--- /dev/null
+++ b/FEATURES/143970
@@ -0,0 +1,10 @@
+        '143970' =>
+            ['Label' => $this->_('Récupération des prêts PNB avec Baobab (drm LCP)'),
+             'Desc' => 'Synchronisation des prêts PNB avec Baobab',
+             'Image' => '',
+             'Video' => '',
+             'Category' => '',
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => 'https://wiki.bokeh-library-portal.org/index.php?title=PNB_Baobab',
+             'Test' => '',
+             'Date' => '2022-06-23'],
\ No newline at end of file
diff --git a/VERSIONS_WIP/143970 b/VERSIONS_WIP/143970
new file mode 100644
index 0000000000000000000000000000000000000000..907349a680174ee90c23ca005c72bfa737e9c975
--- /dev/null
+++ b/VERSIONS_WIP/143970
@@ -0,0 +1 @@
+ - fonctionnalité #143970 : Implémentation de l'API-APP de Dilicom permettant de consulter ses  prêts PNB sur l'appi Baobab, uniformisation et configuration des clients OAuth. Administration: journalisation des authentifications CAS et OAuth
\ No newline at end of file
diff --git a/application/modules/admin/controllers/IdentityClientsController.php b/application/modules/admin/controllers/IdentityClientsController.php
new file mode 100644
index 0000000000000000000000000000000000000000..42a6b0d5a5404512c755747b0fef0fad86537a84
--- /dev/null
+++ b/application/modules/admin/controllers/IdentityClientsController.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, 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 Admin_IdentityClientsController extends ZendAfi_Controller_Action {
+  public function getPlugins() {
+    return [ZendAfi_Controller_Plugin_ResourceDefinition_IdentityClient::class,
+            ZendAfi_Controller_Plugin_Manager_IdentityClient::class];
+  }
+
+  public function indexAction() {
+    parent::indexAction();
+
+    $this->view->titre = $this->_('Clients d\'identité');
+  }
+}
+?>
diff --git a/application/modules/admin/controllers/IdentityProvidersController.php b/application/modules/admin/controllers/IdentityProvidersController.php
index 8e51050a3d9306a9c1a5ee630dec912c14d36ad9..8f42f0881378b477bd027427022f68b5009e72f0 100644
--- a/application/modules/admin/controllers/IdentityProvidersController.php
+++ b/application/modules/admin/controllers/IdentityProvidersController.php
@@ -30,5 +30,10 @@ class Admin_IdentityProvidersController extends ZendAfi_Controller_Action {
     parent::indexAction();
     $this->view->titre = $this->_('Fournisseurs d\'identité');
   }
+
+  public function federationAction() {
+    parent::indexAction();
+    $this->view->titre =  $this->_('Fédération d\'identités');
+  }
 }
-?>
\ No newline at end of file
+?>
diff --git a/application/modules/admin/controllers/IndexController.php b/application/modules/admin/controllers/IndexController.php
index d9ac1a0a5f25ff50d5ad68dca570d2464052c25d..2c5c1278b8504ea8b79da9e35e534ac7d77f30a6 100644
--- a/application/modules/admin/controllers/IndexController.php
+++ b/application/modules/admin/controllers/IndexController.php
@@ -91,6 +91,24 @@ class Admin_IndexController extends ZendAfi_Controller_Action {
   }
 
 
+  public function adminvarSetAction() {
+    if(!$id = $this->_getParam('cle')) {
+      $this->_helper->notify($this->_('Veuillez renseigner le paramètre "cle".'));
+      return $this->_redirectClose($this->_getReferer());
+    }
+
+    if(!$var = Class_AdminVar::find($id)) {
+      $this->_helper->notify($this->_('La clé "%s" n\'existe pas.', $id));
+      return $this->_redirectClose($this->_getReferer());
+    }
+
+    $this->_saveVariable($var,  $this->_getParam('valeur'));
+
+    $this->view->admin_var = $var;
+    $this->_redirect($this->_getReferer());
+  }
+
+
   protected function _saveVariable($var, $new_value) {
     $var->setValeur($new_value);
     $id = $var->getId();
@@ -101,7 +119,6 @@ class Admin_IndexController extends ZendAfi_Controller_Action {
         ? $this->_redirect($url)
         : $this->_redirectClose($url);
     }
-
     $this->view->form->getElement('valeur')->addErrors($var->getErrors());
     $this->_helper
       ->notify($this->_('Erreur(s) : %s, variable %s NON sauvegardée',
diff --git a/application/modules/admin/controllers/UserApiTokensController.php b/application/modules/admin/controllers/UserApiTokensController.php
new file mode 100644
index 0000000000000000000000000000000000000000..13be1bc2c46c4bb4bddec9ece32967e4b8cdca2b
--- /dev/null
+++ b/application/modules/admin/controllers/UserApiTokensController.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Admin_UserApiTokensController extends ZendAfi_Controller_Action {
+  public function getPlugins() {
+    return [ZendAfi_Controller_Plugin_ResourceDefinition_UserApiTokens::class,
+            ZendAfi_Controller_Plugin_Manager_UserApiTokens::class];
+  }
+
+
+  public function showAction() {
+    $this->view->tokens = Class_User_ApiToken::findAllBy([ 'client_id' => $this->_getParam('client_id', 0)]);
+  }
+
+
+  public function indexAction() {
+    $this->view->tokens = Class_User_ApiToken::findAllBy([ 'order' => 'id desc' , 'limit' => 200 ]);
+  }
+
+
+  public function deleteAllAction() {
+    Class_User_ApiToken::deleteBy(['client_id' =>$this->_getParam('client_id')]);
+    $this->_redirectToIndex();
+  }
+}
diff --git a/application/modules/admin/views/scripts/identity-clients/add.phtml b/application/modules/admin/views/scripts/identity-clients/add.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..c52ca489f905555642d9f8210fc3130ba4abfdcd
--- /dev/null
+++ b/application/modules/admin/views/scripts/identity-clients/add.phtml
@@ -0,0 +1 @@
+<?php echo $this->renderForm($this->form); ?>
diff --git a/application/modules/admin/views/scripts/identity-clients/edit.phtml b/application/modules/admin/views/scripts/identity-clients/edit.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..c52ca489f905555642d9f8210fc3130ba4abfdcd
--- /dev/null
+++ b/application/modules/admin/views/scripts/identity-clients/edit.phtml
@@ -0,0 +1 @@
+<?php echo $this->renderForm($this->form); ?>
diff --git a/application/modules/admin/views/scripts/identity-clients/index.phtml b/application/modules/admin/views/scripts/identity-clients/index.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..9e548b16f29b3c816ddb9fd4e3827ae7f2077e25
--- /dev/null
+++ b/application/modules/admin/views/scripts/identity-clients/index.phtml
@@ -0,0 +1,33 @@
+<?php
+
+echo $this->Button_New((new Class_Button)
+                       ->setText($this->_('Ajouter un client d\'identité')));
+
+
+echo $this->Button((new Class_Button)
+                   ->setText($this->_('Voir les fournisseurs d\'identité'))
+                   ->setUrl($this->url(['controller' =>'identity-providers','action' => 'index'])));
+
+
+echo  $this->Button_ActivationLog();
+
+if ( Class_AdminVar::get( 'ACTIVATE_AUTH_LOG'))
+  echo $this->button((new Class_Button)
+                     ->setText('Voir logs d\'authentification')
+                     ->setUrl($this->url(['controller' =>'journal','action' => 'index',
+                                          'order' => 'created_at+desc',
+                                          'search_type'  =>
+                                          Class_Journal_OauthRequestType::MY_TYPE])));
+
+echo $this
+  ->renderTable((new Class_TableDescription('servers'))
+                ->addColumn($this->_('Libellé'), ['attribute' => 'label'])
+                ->addColumn($this->_('Actif ?'),
+                            function($model)
+                            {
+                              return $model->getActive()
+                                ? $this->_('Oui')
+                                : $this->_('Non');
+                            })
+                ->addRowAction(function($model) { return $this->renderPluginsActions($model); }),
+                $this->servers);
diff --git a/application/modules/admin/views/scripts/identity-providers/index.phtml b/application/modules/admin/views/scripts/identity-providers/index.phtml
index 0ff46700631a4d627cb53de06d30998e1294b7b9..ae9e4305656cb72d5d8c597361adaec9fc74679e 100644
--- a/application/modules/admin/views/scripts/identity-providers/index.phtml
+++ b/application/modules/admin/views/scripts/identity-providers/index.phtml
@@ -3,6 +3,17 @@
 echo $this->Button_New((new Class_Button)
                        ->setText($this->_('Ajouter un fournisseur d\'identité')));
 
+echo $this->Button((new Class_Button)
+                   ->setText($this->_('Voir les clients d\'identité'))
+                   ->setUrl($this->url(['controller' =>'identity-clients','action' => 'index'])));
+
+echo $this->button((new Class_Button)
+                   ->setText('Voir logs d\'authentification')
+                   ->setUrl($this->url(['controller' =>'journal','action' => 'index',
+                                        'order' => 'created_at+desc',
+                                        'search_type'  =>
+                                       Class_Journal_CasRequestType::MY_TYPE])));
+
 echo $this
   ->renderTable((new Class_TableDescription('providers'))
                 ->addColumn($this->_('Libellé'), ['attribute' => 'label'])
diff --git a/application/modules/admin/views/scripts/journal/index.phtml b/application/modules/admin/views/scripts/journal/index.phtml
index 61b0c6545a8243035b861bd65379c05c53590f28..fb4561599eee8152b4735484640d9a0f64b7e7d4 100644
--- a/application/modules/admin/views/scripts/journal/index.phtml
+++ b/application/modules/admin/views/scripts/journal/index.phtml
@@ -1,2 +1,5 @@
 <?php
+
+echo  $this->Button_ActivationLog();
+
 echo $this->searchJournal($this->search);
diff --git a/application/modules/admin/views/scripts/user-api-tokens/index.phtml b/application/modules/admin/views/scripts/user-api-tokens/index.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..d3dca92569dcc7118b9da3df166dcd937795e7e9
--- /dev/null
+++ b/application/modules/admin/views/scripts/user-api-tokens/index.phtml
@@ -0,0 +1,16 @@
+<?php
+
+
+echo $this->button((new Class_Button)
+                   ->setText('Voir logs d\'authentification')
+                   ->setUrl($this->url(['controller' =>'user-api-tokens','action' => 'logs'])));
+
+echo $this
+  ->renderTable((new Class_TableDescription('providers'))
+                ->addColumn($this->_('User'), ['attribute' => 'user_name'])
+                ->addColumn($this->_('Créé le'), ['attribute' => 'created_at'])
+                ->addColumn($this->_('Token'), ['attribute' => 'token'])
+                ->addColumn($this->_('Date d\'expiration'), ['attribute' => 'expired_at'])
+                ->addColumn($this->_('Refresh token'), ['attribute' => 'refresh_token'])
+                ->addRowAction(function($model) { return $this->renderPluginsActions($model); }),
+                $this->tokens);
diff --git a/application/modules/admin/views/scripts/user-api-tokens/logs.phtml b/application/modules/admin/views/scripts/user-api-tokens/logs.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..381115b30428ca08a2553a172970177a32be8fca
--- /dev/null
+++ b/application/modules/admin/views/scripts/user-api-tokens/logs.phtml
@@ -0,0 +1,25 @@
+<?php
+$val = '1';
+$text = $this->_( 'Activer les logs');
+
+if ( Class_AdminVar::get( 'ACTIVATE_AUTH_LOG')) {
+$val = '0';
+$text = $this->_( 'Desactiver les logs');
+}
+
+echo $this->button((new Class_Button)
+  ->setText($text)
+  ->setUrl($this->url(['module' => 'admin',
+                         'controller' => 'index',
+                         'action' => 'adminvar_set',
+                         'cle' => 'ACTIVATE_AUTH_LOG',
+                         'valeur'  => $val,
+                         'redirect'  =>'/admin/user-api-tokens/logs'])));
+
+
+echo $this->button((new Class_Button)
+  ->setText( $this->_( 'Supprimer les logs'))
+  ->setUrl($this->url(['do' => 'clean'])));
+echo '<br/>';
+
+echo '<code>'. $this->log.'</code>';
diff --git a/application/modules/admin/views/scripts/user-api-tokens/show.phtml b/application/modules/admin/views/scripts/user-api-tokens/show.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..3d6d738b1792a13bc4fc4fcb80204558a651aeef
--- /dev/null
+++ b/application/modules/admin/views/scripts/user-api-tokens/show.phtml
@@ -0,0 +1,18 @@
+<?php
+
+
+echo $this->button((new Class_Button)
+                   ->setText('Voir logs d\'authentification')
+                   ->setUrl($this->url(['controller' =>'journal','action' => 'index',
+                                        'order' => 'created_at+desc',
+                                        'search_type'  =>
+                                        Class_Journal_CasRequestType::MY_TYPE])));
+echo $this
+  ->renderTable((new Class_TableDescription('providers'))
+                ->addColumn($this->_('User'), ['attribute' => 'user_name'])
+                ->addColumn($this->_('Créé le'), ['attribute' => 'created_at'])
+                ->addColumn($this->_('Token'), ['attribute' => 'token'])
+                ->addColumn($this->_('Date d\'expiration'), ['attribute' => 'expired_at'])
+                ->addColumn($this->_('Refresh token'), ['attribute' => 'refresh_token'])
+                ->addRowAction(function($model) { return $this->renderPluginsActions($model); }),
+                $this->tokens);
diff --git a/application/modules/api/controllers/CatalogController.php b/application/modules/api/controllers/CatalogController.php
index 2ce045a0e47c34f537fbdedd8587aa80caa9d657..a27ef30540a2e51223590b747e31658317a5fe2e 100644
--- a/application/modules/api/controllers/CatalogController.php
+++ b/application/modules/api/controllers/CatalogController.php
@@ -21,6 +21,20 @@
 
 
 class Api_CatalogController extends ZendAfi_Controller_Action {
+
+  public function discoverAction() {
+    $this->_helper->json([
+                          'infos' => [ 'mail' => 'dev-opac@afi-sa.fr',
+                                      'company' => 'AFI'],
+                          'authentication' => [
+                                               'get_token' => Class_Url::absolute('/auth/oauth'),
+                                               'refresh_token'  => Class_Url::absolute('/auth/refresh')],
+                          'resources' => [['code'=> 'loans',
+                                           'endpoint' => Class_Url::absolute('/api/user/pnbloans'),
+                                           'version' =>'1']]]);
+  }
+
+
   public function itemAction() {
     if (!$barcode = $this->_getParam('barcode'))
       return $this->_helper->throwHTTPError($this->_('Paramètre barcode obligatoire'), 403);
@@ -31,4 +45,4 @@ class Api_CatalogController extends ZendAfi_Controller_Action {
 
     return $this->_helper->json($this->view->item($item));
   }
-}
\ No newline at end of file
+}
diff --git a/application/modules/api/controllers/ErrorController.php b/application/modules/api/controllers/ErrorController.php
index 13821b4bec0aed5126c92ee988f1e88865636829..ce3a623e1642605958115534459199648c54fb5d 100644
--- a/application/modules/api/controllers/ErrorController.php
+++ b/application/modules/api/controllers/ErrorController.php
@@ -22,10 +22,13 @@
 class Api_ErrorController extends Zend_Controller_Action {
   public function errorAction() {
     $errors = $this->_getParam('error_handler');
+    $json =['error'   => 'invalid_request',
+            'message' => $errors->exception->getMessage(),
+            'error_description' => $errors->exception->getMessage()
+    ];
 
-    $this->_helper->json(['error'   => 'invalid_request',
-                          'message' => $errors->exception->getMessage()]);
-
+    $this->_helper->json($json);
+    Class_Journal_RequestType::createWith( $this, json_encode($json), $errors->exception->getCode());
     $this->_response->setHttpResponseCode($errors->exception->getCode());
   }
 }
diff --git a/application/modules/api/controllers/UserController.php b/application/modules/api/controllers/UserController.php
index 416abf4d8c7d88252b5dcfa9e4cd0e1c4f38c61f..62eee011d02b630e82ae7adcc0be581eec176f70 100644
--- a/application/modules/api/controllers/UserController.php
+++ b/application/modules/api/controllers/UserController.php
@@ -23,6 +23,7 @@
 class Api_UserController extends ZendAfi_Controller_Action {
   public function preDispatch() {
     parent::preDispatch();
+
     if (!Class_Users::hasIdentity())
       $this->_authenticate();
   }
@@ -30,23 +31,49 @@ class Api_UserController extends ZendAfi_Controller_Action {
 
   public function accountAction() {
     $user = Class_Users::getIdentity();
+    $json = ['account' => ['label' => $user->getNomAff(),
+                           'login' => $user->getLogin(),
+                           'card' => ['id'=> $user->getIdabon(),
+                                      'expire_at' => $user->getDateFin()]
+      ]];
+
+    Class_Journal_RequestType::createWith($this, json_encode($json));
+
     $this->_helper
-      ->json(['account' => ['label' => $user->getNomAff(),
-                            'login' => $user->getLogin(),
-                            'card' => ['id'=> $user->getIdabon(),
-                                       'expire_at' => $user->getDateFin()]
-              ]]);
+      ->json($json);
   }
 
 
   public function loansAction() {
     $this->_clearUserCache();
-    $this->view->loans = $this->_userCards()->getLoans();
+    $this->_helper->viewRenderer->setNoRender();
+    $this->getResponse()->setHeader('Content-Type', 'application/json; charset=utf-8');
+
+    $json = '{ "loans":'.$this->view->loans($this->_userCards()->getLoans()).'}';
+    Class_Journal_RequestType::createWith($this, $json);
+    $this->getResponse()->setBody($json);
+  }
+
+
+  public function pnbloansAction() {
+
+    $this->_helper->viewRenderer->setNoRender();
+    $this->getResponse()->setHeader('Content-Type', 'application/json; charset=utf-8');
+
+    $this->_clearUserCache();
+    $this->view->loans = $this->_userCards()->getPNBLoans();
+    $json='{ "loans":'.$this->view->pnbLoans($this->view->loans).'}';
+    Class_Journal_RequestType::createWith($this, $json);
+    $this->getResponse()->setBody($json);
   }
 
 
   public function holdsAction() {
-    $this->view->holds = $this->_userCards()->getHolds();
+    $this->_helper->viewRenderer->setNoRender();
+    $this->getResponse()->setHeader('Content-Type', 'application/json; charset=utf-8');
+    $json='{ "holds":'.$this->view->holds($this->_userCards()->getHolds()).'}';
+    Class_Journal_RequestType::createWith($this, $json);
+    $this->getResponse()->setBody($json);
   }
 
 
@@ -56,18 +83,23 @@ class Api_UserController extends ZendAfi_Controller_Action {
 
     $status = $cards->renewLoan($loan_id);
 
-    if ($status['statut'] == false)
-      return $this->_helper->json(['status' => 'error',
-                                   'error' => $status['erreur']]);
+    if ($status['statut'] == false){
+      $json = ['status' => 'error',
+       'error' => $status['erreur']];
+      Class_Journal_RequestType::createWith( $this, json_encode($json), '404');
 
+      return $this->_helper->json($json);
+    }
     $loan = $cards->getLoans()
                   ->detect(function($loan) use ($loan_id)
                            {
                              return $loan->getId() == $loan_id;
                            });
+    $json = ['status' => 'renewed',
+            'date_due' => $loan->getDateRetourISO8601()];
+    Class_Journal_RequestType::createWith( $this, json_encode($json));
 
-    return $this->_helper->json(['status' => 'renewed',
-                                 'date_due' => $loan->getDateRetourISO8601()]);
+    return $this->_helper->json($json);
   }
 
 
@@ -95,6 +127,7 @@ class Api_UserController extends ZendAfi_Controller_Action {
 
 
   protected function _authenticate() {
+
     if (Class_AdminVar_OAuthAcceptHTTP::shouldRejectRequest($this->_request))
       return $this->_helper->throwHTTPError($this->_('Protocole HTTPS obligatoire'), 403);
 
@@ -108,6 +141,10 @@ class Api_UserController extends ZendAfi_Controller_Action {
     if (!$token = Class_User_ApiToken::findFirstBy(['token' => $parts[1]]))
       return $this->_helper->throwHTTPError($this->_('Jeton d\'autorisation invalide'), 403);
 
+    if ($token->isExpired()) {
+      return $this->_helper->throwHTTPError(implode(',',array_unique($token->getErrors())), 403);
+    }
+
     if (!$user = $token->getUser())
       return $this->_helper->throwHTTPError($this->_('Utilisateur non trouvé'), 403);
 
diff --git a/application/modules/api/views/scripts/user/holds.pjson b/application/modules/api/views/scripts/user/holds.pjson
deleted file mode 100644
index 87732946fb1170d8d50fbf7a5b3c62d234ebddb1..0000000000000000000000000000000000000000
--- a/application/modules/api/views/scripts/user/holds.pjson
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-	"holds": <?php echo $this->holds($this->holds) ?>
-}
diff --git a/application/modules/api/views/scripts/user/loans.pjson b/application/modules/api/views/scripts/user/loans.pjson
deleted file mode 100644
index 94d1684f134585f6fea7cbd946598ebe163f13d1..0000000000000000000000000000000000000000
--- a/application/modules/api/views/scripts/user/loans.pjson
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-	"loans": <?php echo $this->loans($this->loans) ?>
-}
diff --git a/application/modules/opac/controllers/AuthController.php b/application/modules/opac/controllers/AuthController.php
index d38cbea151628a3dafbbd87c257d697ed7e1844a..3be3ae257e6c31aa12fbdf0b03cea261e992c963 100644
--- a/application/modules/opac/controllers/AuthController.php
+++ b/application/modules/opac/controllers/AuthController.php
@@ -236,13 +236,20 @@ class AuthController extends ZendAfi_Controller_Action {
 
   public function oauthAction() {
     $validator = new ZendAfi_Validate_Url();
-    if (('code' !== $this->_getParam('response_type'))
-        || !$this->_getParam('client_id')
-        || !$this->_getParam('redirect_uri')) {
 
-      throw new Zend_Controller_Action_Exception($this->view->_('Désolé, requête incomplète'), 400);
+    if (!$client_id = $this->_getParam('client_id'))
+      throw $this->_helper->throwHTTPError($this->view->_('Désolé, requête incomplète'), 400);
+
+    if (!$server = Class_IdentityClient::findFirstBy(['client_id'  => $client_id]))
+      throw $this->_helper->throwHTTPError($this->view->_('Désolé,  aucune configuration correspondante pour '.$client_id), 400);
+
+    if (('code' == $this->_getParam('response_type'))
+        && ( !$this->_getParam('redirect_uri'))) {
+      Class_Journal_RequestType::createWith( $this, "Requete incomplete, missing redirect_uri", 400);
+      throw $this->_helper->throwHTTPError($this->view->_('Désolé, requête incomplète'), 400);
     }
 
+
     $this->view->titre = $this->_('Authentifiez-vous pour autoriser "%s" à accéder à votre compte',
                                   $this->_getParam('client_id'));
     $preferences = Class_Profil::getCurrentProfil()->getCfgModulesPreferences('auth', 'login');
@@ -257,6 +264,19 @@ class AuthController extends ZendAfi_Controller_Action {
     $redirect_uri = $this->_getParam('redirect_uri');
     $strategy = new Class_Auth_OAuth($this);
     $strategy->processLogin();
+
+  }
+
+  public function  refreshAction() {
+
+    if (!$refresh_token = $this->_getParam('refresh_token')) {
+      $viewRenderer = $this->getHelper('ViewRenderer');
+      $viewRenderer->setNoRender();
+
+      return $this->_helper->sendHttpErrorCode('invalid_request','Missing parameter',400);
+    }
+    $strategy = new Class_Auth_OAuth($this);
+    $strategy->refresh($refresh_token);
   }
 
 
diff --git a/application/modules/opac/controllers/CasServerController.php b/application/modules/opac/controllers/CasServerController.php
index 7b56e703dbddc711244a033e158dc5120c58bd36..02af67ed3febc9f0263f3ee2fa2a6a5b720f21d7 100644
--- a/application/modules/opac/controllers/CasServerController.php
+++ b/application/modules/opac/controllers/CasServerController.php
@@ -64,14 +64,13 @@ class CasServerController extends ZendAfi_Controller_Action {
     $this->getHelper('ViewRenderer')->setNoRender();
     $service = $this->_getParam('service');
     $ticket = $this->_getParam('ticket');
-
     if (!$ticket || !$service) {
       return $this->returnFailureTicketResponse(Class_CasTicket::CODE_INVALID_REQUEST);
     }
 
     $cas_ticket = $this->_getCasTicket($service);
     return ($user = $cas_ticket->userForTicket($ticket))
-      ?  $this->returnValidTicketResponse($user, $ticket)
+      ? $this->returnValidTicketResponse($user, $ticket)
       : $this->returnFailureTicketResponse($cas_ticket->getErrorCode(), $ticket, $cas_ticket->getService());
   }
 
@@ -124,6 +123,7 @@ class CasServerController extends ZendAfi_Controller_Action {
 
   public function loginAction() {
     $this->_addParamsOnRequest();
+    Class_Journal_RequestType::createWith( $this, 'forward to auth/login');
     $this->_forward('login', 'auth');
   }
 
@@ -134,9 +134,11 @@ class CasServerController extends ZendAfi_Controller_Action {
 
 
   public function logoutAction() {
+
     ZendAfi_Auth::getInstance()->clearIdentity();
     if ($url_redirect = $this->_getParam('url'))
       $this->_redirect($url_redirect);
+    Class_Journal_RequestType::createWith( $this, $url_redirect ? 'redirect to '. $url_redirect:'' );
   }
 }
 ?>
diff --git a/application/modules/opac/controllers/UserApiTokensController.php b/application/modules/opac/controllers/UserApiTokensController.php
new file mode 100644
index 0000000000000000000000000000000000000000..0164cb49675eda026b4ca896693f778ac6042c01
--- /dev/null
+++ b/application/modules/opac/controllers/UserApiTokensController.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Admin_UserApiTokensController extends ZendAfi_Controller_Action {
+  public function getPlugins() {
+    return ['ZendAfi_Controller_Plugin_ResourceDefinition_UserApiTokens',
+            'ZendAfi_Controller_Plugin_Manager_UserApiTokens'];
+  }
+
+
+
+}
diff --git a/cosmogramme/sql/patch/patch_440.php b/cosmogramme/sql/patch/patch_440.php
new file mode 100644
index 0000000000000000000000000000000000000000..18df7f8e133acfb097ea4f206d3351a0c2c39943
--- /dev/null
+++ b/cosmogramme/sql/patch/patch_440.php
@@ -0,0 +1,48 @@
+<?php
+$adapter = Zend_Db_Table_Abstract::getDefaultAdapter();
+
+try {
+  $adapter->query('alter table user_api_tokens  add column  refresh_token varchar(255) default ""');
+  $adapter->query('alter table user_api_tokens  add column  created_at datetime default NULL');
+  $adapter->query('alter table user_api_tokens  add column  expired_at datetime default NULL');
+
+
+}
+catch(Exception $e) {
+}
+try{
+
+  $adapter->query('CREATE TABLE if not exists `identity_client` ( '
+                  . 'id int(11) unsigned not null auto_increment,'
+                  . 'type varchar(255),'
+                  . 'client_id varchar(255),'
+                  . 'protocol varchar(255),'
+                  . 'active boolean default true,'
+                  . 'label varchar(255) not null,'
+                  . 'config text,'
+                  . 'primary key (id)'
+                  . ') engine=MyISAM default charset=utf8');
+
+} catch(Exception $e) {
+
+}
+try {
+  $adapter->query('delete from identity_client where client_id="MyBibApp"');
+  $adapter->query('delete from identity_client where type="Class_IdentityClient_Dilicom"');
+  $adapter->query('insert into identity_client (client_id, type, active,label, protocol) values ("MyBibApp","'.Class_IdentityClient_MyBibApp::class.'", true, "MyBibApp", "oauth2")');
+
+  $rows = $adapter->query('select distinct gln from bib_c_site where gln is not null and gln<>"" and gln not in(select distinct client_id from identity_client) ')
+              ->fetchAll();
+  foreach ($rows as $row)
+    $adapter->query('insert into identity_client (client_id, type, active,label, protocol) values ("'.$row["gln"].'","'.Class_IdentityClient_Dilicom::class.'", true, "Dilicom '.$row["gln"].'", "oauth2") ');
+
+
+}
+catch(Exception $e) {
+    var_dump($e);
+}
+try {
+  $adapter->query('update loan_pnb set loan_link=REGEXP_REPLACE(loan_link, \'(\\\?userAgentId\\\=.*$)\', "")');
+
+}catch(Exception $e) {
+}
diff --git a/library/Bluga/Webthumb/Job.php b/library/Bluga/Webthumb/Job.php
index 0c8aa8b7dbefe584fa743a2c61591e1af7faa332..8c2fc0cae2d505d4af1d723cedee9177d79cc6bb 100644
--- a/library/Bluga/Webthumb/Job.php
+++ b/library/Bluga/Webthumb/Job.php
@@ -19,7 +19,7 @@ class Bluga_Webthumb_Job {
 			'excerpt',
 			'notify'
 			));
-		$this->options->setters['url'] = create_function('$url','return trim($url);');
+		$this->options->setters['url'] = fn($url) => trim($url);
 		$this->status = new Bluga_Propertybag(array(
 			'start_time',
 			'end_time',
@@ -66,4 +66,3 @@ class Bluga_Webthumb_Job {
 		return Bluga_Webthumb_Request::prettyPrint($this->render());
 	}
 }
-
diff --git a/library/Class/AdminVar.php b/library/Class/AdminVar.php
index 409cc5b6963229ae95d8fd13127cbf1092197857..e4cbaf7b414c644f5f582a14c8a56610da899208 100644
--- a/library/Class/AdminVar.php
+++ b/library/Class/AdminVar.php
@@ -376,6 +376,7 @@ Pour vous désabonner de la lettre d\'information, merci de cliquer sur le lien
                                                                        . '<br/>'
                                                                        . $this->_('De plus, à la connexion, l\'enregistrement des mots de passes des abonnés est désactivé.')),
             'OAUTH_ACCEPT_HTTP' => Class_AdminVar_Meta::newOnOff($this->_('Autoriser l\'accès aux API OAUTH via HTTP (non sécurisé - déconseillé)'), ['value' => 0]),
+            'ACTIVATE_AUTH_LOG'  => Class_AdminVar_Meta::newOnOff($this->_('Activer les log pour l\'authentification OAUTH ou CAS')),
             'NB_AFFICH_AVIS_PAR_AUTEUR'  => Class_AdminVar_Meta::newDefault($this->_('Nombre d\'avis maximum à afficher par utilisateur.')),
             'REGISTER_OK' => Class_AdminVar_Meta::newEncodedData($this->_('Texte visible par l\'internaute après son inscription.')),
             'RESA_CONDITION' => Class_AdminVar_Meta::newEncodedData($this->_('Texte visible après l\'envoi d\'e-mail de demande de réservation.')),
diff --git a/library/Class/Auth/OAuth.php b/library/Class/Auth/OAuth.php
index fe353a2834e0694531a13b3f9e95654eba08ee2e..5a19ea2fa5fb0cd04debecc634823e88268f492f 100644
--- a/library/Class/Auth/OAuth.php
+++ b/library/Class/Auth/OAuth.php
@@ -21,20 +21,101 @@
 
 
 class Class_Auth_OAuth extends Class_Auth_NotLogged {
-  protected function _handlePost() {
-    parent::_handlePost();
 
-    if (!$user = Class_Users::getIdentity())
-      return function() {};
+  public function refresh(string $refresh_token):self {
+    $token = Class_User_ApiToken::refreshToken($refresh_token);
+    if ($token->hasErrors()){
+      $this->disable_redirect = true;
+      $this->controller->getHelperBroker()->sendHttpErrorCode('invalid_request',
+                                                              implode(',',array_unique($token->getErrors())),401);
+      return $this;
+    }
+
+    $json = [
+             'access_token' => $token->getToken(),
+             'expires_in' => $token->getExpiresIn(),
+             'token_type' => 'Bearer',
+             'refresh_token' => $token->getRefreshToken()];
+    Class_Journal_RequestType::createWith( $this->controller, json_encode($json));
+    $this->controller->getHelperBroker()->json($json);
+
+    return $this;
+  }
+
+
+  protected function _handlePostWithAuth() {
 
+    $response = $this->controller->getResponse();
+    $view = $this->controller->view;
     $request = $this->controller->getRequest();
 
-    $token = Class_User_ApiToken::findOrCreateForUserAndApplication($user,
+    $login = $request->getPost('username');
+    $password = $request->getPost('password');
+
+    if ( !ZendAfi_Auth::getInstance()->authenticateLoginPassword($login, $password) ) {
+      $this->controller->getHelperBroker()->sendHttpErrorCode('invalid_grant','Authentication failure', 400);
+      return function()  {
+      };
+    }
+    $user = Class_Users::getIdentity();
+    $token = Class_User_ApiToken::createForUserAndApplication($user,
                                                                     $request->getParam('client_id'));
+    if ($token->hasErrors()){
+      $this->disable_redirect = true;
+      $this->controller->getHelperBroker()->sendHttpErrorCode('invalid_request',implode(',',array_unique($token->getErrors())),401);
+      return function()  {
+      };
+    }
+
+    $json =['access_token' => $token->getToken(),
+            'expires_in' => '86400',
+            'token_type' => 'Bearer',
+            'refresh_token' => $token->getRefreshToken()];
+
+    $this->disable_redirect = true;
+    Class_Journal_RequestType::createWith( $this->controller, json_encode($json));
+    $this->controller->getHelperBroker()->json($json);
+
+    return function() {};
+  }
+
+
+  protected function _handlePost(){
+    parent::_handlePost();
+    $request = $this->controller->getRequest();
+
+    if ($request->getParam('response_type') != 'code')
+      return $this->_handlePostWithAuth();
+
+    if (!$user = Class_Users::getIdentity()) {
+      Class_Journal_RequestType::createWith( $this->controller, "Authentication failed, wrong identity");
+      $this->disable_redirect = true;
+      return function() {};
+    }
+    $redirect_uri = $request->getParam('redirect_uri', '');
+
+    $token = Class_User_ApiToken::createForUserAndApplication($user,
+                                                                    $request->getParam('client_id'));
+
+    $token->checkRedirectUri($redirect_uri);
+    if ($token->hasErrors()){
+      $this->disable_redirect = true;
+      $this->controller->getHelperBroker()->sendHttpErrorCode('invalid_request',
+                                                              implode(',',array_unique($token->getErrors())),401);
+      return function() {};
+    }
+
     $this->redirect_url = sprintf('%s#token=%s',
-                                  $request->getParam('redirect_uri'),
+                                  $redirect_uri,
                                   $token->getToken());
 
+    if (substr($redirect_uri,0,4)  == 'http')
+      $this->redirect_url = sprintf('%s?token=%s',
+                                    $redirect_uri,
+                                    $token->getToken());
+
+    Class_Journal_RequestType::createWith( $this, "Authentication successful, redirected to :".$this->redirect_url);
     return function() {};
   }
+
 }
diff --git a/library/Class/IdentityClient.php b/library/Class/IdentityClient.php
new file mode 100644
index 0000000000000000000000000000000000000000..5f91232020f8bb18fd52ab2f59616f62fcf8a3c4
--- /dev/null
+++ b/library/Class/IdentityClient.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, 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 wxbill 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 IdentityClientLoader extends Storm_Model_Loader {
+  protected $_actives;
+
+  public function findAllActiveServers() {
+    return Class_IdentityClient::findAllBy(['active' => 1]);
+  }
+}
+
+
+
+
+class Class_IdentityClient extends Storm_Model_Abstract{
+  use Trait_Translator, Trait_LastMessage;
+
+  protected
+    $_table_name = 'identity_client',
+    $_loader_class = IdentityClientLoader::class,
+    $_default_attribute_values = ['label' => '',
+                                  'config' => '',
+                                  'client_id'=>'',
+                                  'active' => true,
+                                  'type' => '',
+                                  'protocol' => 'oauth2'],
+
+    $_has_many = ['user_api_tokens' => ['model' => Class_User_ApiToken::class,
+                                        'role' => 'client',
+                                        'dependents' => 'delete'] ],
+
+    $_config_fields = [
+                       'client_secret',
+                       'redirect_uri',
+                       'allow_refresh_token',
+                       'token_expires_in',
+                       'disable_state',
+                       'disable_response_type',
+                       'scope'],
+
+    $_config_as_array,
+    $_context,
+    $_identity_client_connector;
+
+  public function validate() {
+    $this->checkAttribute('client_id',
+                          $this->hasClientId() ,
+                          $this->_('Identifiant client est obligatoire'));
+  }
+
+
+  public function validateRequest($request) {
+  }
+
+
+  public function getLibelle() {
+    return $this->getLabel();
+  }
+
+
+  public function getConnector() {
+    return isset($this->_identity_client_connector)
+      ? $this->_identity_client_connector
+      : $this->_identity_client_connector = (new Class_IdentityClient_Types)->newForServer($this);
+
+    return new Class_IdentityClient_OAuth2;
+  }
+
+
+  public function acceptVisitor($visitor) {
+    foreach($this->getConfigAsArray() as $key => $value)
+      $visitor->visitParam($key, $value);
+
+    return $this;
+  }
+
+
+  public function _get($key) {
+    if (in_array($key, $this->_config_fields))
+      return $this->getConnector()->getParam($key);
+    return parent::_get($key);
+  }
+
+
+  public function _set($key, $value) {
+    if (in_array($key, $this->_config_fields))
+      return $this->setConfigValue($key, $value);
+    return parent::_set($key, $value);
+  }
+
+
+  public function setConfigValue($key,$value) {
+    $config = $this->getConfigAsArray();
+    $config[$key] = $value;
+    $this->setConfig(json_encode($config));
+    $this->_config_as_array = $config;
+    return $this;
+  }
+
+
+  public function getConfigValue($key) {
+    $config = $this->getConfigAsArray();
+    return isset($config[$key])
+      ? $config[$key]
+      : '';
+  }
+
+
+  public function getConfigAsArray() {
+    if (null !== $this->_config_as_array)
+      return $this->_config_as_array;
+
+    $config = json_decode($this->getConfig(), true);
+    return $this->_config_as_array = $config ? $config : [];
+  }
+
+
+  public function setLoginSuccessRedirectUrl($url) {
+    $this->getConnector()->setLoginSuccessRedirectUrl($url);
+    return $this;
+  }
+}
diff --git a/library/Class/IdentityClient/Dilicom.php b/library/Class/IdentityClient/Dilicom.php
new file mode 100644
index 0000000000000000000000000000000000000000..f29f85431a07de1c93684fc29502e587652829de
--- /dev/null
+++ b/library/Class/IdentityClient/Dilicom.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_IdentityClient_Dilicom extends Class_IdentityClient_OAuth2 {
+  protected $_service_class = Class_Auth_OAuth::class;
+
+  protected  $_config = ['client_secret'=> '',
+                         'redirect_uri'=> '',
+                         'allow_refresh_token'=> true ,
+                         'token_expires_in' => '86400',
+                         'disable_state'   => true,
+                         'disable_response_type' => true];
+
+
+}
diff --git a/library/Class/IdentityClient/MyBibApp.php b/library/Class/IdentityClient/MyBibApp.php
new file mode 100644
index 0000000000000000000000000000000000000000..16baf26fe8425671c2844b58ab4c8c7a7505dd48
--- /dev/null
+++ b/library/Class/IdentityClient/MyBibApp.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_IdentityClient_MyBibApp extends Class_IdentityClient_OAuth2 {
+  protected $_service_class = Class_Auth_OAuth::class;
+
+  protected  $_config = ['client_secret'=> '',
+                         'redirect_url'=> 'bokeh://authorize',
+                         'allow_refresh_token'=> false,
+                         'token_expires_in' => 0,
+                         'disable_state'   => true,
+                         'disable_response_type' => true];
+
+
+}
diff --git a/library/Class/IdentityClient/OAuth2.php b/library/Class/IdentityClient/OAuth2.php
new file mode 100644
index 0000000000000000000000000000000000000000..e93418d2d22c596a43ddfd757f84575bc8692d38
--- /dev/null
+++ b/library/Class/IdentityClient/OAuth2.php
@@ -0,0 +1,61 @@
+<?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_IdentityClient_OAuth2 {
+  protected $_service_class = Class_Auth_OAuth::class;
+
+  public function __construct($client) {
+    $this->_client = $client;
+    $this->_client->acceptVisitor($this);
+  }
+
+  public function getType() {
+    return 'oauth';
+  }
+
+  public function visitParam($key, $value) {
+    if ($value)
+      $this->_config[$key] = $value;
+
+    return $this;
+  }
+
+
+  public function getParam($key) {
+    return isset($this->_config[$key])
+      ? $this->_config[$key]
+      : '';
+  }
+
+
+  public function getService() {
+    $class_name = $this->_service_class;
+
+    return $this->_service
+      ? $this->_service
+      : $this->_service = new $class_name($this->_client);
+  }
+
+  public function setLoginSuccessRedirectUrl($url) {
+    return $this;
+  }
+}
diff --git a/library/Class/IdentityClient/Types.php b/library/Class/IdentityClient/Types.php
new file mode 100644
index 0000000000000000000000000000000000000000..3f8b4d1603eca03f1f0187b07ceb4cf21d49c783
--- /dev/null
+++ b/library/Class/IdentityClient/Types.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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_IdentityClient_Types {
+  use Trait_Translator;
+
+  const
+    OAUTH = Class_IdentityClient_OAuth2::class,
+    MYBIBAPP = Class_IdentityClient_MyBibApp::class,
+    DILICOM = Class_IdentityClient_Dilicom::class;
+
+
+  protected $_services;
+
+
+  public function asMultiOptions() {
+    return [static::OAUTH => $this->_('OAuth 2'),
+            static::MYBIBAPP => $this->_('MyBibApp OAuth'),
+            static::DILICOM => $this->_('Dilicom Baobab OAuth')];
+  }
+
+
+  public function newForServer($server) {
+    $class_name = $server->getType();
+
+    return class_exists($class_name)
+      ? new $class_name($server)
+      : new Class_IdentityClient_OAuth2($server);
+  }
+
+}
diff --git a/library/Class/Journal/CasRequestType.php b/library/Class/Journal/CasRequestType.php
new file mode 100644
index 0000000000000000000000000000000000000000..cca94268f152dcb9b4cee72e11b3a12a8979aebe
--- /dev/null
+++ b/library/Class/Journal/CasRequestType.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Copyright (c) 2022, 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_Journal_CasRequestType extends Class_Journal_RequestType {
+  const
+    MY_TYPE = 'CAS_CALL';
+
+}
diff --git a/library/Class/Journal/DetailType.php b/library/Class/Journal/DetailType.php
index 2bd9d1acb83a320bc048188dbff137eec9299d71..fd9746283ef53c9a0153ca4aa81fec908a5a7488 100644
--- a/library/Class/Journal/DetailType.php
+++ b/library/Class/Journal/DetailType.php
@@ -201,6 +201,16 @@ class Class_Journal_DetailType_NewValue extends Class_Journal_DetailType {
 
 
 
+class Class_Journal_DetailType_BokehResponse extends Class_Journal_DetailType {
+  public function renderValueOn(Zend_View_Interface $view) : string {
+    return  htmlentities($this->_detail->getValue());
+  }
+
+}
+
+
+
+
 class Class_Journal_DetailType_PreviousValue extends Class_Journal_DetailType {
   public function getTypeLabel() : string {
     return $this->_('Ancienne valeur');
diff --git a/library/Class/Journal/OauthRequestType.php b/library/Class/Journal/OauthRequestType.php
new file mode 100644
index 0000000000000000000000000000000000000000..cec938c88dd6f87ce188fb862b09812fe70fd597
--- /dev/null
+++ b/library/Class/Journal/OauthRequestType.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Copyright (c) 2022, 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_Journal_OauthRequestType extends Class_Journal_RequestType {
+  const
+    MY_TYPE = 'OAUTH_CALL';
+}
diff --git a/library/Class/Journal/RequestType.php b/library/Class/Journal/RequestType.php
new file mode 100644
index 0000000000000000000000000000000000000000..52e6647bf2ca2746da3d75d0896506987d8ee2be
--- /dev/null
+++ b/library/Class/Journal/RequestType.php
@@ -0,0 +1,128 @@
+<?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_Journal_RequestType extends Class_Journal_Type {
+
+  const
+    MY_TYPE = 'REQUEST_CALL',
+    BOKEH_RESPONSE = 'bokeh_response',
+    HTTP_RESPONSE_CODE='Http response code',
+    CLIENT_REQUEST = 'Client request',
+    REQUEST_URL = 'Request url';
+
+  public static function createWith( $controller, $response, $response_code = 200) {
+    if ( !Class_AdminVar::get( 'ACTIVATE_AUTH_LOG'))
+      return;
+    $model = new Class_Journal_RequestResponse($controller, $response, $response_code);
+    $classname  = $model->getType();
+    $journal = new $classname((new Class_Journal));
+    $journal->save($model);
+    return $journal;
+  }
+
+
+  /**
+   * @param $model Zend_Controller_Action
+   */
+  public function save($model) {
+    if (!parent::save($model))
+      return false;
+    $request = $model->getRequest();
+
+    $details = ['Request uri :' =>    $request->getRequestUri(),
+                'Header Authorization' => $request->getHeader('Authorization'),
+                'Header Content-Type' => $request->getHeader('Content_type')];
+
+    if (!empty($request->getPost()))
+      $details = array_merge($details,[ 'With params: ' => $request->getPost()]);
+    $this->_journal->addDetail(static::REQUEST_URL, $request->getRequestUri());
+    $this->_journal->addDetail(static::HTTP_RESPONSE_CODE, $model->getResponseCode());
+    $this->_journal->addDetail(static::CLIENT_REQUEST, json_encode($details));
+    $this->_journal->addDetail(static::BOKEH_RESPONSE,$model->getResponseBody());
+
+    return true;
+  }
+
+
+  public function renderLabelOn(Zend_View_Interface $view) : string {
+    if (!$url_without_param=strstr($this->renderDetailValueOn(static::REQUEST_URL, $view),'?',TRUE))
+      $url_without_param = $this->renderDetailValueOn(static::REQUEST_URL, $view);
+
+    return $this->_('call for %s with response code %s',$url_without_param,$this->renderDetailValueOn(static::HTTP_RESPONSE_CODE, $view));
+  }
+
+
+  public function getModelLabel() : string {
+    return $this->_('Variable');
+  }
+}
+
+
+
+
+class Class_Journal_RequestResponse {
+  protected $_controller,
+    $_response_body;
+
+  public function __construct($controller,$response_body ,$response_code)  {
+    $this->_controller = $controller;
+    $this->_response_body = $response_body;
+    $this->_response_code = $response_code;
+  }
+
+
+  public function getController() {
+    return $this->_controller;
+  }
+
+
+  public function getRequest() {
+    return $this->_controller->getRequest();
+  }
+
+
+  public function getType() : string {
+    if ((!$request = $this->getRequest()) ||
+        (!$url = $request->getRequestUri()))
+      return Class_Journal_RequestType::class;
+
+    if (strpos($url, '/cas-server') !== false)
+      return Class_Journal_CasRequestType::class;
+
+    if ((strpos($url, '/oauth') !== false) ||
+        (strpos($url, '/auth/refresh') !== false) ||
+        (strpos($url, '/api/') !== false))
+      return Class_Journal_OauthRequestType::class;
+
+    return Class_Journal_RequestType::class;
+  }
+
+
+  public function getResponseBody() {
+    return $this->_response_body;
+  }
+
+
+  public function getResponseCode() {
+    return $this->_response_code;
+  }
+}
diff --git a/library/Class/Journal/Type.php b/library/Class/Journal/Type.php
index 3d203b3b73b902ff5a7d34eb5e049b70e90a586a..eaea279abf53ea07518e3a7188aa28f5a4b4e5c3 100644
--- a/library/Class/Journal/Type.php
+++ b/library/Class/Journal/Type.php
@@ -30,7 +30,8 @@ class Class_Journal_Type {
     STACK = 'stack',
     PREVIOUS_VALUE = 'previous_value',
     NEW_VALUE = 'new_value',
-    MODEL_ID = 'model_id';
+    MODEL_ID = 'model_id',
+    APPLY_ON = '';
 
   protected $_journal;
 
@@ -43,6 +44,11 @@ class Class_Journal_Type {
   }
 
 
+  public static function create() : self {
+    return new static(new Class_Journal);
+  }
+
+
   public static function canHandleEvent(Storm_Event_Abstract $event) : bool {
     return get_class($event->getModel()) === static::APPLY_ON;
   }
diff --git a/library/Class/Journal/TypeMapping.php b/library/Class/Journal/TypeMapping.php
index 2434f1c301ee4d3270ee5f240077f22d766ffcfa..c113a041ff7d9770f7b8704a1025cef6ab9db34e 100644
--- a/library/Class/Journal/TypeMapping.php
+++ b/library/Class/Journal/TypeMapping.php
@@ -31,7 +31,10 @@ class Class_Journal_TypeMapping {
             Class_Journal_CatalogueDeleteType::class => $this->_('Domaines : suppression'),
             Class_Journal_ProfileType::class => $this->_('Profils : mise à jour'),
             Class_Journal_ProfileDeleteType::class => $this->_('Profils : suppression'),
-            Class_Journal_ArticleType::class => $this->_('Articles')
+            Class_Journal_ArticleType::class => $this->_('Articles'),
+            Class_Journal_RequestType::class => $this->_('Requêtes'),
+            Class_Journal_CasRequestType::class => $this->_('Requêtes CAS'),
+            Class_Journal_OauthRequestType::class => $this->_('Requêtes OAUTH'),
     ];
   }
 
diff --git a/library/Class/Loan/Pnb.php b/library/Class/Loan/Pnb.php
index 3a179a8b78a5b661d13ead5d0229d26656e084aa..efc94a204d0f1ab03caf36e3778943d8d54e5b0c 100644
--- a/library/Class/Loan/Pnb.php
+++ b/library/Class/Loan/Pnb.php
@@ -96,7 +96,6 @@ class Class_Loan_PnbLoader extends Storm_Model_Loader {
   }
 
 
-
   protected function _applyOngoingAndDo($params, $closure) {
     $prefix = isset($params['where']) ? '(' . $params['where'] . ') and ' : '';
     $params['where'] = $prefix . 'expected_return_date > "' . $this->_getDate() . '"';
@@ -111,12 +110,13 @@ class Class_Loan_PnbLoader extends Storm_Model_Loader {
 
 
   protected function _getCNILMaxRetentionDate() :string {
-    return Class_Loan_Pnb::addDaysToCurrentDate(static::CNIL_DAYS_MAX_RETENTION);
+    return Class_Loan_Pnb::addDaysToCurrentDateTime(static::CNIL_DAYS_MAX_RETENTION);
   }
 }
 
 
 
+
 class Class_Loan_Pnb extends Storm_Model_Abstract {
   use Trait_TimeSource, Trait_AlbumDelegator;
 
@@ -132,7 +132,6 @@ class Class_Loan_Pnb extends Storm_Model_Abstract {
                                   'record_origin_id' => 0,
                                   'options' => ''];
 
-
   public function getAlbum() {
     return Class_Album::findFirstBy(['id_origine' => $this->getRecordOriginId()]);
   }
@@ -199,6 +198,77 @@ class Class_Loan_Pnb extends Storm_Model_Abstract {
   }
 
 
+  protected function _addGenericFormatAudioBooksIfNotSet($formats)  {
+    if ( (!in_array('AN', $formats) and !in_array('AJ', $formats))
+        and in_array('A103', $formats))
+      $formats[] ='AN';
+    return $formats;
+  }
+
+
+  public function getFormats() : array {
+    if  (!$album = $this->getAlbum())
+      return [];
+    return $this->_addGenericFormatAudioBooksIfNotSet(explode(';', $album->getFormat()));
+  }
+
+
+  public function getFirstCollection() : string {
+    return ($album = $this->getAlbum())
+      ? $album->getFirstCollection()
+      : '';
+  }
+
+
+  public function getLanguage() : array {
+    return ($album = $this->getAlbum())
+      ? $album->getIdLangue()
+      : [];
+  }
+
+
+  public function getSubject() : string {
+    return ($album = $this->getAlbum())
+      ? $album->getMatiere()
+      : '';
+  }
+
+
+  public function getCollection() : string {
+    return ($album = $this->getAlbum())
+      ? $album->getCollection()
+      : '';
+  }
+
+
+  public function getPoster() : string {
+    return ($album = $this->getAlbum())
+      ? $album->getPoster()
+      : '';
+  }
+
+
+  public function getDescription():  string {
+    return ($notice = $this->getNoticeOPAC())
+      ?  strip_tags($notice->getResume())
+      :'';
+  }
+
+
+  public function getIsbnOrEan():  string {
+    return ($notice = $this->getNoticeOPAC())
+      ?  $notice->getIsbnOrEan()
+      :'';
+  }
+
+
+  public function getFirstEditor():  string {
+    return ($notice = $this->getNoticeOPAC())
+      ?  $notice->getFirstEditeur()
+      :'';
+  }
+
+
   protected function _withAlbumDo($callback) {
     return array_filter(array_map('trim', explode(';', $callback())));
   }
@@ -235,7 +305,6 @@ class Class_Loan_Pnb extends Storm_Model_Abstract {
   }
 
 
-
   public function isRenewable() {
     return false;
   }
diff --git a/library/Class/Log.php b/library/Class/Log.php
index b47bc1cad0b634f4a7edefcebdb7cc817fa4dfcd..c99d4b93c3062a9c2e15b8300cfdecb582fc0fc0 100644
--- a/library/Class/Log.php
+++ b/library/Class/Log.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * Copyright (c) 2012-2014, Agence Française Informatique (AFI). All rights reserved.
+ * Copyright (c) 2012-2022, 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
@@ -25,24 +25,24 @@ class Class_Log {
 
   protected $_messages = [];
 
-  public function log($message) {
+  public function log($message) : self {
     $this->_messages[] = $message;
     return $this;
   }
 
 
-  public function getLastMessage() {
+  public function getLastMessage() : string {
     return end($this->_messages);
   }
 
 
-  public function getMessages() {
+  public function getMessages() : array {
     return $this->_messages;
   }
 
 
-  public function reset() {
+  public function reset() : self {
     $this->_messages = [];
     return $this;
   }
-}
\ No newline at end of file
+}
diff --git a/library/Class/LogFile.php b/library/Class/LogFile.php
new file mode 100644
index 0000000000000000000000000000000000000000..aa3713a82ea1c0b96b4e776d9ea10039aa0b6b60
--- /dev/null
+++ b/library/Class/LogFile.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_LogFile extends Class_Log {
+  use Trait_StaticFileSystem, Trait_Singleton, Trait_TimeSource;
+  public static $LOG,
+    $MAX_FILE_SIZE = 100000 ;
+  protected $_messages = [],
+    $_file;
+
+  public static function pushRequestMessage($request)  {
+    Class_LogFile::pushMessage("\nRequest url",$request->getRequestUri());
+    if (!empty($request->getPost()))
+      Class_LogFile::pushMessage("With params",$request->getPost());
+    Class_LogFile::pushMessage("Header Authorization",$request->getHeader('Authorization'));
+    Class_LogFile::pushMessage("Header Content-Type",$request->getHeader('Content_type'));
+  }
+
+
+  public static function pushMessage($title, $message)  {
+    if (is_array($message))
+      return (self::getLog())->log('['. static::getCurrentDateTime().'] '.$title.':'.print_r($message,true )."\n");
+    return (self::getLog())->log('['. static::getCurrentDateTime().'] '.$title.':'.$message."\n");
+  }
+
+
+  public static function setLog(Class_Log $log) {
+    self::$LOG = $log;
+  }
+
+
+  public static function getLog() : Class_Log {
+    if (self::$LOG)
+      return self::$LOG;
+    self::$LOG  = new Class_LogFile('temp/log_auth');
+    return self::$LOG;
+  }
+
+
+  public function __construct( string $file_name )  {
+    $this->_file = $file_name;
+  }
+
+
+  public function log($message) :self{
+    $this->_messages[] = $message;
+    if (static::getFileSystem()->filesize( $this->_file) > static::$MAX_FILE_SIZE) {
+      static::getFileSystem()->file_put_contents($this->_file,$message);
+      return $this;
+    }
+
+    static::getFileSystem()->file_put_contents($this->_file,$message, FILE_APPEND);
+    return $this;
+  }
+
+
+  public function getLastMessage() : string {
+    return end($this->_messages);
+  }
+
+
+  public function getMessages()  : array {
+    return explode("\n", htmlentities(static::getFileSystem()->file_get_contents($this->_file)));
+  }
+
+
+  public function reset() : self {
+    $this->_messages = [];
+    static::getFileSystem()->file_put_contents($this->_file,'');
+    return $this;
+  }
+}
diff --git a/library/Class/User/AdminMenu/Back.php b/library/Class/User/AdminMenu/Back.php
index acc84256ae33cd80d5fbef251074c706303f2d58..9fcfb7f6decf1cb59fee31c18ed236492eaebf4d 100644
--- a/library/Class/User/AdminMenu/Back.php
+++ b/library/Class/User/AdminMenu/Back.php
@@ -214,9 +214,10 @@ class Class_User_AdminMenu_Back extends Class_User_AdminMenu_Abstract {
                'icon' => 'computers'],
 
               ['url' => '/admin/identity-providers',
-               'label' => $this->_('Fournisseurs d\'identités'),
+               'label' => $this->_('Fédération d\'identités'),
                'icon' => 'identity_providers']];
 
+
     return $this->_menuObjectsFromArray($items);
   }
 
diff --git a/library/Class/User/AdminMenu/Default.php b/library/Class/User/AdminMenu/Default.php
index 21916410c7e875bbca37fb0ab9a0281466463da9..56c8e1d1adc25f33fa8cfddf1281ea7358d585b9 100644
--- a/library/Class/User/AdminMenu/Default.php
+++ b/library/Class/User/AdminMenu/Default.php
@@ -124,6 +124,7 @@ class Class_User_AdminMenu_Default {
                   '/admin/users',
                   '/admin/usergroup',
                   '/admin/url-manager',
+                  '/admin/identity-providers',
                   '/admin/index/adminvar']);
 
     return $this;
diff --git a/library/Class/User/ApiToken.php b/library/Class/User/ApiToken.php
index 341ec965c787168838e0e90a09878e82837d8457..93f4a33bd3d91a9f4ff7b0343d0728e3d065b9e7 100644
--- a/library/Class/User/ApiToken.php
+++ b/library/Class/User/ApiToken.php
@@ -19,6 +19,7 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 class User_ApiTokenLoader extends Storm_Model_Loader {
+  use Trait_TimeSource;
   public function findOrCreateForUserAndApplication($user, $client_id) {
     if ($token = Class_User_ApiToken::findFirstBy(['role' => 'user',
                                                    'model' => $user,
@@ -29,24 +30,158 @@ class User_ApiTokenLoader extends Storm_Model_Loader {
     $token
       ->setUser($user)
       ->setClientId($client_id)
-      ->setToken(md5(uniqid()))
-      ->save();
+      ->setToken(md5(uniqid()));
+    if ( !$client = $token->getClient() )
+      return $token;
+
+    if ($client->getAllowRefreshToken())
+      $token->setRefreshToken(md5(uniqid()));
+    $token->save();
 
     return $token;
   }
+
+  public function createForUserAndApplication($user, $client_id) {
+    if ($token = Class_User_ApiToken::findFirstBy(['role' => 'user',
+                                                   'model' => $user,
+                                                   'client_id' => $client_id]))
+      $token->delete();
+
+    $token = new Class_User_ApiToken();
+    $token
+      ->setUser($user)
+      ->setClientId($client_id)
+      ->setToken(md5(uniqid()));
+    if ( !$client = $token->getClient() )
+      return $token;
+
+    if ($client->getAllowRefreshToken())
+      $token->setRefreshToken(md5(uniqid()));
+    $token->save();
+
+    return $token;
+  }
+
+
+  public function refreshToken( string $refresh_token ) : Class_User_ApiToken {
+    if (!$token = Class_User_ApiToken::findFirstBy(['refresh_token' => $refresh_token]))
+      return (new Class_User_ApiToken())->addExpiredError();
+    return $token->refresh();
+  }
 }
 
 
 
 
 class Class_User_ApiToken extends Storm_Model_Abstract {
-
+  use Trait_TimeSource;
   protected
     $_table_name = 'user_api_tokens',
+    $_client,
     $_loader_class = 'User_ApiTokenLoader',
     $_belongs_to = ['user' => ['model' => 'Class_Users',
-                               'role' => 'api_token']];
+                               'role' => 'api_token']],
+    $_default_attribute_values =['expired_at' =>null,
+                                 'client_id' => '',
+                                 'refresh_token' =>''];
 
+  public function __construct() {
+    parent::__construct();
+    $this->setCreatedAt(static::getCurrentDateTime());
+  }
+
+
+  public function isExpired() : bool{
+    if (!$this->getClient())
+      return true;
+
+    if (!$this->getClient()->getTokenExpiresIn())
+      return false;
+
+    if ($expired=$this->getExpiredAt() && ($this->getCurrentTime() > strtotime($this->getExpiredAt())))
+      $this->addExpiredError();
+
+    return $expired;
+  }
 
+
+  public function getUserName() : string {
+    return $this->getUser() ?
+      $this->getUser()->getNomComplet() : '';
+  }
+
+
+  public function getClient() {
+    if ($this->_client)
+      return  $this->_client;
+    if ($this->_client = Class_IdentityClient::findFirstBy(['client_id'=> $this->getClientId(),
+                                                            'active' => true]))
+      return $this->_client;
+    $this->addError( 'Client configuration missing for '.$this->getClientId());
+    return null;
+  }
+
+
+  public function addExpiredError() : self {
+    $this->addError( 'The access token has expired');
+    return $this;
+  }
+
+
+  public function getExpiresIn() {
+    if ( $client =  $this->getClient() )
+      return $client->getTokenExpiresIn();
+    return 0;
+  }
+
+
+  public function checkRedirectUri($redirect_uri) : self{
+    if (!$client = $this->getClient()) {
+      return $this;
+    }
+
+    if ( $client->getRedirectUri() == ''
+        || $client->getRedirectUri()==$redirect_uri)
+      return $this;
+    $this->addError(sprintf('redirect_uri %s differs of client config :%s',
+                            $redirect_uri,
+                            $client->getRedirectUri()));
+    return $this;
+  }
+
+
+  public function refresh() : self{
+    if ( !$client =  $this->getClient() ) {
+      return $this;
+    }
+    if (!$client->getAllowRefreshToken()){
+      $this->addError( 'Client configuration is not allowed refresh token');
+      return $this;
+    }
+
+    $this->setRefreshToken(md5(uniqid()));
+    $this->setToken(md5(uniqid()));
+    $this->setExpiredAt('');
+    if ( $expires_in = $client->getTokenExpiresIn()){
+      $this->setExpiredAt( $this->addSecondsToCurrentDate($expires_in));
+    }
+
+    $this->save();
+    return $this;
+  }
+
+
+  public function beforeSave() {
+    if ( !$client =  $this->getClient() )
+      return $this;
+    if ($client->getTokenExpiresIn())
+      $this->setExpiredAt( $this->addSecondsToCurrentDate( $client->getTokenExpiresIn()));
+    return $this;
+  }
+
+
+  public function getLibelle()  : string{
+    return $this->getToken();
+  }
 }
-?>
\ No newline at end of file
+?>
diff --git a/library/Class/WebService/BibNumerique/Dilicom/Book.php b/library/Class/WebService/BibNumerique/Dilicom/Book.php
index f41ba06518c4c5d7f4d3a25d296e24b0913c6d5f..78660b21f706d38740f4e59c5f30263ded2141e9 100644
--- a/library/Class/WebService/BibNumerique/Dilicom/Book.php
+++ b/library/Class/WebService/BibNumerique/Dilicom/Book.php
@@ -25,6 +25,7 @@ class Class_WebService_BibNumerique_Dilicom_Book extends Class_WebService_BibNum
   protected
     $_isbn,
     $_subtitle,
+    $_formats,
     $_order_line_id,
     $_order_date,
     $_items = [],
@@ -104,6 +105,7 @@ class Class_WebService_BibNumerique_Dilicom_Book extends Class_WebService_BibNum
       ->setISBN($this->_isbn)
       ->setSousTitre($this->_subtitle)
       ->addEditor($this->getEditeur())
+      ->setFormat( implode(';',$this->_formats))
       ->setDroits($this->_('Tous droits réservés'))
       ->setItems($this->_items);
     return $this;
@@ -161,5 +163,15 @@ class Class_WebService_BibNumerique_Dilicom_Book extends Class_WebService_BibNum
     return 'Livre numérique (PNB)';
   }
 
+
+  public function addFormat($format) {
+    $this->_formats[] = $format;
+    return $this;
+  }
+
+
+  public function getFormats() {
+    return implode(';', array_unique($this->_formats));
+  }
 }
 ?>
diff --git a/library/Class/WebService/BibNumerique/Dilicom/Hub.php b/library/Class/WebService/BibNumerique/Dilicom/Hub.php
index 1bfbc81221b21383520b8511131b74a6a12fed97..67a94e9840d0f7242f6cd8ede141bec48733d461 100644
--- a/library/Class/WebService/BibNumerique/Dilicom/Hub.php
+++ b/library/Class/WebService/BibNumerique/Dilicom/Hub.php
@@ -205,10 +205,7 @@ class Class_WebService_BibNumerique_Dilicom_Hub extends Class_WebService_Abstrac
     }
 
     if (isset($content->link) && ($link = $content->link) && isset($link->url) && ($url = $link->url)) {
-      $loan->setLoanLink($url
-                         .($user_agent
-                           ? '?userAgentId='.$user_agent
-                           :''));
+      $loan->setLoanLink($url);
       if (isset($content->protection))
         $loan->setOptions($content->protection)->save();
       $loan->save();
diff --git a/library/Class/WebService/BibNumerique/Dilicom/ONIXFile.php b/library/Class/WebService/BibNumerique/Dilicom/ONIXFile.php
index fb4d8659fad8d68a4c83cafc7e629189ecf91f24..0be0e4e2a08ad6ec50e1eecc1391d2b48389d433 100644
--- a/library/Class/WebService/BibNumerique/Dilicom/ONIXFile.php
+++ b/library/Class/WebService/BibNumerique/Dilicom/ONIXFile.php
@@ -38,6 +38,7 @@ class Class_WebService_BibNumerique_Dilicom_ONIXFile {
   protected
     $_parser,
     $_book,
+    $_formats = [],
     $_current_publishing_date_role,
     $_current_text_type,
     $_current_product_id_type,
@@ -273,5 +274,15 @@ class Class_WebService_BibNumerique_Dilicom_ONIXFile {
   public function endEPubUsageUnit($content) {
     $this->_current_usage_unit = $content;
   }
+
+
+  public function endProductFormDetail($format) {
+    $this->_book->addFormat($format);
+  }
+
+  public function endProductForm($format) {
+    $this->_book->addFormat($format);
+  }
+
 }
-?>
\ No newline at end of file
+?>
diff --git a/library/Trait/TimeSource.php b/library/Trait/TimeSource.php
index 5dac78b967a9654c56c053984651a7ed488fe643..0e2f04409eb17435a4340296239eab134e1a55d7 100644
--- a/library/Trait/TimeSource.php
+++ b/library/Trait/TimeSource.php
@@ -58,7 +58,17 @@ trait Trait_TimeSource {
 
 
   public static function addDaysToCurrentDate($days) {
-    return date('Y-m-d', strtotime((string)$days.' day', self::getTimeSource()->time()));
+    return date('Y-m-d', static::addIntervalToDate((string)$days.' day', null));
+  }
+
+
+  public static function addDaysToCurrentDateTime($days) {
+    return date('Y-m-d H:i:s', static::addIntervalToDate((string)$days.' day', null));
+  }
+
+
+  public static function addSecondsToCurrentDate($seconds) {
+    return date('Y-m-d H:i:s', static::addIntervalToDate((string)$seconds .' second' , null));
   }
 
 
diff --git a/library/ZendAfi/Acl/AdminControllerRoles.php b/library/ZendAfi/Acl/AdminControllerRoles.php
index fc05362afea8b53f58d4e05f69fcc0fc9def4041..2094bf174e77091200e04424ee258d2147601161 100644
--- a/library/ZendAfi/Acl/AdminControllerRoles.php
+++ b/library/ZendAfi/Acl/AdminControllerRoles.php
@@ -118,6 +118,8 @@ class ZendAfi_Acl_AdminControllerRoles extends Zend_Acl {
     $this->add(new Zend_Acl_Resource('rendez-vous'));
     $this->add(new Zend_Acl_Resource('journal'));
     $this->add(new Zend_Acl_Resource('identity-providers'));
+    $this->add(new Zend_Acl_Resource('identity-clients'));
+    $this->add(new Zend_Acl_Resource('user-api-tokens'));
     $this->add(new Zend_Acl_Resource('drive-checkout'));
     $this->add(new Zend_Acl_Resource('template'));
     $this->add(new Zend_Acl_Resource('multimedia'));
diff --git a/library/ZendAfi/Controller/Action/Helper/CasFailureResponse.php b/library/ZendAfi/Controller/Action/Helper/CasFailureResponse.php
index 4adb1c4b87dba660c7618234862eb1218676e536..870561b9cee200106fcbe0be96a8318153e921ed 100644
--- a/library/ZendAfi/Controller/Action/Helper/CasFailureResponse.php
+++ b/library/ZendAfi/Controller/Action/Helper/CasFailureResponse.php
@@ -45,6 +45,8 @@ class ZendAfi_Controller_Action_Helper_CasFailureResponse extends ZendAfi_Contro
     $this->_controller->getHelper('ViewRenderer')->setNoRender();
     $this->_response->setHeader('Content-Type', 'application/xml;charset=utf-8');
     $xml = $xml->_xmlString('cas:serviceResponse', implode('\n', $body), ' xmlns:cas="http://www.yale.edu/tp/cas"');
+
+    Class_Journal_RequestType::createWith( $this->_controller, $xml);
     $this->getResponse()->setBody($xml);
   }
-}
\ No newline at end of file
+}
diff --git a/library/ZendAfi/Controller/Action/Helper/CasValidResponse.php b/library/ZendAfi/Controller/Action/Helper/CasValidResponse.php
index 540a0362d1ed185e3b60f3feaaea4698e0a2046d..a4bd01975f5d34eca15cc27d531cb7ad1a8366b8 100644
--- a/library/ZendAfi/Controller/Action/Helper/CasValidResponse.php
+++ b/library/ZendAfi/Controller/Action/Helper/CasValidResponse.php
@@ -25,15 +25,16 @@ class ZendAfi_Controller_Action_Helper_CasValidResponse extends ZendAfi_Controll
   public function direct($user, $ticket, $attributes = []) {
     $this->_controller->getHelper('ViewRenderer')->setNoRender();
     $this->_response->setHeader('Content-Type', 'application/xml;charset=utf-8');
-
+    $xml="<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>\n"
+      . "  <cas:authenticationSuccess>\n"
+      . "    <cas:user>".$user->getId()."</cas:user>\n"
+      . "    <cas:proxyGrantingTicket>".$ticket."</cas:proxyGrantingTicket>\n"
+      . $this->renderAttributes($attributes)
+      . "  </cas:authenticationSuccess>\n"
+      . "</cas:serviceResponse>";
+    Class_Journal_RequestType::createWith( $this->_controller, $xml);
     $this->_response
-      ->setBody("<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>\n"
-                . "  <cas:authenticationSuccess>\n"
-                . "    <cas:user>".$user->getId()."</cas:user>\n"
-                . "    <cas:proxyGrantingTicket>".$ticket."</cas:proxyGrantingTicket>\n"
-                . $this->renderAttributes($attributes)
-                . "  </cas:authenticationSuccess>\n"
-                . "</cas:serviceResponse>");
+      ->setBody($xml);
   }
 
 
@@ -64,4 +65,4 @@ class ZendAfi_Controller_Action_Helper_CasValidResponse extends ZendAfi_Controll
       . "\n    </cas:attributes>\n";
   }
 }
-?>
\ No newline at end of file
+?>
diff --git a/library/ZendAfi/Controller/Action/Helper/SendHttpErrorCode.php b/library/ZendAfi/Controller/Action/Helper/SendHttpErrorCode.php
new file mode 100644
index 0000000000000000000000000000000000000000..0eb1db1d33cc7ff30fe5fae309bb6f8f25cd8f2e
--- /dev/null
+++ b/library/ZendAfi/Controller/Action/Helper/SendHttpErrorCode.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+class ZendAfi_Controller_Action_Helper_SendHttpErrorCode extends ZendAfi_Controller_Action_Helper_Abstract {
+
+
+  public function sendHttpErrorCode($error,$error_description, $code = 400 ) {
+    $this->_controller->getHelper('ViewRenderer')->setNoRender();
+    $json = [
+             'error' =>   $error,
+             'error_description' => $error_description
+    ];
+    Class_Journal_RequestType::createWith( $this->_controller, json_encode($json), $code);
+    $this->_response->setHttpResponseCode($code);
+    $this->_controller->getHelper('json')->direct($json);
+
+    return true;
+  }
+
+
+  public function direct($error,$error_description, $code) {
+    return $this->sendHttpErrorCode($error,$error_description, $code);
+  }
+}
diff --git a/library/ZendAfi/Controller/Action/Helper/ThrowHTTPError.php b/library/ZendAfi/Controller/Action/Helper/ThrowHTTPError.php
index af6898765ccf0272f24b7872d1000c6fbeb1ce30..6a72cfbc963f76ef5c0f5624d62dc22d078074b7 100644
--- a/library/ZendAfi/Controller/Action/Helper/ThrowHTTPError.php
+++ b/library/ZendAfi/Controller/Action/Helper/ThrowHTTPError.php
@@ -27,11 +27,11 @@ class ZendAfi_Controller_Action_Helper_ThrowHTTPError extends ZendAfi_Controller
       ->getFrontController()
       ->getPlugin('Zend_Controller_Plugin_ErrorHandler')
       ->setErrorHandlerModule($this->getRequest()->getModuleName());
-
-    throw new Zend_Controller_Action_Exception($message, $code);
+    Class_Journal_RequestType::createWith( $this->getFrontController(),  $message, $code);
+    throw new Zend_Controller_Action_Exception($message , $code);
   }
 
   public function direct($message, $code) {
     return $this->throwHTTPError($message, $code);
   }
-}
\ No newline at end of file
+}
diff --git a/library/ZendAfi/Controller/Plugin/Manager/IdentityClient.php b/library/ZendAfi/Controller/Plugin/Manager/IdentityClient.php
new file mode 100644
index 0000000000000000000000000000000000000000..8a8e7f094b0775975ea823e107bf2edb209d05c2
--- /dev/null
+++ b/library/ZendAfi/Controller/Plugin/Manager/IdentityClient.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, 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_Controller_Plugin_Manager_IdentityClient
+  extends ZendAfi_Controller_Plugin_Manager_Manager {
+
+  public function getActions($model) {
+    return [
+            ['url' => '/admin/identity-clients/edit/id/%s',
+             'icon' => 'edit',
+             'label' => $this->_('Modifier le client d\'identité')],
+            ['url' => '/admin/user-api-tokens/show/client_id/'.$model->getClientId(),
+             'icon' => 'view',
+             'label' => $this->_('Voir les connexions')],
+
+            ['url' => '/admin/identity-clients/delete/id/%s',
+             'icon' => 'delete',
+             'label' => $this->_('Supprimer le client d\'identité')],
+    ];
+  }
+
+
+  protected function _getFormValues($model) {
+    return array_merge($model->toArray(),
+                       $model->getConfigAsArray());
+  }
+
+
+  protected function _getFormWith($model, $custom_form) {
+    $form = parent::_getFormWith($model,$custom_form);
+    return $form;
+  }
+}
diff --git a/library/ZendAfi/Controller/Plugin/Manager/IdentityProvider.php b/library/ZendAfi/Controller/Plugin/Manager/IdentityProvider.php
index c0d3d9cd99711f2d2fc1394aee9e494c58b3a8e4..ab73f67d8f2ee78d2e7dfb7735c4bd5e398e014e 100644
--- a/library/ZendAfi/Controller/Plugin/Manager/IdentityProvider.php
+++ b/library/ZendAfi/Controller/Plugin/Manager/IdentityProvider.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * Copyright (c) 2012-2017, Agence Française Informatique (AFI). All rights reserved.
+ * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
  *
  * BOKEH is free software; you can redistribute it and/or modify
  * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
diff --git a/library/ZendAfi/Controller/Plugin/Manager/UserApiTokens.php b/library/ZendAfi/Controller/Plugin/Manager/UserApiTokens.php
new file mode 100644
index 0000000000000000000000000000000000000000..6d97872b7fb730c635242ae89461a82640a5988f
--- /dev/null
+++ b/library/ZendAfi/Controller/Plugin/Manager/UserApiTokens.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Zendafi_Controller_Plugin_Manager_UserApiTokens
+  extends ZendAfi_Controller_Plugin_Manager_Manager {
+
+  public function getActions($model) {
+    return [
+            ['url' => '/admin/user-api-tokens/delete/id/%s',
+             'icon' => 'delete',
+             'label' => $this->_('Révoquer')],
+
+    ];
+  }
+}
diff --git a/library/ZendAfi/Controller/Plugin/ResourceDefinition/IdentityClient.php b/library/ZendAfi/Controller/Plugin/ResourceDefinition/IdentityClient.php
new file mode 100644
index 0000000000000000000000000000000000000000..d5e6b2e645815673784ec1804e3f5dacbc9aee10
--- /dev/null
+++ b/library/ZendAfi/Controller/Plugin/ResourceDefinition/IdentityClient.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, 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_Controller_Plugin_ResourceDefinition_IdentityClient
+  extends ZendAfi_Controller_Plugin_ResourceDefinition_Abstract {
+
+  public function getDefinitions() {
+    return ['model' => ['class' => Class_IdentityClient::class,
+                        'name' => 'server',
+                        'order' => 'label'],
+
+            'messages' => ['successful_save' => $this->_('Client d\'identité %s sauvegardé'),
+                           'successful_add' => $this->_('Le client d\'identité %s a été sauvegardé'),
+                           'successful_delete' => $this->_('Client d\'identité %s supprimé')],
+
+            'actions' => ['add' => ['title' => $this->_('Ajouter un client d\'identité')],
+                          'edit' => ['title' => $this->_('Modifier le client d\'identité: %s')]],
+
+            'form_class_name' => 'ZendAfi_Form_Admin_IdentityClient'
+    ];
+  }
+}
diff --git a/library/ZendAfi/Controller/Plugin/ResourceDefinition/UserApiTokens.php b/library/ZendAfi/Controller/Plugin/ResourceDefinition/UserApiTokens.php
new file mode 100644
index 0000000000000000000000000000000000000000..fd396eae56982714d45cbf82fbaa194562b5204b
--- /dev/null
+++ b/library/ZendAfi/Controller/Plugin/ResourceDefinition/UserApiTokens.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, 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_Controller_Plugin_ResourceDefinition_UserApiTokens
+  extends ZendAfi_Controller_Plugin_ResourceDefinition_Abstract {
+
+  public function getDefinitions() {
+    return ['model' => ['class' => 'Class_User_ApiToken',
+                        'name' => 'api_token',
+                        'order' => 'label'],
+
+            'messages' => [ 'successful_delete' => $this->_('Token %s supprimé')],
+    ];
+  }
+}
diff --git a/library/ZendAfi/Form/Admin/IdentityClient.php b/library/ZendAfi/Form/Admin/IdentityClient.php
new file mode 100644
index 0000000000000000000000000000000000000000..5753829d605535b43be8091c22811d2f47a00609
--- /dev/null
+++ b/library/ZendAfi/Form/Admin/IdentityClient.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, 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_Form_Admin_IdentityClient extends ZendAfi_Form {
+  public function init() {
+    parent::init();
+
+    Class_ScriptLoader::getInstance()
+      ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#client_secret,#redirect_uri,#disable_state,#disable_response_type, #allow_refresh_token, #token_expires_in").closest("tr"), ["Class_IdentityClient_OAuth2"]);');
+
+
+    $this
+      ->addElement('text',
+                   'label',
+                   ['label' => $this->_('Libellé'),
+                    'size' => 50,
+                    'required' => true,
+                    'allowEmpty' => false])
+
+      ->addElement('checkbox',
+                   'active',
+                   ['label' => $this->_('Actif')])
+
+      ->addElement('select',
+                   'type',
+                   ['label' => $this->_('Type'),
+                    'multiOptions' => (new Class_IdentityClient_Types)->asMultiOptions()])
+
+      ->addElement('text',
+                   'client_id',
+                   ['label' => $this->_('Identifiant client'),
+                    'size' => 50])
+
+      ->addElement('text',
+                   'client_secret',
+                   ['label' => $this->_('Clé secrète'),
+                    'size' => 50])
+
+      ->addElement('url',
+                   'redirect_uri',
+                   ['label' => $this->_('URL de redirection'),
+                    'size' => 50,
+                    'allowEmpty' => false,
+                    'title' => $this->_('URL de redirection')])
+
+      ->addElement('checkbox',
+                   'disable_response_type',
+                   ['label' => $this->_('Désactiver la vérification du response_type')])
+
+      ->addElement('checkbox',
+                   'allow_refresh_token',
+                   ['label' => $this->_('Autoriser la génération d\'un refresh token')])
+
+      ->addElement('text',
+                   'token_expires_in',
+                   ['label' => $this->_('Temps d\'expiration d\'un token')])
+
+
+      ->addElement('checkbox',
+                   'disable_state',
+                   ['label' => $this->_('Désactiver la vérification du state')])
+
+      ->addUniqDisplayGroup('server');
+  }
+}
diff --git a/library/ZendAfi/View/Helper/Admin/HelpLink.php b/library/ZendAfi/View/Helper/Admin/HelpLink.php
index c06115feae4987edf8e4a2df03ba0edf17adb089..0330820343cc171b2738b54da81ce40019164c30 100644
--- a/library/ZendAfi/View/Helper/Admin/HelpLink.php
+++ b/library/ZendAfi/View/Helper/Admin/HelpLink.php
@@ -134,6 +134,8 @@ class ZendAfi_View_Helper_Admin_HelpLinkBokehWiki {
      'drive-checkout'         => ['index' => 'Gérer_les_listes_de_rendez-vous_en_mode_drive',
                                   'plan' => 'Prise_de_rendez-vous_par_les_professionnels'],
      'identity-providers'     => ['index' => 'Fournisseurs_d\'identités'],
+     'identity-clients'     => ['index' => 'Clients_d\'identités'],
+     'identity-providers/federation'     => ['index' => 'Fédération_d\'identités'],
      'variable'               => ['index' => 'Liste_des_variables'],
      'pnb'                    => ['index' => 'PNB_Dilicom_tableau_de_bord'],
      'journal'                => ['index' => 'Journal_des_événements']
diff --git a/library/ZendAfi/View/Helper/Api/PnbLoans.php b/library/ZendAfi/View/Helper/Api/PnbLoans.php
new file mode 100644
index 0000000000000000000000000000000000000000..04fa5485930815039984e3794a0421a93dfeffff
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Api/PnbLoans.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * Copyright (c) 2012-2014, 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_Api_PnbLoans extends ZendAfi_View_Helper_Api_Abstract {
+  public function pnbLoans(Storm_Collection $loans):string {
+    return json_encode(
+                       $loans->collect(fn($loan) => $this->loanToArray($loan))
+                       ->getArrayCopy()
+    );
+  }
+
+
+  public function loanToArray($loan) {
+    if (! $user = Class_Users::getIdentity())
+      return [] ;
+
+    $datas = [
+              'loanId' => $loan->getId(),
+              'userId'=> $loan->getSubscriberId(),
+              'orderLineId' => $loan->getOrderLineId(),
+              'loanhLink' => $loan->getLoanLink(),
+              'beginDate'=>date('Y-m-d\TH:i:s\Z',strtotime($loan->getLoanDate())),
+
+              'description' => $loan->getDescription(),
+              'endDate'=> date('Y-m-d\TH:i:s\Z',strtotime($loan->getExpectedReturnDate())),
+              'standardTitle' => strip_tags($loan->getTitre()),
+              'frontCoverMedium' => $loan->getPoster(),
+              'gtin13'=> $loan->getIsbnOrEan(),
+              'loanerGln'=>  $user->getBibGLN(),
+              'epubTechnicalProtection' => $loan->getOptions(),
+              'imprintName' => $loan->getFirstEditor(),
+              'collection' => $loan->getFirstCollection(),
+              'contributors'=>[],
+              'categoryClil'=> $loan->getSubject(),
+              'author' => $loan->getAuteur(),
+              'productFormDetails' =>  $loan->getFormats()
+      ];
+
+      return $datas;
+
+  }
+}
+?>
diff --git a/library/ZendAfi/View/Helper/Button/ActivationLog.php b/library/ZendAfi/View/Helper/Button/ActivationLog.php
new file mode 100644
index 0000000000000000000000000000000000000000..23250f99e1867e756ce157d84cf62eea0adde6e3
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Button/ActivationLog.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, 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_Button_ActivationLog extends ZendAfi_View_Helper_BaseHelper {
+
+  public function Button_ActivationLog() {
+    $val = '1';
+    $text = $this->_( 'Activer les logs sur les requêtes');
+
+    if ( Class_AdminVar::get( 'ACTIVATE_AUTH_LOG')) {
+      $val = '0';
+      $text = $this->_( 'Desactiver les logs sur les requêtes');
+    }
+
+    return $this->view->button((new Class_Button)
+                               ->setText($text)
+                               ->setUrl($this->view->url(['module' => 'admin',
+                                                    'controller' => 'index',
+                                                    'action' => 'adminvar_set',
+                                                    'cle' => 'ACTIVATE_AUTH_LOG',
+                                                    'valeur'  => $val])));
+  }
+}
diff --git a/library/templates/Intonation/Library/Record/DigitalResources.php b/library/templates/Intonation/Library/Record/DigitalResources.php
index 717b795eeeb7cb85061fe7ca269651a7efb04deb..44f2df363034ad434df7fa46fa99f7d4d6872237 100644
--- a/library/templates/Intonation/Library/Record/DigitalResources.php
+++ b/library/templates/Intonation/Library/Record/DigitalResources.php
@@ -57,7 +57,9 @@ class Intonation_Library_Record_DigitalResources {
                     ['class' => 'link_album',
                      'data-popup' => 'true']);
 
+
     $html .= $this->_view->renderAlbum($this->_record->getAlbum());
+
     $html .= $this->_view->recordAlbums($this->_record);
 
     if ('' == $html)
diff --git a/tests/application/modules/admin/controllers/AdminIndexControllerTest.php b/tests/application/modules/admin/controllers/AdminIndexControllerTest.php
index 61c99a18e85aced46f4941db2a1d07019ea29b5f..4e5cf71a55d0edcc10a30742ab53fc88ce15b7ce 100644
--- a/tests/application/modules/admin/controllers/AdminIndexControllerTest.php
+++ b/tests/application/modules/admin/controllers/AdminIndexControllerTest.php
@@ -307,7 +307,6 @@ class AdminIndexControllerAdminVarEditActionTest extends Admin_AbstractControlle
   }
 
 
-
   /** @test */
   public function postWithStylesReloadPopupShouldReopenEditSameKey() {
     $this->postDispatch('/admin/index/adminvaredit/cle/FACETTE_GENRE_LIBELLE/styles_reload/1/render/popup',
@@ -327,11 +326,13 @@ class AdminIndexControllerAdminVarEditCKEditorActionTest extends Admin_AbstractC
     parent::setUp();
   }
 
+
   /** @test */
   public function defaultValueShouldBeSet() {
     $this->assertEquals('Votre compte sera mis à jour dans un délai de 15 minutes après le retour anticipé du document.',Class_AdminVar::getValueOrDefault('DILICOM_PNB_LOAN_WARNING_MESSAGE'));
   }
 
+
   /** @test */
   public function editPageShouldContainsTitlePNBLoanMessage() {
     $this->fixture('Class_AdminVar',
@@ -344,6 +345,7 @@ class AdminIndexControllerAdminVarEditCKEditorActionTest extends Admin_AbstractC
                                       $this->_response->getBody());
   }
 
+
   /** @test */
   public function postEnrichTextShouldNotRemoveTags() {
     $this->fixture('Class_AdminVar',
@@ -357,8 +359,6 @@ class AdminIndexControllerAdminVarEditCKEditorActionTest extends Admin_AbstractC
     $this->dispatch('/admin/index/adminvaredit/cle/DILICOM_PNB_LOAN_WARNING_MESSAGE');
     $this->assertEquals('<b>don\'t use this, drm are dangerous!</b>', Class_AdminVar::get('DILICOM_PNB_LOAN_WARNING_MESSAGE'));
   }
-
-
 }
 
 
@@ -458,7 +458,6 @@ class AdminIndexControllerAdminVarEditWorkflowPostPermissionTest extends AdminIn
   }
 
 
-
   protected function _fixDynamicWorkflowPermission($id, $status_id, $label) {
     $this->fixture('Class_Permission', ['id' => $id,
                                         'code' => 'DYNAMIC_' . $status_id,
@@ -470,6 +469,7 @@ class AdminIndexControllerAdminVarEditWorkflowPostPermissionTest extends AdminIn
 
 
 
+
 class AdminIndexControllerAdminVarEditWorkflowWithUsedPostTest extends AdminIndexControllerAdminVarEditWithWorkflowTestCase {
   public function setUp() {
     parent::setUp();
@@ -496,6 +496,7 @@ class AdminIndexControllerAdminVarEditWorkflowWithUsedPostTest extends AdminInde
 
 
 
+
 class AdminIndexControllerAdminVarEditWorkflowGoSimpleWithUsedPostTest extends AdminIndexControllerAdminVarEditWithWorkflowTestCase {
   public function setUp() {
     parent::setUp();
@@ -579,10 +580,10 @@ abstract class AdminIndexControllerDilicomPnbIpAdressesTestCase extends Admin_Ab
 
     RessourcesNumeriquesFixtures::activateDilicom();
 
-
     Class_WebService_BibNumerique_Dilicom_Hub::setDefaultHttpClient($this->_http);
   }
 
+
   public function tearDown() {
     Class_WebService_BibNumerique_Dilicom_Hub::setDefaultHttpClient(null);
     RessourcesNumeriquesFixtures::deactivateDilicom();
@@ -636,6 +637,33 @@ class AdminIndexControllerAdminVarEditDilicomPnbIpAdressesTest extends AdminInde
 
 
 
+
+class AdminIndexControllerSetTestCase extends Admin_AbstractControllerTestCase{
+  /**
+   * @test
+   */
+  public function setVarShouldSetAdminVar(){
+    Class_AdminVar::set('ACTIVATE_AUTH_LOG',1);
+    $this->assertEquals( 1 , Class_AdminVar::get( 'ACTIVATE_AUTH_LOG'));
+    $this->dispatch('/admin/index/adminvar_set/cle/ACTIVATE_AUTH_LOG/valeur/0');
+    $this->assertEquals( 0, Class_AdminVar::get( 'ACTIVATE_AUTH_LOG'));
+  }
+
+
+  /**
+   * @test
+   */
+  public function unsetVarShouldActivateAdminVar(){
+    Class_AdminVar::set('ACTIVATE_AUTH_LOG',0);
+    $this->assertEquals( 0 , Class_AdminVar::get( 'ACTIVATE_AUTH_LOG'));
+    $this->dispatch('/admin/index/adminvar_set/cle/ACTIVATE_AUTH_LOG/valeur/1/');
+    $this->assertEquals( 1, Class_AdminVar::get( 'ACTIVATE_AUTH_LOG'));
+  }
+}
+
+
+
+
 class AdminIndexControllerAdminVarDilicomDeclareIpPostTest extends AdminIndexControllerDilicomPnbIpAdressesTestCase {
   /** @test */
   public function hubErrorMessageShouldAppearInNotification() {
@@ -696,7 +724,6 @@ class AdminIndexControllerAdminVarDilicomDeclareIpPostTest extends AdminIndexCon
 }
 
 
-
 /** @see http://forge.afi-sa.fr/issues/24383 */
 class AdminIndexControllerAdminVarEditTextReplacementsActionTest extends Admin_AbstractControllerTestCase {
 
@@ -776,6 +803,7 @@ class AdminIndexControllerAdminVarEditComboInvalidPostActionTest
 
 
 
+
 class AdminIndexControllerDisplayModifierNomDomaineTest extends AbstractControllerTestCase {
 
   /** @test */
@@ -793,6 +821,7 @@ class AdminIndexControllerDisplayModifierNomDomaineTest extends AbstractControll
     $this->assertXpathContentContains('//div[@class="modules"]//a', 'Modifier');
   }
 
+
   /** @test */
   public function ModifierNomDomaineShouldNotBePresent() {
     $user = $this->fixture('Class_Users', ['id' => 8,
@@ -821,13 +850,12 @@ abstract class AdminIndexControllerAdminVarEditSearchAlsoInTestCase extends Abst
                    ['id' => 'SEARCH_ALSO_IN',
                     'valeur' => json_encode(['site_label' => ['Jumel'],
                                              'site_url' => ['http://testing.fr?q=%s']])]);
-
-
   }
 }
 
 
 
+
 class AdminIndexControllerAdminVarEditSearchAlsoInTest
   extends AdminIndexControllerAdminVarEditSearchAlsoInTestCase {
 
@@ -876,6 +904,7 @@ class AdminIndexControllerAdminVarEditSearchAlsoInTest
 
 
 
+
 class AdminIndexControllerAdminVarEditSearchAlsoInPostTest
   extends AdminIndexControllerAdminVarEditSearchAlsoInTestCase {
 
@@ -902,17 +931,16 @@ class AdminIndexControllerAdminVarEditSearchAlsoInPostTest
 
 
 
+
 class AdminIndexControllerAdminVarEditHTTPSTest
   extends Admin_AbstractControllerTestCase  {
   protected $_storm_default_to_volatile = true;
   protected $oldServerName;
 
-
   public function setUp() {
     parent::setUp();
     $this->oldServerName = $_SERVER['SERVER_NAME'];
     $_SERVER['SERVER_NAME'] = "MyWebsite";
-
   }
 
 
@@ -922,6 +950,7 @@ class AdminIndexControllerAdminVarEditHTTPSTest
     $_SERVER['SERVER_NAME'] = $this->oldServerName;
   }
 
+
   /** @test */
   public function editAdminVarShouldPostHTTPS() {
     Class_AdminVar::set('FORCE_HTTPS', 0);
@@ -935,10 +964,7 @@ class AdminIndexControllerAdminVarEditHTTPSTest
     Class_AdminVar_ForceHTTPS::$FORCE_HTTPS = true;
     $this->dispatch('/admin/index/adminvaredit/cle/FORCE_HTTPS', true);
     $this->assertXPath('//form[@action="https://MyWebsite/admin/index/adminvaredit/cle/FORCE_HTTPS"]',$this->_response->getBody());
-
   }
-
-
 }
 
 
@@ -948,7 +974,6 @@ class AdminIndexControllerWithFormLinkTest extends Admin_AbstractControllerTestC
 
   protected $_storm_default_to_volatile = true;
 
-
   public function setUp() {
     parent::setUp();
     $super_admin = $this->fixture('Class_Users',
diff --git a/tests/application/modules/opac/controllers/CasServerControllerTest.php b/tests/application/modules/opac/controllers/CasServerControllerTest.php
index d8d9e4721001038f9d7c5b88b93b09c4655da254..bc0a894f581adaf077390034daff05a9457967c1 100644
--- a/tests/application/modules/opac/controllers/CasServerControllerTest.php
+++ b/tests/application/modules/opac/controllers/CasServerControllerTest.php
@@ -23,9 +23,11 @@
 class CasServerControllerValidateActionTest extends AbstractControllerTestCase {
   protected $session_file_contents_logged;
   protected $session_file_contents_nologin;
-
+  protected $_storm_default_to_volatile = true;
   public function setUp() {
     parent::setUp();
+    Class_adminvar::set('ACTIVATE_AUTH_LOG', 1);
+
     Storm_Cache::beVolatile();
     $user = new StdClass();
     $user->ID_USER=300;
@@ -42,15 +44,17 @@ class CasServerControllerValidateActionTest extends AbstractControllerTestCase {
   /** @test */
   public function requestWithNoServiceShouldRespondinvalidRequestFailureXML() {
     $this->dispatch('/opac/cas-server/validate?ticket=myticket');
-
     $this->assertContains('<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"><cas:authenticationFailure code="INVALID_REQUEST"></cas:authenticationFailure></cas:serviceResponse>', $this->_response->getBody());
+    $this->assertContains('INVALID_REQUEST',
+                          Class_Journal::lastOf(Class_Journal_CasRequestType::MY_TYPE)->getDetail(Class_Journal_RequestType::BOKEH_RESPONSE)->getValue());
+
   }
 
 
   /** @test */
   public function requestWithNoTicketShouldRespondinvalidRequestFailureXML() {
     $this->dispatch('/opac/cas-server/validate?service='.urlencode('http://test.com'));
-    $this->assertContains('<cas:authenticationFailure code="INVALID_REQUEST">', $this->_response->getBody());
+    $this->assertContains('<cas:authenticationFailure code="INVALID_REQUEST">', Class_Journal::lastOf(Class_Journal_CasRequestType::MY_TYPE)->getDetail(Class_Journal_CasRequestType::BOKEH_RESPONSE)->getValue());
   }
 
 
@@ -58,6 +62,8 @@ class CasServerControllerValidateActionTest extends AbstractControllerTestCase {
   public function requestWithInvalidTicketShouldRespondInvalidTicketFailureXML() {
     $this->dispatch('/opac/cas-server/validate?ticket=STmarchepo&service=http://test.com', true);
     $this->assertContains('<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"><cas:authenticationFailure code="INVALID_TICKET"><![CDATA[ Ticket STmarchepo not recognized]]></cas:authenticationFailure></cas:serviceResponse>', $this->_response->getBody());
+    $this->assertContains('INVALID_TICKET', Class_Journal::lastOf(Class_Journal_CasRequestType::MY_TYPE)->getDetail(Class_Journal_RequestType::BOKEH_RESPONSE)->getValue());
+
   }
 
 
@@ -75,6 +81,8 @@ class CasServerControllerValidateActionTest extends AbstractControllerTestCase {
                             $ticket,
                             urlencode('http://test.com')));
     $this->assertContains("<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>\n  <cas:authenticationSuccess>\n    <cas:user>300</cas:user>\n    <cas:proxyGrantingTicket>".$ticket."</cas:proxyGrantingTicket>\n  </cas:authenticationSuccess>\n</cas:serviceResponse>", $this->_response->getBody());
+    $this->assertContains('<cas:authenticationSuccess>', Class_Journal::lastOf(Class_Journal_CasRequestType::MY_TYPE)->getDetail(Class_Journal_RequestType::BOKEH_RESPONSE)->getValue());
+
   }
 
 
@@ -158,6 +166,9 @@ class CasServerControllerValidateActionTest extends AbstractControllerTestCase {
   public function loginOnCasOneZeroWithoutOpenedSessionShouldDisplayLoginForm() {
     ZendAfi_Auth::getInstance()->clearIdentity();
     $this->dispatch('/opac/cas-server-v10/login?service=http://test.com', true);
+    $this->assertContains('/opac/cas-server-v10/login?service=http://test.com',
+                          Class_Journal::lastOf(Class_Journal_CasRequestType::MY_TYPE)->getDetail(Class_Journal_RequestType::REQUEST_URL)->getValue());
+
     $this->assertXPath('//form//input[@name="password"]');
   }
 
@@ -166,6 +177,9 @@ class CasServerControllerValidateActionTest extends AbstractControllerTestCase {
   public function logoutOnCasOneZeroShouldClearIdentityAndDisplayThatYouHaveBeenDisconnected() {
     $this->dispatch('/opac/cas-server-v10/logout', true);
     $this->assertXPathContentContains('//p', 'Vous avez été déconnecté');
+    $this->assertContains('/opac/cas-server-v10/logout',
+                          Class_Journal::lastOf(Class_Journal_CasRequestType::MY_TYPE)->getDetail(Class_Journal_RequestType::REQUEST_URL)->getValue());
+
     $this->assertEmpty(ZendAfi_Auth::getInstance()->getIdentity());
   }
 
diff --git a/tests/db/UpgradeDBTest.php b/tests/db/UpgradeDBTest.php
index acf9e303ec9aefee0279772a1921766612aaeb80..379433965a0973d05b47e8dc3835508c51af8b23 100644
--- a/tests/db/UpgradeDBTest.php
+++ b/tests/db/UpgradeDBTest.php
@@ -4824,6 +4824,7 @@ class UpgradeDB_437_Test extends UpgradeDBTestCase {
 
 
 
+
 class UpgradeDB_438_Test extends UpgradeDBTestCase {
   public function prepare() {
     $this->silentQuery('ALTER TABLE `codif_thesaurus` drop column index_labels');
@@ -4850,3 +4851,76 @@ class UpgradeDB_439_Test extends UpgradeDBTestCase {
   }
 
 }
+
+
+
+
+class UpgradeDB_440_Test extends UpgradeDBTestCase {
+  public function prepare() {
+    $this->silentQuery('alter table user_api_tokens drop column refresh_token');
+    $this->silentQuery('alter table user_api_tokens drop column expirated_at');
+    $this->silentQuery('alter table user_api_tokens drop column created_at');
+    $this->silentQuery('alter table user_api_tokens drop column created_at');
+    $this->query("insert into loan_pnb (record_origin_id, subscriber_id, user_id, loan_link) values('TestDilicom' , '1234','1','https://pnb-dilicom.centprod.com/v2//link/3056000551506/LOAN/4/9782404003566-P4BRGR1CXJI4FZBRYLO64KEJZT7F02JZ.do?userAgentId=0932')");
+    $this->query("insert into loan_pnb (record_origin_id, subscriber_id, user_id, loan_link) values('TestDilicom2' , '1234','1','https://pnb-dilicom.centprod.com/v2//link/3056000551506/LOAN/4/9782404003566-P4BRGR1CXJI4FZBRYLO64KEJZT7F02JZ.do?userAgentId=0932&other=wedontcare')");
+    $this->query("insert into loan_pnb (record_origin_id, subscriber_id, user_id, loan_link) values('TestDilicom3' , '1234','1','https://pnb-dilicom.centprod.com/v2//link/3056000551506/LOAN/4/9782404003566-P4BRGR1CXJI4FZBRYLO64KEJZT7F02JuserAgentId=0932')");
+
+  }
+
+
+  public function tearDown() {
+    $this->query('delete from loan_pnb where record_origin_id="TestDilicom"');
+    $this->query('delete from loan_pnb where record_origin_id="TestDilicom2"');
+    $this->query('delete from loan_pnb where record_origin_id="TestDilicom3"');
+  }
+
+
+  /** @test */
+  public function userApiShouldHaveColumnRefreshToken() {
+    $this->assertFieldType('user_api_tokens', 'refresh_token', 'varchar(255)');
+  }
+
+
+  /** @test */
+  public function userApiShouldHaveColumnExpirationDate() {
+    $this->assertFieldType('user_api_tokens', 'expired_at', 'datetime');
+  }
+
+
+  /** @test */
+  public function userApiShouldHaveColumnCreatedAt() {
+    $this->assertFieldType('user_api_tokens', 'created_at', 'datetime');
+  }
+
+
+  /** @test */
+  public function  identityShouldHaveColumnCreatedAt() {
+    $this->assertFieldType('identity_client', 'client_id', 'varchar(255)');
+  }
+
+
+  /** @test */
+  public function  loanLinkShouldHaveRemovedUserAgentId() {
+      $row = $this->query('select loan_link from loan_pnb  where record_origin_id="TestDilicom"')
+           ->fetch();
+      $this->assertTrue("https://pnb-dilicom.centprod.com/v2//link/3056000551506/LOAN/4/9782404003566-P4BRGR1CXJI4FZBRYLO64KEJZT7F02JZ.do" === $row['loan_link']);
+  }
+
+
+  /** @test */
+  public function  loan2LinkShouldHaveRemovedUserAgentId() {
+    $row = $this->query('select loan_link from loan_pnb  where record_origin_id="TestDilicom2"')
+                ->fetch();
+    $this->assertTrue("https://pnb-dilicom.centprod.com/v2//link/3056000551506/LOAN/4/9782404003566-P4BRGR1CXJI4FZBRYLO64KEJZT7F02JZ.do" === $row['loan_link']);
+
+  }
+
+
+  /** @test */
+  public function  loan3LinkShouldNotHaveRemovedUserAgentId() {
+    $row = $this->query('select loan_link from loan_pnb  where record_origin_id="TestDilicom3"')
+                ->fetch();
+    $this->assertTrue("https://pnb-dilicom.centprod.com/v2//link/3056000551506/LOAN/4/9782404003566-P4BRGR1CXJI4FZBRYLO64KEJZT7F02JuserAgentId=0932" === $row['loan_link']);
+
+  }
+}
diff --git a/tests/fixtures/DilicomFixtures.php b/tests/fixtures/DilicomFixtures.php
index 72e5197026f2adaa8373470b25d8e2d07134c057..eb48aff2fc7d7c848e5cdb6fceb73a3f1c10e8be 100644
--- a/tests/fixtures/DilicomFixtures.php
+++ b/tests/fixtures/DilicomFixtures.php
@@ -241,7 +241,7 @@ class DilicomFixtures {
                             'quantity' => 4,
                             'usage_constraints' => $constraints]);
 
-    return $this->fixture('Class_Album',
+    $album = $this->fixture(Class_Album::class,
                           ['id' => 3,
                            'titre' => 'Totem et Thora',
                            'id_origine' => 'Dilicom-88817216',
@@ -249,6 +249,9 @@ class DilicomFixtures {
                            'url_origine' => 'https://url_dilicom.org/ressource/id/1',
                            'type_doc_id' => Class_TypeDoc::DILICOM,
                            'items' => [$item]])
-                ->addAuthor('Raphaël Draï');
+
+                  ->addAuthor('Raphaël Draï');
+    $album->save();
+    return $album;
   }
 }
diff --git a/tests/scenarios/AdminMenuComposition/AdminMenuCompositionTest.php b/tests/scenarios/AdminMenuComposition/AdminMenuCompositionTest.php
index 4014735c97a50e98ac92b1fa92d8216eac197abc..0b7fbeadf00f315ed18cdaf55b7eba46fa52f2bd 100644
--- a/tests/scenarios/AdminMenuComposition/AdminMenuCompositionTest.php
+++ b/tests/scenarios/AdminMenuComposition/AdminMenuCompositionTest.php
@@ -227,7 +227,7 @@ class AdminMenuCompositionFormForSysadmTest extends AdminMenuCompositionFormTest
        ['Utilisateurs', '/admin/users'],
        ['Groupes', '/admin/usergroup'],
        ['Multimedia', '/admin/multimedia'],
-       ['Fournisseurs d\'identités', '/admin/identity-providers'],
+       ['Fédération d\'identités', '/admin/identity-providers'],
 
        ['Avis', '/admin/federation-reviews'],
 
@@ -341,7 +341,7 @@ class AdminMenuCompositionFormForAdminPortailTest extends AdminMenuCompositionFo
        ['Utilisateurs', '/admin/users'],
        ['Groupes', '/admin/usergroup'],
        ['Multimedia', '/admin/multimedia'],
-       ['Fournisseurs d\'identités', '/admin/identity-providers'],
+       ['Fédération d\'identités', '/admin/identity-providers'],
 
        ['Avis', '/admin/federation-reviews'],
 
@@ -389,7 +389,6 @@ class AdminMenuCompositionFormForAdminPortailTest extends AdminMenuCompositionFo
        ['Afficher les icones d\'administration', 'show_admin_icons'],
        ['Éditeur CSS', 'css_editor'],
        ['Niveau expert', 'admin_level'],
-
        ['Synchronisation du CSS avec GIT', '/admin/index/update-skin'],
       ];
   }
@@ -1522,6 +1521,7 @@ class AdminMenuCompositionAddNewUsersTestCase extends Admin_AbstractControllerTe
          '/admin/users' => 'Utilisateurs',
          '/admin/usergroup' => 'Groupes',
          '/admin/url-manager' => 'Contrôle des URL',
+         '/admin/identity-providers' => 'Fédération d\'identité',
          '/admin/index/adminvar' => 'Variables'],
 
         ['/admin' => 'Accès pro',
diff --git a/tests/scenarios/IdentityClient/IdentityClientAdminTest.php b/tests/scenarios/IdentityClient/IdentityClientAdminTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9f8f46fee943cbb52a5f9e589f9a90c92aa6b7bc
--- /dev/null
+++ b/tests/scenarios/IdentityClient/IdentityClientAdminTest.php
@@ -0,0 +1,209 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, 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 IdentityClientAdminIndexTest extends Admin_AbstractControllerTestCase {
+  protected $_storm_default_to_volatile = true;
+  public function setUp() {
+    parent::setUp();
+    $this->fixture( Class_IdentityClient::class,
+                   [ 'id' => 1,
+                    'client_id'=>'MyBibApp',
+                    'type'  => 'oauth2',
+                    'label'  => 'MyBibApp',
+                    'active' => 1,
+                    'config'  => "{}"
+                   ]);
+    $this->dispatch('/admin/identity-clients/index');
+  }
+
+
+  /**
+   * @test
+   */
+  public function h1ShouldBeIdentityClient() {
+    $this->assertXPathContentContains('//h1', 'Clients d\'identité');
+  }
+
+
+  /**
+   * @test
+   */
+  public function mybibaddShouldBeDisplayed(){
+    $this->assertXPathContentContains('//div','MyBibApp');
+  }
+}
+
+
+
+
+class identityClientAdminAddTest extends Admin_AbstractControllerTestCase {
+  protected $_storm_default_to_volatile = true;
+  public function setUp() {
+    parent::setUp();
+    $this->fixture( Class_IdentityClient::class,
+                   [ 'id' => 1,
+                    'client_id'=>'MyBibApp',
+                    'type'  => 'oauth2',
+                    'label'  => 'MyBibApp',
+                    'active' => 1,
+                    'config'  => "{}"
+                   ]);
+    $this->dispatch('/admin/identity-clients/add');
+  }
+
+
+  /**
+   * @test
+   */
+  public function clientIdShouldBeInput() {
+    $this->assertXPath('//input[@id="client_id"]',$this->_response->getBody());
+  }
+}
+
+
+
+
+class identityClientAdminDeleteTest extends Admin_AbstractControllerTestCase {
+  protected $_storm_default_to_volatile = true;
+  public function setUp() {
+    parent::setUp();
+    $this->fixture( Class_IdentityClient::class,
+                   [ 'id' => 1,
+                    'client_id'=>'MyBibApp',
+                    'type'  => 'oauth2',
+                    'label'  => 'MyBibApp',
+                    'active' => 1,
+                    'config'  => "{}"
+                   ]);
+    $this->dispatch('/admin/identity-clients/delete/id/1');
+  }
+
+
+  /**
+   * @test
+   */
+  public function clientShouldBeDeleted() {
+    $this->assertNull(Class_IdentityClient::find(1));
+    $this->assertRedirect();
+  }
+}
+
+
+
+
+class identityClientAdminUserApiTokensTest extends Admin_AbstractControllerTestCase {
+  protected $_storm_default_to_volatile = true;
+  public function setUp() {
+    parent::setUp();
+    $this->fixture( Class_IdentityClient::class,
+                   [ 'id' => 1,
+                    'client_id'=>'MyBibApp',
+                    'type'  => 'oauth2',
+                    'label'  => 'MyBibApp',
+                    'active' => 1,
+                    'config'  => "{}"
+                   ]);
+    $this->fixture(Class_User_ApiToken::class,
+                   ['id'=>'1',
+                    'user_id'  => 1,
+                    'token'=>'1234679',
+                    'created_at' =>'2022-09-01 10:00:00',
+                    'expired_at' => '2022-09-03 10:00:00',
+                    'client_id'=> 'MyBibApp']);
+
+    $this->dispatch('/admin/user-api-tokens/show/client_id/MyBibApp');
+  }
+
+
+  /**
+   * @test
+   */
+  public function tokenShouldBeDisplayed() {
+    $this->assertXPath('//td', '1234679');
+  }
+
+
+  /**
+   * @test
+   */
+  public function mybibappShouldBeDisplayed() {
+    $this->assertXPath('//td', 'MyBibApp');
+  }
+
+
+  /**
+   * @test
+   */
+  public function expiredAtShouldBeDisplayed() {
+    $this->assertXPath('//td', '2022-09-01 10:00:00');
+  }
+
+}
+
+
+
+
+class identityClientAdminUserApiTokensDeleteTest extends Admin_AbstractControllerTestCase {
+  protected $_storm_default_to_volatile = true;
+  public function setUp() {
+    parent::setUp();
+    $this->fixture( Class_IdentityClient::class,
+                   [ 'id' => 1,
+                    'client_id'=>'MyBibApp',
+                    'type'  => 'oauth2',
+                    'label'  => 'MyBibApp',
+                    'active' => 1,
+                    'config'  => "{}"
+                   ]);
+    $puppy = $this->fixture('Class_Users',
+                            ['id' => 345,
+                             'pseudo' => 'Puppy',
+                             'date_fin' => '2018-02-12',
+                             'login' => 'puppy',
+                             'password' => 'opied',
+                             'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                             'idabon' => '234',
+                             'id_site' => 1]);
+
+    $this->fixture('Class_User_ApiToken',
+                   ['id' => 1,
+                    'token' => 'nonos',
+                    'client_id' => 'MyBibApp',
+                    'user' => $puppy]);
+
+    $this->fixture('Class_User_ApiToken',
+                   ['id' => 2,
+                    'token' => 'nonos',
+                    'client_id' => 'MyOtherApp',
+                    'user' => $puppy]);
+
+    $this->dispatch('/admin/user-api-tokens/delete-all/client_id/MyBibApp');
+  }
+
+
+  /**
+   * @test
+   */
+  public function  allTokenShouldBeDeletedForMyBibappButNotOtherApp() {
+    $this->assertEmpty(Class_User_ApiToken::findAllBy([ 'client_id' => 'MyBibApp']));
+    $this->assertNotEmpty(Class_User_ApiToken::findAllBy([ 'client_id' => 'MyOtherApp']));
+  }
+}
diff --git a/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php b/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php
index fd74aa17e74531b4043b882ff5b401c0b6fa123f..8fdd4aa22f6bbbf3faf7865547b7709ee55d8c93 100644
--- a/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php
+++ b/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php
@@ -62,7 +62,7 @@ class IdentityProviderAdminIndexTest extends IdentityProviderAdminTestCase {
 
 
   /** @test */
-  public function h1ShouldBeIdentityServer() {
+  public function h1ShouldBeIdentityClient() {
     $this->assertXPathContentContains('//h1', 'Fournisseurs d\'identité');
   }
 
diff --git a/tests/scenarios/MobileApplication/RestfulApiTest.php b/tests/scenarios/MobileApplication/RestfulApiTest.php
index b1991a26d4f4ac5caa89bbdd3f01a2070d7543fd..500a0b06c1828c2966b199ab6eafe9a339a4587b 100644
--- a/tests/scenarios/MobileApplication/RestfulApiTest.php
+++ b/tests/scenarios/MobileApplication/RestfulApiTest.php
@@ -18,6 +18,9 @@
  * 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/DilicomFixtures.php';
+
 abstract class Scenario_MobileApplication_RestfulApi_UserAccountTestCase extends AbstractControllerTestCase {
   protected
     $_storm_default_to_volatile = true,
@@ -26,13 +29,14 @@ abstract class Scenario_MobileApplication_RestfulApi_UserAccountTestCase extends
 
   public function setUp() {
     parent::setUp();
-
     $_SERVER['HTTPS'] = 'on';
-
+    Class_User_ApiToken::setTimeSource(new TimeSourceForTest('2019-03-15 15:48:03'));
     Class_CommSigb::setInstance($this->_sigb = $this->mock());
+    Class_AdminVar::set('ACTIVATE_AUTH_LOG', '1');
 
     $this->fixture('Class_Bib',
                    ['id' => 1,
+                    'gln' => '3056032160004',
                     'libelle' => 'Vaise-sur-Ravoire']);
 
     $puppy = $this->fixture('Class_Users',
@@ -44,11 +48,58 @@ abstract class Scenario_MobileApplication_RestfulApi_UserAccountTestCase extends
                              'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
                              'idabon' => '234',
                              'id_site' => 1]);
+    $this->fixture( Class_IdentityClient::class,
+                   [ 'id' => 1,
+                    'client_id'=>'MyBibApp',
+                    'type'  => 'Class_IdentityClient_MyBibApp',
+                    'label'  => 'MyBibApp',
+                    'active' => 1,
+                    'config'  => "{
+                       'client_secret',
+                       'redirect_url',
+                       'disable_state',
+                       'disable_response_type',
+                       'scope'}"
+                   ]);
+    $this->fixture( Class_IdentityClient::class,
+                   [ 'id' => 2,
+                    'client_id'=>'PNB',
+                    'type'  => 'Class_IdentityClient_Dilicom',
+                    'label'  => 'PNB',
+                    'active' => 1,
+                    'config'  => "{
+                       'client_secret',
+                       'redirect_url',
+                       'disable_state',
+                       'disable_response_type',
+                       'scope'}"
+                   ]);
+
+    $this->fixture( Class_IdentityClient::class,
+                   [ 'id' => 3,
+                    'client_id'=>'pc',
+                    'type'  => 'Class_IdentityClient_OAuth2',
+                    'label'  => 'PortailCitoyen',
+                    'active' => 1,
+                    'config'  => "{
+                       'client_secret'  => '',,
+                       'redirect_url'  =>'myredirect',
+                       'disable_state' => true,
+                       'disable_response_type'  => true,
+                       'scope'}"
+                   ]);
+
 
     $this->fixture('Class_User_ApiToken',
                    ['id' => 1,
                     'token' => 'nonos',
-                    'client_id' => 'My mobile app',
+                    'client_id' => 'MyBibApp',
+                    'user' => $puppy]);
+    $this->fixture('Class_User_ApiToken',
+                   ['id' => 3,
+                    'token' => 'exptoken',
+                    'client_id' => 'MyBibApp',
+                    'expired_at'  => '2022-01-01',
                     'user' => $puppy]);
 
     $this->_potter = new Class_WebService_SIGB_Emprunt('12', new Class_WebService_SIGB_Exemplaire(123));
@@ -64,23 +115,22 @@ abstract class Scenario_MobileApplication_RestfulApi_UserAccountTestCase extends
 
     $alice = new Class_WebService_SIGB_Emprunt('13', new Class_WebService_SIGB_Exemplaire(456));
     $alice
-      ->setDateRetour(date('d/m/Y', strtotime('tomorrow')))
+      ->setDateRetour( '01-03-2022')
       ->getExemplaire()
       ->setTitre('Alice')
       ->setCodeBarre('A-1234');
 
-
     $afrodeezia = new Class_WebService_SIGB_Reservation('18', new Class_WebService_SIGB_Exemplaire(938));
     $afrodeezia
-            ->setBibliotheque('Annecy')
-            ->setEtat('En attente')
-            ->getExemplaire()
-            ->setCodeBarre('M-456')
-            ->setNoticeOPAC($this->fixture('Class_Notice',
-                                           ['id' => 83,
-                                            'url_vignette' => 'http://img.com/marcus.jpg',
-                                            'titre_principal' => 'Afrodeezia',
-                                            'auteur_principal' => 'Marcus Miller' ]));
+      ->setBibliotheque('Annecy')
+      ->setEtat('En attente')
+      ->getExemplaire()
+      ->setCodeBarre('M-456')
+      ->setNoticeOPAC($this->fixture('Class_Notice',
+                                     ['id' => 83,
+                                      'url_vignette' => 'http://img.com/marcus.jpg',
+                                      'titre_principal' => 'Afrodeezia',
+                                      'auteur_principal' => 'Marcus Miller' ]));
 
     $this->fixture('Class_Exemplaire',
                    ['id' => 2,
@@ -88,10 +138,9 @@ abstract class Scenario_MobileApplication_RestfulApi_UserAccountTestCase extends
                     'bib' => $this->fixture('Class_Bib', ['id' => 3, 'libelle' => 'Annecy']),
                     'id_notice' => 83]);
 
-
     $emprunteur = (new Class_WebService_SIGB_Emprunteur(345, 'puppy'))
-                      ->empruntsAddAll([$this->_potter, $alice])
-                      ->reservationsAddAll([$afrodeezia]);
+      ->empruntsAddAll([$this->_potter, $alice])
+      ->reservationsAddAll([$afrodeezia]);
     Storm_Cache::beVolatile();
     Class_WebService_SIGB_EmprunteurCache::newInstance()->save($puppy, $emprunteur);
 
@@ -99,24 +148,43 @@ abstract class Scenario_MobileApplication_RestfulApi_UserAccountTestCase extends
       ->setFicheSigb(['fiche' => $emprunteur ])
       ->assertSave();
 
-
     Class_WebService_BibNumerique_Dilicom_Hub::setDefaultHttpClient($this->mock()
                                                                     ->whenCalled('open_url')
                                                                     ->answers(''));
-    $this->fixture('Class_Loan_Pnb',
-                   ['id' => 5,
-                    'subscriber_id' => $puppy->getIdabon(),
-                    'ongoing' => true,
-                    'expected_return_date' => '2032-05-02T18:14:14+02:00',
-                    'loan_date' => '2016-06-13T15:10:02+02:00',
-                    'album' => $this->fixture('Class_Album',
-                                              ['id' => 4,
-                                               'notice_id' => 5,
-                                               'id_origine' => 'Dilicom-88817216',
-                                               'titre' => 'Pinocchio'])->addAuthor('Collodi')
-                   ]);
-
-
+    RessourcesNumeriquesFixtures::activateDilicom();
+    $book = (new DilicomFixtures())->albumTotemThora();
+    $book->setMatiere('Roman')
+         ->addEditor("Ed. de l'Olivier. Paris")
+         ->addCollection('Roman de gare')
+         ->addPosterURI('http://mylink/toetto.jpg');
+
+    $book->setFormat('A103;E200');
+    $book->setISBN('9782823608489');
+    $album =$this->fixture('Class_Album',
+                           ['id' => 4,
+                            'notice_id' => 5,
+                            'id_origine' => 'Dilicom-88817216',
+                            'titre' => 'Pinocchio'])
+                 ->addAuthor('Collodi');
+    $album->save();
+
+    $book->setVisible(true)
+         ->setStatus(Class_Album::STATUS_VALIDATED)
+         ->assertSave();
+    $book->index();
+
+    $loan = $this->fixture('Class_Loan_Pnb',
+                           ['id' => 5,
+                            'subscriber_id' => $puppy->getIdabon(),
+                            'ongoing' => true,
+                            'order_line_id' => '1234',
+                            'expected_return_date' => '2032-05-02 18:14:14',
+                            'loan_date' => '2016-06-13 15:10:02',
+                            'options' =>'LCP',
+                            'loan_link'=> 'https://pnb-test.centprod.com/v3//link/3056000302801/LOAN/303/9791033159216-AZ585E8AKPX5HZZ86D4PUWPL6X5P9I7K.do',
+                           ]);
+    $loan->setAlbum($book);
+    $loan->assertSave();
 
     ZendAfi_Auth::getInstance()->clearIdentity();
   }
@@ -132,6 +200,83 @@ abstract class Scenario_MobileApplication_RestfulApi_UserAccountTestCase extends
 
 
 
+class Scenario_MobileApplication_RestfulApi_UserPnbLoanWithTokenTest extends Scenario_MobileApplication_RestfulApi_UserAccountTestCase  {
+  protected $_json,
+    $_storm_default_to_volatile=true;
+  public function setUp() {
+    parent::setUp();
+    Class_Loan_pnb::setTimeSource(new TimeSourceForTest('2019-03-15 15:48:03'));
+  }
+
+
+  /**
+   * @test
+   */
+  public function apiLoanTokenWithoutValidConfigServer(){
+    Class_User_ApiToken::getLoader()->setTimeSource(new TimeSourceForTest('2019-03-15 15:48:03'));
+    Class_IdentityClient::find(2)->setActive(false)->save();
+
+    $this->fixture('Class_User_ApiToken',
+                   ['id' => 3,
+                    'token' => 'mytoken',
+                    'refresh_token'=>'1234',
+                    'client_id' => 'PNB',
+                    'expiration_date' => '2027-01-01 20:00:00',
+                    'user_id' => 345]);
+    try{
+      $this->dispatch( '/api/user/pnbloans',
+                      true,
+                      [ "Authorization" =>"Bearer mytoken" ,
+                       "Content-Type" => "application/json"]);
+    }  catch(Zend_Controller_Action_Exception $e) {
+
+      $this->assertEquals('Client configuration missing for PNB',Class_Journal::lastOf(Class_Journal_OauthRequestType::MY_TYPE)->getDetail(Class_Journal_RequestType::BOKEH_RESPONSE)->getValue()) ;
+      $this->assertEquals(403, $e->getCode());
+    }
+  }
+
+
+
+  /** @test */
+  public function responseShouldContainsPnbLoan() {
+    $this->dispatch( '/api/user/pnbloans',
+                    true,
+                    [ "Authorization" =>"Bearer nonos" ,
+                     "Content-Type" => "application/json"]);
+    $this->_json = json_decode($this->_response->getBody(), true);
+
+    $this->assertEquals(['loanId' => '345_5',
+                         'userId' => '234',
+                         'orderLineId' => '1234',
+                         'loanhLink'=> 'https://pnb-test.centprod.com/v3//link/3056000302801/LOAN/303/9791033159216-AZ585E8AKPX5HZZ86D4PUWPL6X5P9I7K.do',
+                         'description'=>'',
+                         "standardTitle"=> "Totem et Thora",
+                         'author' => 'Raphaël Draï',
+                         "frontCoverMedium"=>
+                         "http://mylink/toetto.jpg",
+                         "gtin13"=> "9782823608489",
+                         "loanerGln"=> "3056032160004",
+                         "epubTechnicalProtection"=> "LCP",
+                         "imprintName"=> 'Ed. de l\'Olivier. Paris',
+                         "collection"=> "Roman de gare",
+                         "categoryClil"=> "Roman",
+                         "contributors"=> [],
+                         "productFormDetails"=> [
+                                                 "A103",
+                                                 "E200",
+                                                 "AN"
+                         ],
+                         'beginDate' =>'2016-06-13T15:10:02Z',
+                         'endDate' => '2032-05-02T18:14:14Z',
+
+                         ],
+                        $this->_json['loans'][0]);
+  }
+}
+
+
+
+
 class Scenario_MobileApplication_RestfulApi_UserAccountLoansWithTokenTest extends Scenario_MobileApplication_RestfulApi_UserAccountTestCase {
   protected
     $_json;
@@ -164,25 +309,33 @@ class Scenario_MobileApplication_RestfulApi_UserAccountLoansWithTokenTest extend
                          'loaned_by' => 'puppy',
                          'library' => 'Annecy',
                          'record' => [ 'id' => '34',
-                                       'thumbnail' => 'http://img.com/potter.jpg' ]
+                                      'thumbnail' => 'http://img.com/potter.jpg' ]
                          ],
-                        $this->_json['loans'][0]);
+                        $this->_json['loans'][1]);
   }
 
 
   /** @test */
   public function responseShouldContainsPinocchioPnbLoan() {
     $this->assertEquals(['id' => '345_5',
-                         'title' => 'Pinocchio',
-                         'author' => 'Collodi',
+                         'title' => 'Totem et Thora',
+                         'author' => 'Raphaël Draï',
                          'date_due' => '2032-05-02',
                          'loaned_by' => 'puppy',
-                         'library' => 'Vaise-sur-Ravoire'
+                         'library' => 'Vaise-sur-Ravoire',
+                         'record' => [ 'id' => 84]
+
                          ],
                         $this->_json['loans'][2]);
   }
 
 
+  /** @test */
+  public function jsonShouldBeValid() {
+    $this->assertTrue(json_last_error() == JSON_ERROR_NONE);
+  }
+
+
   /** @test */
   public function responseHeaderContentTypeShouldBeApplicationJson() {
     $this->assertArraySubset(['name' => 'Content-Type',
@@ -247,14 +400,11 @@ class Scenario_MobileApplication_RestfulApi_UserAccountRenewTest extends Scenari
                          'error' => 'Prêt introuvable'],
                         json_decode($this->_response->getBody(), true));
   }
-
 }
 
 
 
 
-
-
 class Scenario_MobileApplication_RestfulApi_UserAccountLoansWithoutTokenTest extends Scenario_MobileApplication_RestfulApi_UserAccountTestCase {
   /** @test */
   public function withoutAuthorizationShouldAnswerInvalidRequest() {
@@ -262,8 +412,10 @@ class Scenario_MobileApplication_RestfulApi_UserAccountLoansWithoutTokenTest ext
                     false,
                     ["Content-Type" => "application/json"]);
 
+
     $this->assertEquals(['error' => 'invalid_request',
-                         'message' => 'Autorisation non spécifiée'],
+                         'message' => 'Autorisation non spécifiée',
+                         'error_description' => 'Autorisation non spécifiée'],
                         json_decode($this->_response->getBody(), true));
   }
 
@@ -276,7 +428,8 @@ class Scenario_MobileApplication_RestfulApi_UserAccountLoansWithoutTokenTest ext
                      "Content-Type" => "application/json"]);
 
     $this->assertEquals(['error' => 'invalid_request',
-                         'message' => 'Jeton d\'autorisation non fourni'],
+                         'message' => 'Jeton d\'autorisation non fourni',
+                         'error_description' => 'Jeton d\'autorisation non fourni'],
                         json_decode($this->_response->getBody(), true));
   }
 
@@ -289,7 +442,9 @@ class Scenario_MobileApplication_RestfulApi_UserAccountLoansWithoutTokenTest ext
                      "Content-Type" => "application/json"]);
 
     $this->assertEquals(['error' => 'invalid_request',
-                         'message' => 'Jeton d\'autorisation invalide'],
+                         'message' => 'Jeton d\'autorisation invalide',
+                         'error_description' => 'Jeton d\'autorisation invalide',
+                         ],
                         json_decode($this->_response->getBody(), true));
   }
 
@@ -299,15 +454,17 @@ class Scenario_MobileApplication_RestfulApi_UserAccountLoansWithoutTokenTest ext
     $this->fixture('Class_User_ApiToken',
                    ['id' => 2,
                     'token' => 'veget@ble',
+                    'client_id' =>  'MyBibApp',
                     'user_id' => 987]);
 
     $this->dispatch('/api/user/loans',
                     false,
                     ["Authorization" => 'Bearer veget@ble',
                      "Content-Type" => "application/json"]);
-
+    $this->assertEquals(403, $this->_response->getHttpResponseCode());
     $this->assertEquals(['error' => 'invalid_request',
-                         'message' => 'Utilisateur non trouvé'],
+                         'message' => 'Utilisateur non trouvé',
+                         'error_description' => 'Utilisateur non trouvé'],
                         json_decode($this->_response->getBody(), true));
   }
 
@@ -322,7 +479,8 @@ class Scenario_MobileApplication_RestfulApi_UserAccountLoansWithoutTokenTest ext
                      "Content-Type" => "application/json"]);
 
     $this->assertEquals(['error' => 'invalid_request',
-                         'message' => 'Protocole HTTPS obligatoire'],
+                         'message' => 'Protocole HTTPS obligatoire',
+                         'error_description' => 'Protocole HTTPS obligatoire'],
                         json_decode($this->_response->getBody(), true));
   }
 
@@ -343,7 +501,7 @@ class Scenario_MobileApplication_RestfulApi_UserAccountLoansWithoutTokenTest ext
                               'loaned_by' => 'puppy',
                               'library' => 'Annecy'
                               ],
-                             $loans['loans'][0]);
+                             $loans['loans'][1]);
   }
 
 
@@ -361,27 +519,28 @@ class Scenario_MobileApplication_RestfulApi_UserAccountLoansWithoutTokenTest ext
                               'loaned_by' => 'puppy',
                               'library' => 'Annecy'
                               ],
-                             $loans['loans'][0]);
+                             $loans['loans'][1]);
   }
 }
 
 
 
+
 class Scenario_MobileApplication_RestfulApi_UserAccountOAuthForLoginErrorsTest extends Scenario_MobileApplication_RestfulApi_UserAccountTestCase {
   public function wrongUrls() {
     return [
-            ['/auth/oauth/response_type/code/client_id/My%20mobile%20app/redirect_uri/'],
-            ['/auth/oauth/response_type/code/client_id/My%20mobile%20app/'],
-            ['/auth/oauth/response_type/code/redirect_uri/http%3A%2F%2Fsomewhere.com'],
-            ['/auth/oauth/response_type/something/client_id/My%20mobile%20app/redirect_uri/http%3A%2F%2Fsomewhere.com'],
+            ['/auth/oauth/response_type/code/client_id/MyBibApp/redirect_uri/'],
+            ['/auth/oauth/response_type/code/client_id/MyBibApp/'],
+            ['/auth/oauth/response_type/code/redirect_uri/http%3A%2F%2Fsomewhere.com']
     ];
   }
 
+
   /**
    * @dataProvider wrongUrls
    * @test
    */
-  public function withIncompleUrlShouldError400BadRequest($url) {
+  public function withIncompleUrlForResponseTypeCodeShouldError400BadRequest($url) {
     try {
       $this->dispatch($url, true);
     }  catch(Zend_Controller_Action_Exception $e) {
@@ -398,7 +557,7 @@ class Scenario_MobileApplication_RestfulApi_UserAccountOAuthForLoginErrorsTest e
 class Scenario_MobileApplication_RestfulApi_UserAccountOAuthForLoginTest extends Scenario_MobileApplication_RestfulApi_UserAccountTestCase {
   public function setUp() {
     parent::setUp();
-    $this->dispatch('/auth/oauth/response_type/code/client_id/My%20mobile%20app?redirect_uri='
+    $this->dispatch('/auth/oauth/response_type/code/client_id/MyBibApp?redirect_uri='
                     . urlencode('bokeh://authorize'),
                     true);
   }
@@ -413,7 +572,7 @@ class Scenario_MobileApplication_RestfulApi_UserAccountOAuthForLoginTest extends
   /** @test */
   public function pageShouldContainsMyMobileAppWantsToConnect() {
     $this->assertXPathContentContains('//h1',
-                                      'Authentifiez-vous pour autoriser "My mobile app" à accéder à votre compte');
+                                      'Authentifiez-vous pour autoriser "MyBibApp" à accéder à votre compte');
   }
 
 
@@ -430,11 +589,12 @@ class Scenario_MobileApplication_RestfulApi_UserAccountOAuthForLoginOnPhoneTest
   public function setUp() {
     parent::setUp();
     $_SERVER['HTTP_USER_AGENT'] = 'iPhone';
+    Class_AdminVar::set('ACTIVATE_AUTH_LOG', 1);
     Class_Profil::getCurrentProfil()
       ->beTelephone()
       ->assertSave();
 
-    $this->dispatch('/auth/oauth/response_type/code/client_id/My%20mobile%20app?redirect_uri='
+    $this->dispatch('/auth/oauth/response_type/code/client_id/MyBibApp?redirect_uri='
                     . urlencode('bokeh://authorize'),
                     true);
   }
@@ -461,8 +621,70 @@ class Scenario_MobileApplication_RestfulApi_UserAccountOAuthForLoginOnPhoneTest
   /** @test */
   public function pageShouldContainsMyMobileAppWantsToConnect() {
     $this->assertXPathContentContains('//h1',
-                                      'Authentifiez-vous pour autoriser "My mobile app" à accéder à votre compte');
+                                      'Authentifiez-vous pour autoriser "MyBibApp" à accéder à votre compte');
+  }
+}
+
+
+
+class Scenario_MobileApplication_RestfulApi_UserAccountOAuth2ForLoginOnPhoneTest extends Scenario_MobileApplication_RestfulApi_UserAccountTestCase {
+  public function setUp() {
+    parent::setUp();
+    $_SERVER['HTTP_USER_AGENT'] = 'iPhone';
+    Class_AdminVar::set('ACTIVATE_AUTH_LOG', 1);
+    Class_Profil::getCurrentProfil()
+      ->beTelephone()
+      ->assertSave();
+
+    $this->dispatch('/auth/oauth/response_type/code/client_id/pc?redirect_uri='
+                    . urlencode('http://myurl'),
+                    true);
+  }
+
+
+  public function tearDown() {
+    unset($_SERVER['HTTP_USER_AGENT']);
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function pageShouldDisplayLoginForm() {
+    $this->assertXPath('//form//input[@name="username"]');
+  }
+
+
+  /** @test */
+  public function formActionShouldBeEmpty() {
+    $this->assertXpath('//form[@action=""]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsMyMobileAppWantsToConnect() {
+    $this->assertXPathContentContains('//h1',
+                                      'Authentifiez-vous pour autoriser "pc" à accéder à votre compte');
   }
+  /** @test */
+  public function responseShouldRedirectToBokehAuthorizeWithExistingToken() {
+    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/pc/redirect_uri/' . urlencode('http://myurl'),
+                        ['username' => 'puppy', 'password' => 'opied'], true);
+    $token = Class_User_ApiToken::findFirstBy(['client_id'=>'pc']);
+    $this->assertRedirectTo('http://myurl?token='.$token->getToken());
+  }
+
+
+  /** @test */
+  public function responseShouldNotRedirectToBokehAuthorizeWithWrongId() {
+    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/pc/redirect_uri/' . urlencode('http://myurl'),
+                        ['username' => 'puppy', 'password' => 'eopied'], true);
+
+    $this->assertXPathContentContains('//h1',
+                                      'Authentifiez-vous pour autoriser "pc" à accéder à votre compte');
+
+  }
+
+
 }
 
 
@@ -504,10 +726,14 @@ class Scenario_MobileApplication_RestfulApi_UserAccountOAuthPostLoginSuccessTest
 
   /** @test */
   public function responseShouldRedirectToBokehAuthorizeWithExistingToken() {
-    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/My%20mobile%20app/redirect_uri/' . urlencode('bokeh://authorize'),
+    Class_User_ApiToken::setTimeSource(new TimeSourceForTest('2019-03-17 15:48:03'));
+    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/MyBibApp/redirect_uri/' . urlencode('bokeh://authorize'),
                         ['username' => 'puppy', 'password' => 'opied'], true);
+    $token = Class_User_ApiToken::findFirstBy([ 'client_id'  => 'MyBibApp',
+                                               'user_id' => 345,
+                                               'expired_at' =>'']);
 
-    $this->assertRedirectTo('bokeh://authorize#token=nonos');
+    $this->assertRedirectTo('bokeh://authorize#token='. $token->getToken());
   }
 
 
@@ -515,7 +741,7 @@ class Scenario_MobileApplication_RestfulApi_UserAccountOAuthPostLoginSuccessTest
   public function tokenShouldBeCreatedIfNotExists() {
     Class_User_ApiToken::deleteBy([]);
 
-    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/My%20mobile%20bokeh/redirect_uri/' . urlencode('bokeh://authorize'),
+    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/MyBibApp/redirect_uri/' . urlencode('bokeh://authorize'),
                         ['username' => 'puppy', 'password' => 'opied'], true);
 
     $token = Class_User_ApiToken::find(1);
@@ -524,12 +750,24 @@ class Scenario_MobileApplication_RestfulApi_UserAccountOAuthPostLoginSuccessTest
   }
 
 
+  /** @test */
+  public function tokenShouldBeCreatedIfWrongExists() {
+    Class_User_ApiToken::deleteBy([]);
+
+    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/MyBibApp/redirect_uri/' . urlencode('bokeh://authorize'),
+                        ['username' => 'puppye', 'password' => 'opied'], true);
+
+    $this->assertNotRedirect('');
+
+  }
+
+
   /**
    * @depends tokenShouldBeCreatedIfNotExists
    * @test
    */
   public function tokenClientIdShouldBeMyMobileBokeh($token) {
-    $this->assertEquals('My mobile bokeh', $token->getClientId());
+    $this->assertEquals('MyBibApp', $token->getClientId());
   }
 }
 
@@ -542,7 +780,8 @@ class Scenario_MobileApplication_RestfulApi_UserAccountWithTokenTest extends Sce
 
   public function setUp() {
     parent::setUp();
-
+    Class_AdminVar::set('ACTIVATE_AUTH_LOG', '1');
+    Class_LogFile::setLog(new Class_LogFile('temp/log_auth'));
     $this->dispatch('/api/user/account',
                     true,
                     ["Authorization" => "Bearer nonos" ,
@@ -565,6 +804,38 @@ class Scenario_MobileApplication_RestfulApi_UserAccountWithTokenTest extends Sce
 
 
 
+
+class Scenario_MobileApplication_RestfulApi_UserAccountWithExpiredTokenNeverExpiredTest extends Scenario_MobileApplication_RestfulApi_UserAccountTestCase {
+  protected
+    $_json;
+
+  public function setUp() {
+    parent::setUp();
+    Class_AdminVar::set('ACTIVATE_AUTH_LOG', '1');
+    Class_LogFile::setLog(new Class_LogFile('temp/log_auth'));
+    $this->dispatch('/api/user/account',
+                    true,
+                    ["Authorization" => "Bearer exptoken" ,
+                     "Content-Type" => "application/json"]);
+    $this->_json = json_decode($this->_response->getBody(), true);
+  }
+
+
+  /** @test */
+  public function responseShouldContainsCardValidityAndLabel() {
+    $this->assertEquals(['label' => 'Puppy',
+                         'login' => 'puppy',
+                         'card' => [
+                                    'id' =>  '234',
+                                    'expire_at' => '2018-02-12']
+                         ],
+                        $this->_json['account']);
+  }
+}
+
+
+
+
 class Scenario_MobileApplication_RestfulApi_UserAccountHoldsWithTokenTest extends Scenario_MobileApplication_RestfulApi_UserAccountTestCase {
   protected
     $_json;
@@ -590,11 +861,10 @@ class Scenario_MobileApplication_RestfulApi_UserAccountHoldsWithTokenTest extend
                          'held_by' => 'puppy',
                          'library' => 'Annecy',
                          'record' => [ 'id' => '83',
-                                       'thumbnail' => 'http://img.com/marcus.jpg' ]
+                                      'thumbnail' => 'http://img.com/marcus.jpg' ]
                          ],
                         $this->_json['holds'][0]);
   }
-
 }
 
 
@@ -645,7 +915,6 @@ class Scenario_MobileApplication_RestfulApi_UserAccountCancelHoldTest extends Sc
                          'error' => 'Réservation introuvable'],
                         json_decode($this->_response->getBody(), true));
   }
-
 }
 
 
@@ -677,7 +946,6 @@ class Scenario_MobileApplication_RestfulApi_ItemByBarcodeTest extends Scenario_M
                       '/api/catalog/item/barcode/345',
                       true,
                       ["Content-Type" => "application/json"]);
-
     }  catch(Zend_Controller_Action_Exception $e) {
       $this->assertEquals(404, $e->getCode());
       return;
@@ -693,7 +961,6 @@ class Scenario_MobileApplication_RestfulApi_ItemByBarcodeTest extends Scenario_M
                       '/api/catalog/item',
                       true,
                       ["Content-Type" => "application/json"]);
-
     }  catch(Zend_Controller_Action_Exception $e) {
       $this->assertEquals(403,
                           $e->getCode(),
@@ -703,4 +970,230 @@ class Scenario_MobileApplication_RestfulApi_ItemByBarcodeTest extends Scenario_M
     $this->fail('should raise error 403 bad request');
   }
 }
-?>
+
+
+
+
+class Scenario_MobileApplication_RestfulApi_DilicomSuccessTest extends Scenario_MobileApplication_RestfulApi_UserAccountTestCase {
+  protected $_auth;
+
+  public function setUp() {
+    parent::setUp();
+    $this->_auth = Storm_Test_ObjectWrapper::mock()
+      ->whenCalled('authenticateLoginPassword')
+      ->answers(false)
+      ->whenCalled('hasIdentity')
+      ->answers(false)
+      ->whenCalled('getIdentity')
+      ->answers(null);
+
+    ZendAfi_Auth::setInstance($this->_auth);
+
+    $this->_auth
+      ->whenCalled('authenticateLoginPassword')
+      ->with('puppy', 'opied')
+      ->willDo(
+               function() {
+                 $user = new stdClass();
+                 $user->ID_USER = 345;
+                 $this->_auth->whenCalled('getIdentity')->answers($user);
+                 return true;
+               });
+  }
+
+
+  public function tearDown() {
+    ZendAfi_Auth::setInstance(null);
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function tokenShouldBeCreatedIfNotExistsAndShouldNotRedirect() {
+    Class_User_ApiToken::deleteBy([]);
+    $this->postDispatch('/opac/auth/oauth/',
+                        ['grant_type'  => 'password','client_id' => 'PNB','username' => 'puppy', 'password' => 'opied'], true);
+
+    $token = Class_User_ApiToken::find(1);
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals($json->access_token , $token->getToken());
+    $this->assertNotRedirect();
+    return $token;
+  }
+
+
+  /** @test */
+  public function tokenShouldBeCreatedAndUpdateIfExistsAndShouldNotRedirect() {
+    Class_User_ApiToken::deleteBy([]);
+
+    $this->fixture('Class_User_ApiToken',
+                   ['id' => 10,
+                    'token' => 'nonos',
+                    'client_id' => 'PNB',
+                    'user_id' => 345]);
+    Class_User_ApiToken::setTimeSource(new TimeSourceForTest('2019-03-17 15:48:03'));
+
+    $token = Class_User_ApiToken::findFirstBy(['client_id'=> 'PNB']);
+    $this->postDispatch('/opac/auth/oauth/',
+                        ['grant_type'  => 'password','client_id' => 'PNB','username' => 'puppy', 'password' => 'opied'], true);
+
+    $token2 = Class_User_ApiToken::findFirstBy(['client_id'=> 'PNB']);
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals($json->access_token , $token2->getToken());
+    $this->assertNotEquals($json->access_token , $token->getToken());
+    $this->assertEquals( '2019-03-18 15:48:03',$token2->getExpiredAt());
+    $this->assertNotRedirect();
+    return $token;
+  }
+
+
+  /** @test */
+  public function invalidGrantIfWrongAuthentication() {
+    Class_User_ApiToken::deleteBy([]);
+
+    $this->postDispatch('/opac/auth/oauth/',
+                        ['client_id'=>'PNB','username' => 'puppy', 'password' => 'opiedee'], true);
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals($json->error, 'invalid_grant' );
+    $this->assertEquals($json->error_description, 'Authentication failure' );
+    $this->assertEquals(302, $this->_response->getHttpResponseCode());
+  }
+
+
+  /** @test */
+  public function invalidGrantIfWrongRefreshToken() {
+    Class_User_ApiToken::deleteBy([]);
+
+    $this->postDispatch('/opac/auth/refresh',
+                        ['refresh_token' => 'wrong'], true);
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals($json->error, 'invalid_request');
+    $this->assertEquals($json->error_description, 'The access token has expired' );
+    $this->assertEquals(401, $this->_response->getHttpResponseCode());
+  }
+
+
+  /** @test */
+  public function invalidRequestIfNoRefreshToken() {
+    Class_User_ApiToken::deleteBy([]);
+
+    $this->postDispatch('/opac/auth/refresh',
+                        ['refresh_ejtoken' => 'wrong'], true);
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals($json->error, 'invalid_request');
+    $this->assertEquals($json->error_description, 'Missing parameter' );
+    $this->assertEquals(400, $this->_response->getHttpResponseCode());
+  }
+
+
+  /**
+   * @test
+   */
+  public function validRefreshTokenShouldReturnToken(){
+    Class_User_ApiToken::setTimeSource(new TimeSourceForTest('2019-03-15 15:48:03'));
+    Class_User_ApiToken::getLoader()->setTimeSource(new TimeSourceForTest('2019-03-15 15:48:03'));
+    $this->fixture('Class_User_ApiToken',
+                   ['id' => 3,
+                    'token' => 'mytoken',
+                    'client_id'=>'PNB',
+                    'refresh_token'=>'1234',
+                    'expiration_date' => '2010-01-01 20:00:00',
+                    'user_id' => 987]);
+
+    $this->postDispatch('/opac/auth/refresh',
+                        ['refresh_token' => '1234'], true);
+    $json = json_decode($this->_response->getBody());
+
+    $this->assertEquals(Class_User_ApiToken::find(3)->getExpiredAt(), '2019-03-16 15:48:03');
+    $this->assertEquals($json->token_type, 'Bearer');
+    $this->assertNotEquals($json->refresh_token, '1234' );
+    $this->assertEquals($json->expires_in, '86400' );
+    $this->assertNotEquals($json->access_token, 'mytoken' );
+  }
+
+
+  /**
+   * @test
+   */
+  public function refreshTokenWithoutConfigServer(){
+    Class_User_ApiToken::setTimeSource(new TimeSourceForTest('2019-03-15 15:48:03'));
+    Class_User_ApiToken::getLoader()->setTimeSource(new TimeSourceForTest('2019-03-15 15:48:03'));
+    $this->fixture('Class_User_ApiToken',
+                   ['id' => 3,
+                    'token' => 'mytoken',
+                    'refresh_token'=>'1234',
+                    'client_id' => 'ghost',
+                    'expiration_date' => '2010-01-01 20:00:00',
+                    'user_id' => 987]);
+
+    $this->postDispatch('/opac/auth/refresh',
+                        ['refresh_token' => '1234'], true);
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals($json->error, 'invalid_request');
+    $this->assertEquals($json->error_description, 'Client configuration missing for ghost' );
+    $this->assertEquals(401, $this->_response->getHttpResponseCode());
+  }
+
+
+  /**
+   * @test
+   */
+  public function refreshTokenWithoutValidConfigServer(){
+    Class_User_ApiToken::setTimeSource(new TimeSourceForTest('2019-03-15 15:48:03'));
+    Class_User_ApiToken::getLoader()->setTimeSource(new TimeSourceForTest('2019-03-15 15:48:03'));
+    Class_IdentityClient::find(2)->setActive(false)->save();
+
+    $this->fixture('Class_User_ApiToken',
+                   ['id' => 3,
+                    'token' => 'mytoken',
+                    'refresh_token'=>'1234',
+                    'client_id' => 'PNB',
+                    'expiration_date' => '2010-01-01 20:00:00',
+                    'user_id' => 987]);
+
+    $this->postDispatch('/opac/auth/refresh',
+                        ['refresh_token' => '1234'], true);
+    $json = json_decode($this->_response->getBody());
+    $this->assertContains('invalid_request',
+                          Class_Journal::lastOf(Class_Journal_OauthRequestType::MY_TYPE)->getDetail(Class_Journal_RequestType::BOKEH_RESPONSE)->getValue());
+
+    $this->assertEquals($json->error, 'invalid_request');
+    $this->assertEquals($json->error_description, 'Client configuration missing for PNB' );
+    $this->assertEquals(401, $this->_response->getHttpResponseCode());
+  }
+
+
+  /**
+   * @test
+   */
+  public function refreshTokenNotAllowerdByConfigServer(){
+    Class_User_ApiToken::setTimeSource(new TimeSourceForTest('2019-03-15 15:48:03'));
+    $this->fixture('Class_User_ApiToken',
+                   ['id' => 3,
+                    'token' => 'mytoken',
+                    'refresh_token'=>'1234',
+                    'client_id' => 'MyBibApp',
+                    'expiration_date' => '2010-01-01 20:00:00',
+                    'user_id' => 987]);
+
+    $this->postDispatch('/opac/auth/refresh',
+                        ['refresh_token' => '1234'], true);
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals($json->error, 'invalid_request');
+    $this->assertEquals($json->error_description, 'Client configuration is not allowed refresh token' );
+    $this->assertEquals(401, $this->_response->getHttpResponseCode());
+  }
+
+
+  /**
+   * @test
+   */
+  public function discoverShoulReturnInformations(){
+    $this->dispatch( '/api/catalog/discover');
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals($json->infos->mail,'dev-opac@afi-sa.fr');
+    $this->assertEquals($json->infos->company,'AFI');
+    $this->assertEquals($json->authentication->get_token,( new Class_Url )->absoluteUrl('/auth/oauth'));
+    $this->assertEquals($json->resources[0]->endpoint,( new Class_Url )->absoluteUrl('/api/user/pnbloans'));
+  }
+}
diff --git a/tests/scenarios/PnbDilicom/PnbDilicomTest.php b/tests/scenarios/PnbDilicom/PnbDilicomTest.php
index fbdb9a7c45560be39210ba1ca0dd3daf1ca268e7..299d1e7843669382ff594834fb4035c55af1607c 100644
--- a/tests/scenarios/PnbDilicom/PnbDilicomTest.php
+++ b/tests/scenarios/PnbDilicom/PnbDilicomTest.php
@@ -788,6 +788,14 @@ class PnbDilicomONIXParserTest extends ModelTestCase {
   }
 
 
+  /**
+   * @test
+   */
+   public function formatShouldBeInFormat(){
+     $this->assertEquals('EA;EC;ED;E127;E101',  $this->_book->getFormats());
+  }
+
+
   /** @test */
   public function publisherShouldBePhebus() {
     $this->assertEquals('Phébus', $this->_book->getEditeur());
@@ -1754,7 +1762,7 @@ class PnbDilicomOldLoansQueryPageTest extends PnbDilicomLoansTestCase{
     $this->assertEquals([['order_line_id' => 'x321',
                           'order' => 'expected_return_date desc',
                           'limit' => 5,
-                          'where' => 'expected_return_date <= "2022-06-10 10:23:10" and expected_return_date >= "2022-02-08"']],$this->_wrapper->getAttributesForLastCallOn('findAllBy'));
+                          'where' => 'expected_return_date <= "2022-06-10 10:23:10" and expected_return_date >= "2022-02-08 10:23:10"']],$this->_wrapper->getAttributesForLastCallOn('findAllBy'));
   }
 }
 
diff --git a/tests/scenarios/Templates/MyBibAppTemplateTest.php b/tests/scenarios/Templates/MyBibAppTemplateTest.php
index 67868cb47697f8dd2f77ec3a058e8a8966062beb..4cc5aab9f5435c7d9d552cb12b6fd2d632b14524 100644
--- a/tests/scenarios/Templates/MyBibAppTemplateTest.php
+++ b/tests/scenarios/Templates/MyBibAppTemplateTest.php
@@ -219,6 +219,7 @@ class MyBibAppTemplateEditTemplateTest extends Admin_AbstractControllerTestCase
 
 
 class MyBibAppTemplateOauthWithUserAgentTest extends MyBibAppTemplateTestCase {
+
   public function setUp() {
     parent::setUp();
 
@@ -229,6 +230,14 @@ class MyBibAppTemplateOauthWithUserAgentTest extends MyBibAppTemplateTestCase {
     Class_AdminVar::set('MYBIBAPP_TEMPLATE', 1);
 
     Class_Profil::find(2)->beCurrentProfil();
+    $this->fixture( Class_IdentityClient::class,
+                   [ 'id' => 1,
+                    'client_id'=>'MyBibApp',
+                    'type'  => 'mybibapp',
+                    'label'  => 'MyBibApp',
+                    'active' => 1,
+                    'config'  => "{}"
+                   ]);
 
     $this->dispatch('/opac/auth/oauth/response_type/code/id_profil/1/client_id/MyBibApp?redirect_uri=www.mon-bokeh.org');
   }
@@ -255,12 +264,29 @@ class MyBibAppTemplateOauthWithUserAgentTest extends MyBibAppTemplateTestCase {
 
 
 class MyBibAppTemplatePostDispatchOauthWithUserAgentTest extends MyBibAppTemplateTestCase {
-
+  protected $_storm_default_to_volatile = true;
   protected $_auth;
 
 
   public function setUp() {
     parent::setUp();
+    $this->fixture( Class_IdentityClient::class,
+                   [ 'id' => 1,
+                    'client_id'=>'MyBibApp',
+                    'type'  => 'mybibapp',
+                    'label'  => 'MyBibApp',
+                    'active' => 1,
+                    'config'  => "{}"
+                   ]);
+
+    $this->fixture( Class_IdentityClient::class,
+                   [ 'id' => 10,
+                    'client_id'=>'My webapp',
+                    'type'  => 'oauth2',
+                    'label'  => 'Portail citoyen',
+                    'redirect_uri' => 'http://mon-portail.org/bokeh/oauth',
+                    'active' => 1,
+                   ]);
 
     $puppy = $this->fixture('Class_Users',
                             ['id' => 345,
@@ -274,10 +300,17 @@ class MyBibAppTemplatePostDispatchOauthWithUserAgentTest extends MyBibAppTemplat
 
     $this->fixture('Class_User_ApiToken',
                    ['id' => 1,
+                    'client_id'=>'MyBibApp',
                     'token' => 'nonos',
-                    'client_id' => 'My mobile app',
                     'user' => $puppy]);
 
+    $this->fixture('Class_User_ApiToken',
+                   ['id' => 10,
+                    'client_id'=>'My webapp',
+                    'token' => '1234',
+                    'user' => $puppy]);
+
+
     $_SERVER['HTTP_USER_AGENT'] = 'MyBibApp/1.0.3 (Android)';
 
     Class_AdminVar::set('MYBIBAPP_TEMPLATE', 1);
@@ -304,8 +337,6 @@ class MyBibAppTemplatePostDispatchOauthWithUserAgentTest extends MyBibAppTemplat
                  $this->_auth->whenCalled('getIdentity')->answers($user);
                  return true;
                });
-
-
   }
 
 
@@ -317,10 +348,30 @@ class MyBibAppTemplatePostDispatchOauthWithUserAgentTest extends MyBibAppTemplat
 
   /** @test */
   public function responseShouldRedirectToBokehAuthorizeWithExistingToken() {
-    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/My%20mobile%20app/redirect_uri/' . urlencode('bokeh://authorize'),
+    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/MyBibApp/redirect_uri/' . urlencode('bokeh://authorize'),
                         ['username' => 'puppy', 'password' => 'opied'], true);
+    $token = Class_User_ApiToken::findFirstBy(['client_id'=>'MyBibApp']);
+    $this->assertRedirectTo('bokeh://authorize#token='.$token->getToken());
+  }
+
 
-    $this->assertRedirectTo('bokeh://authorize#token=nonos');
+  /** @test */
+  public function responseShouldRedirectToMyWebappAuthorizeWithExistingToken() {
+    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/My%20webapp/redirect_uri/' . urlencode('http://mon-portail.org/bokeh/oauth'),
+                        ['username' => 'puppy', 'password' => 'opied'], true);
+    $token = Class_User_ApiToken::findFirstBy(['client_id'=>'My webapp']);
+    $this->assertRedirectTo('http://mon-portail.org/bokeh/oauth?token='.$token->getToken());
+  }
+
+
+  /** @test */
+  public function responseShouldNotRedirectToMyWebappAuthorizeWithExistingToken() {
+    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/My%20webapp/redirect_uri/' . urlencode('http://mon-faux-portail.org/bokeh/oauth'),
+                        ['username' => 'puppy', 'password' => 'opied'], true);
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals($json->error, 'invalid_request');
+    $this->assertEquals($json->error_description, 'redirect_uri http://mon-faux-portail.org/bokeh/oauth differs of client config :http://mon-portail.org/bokeh/oauth' );
+    $this->assertEquals(401, $this->_response->getHttpResponseCode());
   }
 }