diff --git a/cps.py b/cps.py
index 4daa0d9b..7da3f790 100755
--- a/cps.py
+++ b/cps.py
@@ -41,8 +41,13 @@ from cps.shelf import shelf
 from cps.admin import admi
 from cps.gdrive import gdrive
 from cps.editbooks import editbook
-from cps.kobo import kobo
-from cps.kobo_auth import kobo_auth
+
+try:
+    from cps.kobo import kobo
+    from cps.kobo_auth import kobo_auth
+    kobo_available = True
+except ImportError:
+    kobo_available = False
 
 try:
     from cps.oauth_bb import oauth
@@ -61,8 +66,9 @@ def main():
     app.register_blueprint(admi)
     app.register_blueprint(gdrive)
     app.register_blueprint(editbook)
-    app.register_blueprint(kobo)
-    app.register_blueprint(kobo_auth)
+    if kobo_available:
+        app.register_blueprint(kobo)
+        app.register_blueprint(kobo_auth)
     if oauth_available:
         app.register_blueprint(oauth)
     success = web_server.start()
diff --git a/cps/about.py b/cps/about.py
index aa1e866e..ceef7308 100644
--- a/cps/about.py
+++ b/cps/about.py
@@ -67,6 +67,7 @@ _VERSIONS = OrderedDict(
     Unidecode = unidecode_version,
     Flask_SimpleLDAP =  u'installed' if bool(services.ldap) else u'not installed',
     Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed',
+    jsonschema = services.SyncToken.__version__  if bool(services.SyncToken) else u'not installed',
 
 )
 _VERSIONS.update(uploader.get_versions())
diff --git a/cps/admin.py b/cps/admin.py
index b84f5a74..91ebe997 100644
--- a/cps/admin.py
+++ b/cps/admin.py
@@ -45,7 +45,8 @@ from .web import admin_required, render_title_template, before_request, unconfig
 
 feature_support = {
         'ldap': False, # bool(services.ldap),
-        'goodreads': bool(services.goodreads_support)
+        'goodreads': bool(services.goodreads_support),
+        'kobo':  bool(services.kobo)
     }
 
 # try:
@@ -63,6 +64,7 @@ except ImportError:
     oauth_check = {}
 
 
+
 feature_support['gdrive'] = gdrive_support
 admi = Blueprint('admin', __name__)
 log = logger.create()
@@ -568,7 +570,7 @@ def _configuration_update_helper():
     # Remote login configuration
     _config_checkbox("config_remote_login")
     if not config.config_remote_login:
-        ub.session.query(ub.RemoteAuthToken).delete()
+        ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.token_type==0).delete()
 
     # Goodreads configuration
     _config_checkbox("config_use_goodreads")
@@ -693,7 +695,8 @@ def new_user():
         if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
             flash(_(u"Please fill out all fields!"), category="error")
             return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
-                                         registered_oauth=oauth_check, title=_(u"Add new user"))
+                                         registered_oauth=oauth_check, feature_support=feature_support,
+                                         title=_(u"Add new user"))
         content.password = generate_password_hash(to_save["password"])
         existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower())\
             .first()
@@ -704,14 +707,15 @@ def new_user():
             if config.config_public_reg and not check_valid_domain(to_save["email"]):
                 flash(_(u"E-mail is not from valid domain"), category="error")
                 return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
-                                             registered_oauth=oauth_check, title=_(u"Add new user"))
+                                             registered_oauth=oauth_check, feature_support=feature_support,
+                                             title=_(u"Add new user"))
             else:
                 content.email = to_save["email"]
         else:
             flash(_(u"Found an existing account for this e-mail address or nickname."), category="error")
             return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
                                      languages=languages, title=_(u"Add new user"), page="newuser",
-                                     registered_oauth=oauth_check)
+                                     feature_support=feature_support, registered_oauth=oauth_check)
         try:
             ub.session.add(content)
             ub.session.commit()
@@ -729,7 +733,7 @@ def new_user():
         # content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT)
     return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
                                  languages=languages, title=_(u"Add new user"), page="newuser",
-                                 registered_oauth=oauth_check)
+                                 feature_support=feature_support, registered_oauth=oauth_check)
 
 
 @admi.route("/admin/mailsettings")
