diff --git a/VERSIONS_WIP/59718 b/VERSIONS_WIP/59718
new file mode 100644
index 0000000000000000000000000000000000000000..d55499ca47dfa14007633fae3361f832a527b11b
--- /dev/null
+++ b/VERSIONS_WIP/59718
@@ -0,0 +1 @@
+ - ticket #59718 : Intégration de l'enregistrement d'applications et d'authentification via OAuth
\ No newline at end of file
diff --git a/application/modules/api/controllers/UserController.php b/application/modules/api/controllers/UserController.php
new file mode 100644
index 0000000000000000000000000000000000000000..2a512d43f495387bf0249695baac0262a64a93c8
--- /dev/null
+++ b/application/modules/api/controllers/UserController.php
@@ -0,0 +1,50 @@
+<?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 Api_UserController extends ZendAfi_Controller_Action {
+  public function loansAction() {
+    if (!$this->_request->isSecure())
+      return $this->_error($this->_('Protocole HTTP obligatoire'));
+
+    if (!$authorization = $this->_request->getHeader('authorization'))
+      return $this->_error($this->_('Autorisation non spécifiée'));
+
+    $parts = explode(' ', $authorization);
+    if ($parts[0] !== 'Bearer')
+      return $this->_error($this->_('Jeton d\'autorisation non fourni'));
+
+    if (!$token = Class_User_ApiToken::findFirstBy(['token' => $parts[1]]))
+      return $this->_error($this->_('Jeton d\'autorisation invalide'));
+
+    if (!$user = $token->getUser())
+      return $this->_error($this->_('Utilisateur non trouvé'));
+
+    $this->view->loans = (new Class_User_Cards($user))->getLoans();
+  }
+
+
+  protected function _error($message) {
+    $this->view->message = $message;
+    return $this->renderScript('invalid_request.pjson');
+  }
+}
+?>
\ No newline at end of file
diff --git a/application/modules/api/views/scripts/invalid_request.pjson b/application/modules/api/views/scripts/invalid_request.pjson
new file mode 100644
index 0000000000000000000000000000000000000000..153d898f6f513c3f6da36abd64232443fbbe0c02
--- /dev/null
+++ b/application/modules/api/views/scripts/invalid_request.pjson
@@ -0,0 +1,4 @@
+{
+	"error":"invalid_request",
+	"message":"<?php echo $this->message ?>"
+}
\ No newline at end of file
diff --git a/application/modules/api/views/scripts/user/loans.pjson b/application/modules/api/views/scripts/user/loans.pjson
new file mode 100644
index 0000000000000000000000000000000000000000..94d1684f134585f6fea7cbd946598ebe163f13d1
--- /dev/null
+++ b/application/modules/api/views/scripts/user/loans.pjson
@@ -0,0 +1,3 @@
+{
+	"loans": <?php echo $this->loans($this->loans) ?>
+}
diff --git a/application/modules/opac/controllers/AuthController.php b/application/modules/opac/controllers/AuthController.php
index f2cdd2421936a45e905b13bec78f669e131ec2e0..27c66144e3ab8736de027860f28bd0c59f476fd1 100644
--- a/application/modules/opac/controllers/AuthController.php
+++ b/application/modules/opac/controllers/AuthController.php
@@ -99,7 +99,6 @@ class AuthController extends ZendAfi_Controller_Action {
     $this->view->redirect = $redirect;
     $service = $this->_getParam('service','');
     $this->view->service = $service;
-
     $this->view->titreAdd($this->view->_('Connexion'));
 
     $this->view->title = Class_Users::getIdentity()
@@ -138,6 +137,30 @@ 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);
+    }
+
+    $this->view->titre = $this->_('Authentifiez-vous pour autoriser "%s" à accéder à votre compte',
+                                  $this->_getParam('client_id'));
+    $preferences = Class_Profil::getCurrentProfil()->getCfgModulesPreferences('auth', 'login');
+    $options = ['data' => array_merge(
+                                      $preferences,
+                                      ['redirect_url' => $this->_getParam('redirect_uri'),
+                                       'id_notice' => 0])];
+    $this->view->form = ZendAfi_Form_Login::newWithOptions($options);
+
+    $redirect_uri = $this->_getParam('redirect_uri');
+    $strategy = new Auth_Strategy_OAuth($this);
+    $strategy->processLogin();
+  }
+
+
   public function popupLoginAction() {
     $this->view->preferences = $this->_loginPrefFromWidgetOrModule();
     $this->view->redirect = $this->_getParam('redirect');
@@ -523,8 +546,9 @@ class AuthController extends ZendAfi_Controller_Action {
 
 
 abstract class Auth_Strategy_Abstract {
-  protected $redirect_url = '';
-  protected $disable_redirect = false;
+  protected
+    $redirect_url = '',
+    $disable_redirect = false;
 
   static public function strategyForController($controller) {
     if ($controller->isCasRequest() && static::isLogged())
@@ -570,6 +594,7 @@ abstract class Auth_Strategy_Abstract {
       $this->controller->redirect($this->redirect_url);
   }
 
+
   public function setDefaultUrl($url) {
     $this->default_url=$url;
   }
@@ -681,9 +706,9 @@ class Auth_Strategy_Cas_NotLogged extends Auth_Strategy_Cas_Abstract{
 }
 
 
-class Auth_Strategy_Lectura extends Auth_Strategy_Abstract {
 
 
+class Auth_Strategy_Lectura extends Auth_Strategy_Abstract {
   public function handlePost() {
     $this->controller->getHelper('ViewRenderer')->setNoRender();
     $response= $this->controller->getResponse();
@@ -705,3 +730,21 @@ class Auth_Strategy_Lectura extends Auth_Strategy_Abstract {
 
   }
 }
+
+
+
+class Auth_Strategy_OAuth extends Auth_Strategy_NotLogged {
+  public function handlePost() {
+    parent::handlePost();
+    if (!$user = Class_Users::getIdentity())
+      return $this;
+
+    $request = $this->controller->getRequest();
+
+    $token = Class_User_ApiToken::findOrCreateForUserAndApplication($user,
+                                                                    $request->getParam('client_id'));
+    $this->redirect_url = sprintf('%s#token=%s',
+                                  $request->getParam('redirect_uri'),
+                                  $token->getToken());
+  }
+}
\ No newline at end of file
diff --git a/application/modules/opac/views/scripts/auth/oauth.phtml b/application/modules/opac/views/scripts/auth/oauth.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..a4574386fb4cb3f83c41e8c7db0f71faa510ac47
--- /dev/null
+++ b/application/modules/opac/views/scripts/auth/oauth.phtml
@@ -0,0 +1,4 @@
+<?php
+echo $this->tag('h1', $this->titre);
+echo $this->renderForm($this->form);
+?>
diff --git a/application/modules/telephone/controllers/AuthController.php b/application/modules/telephone/controllers/AuthController.php
index 763316755d0a6cca3875498c46f2e48d5bfb6ffe..5cf1f1ef1749f091d0c728839a200030ffdfd555 100644
--- a/application/modules/telephone/controllers/AuthController.php
+++ b/application/modules/telephone/controllers/AuthController.php
@@ -55,6 +55,12 @@ class Telephone_AuthController extends AuthController {
   }
 
 
+  public function oauthAction() {
+    parent::oauthAction();
+    $this->view->form = $this->_getFormLogin();
+  }
+
+
   public function loginReservationAction() {
     if (Class_Users::getLoader()->hasIdentity()) {
       $this->_redirect('/recherche/reservation');
diff --git a/application/modules/telephone/views/scripts/auth/oauth.phtml b/application/modules/telephone/views/scripts/auth/oauth.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..7c3b23818cc4ad917f6bfaf461d4c20b6aec6031
--- /dev/null
+++ b/application/modules/telephone/views/scripts/auth/oauth.phtml
@@ -0,0 +1,4 @@
+<?php
+echo $this->tag('h1', $this->titre);
+echo $this->form;
+?>
diff --git a/cosmogramme/sql/patch/patch_316.php b/cosmogramme/sql/patch/patch_316.php
index e1578072ceab6f73d7934e8d1db6ec51a1721f40..2ac6816a3b9b30181d89f9fca04416754f059422 100644
--- a/cosmogramme/sql/patch/patch_316.php
+++ b/cosmogramme/sql/patch/patch_316.php
@@ -3,4 +3,4 @@ try {
   Zend_Db_Table_Abstract::getDefaultAdapter()
     ->query('ALTER TABLE codif_section MODIFY id_section int(11) NOT NULL AUTO_INCREMENT');
 } catch(Exception $e) {}
-?>
+?>
\ No newline at end of file
diff --git a/cosmogramme/sql/patch/patch_326.php b/cosmogramme/sql/patch/patch_326.php
new file mode 100644
index 0000000000000000000000000000000000000000..6b5fabaeef2d450e3f06a4a320ac212b7bca832d
--- /dev/null
+++ b/cosmogramme/sql/patch/patch_326.php
@@ -0,0 +1,12 @@
+<?php
+Zend_Db_Table_Abstract::getDefaultAdapter()
+  ->query('CREATE TABLE if not exists `user_api_tokens` ( '
+          . 'id int(11) unsigned not null auto_increment,'
+          . 'user_id int(11) unsigned not null,'
+          . 'client_id varchar(255) not null,'
+          . 'token varchar(255) not null,'
+          . 'primary key (id),'
+          . 'key (`user_id`),'
+          . 'key (`token`)'
+          . ') engine=MyISAM default charset=utf8');
+?>
\ No newline at end of file
diff --git a/library/Class/User/ApiToken.php b/library/Class/User/ApiToken.php
new file mode 100644
index 0000000000000000000000000000000000000000..37dcba1366a9da20db85aa5cc35635126a32364f
--- /dev/null
+++ b/library/Class/User/ApiToken.php
@@ -0,0 +1,49 @@
+<?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 User_ApiTokenLoader extends Storm_Model_Loader {
+  public function findOrCreateForUserAndApplication($user, $client_id) {
+    if ($token = Class_User_ApiToken::findFirstBy(['role' => 'user',
+                                                   'model' => $user,
+                                                   'client_id' => $client_id]))
+      return $token;
+
+    $token = new Class_User_ApiToken();
+    $token
+      ->setUser($user)
+      ->setClientId($client_id)
+      ->setToken(md5(uniqid()))
+      ->save();
+
+    return $token;
+  }
+}
+
+
+class Class_User_ApiToken extends Storm_Model_Abstract {
+  protected
+    $_table_name = 'user_api_tokens',
+    $_loader_class = 'User_ApiTokenLoader',
+    $_belongs_to = ['user' => ['model' => 'Class_Users',
+                               'role' => 'api_token']];
+
+
+}
+?>
\ No newline at end of file
diff --git a/library/Class/User/Settings.php b/library/Class/User/Settings.php
index 8368d29a7e1935d4cc30411ffd08e9aa54ff88f0..747a2ed287a722650d434a42d1d73ec4a42c359c 100644
--- a/library/Class/User/Settings.php
+++ b/library/Class/User/Settings.php
@@ -31,11 +31,7 @@ class Class_User_Settings {
 
 
   public static function newWith($user) {
-    $instance = new self();
-    $instance
-      ->setUser($user)
-      ->setUserSettings(unserialize($user->getSettings()));
-    return $instance;
+    return new self($user);
   }
 
 
@@ -60,8 +56,14 @@ class Class_User_Settings {
   }
 
 
+  public function __construct($user) {
+    $this->setUser($user);
+  }
+
+
   public function setUser($user) {
     $this->_user = $user;
+    $this->_user_settings = unserialize($user->getSettings());
     return $this;
   }
 
@@ -72,25 +74,37 @@ class Class_User_Settings {
   }
 
 
-  public function setBookmarkedDomains($domain_ids) {
-    $this->_user_settings[self::BOOKMARKED_DOMAINS] = implode('-', $domain_ids);
+  public function get($name) {
+    return isset($this->_user_settings[$name])
+      ? $this->_user_settings[$name]
+      : null;
+  }
+
+
+  public function set($name, $value) {
+    $this->_user_settings[$name] = $value;
     $this->_user->setSettings(static::serializeSettings($this->_user_settings));
+    return $this;
+  }
+
+
+  public function setBookmarkedDomains($domain_ids) {
+    $this->set(self::BOOKMARKED_DOMAINS, implode('-', $domain_ids));
     return $this->_user;
   }
 
 
   public function setBookmarkedLibraries($libraries_ids) {
-    $this->_user_settings[self::BOOKMARKED_LIBRARIES] = implode('-', $libraries_ids);
-    $this->_user->setSettings(static::serializeSettings($this->_user_settings));
+    $this->set(self::BOOKMARKED_LIBRARIES, implode('-', $libraries_ids));
     return $this->_user;
   }
 
 
   public function getBookmarkedDomains() {
-    if(!isset($this->_user_settings[self::BOOKMARKED_DOMAINS]))
+    if (!$value = $this->get(self::BOOKMARKED_DOMAINS))
       return [];
 
-    if(!$storm_ids = $this->_getStormIds($this->_user_settings[self::BOOKMARKED_DOMAINS]))
+    if (!$storm_ids = $this->_getStormIds($value))
       return [];
 
     return Class_Catalogue::findAllBy(['id_catalogue' => $storm_ids,
@@ -102,10 +116,10 @@ class Class_User_Settings {
     if (!static::isBookmarkLibraryReady())
       return $this->_clearBookmarkedLibraries();
 
-    if(!isset($this->_user_settings[self::BOOKMARKED_LIBRARIES]))
+    if (!$value = $this->get(self::BOOKMARKED_LIBRARIES))
       return $this->_initDefaultLibrary();
 
-    if(!$storm_ids = $this->_getStormIds($this->_user_settings[self::BOOKMARKED_LIBRARIES]))
+    if (!$storm_ids = $this->_getStormIds($value))
       return [];
 
     return Class_Profil::getCurrentProfil()->isItemAnnexDisplay()
@@ -133,9 +147,9 @@ class Class_User_Settings {
 
 
   public function getRedmineLibrary() {
-    if(!isset($this->_user_settings[self::REDMINE_LIBRARY]) && !$this->_user_settings[self::REDMINE_LIBRARY])
-      return $this->getUserLib();
-    return Class_Bib::find($this->_user_settings[self::REDMINE_LIBRARY]);
+    return ($lib_id = $this->get(self::REDMINE_LIBRARY))
+      ? Class_Bib::find($lib_id)
+      : $this->getUserLib();
   }
 
 
@@ -145,19 +159,17 @@ class Class_User_Settings {
 
 
   public function setRedmineLibrary($library_id) {
-    $this->_user_settings[self::REDMINE_LIBRARY] = $library_id;
-    $this->_user->setSettings(static::serializeSettings($this->_user_settings));
+    $this->set(self::REDMINE_LIBRARY, $library_id);
     return $this->_user;
   }
 
 
   public function getAdminSkin() {
-    if(!isset($this->_user_settings[self::ADMIN_SKIN]))
-      return new Class_Admin_Skin();
+    $admin_skin = $this->get(self::ADMIN_SKIN);
 
-    if (!is_a($this->_user_settings[self::ADMIN_SKIN], 'Class_Admin_Skin'))
-      return new Class_Admin_Skin();
-    return new Class_Admin_Skin($this->_user_settings[self::ADMIN_SKIN]->getName(), $this->_user_settings[self::ADMIN_SKIN]->getColor());
+    return is_a($admin_skin, 'Class_Admin_Skin')
+      ? new Class_Admin_Skin($admin_skin->getName(), $admin_skin->getColor())
+      : new Class_Admin_Skin();
   }
 
 
@@ -165,8 +177,8 @@ class Class_User_Settings {
     if(!$datas || !isset($datas[self::ADMIN_SKIN]))
       return $this->_user;
 
-    $this->_user_settings[self::ADMIN_SKIN] = new Class_Admin_Skin($datas[self::ADMIN_SKIN], $datas[self::ADMIN_SKIN_COLOR]);
-    $this->_user->setSettings(static::serializeSettings($this->_user_settings));
+    $this->set(self::ADMIN_SKIN, new Class_Admin_Skin($datas[self::ADMIN_SKIN],
+                                                      $datas[self::ADMIN_SKIN_COLOR]));
     return $this->_user;
   }
 
@@ -175,11 +187,8 @@ class Class_User_Settings {
     if (!static::isBookmarkLibraryReady())
       return $this->_user;
 
-    if(!isset($this->_user_settings[self::BOOKMARKED_LIBRARIES]))
-      $this->_user_settings[self::BOOKMARKED_LIBRARIES] = '';
-
-    $this->_user_settings[self::BOOKMARKED_LIBRARIES] .= '-' . $id;
-    $this->_user->setSettings(static::serializeSettings($this->_user_settings));
+    $this->set(self::BOOKMARKED_LIBRARIES,
+               (string)$this->get(self::BOOKMARKED_LIBRARIES) . '-' .$id);
     return $this->_user;
   }
 
@@ -188,12 +197,12 @@ class Class_User_Settings {
     if (!static::isBookmarkLibraryReady())
       return $this->_user;
 
-    if(!isset($this->_user_settings[self::BOOKMARKED_LIBRARIES]))
+    if (!$ids = $this->get(self::BOOKMARKED_LIBRARIES))
       return $this->_user;
 
-    $current_settings = $this->_getStormIds($this->_user_settings[self::BOOKMARKED_LIBRARIES]);
-    $this->_user_settings[self::BOOKMARKED_LIBRARIES] = implode('-', array_diff($current_settings, [$id]));
-    $this->_user->setSettings(static::serializeSettings($this->_user_settings));
+    $current_settings = $this->_getStormIds($ids);
+    $this->set(self::BOOKMARKED_LIBRARIES,
+               implode('-', array_diff($current_settings, [$id])));
     return $this->_user;
   }
 
@@ -216,8 +225,7 @@ class Class_User_Settings {
 
 
   protected function _saveUserWithLib($id) {
-    $this->_user_settings[self::BOOKMARKED_LIBRARIES] = $id;
-    $this->_user->setSettings(static::serializeSettings($this->_user_settings));
+    $this->set(self::BOOKMARKED_LIBRARIES, $id);
     $this->_user->save();
   }
 }
diff --git a/library/ZendAfi/Controller/Action/Helper/ViewRenderer.php b/library/ZendAfi/Controller/Action/Helper/ViewRenderer.php
index bf555b2dc012a5f8f8e6be85e64f24a15decfa75..d7bf0b1d2c1093b0a5d4888fc47ba8c7a8e03872 100644
--- a/library/ZendAfi/Controller/Action/Helper/ViewRenderer.php
+++ b/library/ZendAfi/Controller/Action/Helper/ViewRenderer.php
@@ -32,7 +32,7 @@ class ZendAfi_Controller_Action_Helper_ViewRenderer extends Zend_Controller_Acti
 //-------------------------------------------------------------------------------
   public function __construct() {
     $options['viewSuffix'] = 'phtml';
-    $view=new ZendAfi_Controller_Action_Helper_View();
+    $view = new ZendAfi_Controller_Action_Helper_View();
     parent::__construct($view, $options);
   }
 
diff --git a/library/ZendAfi/Controller/Plugin/DefineURLs.php b/library/ZendAfi/Controller/Plugin/DefineURLs.php
index f2ec5aa8b27a14182b5527138a8d71e28bfd27d7..9633a15d7570e394a62e30014372ef041eb254f3 100644
--- a/library/ZendAfi/Controller/Plugin/DefineURLs.php
+++ b/library/ZendAfi/Controller/Plugin/DefineURLs.php
@@ -24,7 +24,8 @@ class ZendAfi_Controller_Plugin_DefineURLs extends Zend_Controller_Plugin_Abstra
   const
     PHONE = 'telephone',
     ADMIN = 'admin',
-    OPAC = 'opac';
+    OPAC = 'opac',
+    API = 'api';
 
 
   public function preDispatch(Zend_Controller_Request_Abstract $request) {
@@ -48,6 +49,16 @@ class ZendAfi_Controller_Plugin_DefineURLs extends Zend_Controller_Plugin_Abstra
 
   protected function updateRequest() {
     $request = $this->getRequest();
+
+    if ($request->getModuleName() == static::API) {
+      Zend_Controller_Action_HelperBroker::removeHelper('ViewRenderer');
+      $view = new Zend_View();
+      $view->addHelperPath('ZendAfi/View/Helper/Api', 'ZendAfi_View_Helper_Api');
+      Zend_Controller_Action_HelperBroker::addHelper(new Zend_Controller_Action_Helper_ViewRenderer($view, ['viewSuffix' => 'pjson']));
+      $this->getResponse()->setHeader('Content-Type', 'application/json');
+      return $this;
+    }
+
     $detector = new ZendAfi_Controller_Plugin_DefineURLs_ProfileDetector();
     Class_Profil::setCurrentProfil($detector->detectFrom($request));
     $profil = Class_Profil::getCurrentProfil();
diff --git a/library/ZendAfi/View/Helper/Api/Loans.php b/library/ZendAfi/View/Helper/Api/Loans.php
new file mode 100644
index 0000000000000000000000000000000000000000..c4542a247d7c893166d2905a249ba9d1b61b44d3
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Api/Loans.php
@@ -0,0 +1,42 @@
+<?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_Loans extends Zend_View_Helper_Abstract {
+  public function loans($loans) {
+    return json_encode(
+                       $loans->collect([$this, 'loanToArray'])
+                       ->getArrayCopy()
+    );
+  }
+
+
+  public function loanToArray($loan) {
+    return [
+            'title' => $loan->getTitre(),
+            'author' => $loan->getAuteur(),
+            'date_due' => implode('-', array_reverse(explode('/', $loan->getDateRetour()))),
+            'loaned_by' => $loan->getUserFullName(),
+            'library' => $loan->getBibliotheque()
+    ];
+  }
+}
+?>
\ No newline at end of file
diff --git a/tests/application/modules/AbstractControllerTestCase.php b/tests/application/modules/AbstractControllerTestCase.php
index 3a98f54253c12f266ce628173b06229d03608be0..9c1fbff6fd7a5455fd38702be5c04af00b949414 100644
--- a/tests/application/modules/AbstractControllerTestCase.php
+++ b/tests/application/modules/AbstractControllerTestCase.php
@@ -219,7 +219,7 @@ abstract class AbstractControllerTestCase extends Zend_Test_PHPUnit_ControllerTe
   }
 
 
-  public function dispatch($url = null, $throw_exceptions = false) {
+  public function dispatch($url = null, $throw_exceptions = false, $headers =  []) {
     // redirector should not exit
     $redirector = Zend_Controller_Action_HelperBroker::getStaticHelper('redirector');
     $redirector->setExit(false);
@@ -228,7 +228,9 @@ abstract class AbstractControllerTestCase extends Zend_Test_PHPUnit_ControllerTe
     $json = Zend_Controller_Action_HelperBroker::getStaticHelper('json');
     $json->suppressExit = true;
 
-    $request    = $this->getRequest();
+    $request = $this->getRequest();
+    $request->setHeaders($headers);
+
     if (null !== $url) {
       $request->setRequestUri($url);
     }
diff --git a/tests/application/modules/opac/controllers/RechercheControllerTest.php b/tests/application/modules/opac/controllers/RechercheControllerTest.php
index 2b3d2fe30fcd1588dfa61d39fc6e8b23a0036131..e2b45d15f0d08cbd375d4a9338d7d4854e86eca4 100644
--- a/tests/application/modules/opac/controllers/RechercheControllerTest.php
+++ b/tests/application/modules/opac/controllers/RechercheControllerTest.php
@@ -1222,6 +1222,32 @@ class RechercheAvanceeControllerSimpleActionWithDefaultConfigTest extends Recher
   }
 
 
+  /** @test */
+  public function userSettingBookmarkedDomainShouldBeOne() {
+    $this->assertEquals(1,
+                        (new Class_User_Settings(Class_Users::find(1)))
+                        ->get(Class_User_Settings::BOOKMARKED_DOMAINS));
+  }
+
+
+  /** @test */
+  public function userSettingZorkShouldBeEmpty() {
+    $this->assertEmpty((new Class_User_Settings(Class_Users::find(1)))->get('zork'));
+  }
+
+
+  /** @test */
+  public function userSettingSetZorkToGlubShouldSetIt() {
+    (new Class_User_Settings(Class_Users::find(1)))->set('zork', 'glub');
+    Class_Users::find(1)->save();
+    Class_Users::clearCache();
+
+    $this->assertEquals('glub',
+                        (new Class_User_Settings(Class_Users::find(1)))
+                        ->get('zork'));
+  }
+
+
   /** @test */
   public function tagsCloudLevel5ShouldBePresent() {
     $this->assertXPath('//a[contains(@class,"nuage_niveau5")]',
diff --git a/tests/db/UpgradeDBTest.php b/tests/db/UpgradeDBTest.php
index 6cdd45f45865bb5b61272f4010563647acbd3e08..7dff59f67e84380d01d5281e165ff96a675bd165 100644
--- a/tests/db/UpgradeDBTest.php
+++ b/tests/db/UpgradeDBTest.php
@@ -1207,7 +1207,6 @@ class UpgradeDB_315_Test extends UpgradeDBTestCase {
 
 
 class UpgradeDB_316_Test extends UpgradeDBTestCase {
-
   public function prepare() {
     try {
       $this->query("ALTER TABLE codif_section MODIFY id_section tinyint(4)");
@@ -1249,7 +1248,7 @@ class UpgradeDB_317_Test extends UpgradeDBTestCase {
   /**
    * @test
    * @dataProvider datas
-   **/
+   */
   public function fieldsShouldBecomeMediumtext($key) {
     $this->assertFieldType('bib_admin_profil', $key, 'mediumtext');
   }
@@ -1452,3 +1451,34 @@ class UpgradeDB_325_Test extends UpgradeDBTestCase {
     $this->assertColumn('session_activity_inscriptions', 'session_activity_id');
   }
 }
+
+
+
+
+
+class UpgradeDB_326_Test extends UpgradeDBTestCase {
+  public function prepare() {
+    try {
+
+      $this->query('drop table user_api_tokens');
+    } catch (Exception $e) {}
+  }
+
+  public function datas() {
+    return
+      [
+       ['id',  'int(11) unsigned'],
+       ['client_id', 'varchar(255)'],
+       ['token', 'varchar(255)'],
+       ['user_id', 'int(11) unsigned'],
+      ];
+  }
+
+  /**
+   * @test
+   * @dataProvider datas
+   */
+  public function fieldShouldBe($field, $type) {
+    $this->assertFieldType('user_api_tokens', $field, $type);
+  }
+}
diff --git a/tests/scenarios/MobileApplication/UserAccountTest.php b/tests/scenarios/MobileApplication/UserAccountTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ded7d15e8bf6f1ccf1360c395300fb842b0ed0b5
--- /dev/null
+++ b/tests/scenarios/MobileApplication/UserAccountTest.php
@@ -0,0 +1,342 @@
+<?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
+ */
+abstract class Scenario_MobileApplication_UserAccountTestCase extends AbstractControllerTestCase {
+  protected
+    $_storm_default_to_volatile = true;
+
+  public function setUp() {
+    parent::setUp();
+
+    $_SERVER['HTTPS'] = 'on';
+
+    $puppy = $this->fixture('Class_Users',
+                            ['id' => 345,
+                             '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' => 'My mobile app',
+                    'user' => $puppy]);
+
+    $potter = new Class_WebService_SIGB_Emprunt('12', new Class_WebService_SIGB_Exemplaire(123));
+    $potter
+      ->setDateRetour('01/01/1974')
+      ->setAuteur('J.K.R')
+      ->setBibliotheque('Annecy')
+      ->getExemplaire()->setTitre('Potter');
+
+    $alice = new Class_WebService_SIGB_Emprunt('13', new Class_WebService_SIGB_Exemplaire(456));
+    $alice
+      ->setDateRetour(date('d/m/Y', strtotime('tomorrow')))
+      ->getExemplaire()->setTitre('Alice');
+
+    $emprunteur = (new Class_WebService_SIGB_Emprunteur(345, 'puppy'))
+      ->empruntsAddAll([$potter, $alice]);
+
+    $puppy->setFicheSIGB(['fiche' => $emprunteur]);
+    $puppy->assertSave();
+
+
+    ZendAfi_Auth::getInstance()->clearIdentity();
+  }
+
+
+  public function tearDown() {
+    unset($_SERVER['HTTPS']);
+    parent::tearDown();
+  }
+}
+
+
+
+
+class Scenario_MobileApplication_UserAccountWithTokenTest extends Scenario_MobileApplication_UserAccountTestCase {
+  protected
+    $_json;
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->dispatch('/api/user/loans',
+                    true,
+                    ["Authorization" => "Bearer nonos" ,
+                     "Content-Type" => "application/json"]);
+
+    $this->_json = json_decode($this->_response->getBody(), true);
+  }
+
+
+  /** @test */
+  public function responseShouldContainsPotterLoan() {
+    $this->assertEquals(['title' => 'Potter',
+                         'author' => 'J.K.R',
+                         'date_due' => '1974-01-01',
+                         'loaned_by' => 'puppy',
+                         'library' => 'Annecy'
+                         ],
+                        $this->_json['loans'][0]);
+  }
+
+
+  /** @test */
+  public function responseHeaderContentTypeShouldBeApplicationJson() {
+    $this->assertArraySubset(['name' => 'Content-Type',
+                              'value' => 'application/json'],
+                             $this->_response->getHeaders()[0]);
+  }
+}
+
+
+
+
+class Scenario_MobileApplication_UserAccountWithoutTokenTest extends Scenario_MobileApplication_UserAccountTestCase {
+  /** @test */
+  public function withoutAuthorizationShouldAnswerInvalidRequest() {
+    $this->dispatch('/api/user/loans',
+                    true,
+                    ["Content-Type" => "application/json"]);
+
+    $this->assertEquals(['error' => 'invalid_request',
+                         'message' => 'Autorisation non spécifiée'],
+                        json_decode($this->_response->getBody(), true));
+  }
+
+
+  /** @test */
+  public function withWrongAuthorizationTypeShouldAnswerInvalidRequest() {
+    $this->dispatch('/api/user/loans',
+                    true,
+                    ["Authorization" => 'Catch nonos',
+                     "Content-Type" => "application/json"]);
+
+    $this->assertEquals(['error' => 'invalid_request',
+                         'message' => 'Jeton d\'autorisation non fourni'],
+                        json_decode($this->_response->getBody(), true));
+  }
+
+
+  /** @test */
+  public function withWrongAuthorizationTokenShouldAnswerInvalidRequest() {
+    $this->dispatch('/api/user/loans',
+                    true,
+                    ["Authorization" => 'Bearer veget@ble',
+                     "Content-Type" => "application/json"]);
+
+    $this->assertEquals(['error' => 'invalid_request',
+                         'message' => 'Jeton d\'autorisation invalide'],
+                        json_decode($this->_response->getBody(), true));
+  }
+
+
+  /** @test */
+  public function withLegacyAuthorizationTokenShouldAnswerInvalidRequest() {
+    $this->fixture('Class_User_ApiToken',
+                   ['id' => 2,
+                    'token' => 'veget@ble',
+                    'user_id' => 987]);
+
+    $this->dispatch('/api/user/loans',
+                    true,
+                    ["Authorization" => 'Bearer veget@ble',
+                     "Content-Type" => "application/json"]);
+
+    $this->assertEquals(['error' => 'invalid_request',
+                         'message' => 'Utilisateur non trouvé'],
+                        json_decode($this->_response->getBody(), true));
+  }
+
+
+  /** @test */
+  public function withoutHttpsShouldAnswerInvalidRequest() {
+    unset($_SERVER['HTTPS']);
+
+    $this->dispatch('/api/user/loans',
+                    true,
+                    ["Authorization" => "Bearer nonos" ,
+                     "Content-Type" => "application/json"]);
+
+    $this->assertEquals(['error' => 'invalid_request',
+                         'message' => 'Protocole HTTP obligatoire'],
+                        json_decode($this->_response->getBody(), true));
+  }
+}
+
+
+
+class Scenario_MobileApplication_UserAccountOAuthForLoginErrorsTest extends Scenario_MobileApplication_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'],
+    ];
+  }
+
+  /**
+   * @dataProvider wrongUrls
+   * @test
+   */
+  public function withIncompleUrlShouldError400BadRequest($url) {
+    try {
+      $this->dispatch($url, true);
+    }  catch(Zend_Controller_Action_Exception $e) {
+      $this->assertEquals(400, $e->getCode());
+      return;
+    }
+    $this->fail('should raise error 400 bad request');
+  }
+}
+
+
+
+
+class Scenario_MobileApplication_UserAccountOAuthForLoginTest extends Scenario_MobileApplication_UserAccountTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/auth/oauth/response_type/code/client_id/My%20mobile%20app/redirect_uri/'
+                    . urlencode('bokeh://authorize'),
+                    true);
+  }
+
+
+  /** @test */
+  public function pageShouldDisplayLoginForm() {
+    $this->assertXPath('//form//input[@name="username"]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsMyMobileAppWantsToConnect() {
+    $this->assertXPathContentContains('//h1',
+                                      'Authentifiez-vous pour autoriser "My mobile app" à accéder à votre compte');
+  }
+}
+
+
+
+
+class Scenario_MobileApplication_UserAccountOAuthForLoginOnPhoneTest extends Scenario_MobileApplication_UserAccountTestCase {
+  public function setUp() {
+    parent::setUp();
+    $_SERVER['HTTP_USER_AGENT'] = 'iPhone';
+    Class_Profil::getCurrentProfil()
+      ->beTelephone()
+      ->assertSave();
+
+    $this->dispatch('/auth/oauth/response_type/code/client_id/My%20mobile%20app/redirect_uri/'
+                    . urlencode('bokeh://authorize'),
+                    true);
+  }
+
+
+  public function tearDown() {
+    unset($_SERVER['HTTP_USER_AGENT']);
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function pageShouldDisplayLoginForm() {
+    $this->assertXPath('//form//input[@name="username"]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsMyMobileAppWantsToConnect() {
+    $this->assertXPathContentContains('//h1',
+                                      'Authentifiez-vous pour autoriser "My mobile app" à accéder à votre compte');
+  }
+}
+
+
+
+
+class Scenario_MobileApplication_UserAccountOAuthPostLoginSuccessTest extends Scenario_MobileApplication_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 responseShouldRedirectToBokehAuthorizeWithExistingToken() {
+    $this->postDispatch('/opac/auth/oauth/response_type/code/client_id/My%20mobile%20app/redirect_uri/' . urlencode('bokeh://authorize'),
+                        ['username' => 'puppy', 'password' => 'opied'], true);
+
+    $this->assertRedirectTo('bokeh://authorize#token=nonos');
+  }
+
+
+  /** @test */
+  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'),
+                        ['username' => 'puppy', 'password' => 'opied'], true);
+
+    $token = Class_User_ApiToken::find(1);
+    $this->assertRedirectTo('bokeh://authorize#token=' . $token->getToken());
+    return $token;
+  }
+
+
+  /**
+   * @depends tokenShouldBeCreatedIfNotExists
+   * @test
+   */
+  public function tokenClientIdShouldBeMyMobileBokeh($token) {
+    $this->assertEquals('My mobile bokeh', $token->getClientId());
+  }
+}
+?>
\ No newline at end of file