diff --git a/invenio/modules/oauthclient/__init__.py b/invenio/modules/oauthclient/__init__.py
new file mode 100644
index 000000000..0eab0f888
--- /dev/null
+++ b/invenio/modules/oauthclient/__init__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+##
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
diff --git a/invenio/modules/oauthclient/client.py b/invenio/modules/oauthclient/client.py
new file mode 100644
index 000000000..6576330f1
--- /dev/null
+++ b/invenio/modules/oauthclient/client.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+##
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+from flask_oauthlib.client import OAuth
+from flask.ext.registry import DictRegistry, RegistryProxy
+
+oauth = OAuth()
+"""
+Flask-OAuthlib extension
+"""
+
+handlers = RegistryProxy('oauthclientext.handlers', DictRegistry)
+"""
+Registry of handlers for authorized handler callbacks
+"""
+
+disconnect_handlers = RegistryProxy(
+    'oauthclientext.disconnecthandlers', DictRegistry
+)
+"""
+Registry of handlers for authorized handler callbacks
+"""
diff --git a/invenio/modules/oauthclient/config.py b/invenio/modules/oauthclient/config.py
new file mode 100644
index 000000000..d4872448a
--- /dev/null
+++ b/invenio/modules/oauthclient/config.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+##
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+OAUTHCLIENT_REMOTE_APPS = {}
+"""
+Configuration of remote applications
+"""
+
+OAUTHCLIENT_SESSION_KEY_PREFIX = "oauth_token"
+"""
+The session key prefix used when storing the access token for a remote app
+in session.
+"""
diff --git a/invenio/modules/oauthclient/handlers.py b/invenio/modules/oauthclient/handlers.py
new file mode 100644
index 000000000..1e275cd9e
--- /dev/null
+++ b/invenio/modules/oauthclient/handlers.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+##
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+import six
+from functools import wraps, partial
+from werkzeug.utils import import_string
+from flask import session, redirect, flash, url_for, current_app
+from flask.ext.login import current_user
+
+from invenio.base.globals import cfg
+
+from .models import RemoteToken, RemoteAccount
+
+
+def token_session_key(remote_app):
+    """ Generate a session key used to store the token for a remote app """
+    return '%s_%s' % (cfg['OAUTHCLIENT_SESSION_KEY_PREFIX'], remote_app)
+
+
+def oauth1_token_setter(remote, resp, token_type='', extra_data=None):
+    return token_setter(
+        remote,
+        resp['oauth_token'],
+        secret=resp['oauth_token_secret'],
+        extra_data=extra_data,
+        token_type=token_type,
+    )
+
+
+def oauth2_token_setter(remote, resp, token_type='', extra_data=None):
+    return token_setter(
+        remote,
+        resp['access_token'],
+        secret='',
+        token_type=token_type,
+        extra_data=extra_data,
+    )
+
+
+def token_setter(remote, token, secret='', token_type='', extra_data=None):
+    """
+    Set token for user
+    """
+    session[token_session_key(remote.name)] = (token, secret)
+
+    # Save token if used is authenticated
+    if current_user.is_authenticated():
+        uid = current_user.get_id()
+        cid = remote.consumer_key
+
+        # Check for already existing token
+        t = RemoteToken.get(uid, cid, token_type=token_type)
+
+        if t:
+            t.update_token(token, secret)
+        else:
+            t = RemoteToken.create(
+                uid, cid, token, secret,
+                token_type=token_type, extra_data=extra_data
+            )
+        return t
+    return None
+
+
+def token_getter(remote, token=''):
+    """
+    Retrieve OAuth access token - used by flask-oauthlib to get the access
+    token when making requests.
+
+    :param token: Type of token to get. Data passed from ``oauth.request()`` to
+         identify which token to retrieve.
+    """
+    session_key = token_session_key(remote.name)
+
+    if session_key not in session and current_user.is_authenticated():
+        # Fetch key from token store if user is authenticated, and the key
+        # isn't already cached in the session.
+        remote_token = RemoteToken.get(
+            current_user.get_id(),
+            remote.consumer_key,
+            token_type=token,
+        )
+
+        if remote_token is None:
+            return None
+
+        # Store token and secret in session
+        session[session_key] = remote_token.token()
+
+    return session.get(session_key, None)
+
+
+def default_handler(resp, remote, *args, **kwargs):
+    """
+    Default authorized handler
+
+    :param resp:
+    :param remote:
+    """
+    if resp is not None:
+        if 'access_token' in resp:
+            oauth2_token_setter(remote, resp)
+        else:
+            oauth1_token_setter(remote, resp)
+    else:
+        flash("You rejected the authentication request.")
+    return redirect('/')
+
+
+def disconnect_handler(remote, *args, **kwargs):
+    if not current_user.is_authenticated():
+        current_app.login_manager.unauthorized()
+
+    account = RemoteAccount.get(
+        user_id=current_user.get_id(),
+        client_id=remote.consumer_key
+    )
+    if account:
+        account.delete()
+
+    return redirect(url_for('oauthclient_settings.index'))
+
+
+def make_handler(f, remote, with_response=True):
+    """
+    Make a handler for authorized and disconnect callbacks
+
+    :param f: Callable or an import path to a callable
+    """
+    if isinstance(f, six.text_type):
+        f = import_string(f)
+
+    @wraps(f)
+    def inner(*args, **kwargs):
+        if with_response:
+            return f(args[0], remote, *args[1:], **kwargs)
+        else:
+            return f(remote, *args, **kwargs)
+    return inner
+
+
+def make_token_getter(remote):
+    """
+    Make a token getter for a remote application
+    """
+    return partial(token_getter, remote)
diff --git a/invenio/modules/oauthclient/models.py b/invenio/modules/oauthclient/models.py
new file mode 100644
index 000000000..fd068bdce
--- /dev/null
+++ b/invenio/modules/oauthclient/models.py
@@ -0,0 +1,197 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of Invenio.
+# Copyright (C) 2014 CERN.
+#
+# Invenio 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 2 of the
+# License, or (at your option) any later version.
+#
+# Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+
+from sqlalchemy.ext.mutable import MutableDict
+from invenio.modules.accounts.models import User
+from invenio.ext.sqlalchemy import db
+
+
+class RemoteAccount(db.Model):
+    """
+    Storage for remote linked accounts
+    """
+
+    __tablename__ = 'remoteACCOUNT'
+
+    __table_args__ = (
+        db.UniqueConstraint('user_id', 'client_id'),
+        db.Model.__table_args__
+    )
+
+    #
+    # Fields
+    #
+    id = db.Column(
+        db.Integer(15, unsinged=True),
+        primary_key=True,
+        autoincrement=True
+    )
+    """ Primary key """
+
+    user_id = db.Column(
+        db.Integer(15, unsigned=True),
+        db.ForeignKey(User.id),
+        nullable=False
+    )
+    """ Local user linked with a remote app via the access token"""
+
+    client_id = db.Column(db.String(255), nullable=False)
+    """ Client ID of remote application (defined in OAUTHCLIENT_REMOTE_APPS)"""
+
+    extra_data = db.Column(MutableDict.as_mutable(db.JSON), nullable=False)
+    """ Extra data associated with this linked account """
+
+    #
+    # Relationships propoerties
+    #
+    user = db.relationship('User')
+    """ SQLAlchemy relationship to user """
+
+    tokens = db.relationship(
+        "RemoteToken",
+        backref="remote_account",
+    )
+    """ SQLAlchemy relationship to RemoteToken objects """
+
+    @classmethod
+    def get(cls, user_id, client_id):
+        """
+        Method to get RemoteAccount object for user
+        """
+        return cls.query.filter_by(
+            user_id=user_id,
+            client_id=client_id,
+        ).first()
+
+    @classmethod
+    def create(cls, user_id, client_id, extra_data):
+        account = cls(
+            user_id=user_id,
+            client_id=client_id,
+            extra_data=extra_data or dict()
+        )
+        db.session.add(account)
+        db.session.commit()
+        return account
+
+    def delete(self):
+        """
+        Delete remote account toegether with all stored tokens
+        """
+        RemoteToken.query.filter_by(id_remote_account=self.id).delete()
+        db.session.delete(self)
+        db.session.commit()
+
+
+class RemoteToken(db.Model):
+    """
+    Storage for the access tokens for linked accounts
+    """
+    __tablename__ = 'remoteTOKEN'
+
+    #
+    # Fields
+    #
+    id_remote_account = db.Column(
+        db.Integer(15, unsigned=True),
+        db.ForeignKey(RemoteAccount.id),
+        nullable=False,
+        primary_key=True
+    )
+    """ Foreign key to account """
+
+    token_type = db.Column(
+        db.String(40), default='', nullable=False, primary_key=True
+    )
+    """ Type of token """
+
+    access_token = db.Column(db.Text(), nullable=False)
+    """ Access token to remote application """
+
+    secret = db.Column(db.Text(), default='', nullable=False)
+    """ Used only by OAuth 1 """
+
+    def token(self):
+        """
+        Return token as expected by Flask-OAuthlib
+        """
+        return (self.access_token, self.secret)
+
+    def update_token(self, token, secret):
+        if self.access_token != token or self.secret != secret:
+            self.access_token = token
+            self.secret = secret
+            db.session.commit()
+
+    @classmethod
+    def get(cls, user_id, client_id, token_type='', access_token=None):
+        """
+        Method to get RemoteToken for user
+        """
+        args = [
+            RemoteAccount.user_id == user_id,
+            RemoteAccount.client_id == client_id,
+            RemoteToken.token_type == token_type,
+        ]
+
+        if access_token:
+            args.append(RemoteToken.access_token == access_token)
+
+        return cls.query.options(db.joinedload('remote_account')).filter(
+            *args
+        ).first()
+
+    @classmethod
+    def get_by_token(cls, client_id, access_token, token_type=''):
+        """
+        Method to get RemoteAccount object for token
+        """
+        return cls.query.options(db.joinedload('remote_account')).filter(
+            RemoteAccount.client_id == client_id,
+            RemoteToken.token_type == token_type,
+            RemoteAccount.access_token == access_token,
+        ).first()
+
+    @classmethod
+    def create(cls, user_id, client_id, token, secret,
+               token_type='', extra_data=None):
+        """
+        Create a new access token - if RemoteAccount doesn't exist create it
+        as well.
+        """
+        account = RemoteAccount.get(user_id, client_id)
+
+        if account is None:
+            account = RemoteAccount(
+                user_id=user_id,
+                client_id=client_id,
+                extra_data=extra_data or dict(),
+            )
+            db.session.add(account)
+
+        token = cls(
+            token_type=token_type,
+            remote_account=account,
+            access_token=token,
+            secret=secret,
+        )
+        db.session.add(token)
+        db.session.commit()
+        return token
diff --git a/invenio/modules/oauthclient/templates/oauthclient/settings/index.html b/invenio/modules/oauthclient/templates/oauthclient/settings/index.html
new file mode 100644
index 000000000..4cdf45da6
--- /dev/null
+++ b/invenio/modules/oauthclient/templates/oauthclient/settings/index.html
@@ -0,0 +1,20 @@
+{#
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+#}
+
+{% extends "oauthclient/settings/index_base.html" %}
\ No newline at end of file
diff --git a/invenio/modules/oauthclient/templates/oauthclient/settings/index_base.html b/invenio/modules/oauthclient/templates/oauthclient/settings/index_base.html
new file mode 100644
index 000000000..471dd0893
--- /dev/null
+++ b/invenio/modules/oauthclient/templates/oauthclient/settings/index_base.html
@@ -0,0 +1,48 @@
+{#
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+#}
+
+{%- import "accounts/settings/helpers.html" as helpers with context %}
+{%- from "_formhelpers.html" import render_field with context %}
+{%- extends "accounts/settings/index.html" %}
+
+{% block settings_body %}
+{{helpers.panel_start(
+    'Linked accounts',
+    with_body=False,
+    icon="fa fa-link fa-fw"
+)}}
+<div class="panel-body">
+    <p>Tired of entering password for {{config.CFG_SITE_NAME}} every time you sign in? Setup single sign-on with one or more of the services below:</p>
+</div>
+<ul class="list-group">
+    {%- for s in services %}
+    <li class="list-group-item">
+    <div class="pull-right">
+        {%- if s.account -%}
+        <a href="{{url_for('oauthclient.disconnect', remote_app=s.appid)}}" class="btn btn-default btn-xs"><i class="fa fa-times-circle"></i> Disconnect</a>
+        {%- else -%}
+        <a href="{{url_for('oauthclient.login', remote_app=s.appid)}}" class="btn btn-default btn-xs"><i class="fa fa-link"></i> Connect</a>
+        {%- endif -%}
+    </div>
+    {% if s.icon %}<i class="{{s.icon}}"></i> {% endif %}{{s.title}}{% if s.account %} <i class="fa fa-check" style="color: #5cb85c;"></i>{% endif %}<br/><small class="text-muted">{{s.description}}</small>
+    </li>
+    {%- endfor %}
+</ul>
+{{helpers.panel_end(with_body=False)}}
+{% endblock %}
\ No newline at end of file
diff --git a/invenio/modules/oauthclient/testsuite/__init__.py b/invenio/modules/oauthclient/testsuite/__init__.py
new file mode 100644
index 000000000..0eab0f888
--- /dev/null
+++ b/invenio/modules/oauthclient/testsuite/__init__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+##
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
diff --git a/invenio/modules/oauthclient/testsuite/test_models.py b/invenio/modules/oauthclient/testsuite/test_models.py
new file mode 100644
index 000000000..6100842a3
--- /dev/null
+++ b/invenio/modules/oauthclient/testsuite/test_models.py
@@ -0,0 +1,60 @@
+from __future__ import absolute_import
+
+from invenio.testsuite import InvenioTestCase
+from invenio.ext.sqlalchemy import db
+
+
+class BaseTestCase(InvenioTestCase):
+    def setUp(self):
+        from ..models import RemoteAccount, RemoteToken
+        RemoteToken.query.delete()
+        RemoteAccount.query.delete()
+        db.session.commit()
+        db.session.expunge_all()
+
+    def tearDown(self):
+        db.session.expunge_all()
+
+
+class RemoteAccountTestCase(BaseTestCase):
+    def test_get_create(self):
+        from ..models import RemoteAccount
+
+        created_acc = RemoteAccount.create(1, "dev", dict(somekey="somevalue"))
+        assert created_acc
+
+        retrieved_acc = RemoteAccount.get(1, "dev")
+        assert created_acc.id == retrieved_acc.id
+        assert retrieved_acc.extra_data == dict(somekey="somevalue")
+
+        db.session.delete(retrieved_acc)
+        assert RemoteAccount.get(1, "dev") is None
+
+
+class RemoteTokenTestCase(BaseTestCase):
+    def test_get_create(self):
+        from ..models import RemoteAccount, RemoteToken
+
+        t = RemoteToken.create(2, "dev", "mytoken", "mysecret")
+        assert t
+        assert t.token() == ('mytoken', 'mysecret')
+
+        acc = RemoteAccount.get(2, "dev")
+        assert acc
+        assert t.remote_account.id == acc.id
+        assert t.token_type == ''
+
+        t2 = RemoteToken.create(
+            2, "dev", "mytoken2", "mysecret2",
+            token_type='t2'
+        )
+        assert t2.remote_account.id == acc.id
+        assert t2.token_type == 't2'
+
+        t3 = RemoteToken.get(2, "dev")
+        t4 = RemoteToken.get(2, "dev", token_type="t2")
+        assert t4.token() != t3.token()
+
+        assert RemoteToken.query.count() == 2
+        acc.delete()
+        assert RemoteToken.query.count() == 0
diff --git a/invenio/modules/oauthclient/testsuite/test_views.py b/invenio/modules/oauthclient/testsuite/test_views.py
new file mode 100644
index 000000000..2a3ac01b0
--- /dev/null
+++ b/invenio/modules/oauthclient/testsuite/test_views.py
@@ -0,0 +1,180 @@
+from __future__ import absolute_import
+
+from mock import MagicMock, patch
+from six.moves.urllib_parse import quote_plus
+from flask import url_for, session
+from invenio.testsuite import InvenioTestCase, make_test_suite, run_test_suite
+from invenio.ext.sqlalchemy import db
+
+
+class RemoteAccountTestCase(InvenioTestCase):
+    def setUp(self):
+        params = lambda x: dict(
+            request_token_params={'scope': ''},
+            base_url='https://foo.bar/',
+            request_token_url=None,
+            access_token_url="https://foo.bar/oauth/access_token",
+            authorize_url="https://foo.bar/oauth/authorize",
+            consumer_key=x,
+            consumer_secret='testsecret',
+        )
+
+        self.app.config['OAUTHCLIENT_REMOTE_APPS'] = dict(
+            test=dict(
+                authorized_handler=self.handler,
+                params=params('testid')
+            ),
+            test_invalid=dict(
+                authorized_handler=self.handler_invalid,
+                params=params('test_invalidid')
+            ),
+            full=dict(
+                params=params("fullid")
+            ),
+        )
+        self.handled_resp = None
+        self.handled_remote = None
+        self.handled_args = None
+        self.handled_kwargs = None
+
+        from invenio.modules.oauthclient.models import RemoteToken, \
+            RemoteAccount
+        RemoteToken.query.delete()
+        RemoteAccount.query.delete()
+        db.session.commit()
+
+    def tearDown(self):
+        self.handled_resp = None
+        self.handled_remote = None
+        self.handled_args = None
+        self.handled_kwargs = None
+
+        from invenio.modules.oauthclient.models import RemoteToken, \
+            RemoteAccount
+        RemoteToken.query.delete()
+        RemoteAccount.query.delete()
+        db.session.commit()
+
+    def handler(self, resp, remote, *args, **kwargs):
+        self.handled_resp = resp
+        self.handled_remote = remote
+        self.handled_args = args
+        self.handled_kwargs = kwargs
+        return "TEST"
+
+    def handler_invalid(self):
+        self.handled_resp = 1
+        self.handled_remote = 1
+        self.handled_args = 1
+        self.handled_kwargs = 1
+
+    def mock_response(self, app='test', data=None):
+        """ Mock the oauth response to use the remote """
+        from invenio.modules.oauthclient.client import oauth
+
+        # Mock oauth remote application
+        oauth.remote_apps[app].handle_oauth2_response = MagicMock(
+            return_value=data or {
+                "access_token": "test_access_token",
+                "scope": "",
+                "token_type": "bearer"
+            }
+        )
+
+    def test_login(self):
+        # Test redirect
+        resp = self.client.get(url_for("oauthclient.login", remote_app='test'))
+        self.assertStatus(resp, 302)
+        self.assertEqual(
+            resp.location,
+            "https://foo.bar/oauth/authorize?response_type=code&"
+            "client_id=testid&redirect_uri=%s" % quote_plus(url_for(
+                "oauthclient.authorized", remote_app='test', _external=True
+            ))
+        )
+
+        # Invalid remote
+        resp = self.client.get(
+            url_for("oauthclient.login", remote_app='invalid')
+        )
+        self.assertStatus(resp, 404)
+
+    def test_authorized(self):
+        # Fake an authorized request
+        with self.app.test_client() as c:
+            # Ensure remote apps have been loaded (due to before first
+            # request)
+            c.get(url_for("oauthclient.login", remote_app='test'))
+            self.mock_response(app='test')
+            self.mock_response(app='test_invalid')
+
+            resp = c.get(
+                url_for(
+                    "oauthclient.authorized",
+                    remote_app='test',
+                    code='test',
+                )
+            )
+            assert resp.data == "TEST"
+            assert self.handled_remote.name == 'test'
+            assert not self.handled_args
+            assert not self.handled_kwargs
+            assert self.handled_resp['access_token'] == 'test_access_token'
+
+            resp = self.assertRaises(
+                TypeError,
+                c.get,
+                url_for(
+                    "oauthclient.authorized",
+                    remote_app='test_invalid',
+                    code='test',
+                )
+            )
+
+    @patch('invenio.ext.session.interface.SessionInterface.save_session')
+    def test_token_getter_setter(self, save_session):
+        from invenio.modules.oauthclient.models import RemoteToken
+        from invenio.modules.oauthclient.handlers import token_getter
+        from invenio.modules.oauthclient.client import oauth
+
+        user = MagicMock()
+        user.get_id = MagicMock(return_value=1)
+        user.is_authenticated = MagicMock(return_value=True)
+        with patch('flask.ext.login._get_user', return_value=user):
+            with self.app.test_client() as c:
+                c.get(url_for("oauthclient.login", remote_app='full'))
+                self.mock_response(app='full')
+
+                c.get(url_for(
+                    "oauthclient.authorized", remote_app='full', code='test',
+                ))
+
+                assert session['oauth_token_full'] == ('test_access_token', '')
+
+                t = RemoteToken.get(1, "fullid")
+                assert t.remote_account.client_id == 'fullid'
+                assert t.access_token == 'test_access_token'
+                assert RemoteToken.query.count() == 1
+
+                self.mock_response(app='full', data={
+                    "access_token": "new_access_token",
+                    "scope": "",
+                    "token_type": "bearer"
+                })
+
+                c.get(url_for(
+                    "oauthclient.authorized", remote_app='full', code='test',
+                ))
+
+                t = RemoteToken.get(1, "fullid")
+                assert t.access_token == 'new_access_token'
+                assert RemoteToken.query.count() == 1
+
+                val = token_getter(oauth.remote_apps['full'])
+                assert val == ('new_access_token', '')
+
+
+TEST_SUITE = make_test_suite(RemoteAccountTestCase)
+
+if __name__ == "__main__":
+    run_test_suite(TEST_SUITE)
diff --git a/invenio/modules/oauthclient/upgrades/__init__.py b/invenio/modules/oauthclient/upgrades/__init__.py
new file mode 100644
index 000000000..0eab0f888
--- /dev/null
+++ b/invenio/modules/oauthclient/upgrades/__init__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+##
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
diff --git a/invenio/modules/oauthclient/upgrades/oauthclient_2014_03_02_initial.py b/invenio/modules/oauthclient/upgrades/oauthclient_2014_03_02_initial.py
new file mode 100644
index 000000000..1c4291957
--- /dev/null
+++ b/invenio/modules/oauthclient/upgrades/oauthclient_2014_03_02_initial.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+##
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+from sqlalchemy import *
+from invenio.ext.sqlalchemy import db
+from invenio.modules.upgrader.api import op
+
+depends_on = []
+
+
+def info():
+    return "Initial creation of tables"
+
+
+def do_upgrade():
+    """ Implement your upgrades here  """
+    op.create_table(
+        'remoteACCOUNT',
+        db.Column('id', db.Integer(display_width=15), nullable=False),
+        db.Column('user_id', db.Integer(display_width=15), nullable=False),
+        db.Column('client_id', db.String(length=255), nullable=False),
+        db.Column('extra_data', db.JSON, nullable=True),
+        db.ForeignKeyConstraint(['user_id'], ['user.id'], ),
+        db.PrimaryKeyConstraint('id'),
+        db.UniqueConstraint('user_id', 'client_id'),
+        mysql_charset='utf8',
+        mysql_engine='MyISAM'
+    )
+    op.create_table(
+        'remoteTOKEN',
+        db.Column('id_remote_account', db.Integer(display_width=15),
+                  nullable=False),
+        db.Column('token_type', db.String(length=40), nullable=False),
+        db.Column('access_token', db.Text(), nullable=False),
+        db.Column('secret', db.Text(), nullable=False),
+        db.ForeignKeyConstraint(['id_remote_account'], ['remoteACCOUNT.id'], ),
+        db.PrimaryKeyConstraint('id_remote_account', 'token_type'),
+        mysql_charset='utf8',
+        mysql_engine='MyISAM'
+    )
+
+
+def estimate():
+    return 1
diff --git a/invenio/modules/oauthclient/utils.py b/invenio/modules/oauthclient/utils.py
new file mode 100644
index 000000000..0ff312a86
--- /dev/null
+++ b/invenio/modules/oauthclient/utils.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+##
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+
+from flask.ext.login import current_user, logout_user, login_user
+from invenio.ext.login import authenticate, UserInfo
+from invenio.ext.sqlalchemy import db
+from invenio.ext.script import generate_secret_key
+
+
+from .models import RemoteToken, RemoteAccount
+
+
+def oauth_authenticate(client_id, email=None, access_token=None,
+                       require_existing_link=True, auto_register=False):
+    """
+    Authenticate and authenticated oauth request
+    """
+    if email is None and access_token is None:
+        return False
+
+    # Authenticate via the access token
+    if access_token:
+        token = RemoteToken.get_by_token(client_id, access_token)
+
+        if token:
+            u = UserInfo(token.remote_account.user_id)
+            if login_user(u):
+                return True
+
+    if email:
+        if authenticate(email):
+            if not require_existing_link:
+                return True
+
+            # Pre-existing link required so check
+            account = RemoteAccount.get(current_user.get_id(), client_id)
+            if account:
+                return True
+
+            # Account doesn't exists, and thus the user haven't linked
+            # the accounts
+            logout_user()
+            return None
+        elif auto_register:
+            from invenio.modules.accounts.models import User
+            if not User.query.filter_by(email=email).first():
+                # Email doesn't exists so we can proceed to register user.
+                u = User(
+                    nickname="",
+                    email=email,
+                    password=generate_secret_key(),
+                    note='1',  # Activated
+                )
+
+                try:
+                    db.session.add(u)
+                    db.session.commit()
+                    login_user(UserInfo(u.id))
+                    return True
+                except Exception:
+                    pass
+    return False
diff --git a/invenio/modules/oauthclient/views/__init__.py b/invenio/modules/oauthclient/views/__init__.py
new file mode 100644
index 000000000..24fea4ac8
--- /dev/null
+++ b/invenio/modules/oauthclient/views/__init__.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+##
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+from __future__ import absolute_import
+from .client import blueprint as client_blueprint
+from .settings import blueprint as settings_blueprint
+
+blueprints = [
+    client_blueprint,
+    settings_blueprint
+]
diff --git a/invenio/modules/oauthclient/views/client.py b/invenio/modules/oauthclient/views/client.py
new file mode 100644
index 000000000..16ebafe50
--- /dev/null
+++ b/invenio/modules/oauthclient/views/client.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+##
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+"""
+OAuth client blueprint
+"""
+
+from __future__ import absolute_import
+
+from flask import Blueprint, abort, current_app, url_for, request
+
+from invenio.base.globals import cfg
+from invenio.ext.sslify import ssl_required
+
+from ..client import oauth, handlers, disconnect_handlers
+from ..handlers import default_handler, make_token_getter, make_handler, \
+    disconnect_handler
+
+
+blueprint = Blueprint(
+    'oauthclient',
+    __name__,
+    url_prefix="/oauth",
+    static_folder="../static",
+    template_folder="../templates",
+)
+
+
+@blueprint.before_app_first_request
+def setup_app():
+    """
+    Setup OAuth clients
+    """
+    oauth.init_app(current_app)
+
+    # Add remote applications
+    for remote_app, conf in cfg['OAUTHCLIENT_REMOTE_APPS'].items():
+        # Prevent double creation problems
+        if remote_app not in oauth.remote_apps:
+            remote = oauth.remote_app(
+                remote_app,
+                **conf['params']
+            )
+
+        remote = oauth.remote_apps[remote_app]
+
+        # Set token getter for remote
+        remote.tokengetter(make_token_getter(remote))
+
+        # Register authorized handler
+        handlers.register(
+            remote_app,
+            remote.authorized_handler(make_handler(
+                conf.get('authorized_handler', default_handler),
+                remote,
+            ))
+        )
+
+        # Register disconnect handler
+        disconnect_handlers.register(
+            remote_app, make_handler(
+                conf.get('disconnect_handler', disconnect_handler),
+                remote,
+                with_response=False,
+            )
+        )
+
+
+@blueprint.route('/login/<remote_app>/')
+@ssl_required
+def login(remote_app):
+    """
+    Send user to remote application for authentication
+    """
+    if remote_app not in oauth.remote_apps:
+        return abort(404)
+
+    callback_url = url_for(
+        '.authorized',
+        remote_app=remote_app,
+        next=request.args.get('next') or request.referrer or None,
+        _external=True,
+    )
+
+    return oauth.remote_apps[remote_app].authorize(callback=callback_url)
+
+
+@blueprint.route('/authorized/<remote_app>/')
+@ssl_required
+def authorized(remote_app=None):
+    """
+    Authorized handler callback
+    """
+    if remote_app not in handlers:
+        return abort(404)
+
+    return handlers[remote_app]()
+
+
+@blueprint.route('/disconnect/<remote_app>/')
+@ssl_required
+def disconnect(remote_app):
+    """
+    Disconnect user from remote application. Removes application as well as
+    associated information.
+    """
+    if remote_app not in disconnect_handlers:
+        return abort(404)
+
+    return disconnect_handlers[remote_app]()
diff --git a/invenio/modules/oauthclient/views/settings.py b/invenio/modules/oauthclient/views/settings.py
new file mode 100644
index 000000000..bd8c6fcda
--- /dev/null
+++ b/invenio/modules/oauthclient/views/settings.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+##
+## This file is part of Invenio.
+## Copyright (C) 2014 CERN.
+##
+## Invenio 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 2 of the
+## License, or (at your option) any later version.
+##
+## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc.,
+## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+"""
+OAuth Client Settings Blueprint
+"""
+
+from __future__ import absolute_import
+
+import six
+from operator import itemgetter
+from flask import Blueprint, render_template, request
+#from flask import redirect, url_for
+from flask.ext.login import login_required, current_user
+from flask.ext.breadcrumbs import register_breadcrumb
+from flask.ext.menu import register_menu
+from invenio.base.i18n import _
+from invenio.base.globals import cfg
+#from invenio.ext.sqlalchemy import db
+from invenio.ext.sslify import ssl_required
+
+from ..models import RemoteAccount
+from ..client import oauth
+
+
+blueprint = Blueprint(
+    'oauthclient_settings',
+    __name__,
+    url_prefix="/account/settings/linkedaccounts",
+    static_folder="../static",
+    template_folder="../templates",
+)
+
+
+@blueprint.route("/", methods=['GET', 'POST'])
+@ssl_required
+@login_required
+@register_menu(
+    blueprint, 'settings.oauthclient',
+    _('%(icon)s Linked accounts', icon='<i class="fa fa-link fa-fw"></i>'),
+    order=3,
+    active_when=lambda: request.endpoint.startswith("oauthclient_settings.")
+)
+@register_breadcrumb(
+    blueprint, 'breadcrumbs.settings.oauthclient', _('Linked accounts')
+)
+def index():
+    services = []
+    service_map = {}
+    i = 0
+
+    for appid, conf in six.iteritems(cfg['OAUTHCLIENT_REMOTE_APPS']):
+        if not conf.get('hide', False):
+            services.append(dict(
+                appid=appid,
+                title=conf['title'],
+                icon=conf.get('icon', None),
+                description=conf.get('description', None),
+                account=None
+            ))
+            service_map[oauth.remote_apps[appid].consumer_key] = i
+            i += 1
+
+    # Fetch already linked accounts
+    accounts = RemoteAccount.query.filter_by(
+        user_id=current_user.get_id()
+    ).all()
+
+    for a in accounts:
+        if a.client_id in service_map:
+            services[service_map[a.client_id]]['account'] = a
+
+    # Sort according to title
+    services.sort(key=itemgetter('title'))
+
+    return render_template(
+        "oauthclient/settings/index.html",
+        services=services
+    )