@@ -850,8 +854,14 @@ def edit_user(user_id):
                         content.kobo_user_key_hash = kobo_user_key_hash
                     else:
                         flash(_(u"Found an existing account for this Kobo UserKey."), category="error")
-                        return render_title_template("user_edit.html", translations=translations, languages=languages,
-                                                     new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check,
+                        return render_title_template("user_edit.html",
+                                                     translations=translations,
+                                                     languages=languages,
+                                                     new_user=0,
+                                                     content=content,
+                                                     downloads=downloads,
+                                                     registered_oauth=oauth_check,
+                                                     feature_support=feature_support,
                                                      title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
             if to_save["email"] and to_save["email"] != content.email:
                 existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \
@@ -860,9 +870,15 @@ def edit_user(user_id):
                     content.email = to_save["email"]
                 else:
                     flash(_(u"Found an existing account for this e-mail address."), category="error")
-                    return render_title_template("user_edit.html", translations=translations, languages=languages,
+                    return render_title_template("user_edit.html",
+                                                 translations=translations,
+                                                 languages=languages,
                                                  mail_configured = config.get_mail_server_configured(),
-                                                 new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check,
+                                                 feature_support=feature_support,
+                                                 new_user=0,
+                                                 content=content,
+                                                 downloads=downloads,
+                                                 registered_oauth=oauth_check,
                                                  title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
             if "nickname" in to_save and to_save["nickname"] != content.nickname:
                 # Query User nickname, if not existing, change
@@ -877,6 +893,7 @@ def edit_user(user_id):
                                                  new_user=0, content=content,
                                                  downloads=downloads,
                                                  registered_oauth=oauth_check,
+                                                 feature_support=feature_support,
                                                  title=_(u"Edit User %(nick)s", nick=content.nickname),
                                                  page="edituser")
 
@@ -888,9 +905,15 @@ def edit_user(user_id):
         except IntegrityError:
             ub.session.rollback()
             flash(_(u"An unknown error occured."), category="error")
-    return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0,
-                                 content=content, downloads=downloads, registered_oauth=oauth_check,
+    return render_title_template("user_edit.html",
+                                 translations=translations,
+                                 languages=languages,
+                                 new_user=0,
+                                 content=content,
+                                 downloads=downloads,
+                                 registered_oauth=oauth_check,
                                  mail_configured=config.get_mail_server_configured(),
+                                 feature_support=feature_support,
                                  title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
 
 
diff --git a/cps/kobo.py b/cps/kobo.py
index 67687008..73536551 100644
--- a/cps/kobo.py
+++ b/cps/kobo.py
@@ -17,10 +17,8 @@
 #  You should have received a copy of the GNU General Public License
 #  along with this program. If not, see <http://www.gnu.org/licenses/>.
 
-import os
 import sys
 import uuid
-from base64 import b64decode, b64encode
 from datetime import datetime
 from time import gmtime, strftime
 try:
@@ -28,14 +26,12 @@ try:
 except ImportError:
     from urllib.parse import unquote
 
-from jsonschema import validate, exceptions
 from flask import (
     Blueprint,
     request,
     make_response,
     jsonify,
     json,
-    current_app,
     url_for,
     redirect,
 )
@@ -44,7 +40,7 @@ from werkzeug.datastructures import Headers
 from sqlalchemy import func
 import requests
 
-from . import config, logger, kobo_auth, db, helper
+from . import config, logger, kobo_auth, db, helper, services
 from .web import download_required
 
 KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB", "EPUB3"]}
@@ -56,19 +52,6 @@ kobo_auth.register_url_value_preprocessor(kobo)
 
 log = logger.create()
 
-
-def b64encode_json(json_data):
-    if sys.version_info < (3, 0):
-        return b64encode(json.dumps(json_data))
-    else:
-        return b64encode(json.dumps(json_data).encode())
-
-
-# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
-def to_epoch_timestamp(datetime_object):
-    return (datetime_object - datetime(1970, 1, 1)).total_seconds()
-
-
 def get_store_url_for_current_request():
     # Programmatically modify the current url to point to the official Kobo store
     base, sep, request_path_with_auth_token = request.full_path.rpartition("/kobo/")
@@ -110,117 +93,11 @@ def redirect_or_proxy_request():
         )
 
 
-class SyncToken:
-    """ The SyncToken is used to persist state accross requests.
-    When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service.
-    As an example use-case, the SyncToken is used to detect books that have been added to the library since the last time the device synced to the server.
-
-    Attributes:
-        books_last_created: Datetime representing the newest book that the device knows about.
-        books_last_modified: Datetime representing the last modified book that the device knows about.
-    """
-
-    SYNC_TOKEN_HEADER = "x-kobo-synctoken"
-    VERSION = "1-0-0"
-    MIN_VERSION = "1-0-0"
-
-    token_schema = {
-        "type": "object",
-        "properties": {"version": {"type": "string"}, "data": {"type": "object"},},
-    }
-    # This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device.
-    # A potential solution might be to keep a list of all known book uuids in the token, and look for any missing from the db.
-    data_schema_v1 = {
-        "type": "object",
-        "properties": {
-            "raw_kobo_store_token": {"type": "string"},
-            "books_last_modified": {"type": "string"},
-            "books_last_created": {"type": "string"},
-        },
-    }
-
-    def __init__(
-        self,
-        raw_kobo_store_token="",
-        books_last_created=datetime.min,
-        books_last_modified=datetime.min,
-    ):
-        self.raw_kobo_store_token = raw_kobo_store_token
-        self.books_last_created = books_last_created
-        self.books_last_modified = books_last_modified
-
-    @staticmethod
-    def from_headers(headers):
-        sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
-        if sync_token_header == "":
-            return SyncToken()
-
-        # On the first sync from a Kobo device, we may receive the SyncToken
-        # from the official Kobo store. Without digging too deep into it, that
-        # token is of the form [b64encoded blob].[b64encoded blob 2]
-        if "." in sync_token_header:
-            return SyncToken(raw_kobo_store_token=sync_token_header)
-
-        try:
-            sync_token_json = json.loads(
-                b64decode(sync_token_header + "=" * (-len(sync_token_header) % 4))
-            )
-            validate(sync_token_json, SyncToken.token_schema)
-            if sync_token_json["version"] < SyncToken.MIN_VERSION:
-                raise ValueError
-
-            data_json = sync_token_json["data"]
-            validate(sync_token_json, SyncToken.data_schema_v1)
-        except (exceptions.ValidationError, ValueError) as e:
-            log.error("Sync token contents do not follow the expected json schema.")
-            return SyncToken()
-
-        raw_kobo_store_token = data_json["raw_kobo_store_token"]
-        try:
-            books_last_modified = datetime.utcfromtimestamp(
-                data_json["books_last_modified"]
-            )
-            books_last_created = datetime.utcfromtimestamp(
-                data_json["books_last_created"]
-            )
-        except TypeError:
-            log.error("SyncToken timestamps don't parse to a datetime.")
-            return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
-
-        return SyncToken(
-            raw_kobo_store_token=raw_kobo_store_token,
-            books_last_created=books_last_created,
-            books_last_modified=books_last_modified,
-        )
-
-    def set_kobo_store_header(self, store_headers):
-        store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token)
-
-    def merge_from_store_response(self, store_response):
-        self.raw_kobo_store_token = store_response.headers.get(
-            SyncToken.SYNC_TOKEN_HEADER, ""
-        )
-
-    def to_headers(self, headers):
-        headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token()
-
-    def build_sync_token(self):
-        token = {
-            "version": SyncToken.VERSION,
-            "data": {
-                "raw_kobo_store_token": self.raw_kobo_store_token,
-                "books_last_modified": to_epoch_timestamp(self.books_last_modified),
-                "books_last_created": to_epoch_timestamp(self.books_last_created),
-            },
-        }
-        return b64encode_json(token)
-
-
 @kobo.route("/v1/library/sync")
 @login_required
 @download_required
 def HandleSyncRequest():
-    sync_token = SyncToken.from_headers(request.headers)
+    sync_token = services.SyncToken.from_headers(request.headers)
     log.info("Kobo library sync request received.")
 
     # TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header
diff --git a/cps/kobo_auth.py b/cps/kobo_auth.py
index 2077ce75..60f3ea5f 100644
--- a/cps/kobo_auth.py
+++ b/cps/kobo_auth.py
@@ -23,13 +23,13 @@ This module also includes research notes into the auth protocol used by Kobo dev
 
 Log-in:
 When first booting a Kobo device the user must sign into a Kobo (or affiliate) account.
-Upon successful sign-in, the user is redirected to 
+Upon successful sign-in, the user is redirected to
     https://auth.kobobooks.com/CrossDomainSignIn?id=<some id>
 which serves the following response:
     <script type='text/javascript'>location.href='kobo://UserAuthenticated?userId=<redacted>&userKey<redacted>&email=<redacted>&returnUrl=https%3a%2f%2fwww.kobo.com';</script>.
 And triggers the insertion of a userKey into the device's User table.
 
-Together, the device's DeviceId and UserKey act as an *irrevocable* authentication 
+Together, the device's DeviceId and UserKey act as an *irrevocable* authentication
 token to most (if not all) Kobo APIs. In fact, in most cases only the UserKey is
 required to authorize the API call.
 
@@ -95,7 +95,7 @@ def load_user_from_kobo_request(request):
         user = (
             ub.session.query(ub.User)
             .join(ub.RemoteAuthToken)
-            .filter(ub.RemoteAuthToken.auth_token == auth_token)
+            .filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
             .first()
         )
         if user is not None:
@@ -108,21 +108,23 @@ def load_user_from_kobo_request(request):
 kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
 
 
-@kobo_auth.route("/generate_auth_token")
+@kobo_auth.route("/generate_auth_token/<int:user_id>")
 @login_required
-def generate_auth_token():
+def generate_auth_token(user_id):
     # Invalidate any prevously generated Kobo Auth token for this user.
-    ub.session.query(ub.RemoteAuthToken).filter(
-        ub.RemoteAuthToken.user_id == current_user.id
-    ).delete()
+    auth_token = ub.session.query(ub.RemoteAuthToken).filter(
+        ub.RemoteAuthToken.user_id == user_id
+    ).filter(ub.RemoteAuthToken.token_type==1).first()
 
-    auth_token = ub.RemoteAuthToken()
-    auth_token.user_id = current_user.id
-    auth_token.expiration = datetime.max
-    auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
+    if not auth_token:
+        auth_token = ub.RemoteAuthToken()
+        auth_token.user_id = user_id
+        auth_token.expiration = datetime.max
+        auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
+        auth_token.token_type = 1
 
-    ub.session.add(auth_token)
-    ub.session.commit()
+        ub.session.add(auth_token)
+        ub.session.commit()
 
     return render_title_template(
         "generate_kobo_auth_url.html",
@@ -131,3 +133,13 @@ def generate_auth_token():
             "kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True
         ),
     )
+
+
+@kobo_auth.route("/deleteauthtoken/<int:user_id>")
+@login_required
+def delete_auth_token(user_id):
+    # Invalidate any prevously generated Kobo Auth token for this user.
+    ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
+        .filter(ub.RemoteAuthToken.token_type==1).delete()
+    ub.session.commit()
+    return ""
diff --git a/cps/services/SyncToken.py b/cps/services/SyncToken.py
new file mode 100644
index 00000000..2a17f7b5
--- /dev/null
+++ b/cps/services/SyncToken.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+#  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
+#    Copyright (C) 2018-2019 shavitmichael, OzzieIsaacs
+#
+#  This program is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  This program 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 General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+from base64 import b64decode, b64encode
+from jsonschema import validate, exceptions, __version__
+from datetime import datetime
+try:
+    from urllib import unquote
+except ImportError:
+    from urllib.parse import unquote
+
+from flask import json
+from .. import logger as log
+
+
+def b64encode_json(json_data):
+    if sys.version_info < (3, 0):
+        return b64encode(json.dumps(json_data))
+    else:
+        return b64encode(json.dumps(json_data).encode())
+
+
+# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
+def to_epoch_timestamp(datetime_object):
+    return (datetime_object - datetime(1970, 1, 1)).total_seconds()
+
+
+class SyncToken:
+    """ The SyncToken is used to persist state accross requests.
+    When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service.
+    As an example use-case, the SyncToken is used to detect books that have been added to the library since the last time the device synced to the server.
+
+    Attributes:
+        books_last_created: Datetime representing the newest book that the device knows about.
+        books_last_modified: Datetime representing the last modified book that the device knows about.
+    """
+
+    SYNC_TOKEN_HEADER = "x-kobo-synctoken"
+    VERSION = "1-0-0"
+    MIN_VERSION = "1-0-0"
+
+    token_schema = {
+        "type": "object",
+        "properties": {"version": {"type": "string"}, "data": {"type": "object"},},
+    }
+    # This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device.
+    # A potential solution might be to keep a list of all known book uuids in the token, and look for any missing from the db.
+    data_schema_v1 = {
+        "type": "object",
+        "properties": {
+            "raw_kobo_store_token": {"type": "string"},
+            "books_last_modified": {"type": "string"},
+            "books_last_created": {"type": "string"},
+        },
+    }
+
+    def __init__(
+        self,
+        raw_kobo_store_token="",
+        books_last_created=datetime.min,
+        books_last_modified=datetime.min,
+    ):
+        self.raw_kobo_store_token = raw_kobo_store_token
+        self.books_last_created = books_last_created
+        self.books_last_modified = books_last_modified
+
+    @staticmethod
+    def from_headers(headers):
+        sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
+        if sync_token_header == "":
+            return SyncToken()
+
+        # On the first sync from a Kobo device, we may receive the SyncToken
+        # from the official Kobo store. Without digging too deep into it, that
+        # token is of the form [b64encoded blob].[b64encoded blob 2]
+        if "." in sync_token_header:
+            return SyncToken(raw_kobo_store_token=sync_token_header)
+
+        try:
+            sync_token_json = json.loads(
+                b64decode(sync_token_header + "=" * (-len(sync_token_header) % 4))
+            )
+            validate(sync_token_json, SyncToken.token_schema)
+            if sync_token_json["version"] < SyncToken.MIN_VERSION:
+                raise ValueError
+
+            data_json = sync_token_json["data"]
+            validate(sync_token_json, SyncToken.data_schema_v1)
+        except (exceptions.ValidationError, ValueError) as e:
+            log.error("Sync token contents do not follow the expected json schema.")
+            return SyncToken()
+
+        raw_kobo_store_token = data_json["raw_kobo_store_token"]
+        try:
+            books_last_modified = datetime.utcfromtimestamp(
+                data_json["books_last_modified"]
+            )
+            books_last_created = datetime.utcfromtimestamp(
+                data_json["books_last_created"]
+            )
+        except TypeError:
+            log.error("SyncToken timestamps don't parse to a datetime.")
+            return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
+
+        return SyncToken(
+            raw_kobo_store_token=raw_kobo_store_token,
+            books_last_created=books_last_created,
+            books_last_modified=books_last_modified,
+        )
+
+    def set_kobo_store_header(self, store_headers):
+        store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token)
+
+    def merge_from_store_response(self, store_response):
+        self.raw_kobo_store_token = store_response.headers.get(
+            SyncToken.SYNC_TOKEN_HEADER, ""
+        )
+
+    def to_headers(self, headers):
+        headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token()
+
+    def build_sync_token(self):
+        token = {
+            "version": SyncToken.VERSION,
+            "data": {
+                "raw_kobo_store_token": self.raw_kobo_store_token,
+                "books_last_modified": to_epoch_timestamp(self.books_last_modified),
+                "books_last_created": to_epoch_timestamp(self.books_last_created),
+            },
+        }
+        return b64encode_json(token)
diff --git a/cps/services/__init__.py b/cps/services/__init__.py
index d468d0b7..18b49b88 100644
--- a/cps/services/__init__.py
+++ b/cps/services/__init__.py
@@ -35,4 +35,9 @@ except ImportError as err:
     log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err)
     ldap = None
 
-
+try:
+    from . import SyncToken as SyncToken
+    kobo = True
+except ImportError as err:
+    log.debug("cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
+    kobo = None
diff --git a/cps/static/js/main.js b/cps/static/js/main.js
index 2b2716bf..8f54ba45 100644
--- a/cps/static/js/main.js
+++ b/cps/static/js/main.js
@@ -228,6 +228,41 @@ $(function() {
             $(this).find(".modal-body").html("...");
         });
 
+    $("#modal_kobo_token")
+        .on("show.bs.modal", function(e) {
+            var $modalBody = $(this).find(".modal-body");
+
+            // Prevent static assets from loading multiple times
+            var useCache = function(options) {
+                options.async = true;
+                options.cache = true;
+            };
+            preFilters.add(useCache);
+
+            $.get(e.relatedTarget.href).done(function(content) {
+                $modalBody.html(content);
+                preFilters.remove(useCache);
+            });
+        })
+        .on("hidden.bs.modal", function() {
+            $(this).find(".modal-body").html("...");
+             $("#config_delete_kobo_token").show();
+        });
+
+    $("#btndeletetoken").click(function() {
+        //get data-id attribute of the clicked element
+        var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length-1].src;
+        var path = src.substring(0,src.lastIndexOf("/"));
+        // var domainId = $(this).value("domainId");
+        $.ajax({
+            method:"get",
+            url: path + "/../../kobo_auth/deleteauthtoken/" + this.value,
+        });
+        $("#modalDeleteToken").modal("hide");
+        $("#config_delete_kobo_token").hide();
+
+    });
+
     $(window).resize(function() {
         $(".discover .row").isotope("layout");
     });
diff --git a/cps/templates/generate_kobo_auth_url.html b/cps/templates/generate_kobo_auth_url.html
index 28b098cf..307b26bd 100644
--- a/cps/templates/generate_kobo_auth_url.html
+++ b/cps/templates/generate_kobo_auth_url.html
@@ -1,7 +1,6 @@
-{% extends "layout.html" %}
+{% extends "fragment.html" %}
 {% block body %}
 <div class="well">
-  <h2 style="margin-top: 0">{{_('Generate Kobo Auth URL')}}</h2>
   <p>
     {{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}</a>.
   </p>
@@ -12,4 +11,4 @@
     {{_('Please note that every visit to this current page invalidates any previously generated Authentication url for this user.')}}</a>.
   </p>
 </div>
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/cps/templates/user_edit.html b/cps/templates/user_edit.html
index 3e1963f1..a044f270 100644
--- a/cps/templates/user_edit.html
+++ b/cps/templates/user_edit.html
@@ -59,6 +59,13 @@
       {% endfor %}
     </div>
     {% endif %}
+    {% if feature_support['kobo'] %}
+    <label>{{ _('Kobo Sync Token')}}</label>
+    <div class="form-group col">
+      <a class="btn btn-default" id="config_create_kobo_token" data-toggle="modal" data-target="#modal_kobo_token" data-remote="false" href="{{ url_for('kobo_auth.generate_auth_token', user_id=content.id) }}">{{_('Create/View')}}</a>
+      <div class="btn btn-danger" id="config_delete_kobo_token" data-toggle="modal" data-target="#modalDeleteToken" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
+    </div>
+    {% endif %}
     <div class="col-sm-6">
         {% for element in sidebar %}
           {% if element['config_show'] %}
@@ -146,6 +153,35 @@
     </div>
   {% endif %}
 </div>
+    <div class="modal fade" id="modal_kobo_token" tabindex="-1" role="dialog" aria-labelledby="kobo_tokenModalLabel">
+      <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+          <div class="modal-header">
+            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+            <h4 class="modal-title" id="kobo_tokenModalLabel">{{_('Generate Kobo Auth URL')}}</h4>
+          </div>
+          <div class="modal-body">...</div>
+          <div class="modal-footer">
+            <button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+<div id="modalDeleteToken" class="modal fade" role="dialog">
+  <div class="modal-dialog modal-sm">
+    <div class="modal-content">
+      <div class="modal-header bg-danger">
+      </div>
+      <div class="modal-body text-center">
+        <p>{{_('Do you really want to delete the Kobo Token?')}}</p>
+        <button type="button" class="btn btn-danger" id="btndeletetoken" value="{{content.id}}">{{_('Delete')}}</button>
+        <button type="button" class="btn btn-default" id="btncancel" data-dismiss="modal">{{_('Back')}}</button>
+      </div>
+    </div>
+  </div>
+</div>
+
 {% endblock %}
 {% block modal %}
 {{ restrict_modal() }}
diff --git a/cps/ub.py b/cps/ub.py
index c4c69500..e79c62b2 100644
--- a/cps/ub.py
+++ b/cps/ub.py
@@ -199,6 +199,7 @@ class User(UserBase, Base):
     allowed_tags = Column(String, default="")
     restricted_column_value = Column(String, default="")
     allowed_column_value = Column(String, default="")
+    remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic')
 
 
 if oauth_support:
@@ -333,6 +334,7 @@ class RemoteAuthToken(Base):
     user_id = Column(Integer, ForeignKey('user.id'))
     verified = Column(Boolean, default=False)
     expiration = Column(DateTime)
+    token_type = Column(Integer, default=0)
 
     def __init__(self):
         self.auth_token = (hexlify(os.urandom(4))).decode('utf-8')
@@ -364,6 +366,15 @@ def migrate_Database(session):
         conn.execute("ALTER TABLE registration ADD column 'allow' INTEGER")
         conn.execute("update registration set 'allow' = 1")
         session.commit()
+    try:
+        session.query(exists().where(RemoteAuthToken.token_type)).scalar()
+        session.commit()
+    except exc.OperationalError:  # Database is not compatible, some columns are missing
+        conn = engine.connect()
+        conn.execute("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0")
+        conn.execute("update remote_auth_token set 'token_type' = 0")
+        session.commit()
+
     # Handle table exists, but no content
     cnt = session.query(Registration).count()
     if not cnt:
diff --git a/cps/web.py b/cps/web.py
index 243ca802..24f26953 100644
--- a/cps/web.py
+++ b/cps/web.py
@@ -55,7 +55,8 @@ from .redirect import redirect_back
 
 feature_support = {
         'ldap': False, # bool(services.ldap),
-        'goodreads': bool(services.goodreads_support)
+        'goodreads': bool(services.goodreads_support),
+        'kobo':  bool(services.kobo)
     }
 
 try:
@@ -1319,6 +1320,7 @@ def profile():
                 flash(_(u"E-mail is not from valid domain"), category="error")
                 return render_title_template("user_edit.html", content=current_user, downloads=downloads,
                                              title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
+                                             feature_support=feature_support,
                                              registered_oauth=oauth_check, oauth_status=oauth_status)
         if "nickname" in to_save and to_save["nickname"] != current_user.nickname:
             # Query User nickname, if not existing, change
@@ -1329,6 +1331,7 @@ def profile():
                 return render_title_template("user_edit.html",
                                              translations=translations,
                                              languages=languages,
+                                             feature_support=feature_support,
                                              new_user=0, content=current_user,
                                              downloads=downloads,
                                              registered_oauth=oauth_check,
@@ -1360,13 +1363,13 @@ def profile():
             flash(_(u"Found an existing account for this e-mail address."), category="error")
             log.debug(u"Found an existing account for this e-mail address.")
             return render_title_template("user_edit.html", content=current_user, downloads=downloads,
-                                         translations=translations,
+                                         translations=translations, feature_support=feature_support,
                                          title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
                                                  registered_oauth=oauth_check, oauth_status=oauth_status)
         flash(_(u"Profile updated"), category="success")
         log.debug(u"Profile updated")
     return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages,
-                                 content=current_user, downloads=downloads,
+                                 content=current_user, downloads=downloads, feature_support=feature_support,
                                  title= _(u"%(name)s's profile", name=current_user.nickname),
                                  page="me", registered_oauth=oauth_check, oauth_status=oauth_status)
 
diff --git a/optional-requirements.txt b/optional-requirements.txt
index 605667d2..84af8426 100644
--- a/optional-requirements.txt
+++ b/optional-requirements.txt
@@ -32,3 +32,6 @@ rarfile>=2.7
 # other
 natsort>=2.2.0
 git+https://github.com/OzzieIsaacs/comicapi.git@5346716578b2843f54d522f44d01bc8d25001d24#egg=comicapi
+
+#kobo integration
+jsonschema>=3.2.0
diff --git a/requirements.txt b/requirements.txt
index 2ef6835d..daf2538d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,4 +13,3 @@ SQLAlchemy>=1.1.0
 tornado>=4.1
 Wand>=0.4.4
 unidecode>=0.04.19
-jsonschema>=3.2.0