Source code for invenio_oauthclient.handlers

# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2014, 2015, 2016 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.

"""Handlers for customizing oauthclient endpoints."""

from __future__ import absolute_import, print_function

from functools import partial, wraps

import six
from flask import current_app, flash, redirect, render_template, request, \
    session, url_for
from flask_babelex import gettext as _
from flask_login import current_user
from invenio_db import db
from werkzeug.utils import import_string

from .errors import AlreadyLinkedError, OAuthClientError, OAuthError, \
    OAuthRejectedRequestError, OAuthResponseError
from .models import RemoteAccount, RemoteToken
from .proxies import current_oauthclient
from .signals import account_info_received, account_setup_committed, \
    account_setup_received
from .utils import disable_csrf, fill_form, oauth_authenticate, \
    oauth_get_user, oauth_register, registrationform_cls


#
# Token handling
#
[docs]def get_session_next_url(remote_app): """Return redirect url stored in session. :param remote_app: The remote application. :returns: The redirect URL. """ return session.get( '%s_%s' % (token_session_key(remote_app), 'next_url') )
[docs]def set_session_next_url(remote_app, url): """Store redirect url in session for security reasons. :param remote_app: The remote application. :param url: the redirect URL. """ session['%s_%s' % (token_session_key(remote_app), 'next_url')] = \ url
[docs]def token_session_key(remote_app): """Generate a session key used to store the token for a remote app. :param remote_app: The remote application. :returns: The session key. """ return '%s_%s' % (current_app.config['OAUTHCLIENT_SESSION_KEY_PREFIX'], remote_app)
[docs]def response_token_setter(remote, resp): """Extract token from response and set it for the user. :param remote: The remote application. :param resp: The response. :raises invenio_oauthclient.errors.OAuthClientError: If authorization with remote service failed. :raises invenio_oauthclient.errors.OAuthResponseError: In case of bad authorized request. :returns: The token. """ if resp is None: raise OAuthRejectedRequestError('User rejected request.', remote, resp) else: if 'access_token' in resp: return oauth2_token_setter(remote, resp) elif 'oauth_token' in resp and 'oauth_token_secret' in resp: return oauth1_token_setter(remote, resp) elif 'error' in resp: # Only OAuth2 specifies how to send error messages raise OAuthClientError( 'Authorization with remote service failed.', remote, resp, ) raise OAuthResponseError('Bad OAuth authorized request', remote, resp)
[docs]def oauth1_token_setter(remote, resp, token_type='', extra_data=None): """Set an OAuth1 token. :param remote: The remote application. :param resp: The response. :param token_type: The token type. (Default: ``''``) :param extra_data: Extra information. (Default: ``None``) :returns: A :class:`invenio_oauthclient.models.RemoteToken` instance. """ return token_setter( remote, resp['oauth_token'], secret=resp['oauth_token_secret'], extra_data=extra_data, token_type=token_type, )
[docs]def oauth2_token_setter(remote, resp, token_type='', extra_data=None): """Set an OAuth2 token. The refresh_token can be used to obtain a new access_token after the old one is expired. It is saved in the database for long term use. A refresh_token will be present only if `access_type=offline` is included in the authorization code request. :param remote: The remote application. :param resp: The response. :param token_type: The token type. (Default: ``''``) :param extra_data: Extra information. (Default: ``None``) :returns: A :class:`invenio_oauthclient.models.RemoteToken` instance. """ return token_setter( remote, resp['access_token'], secret='', token_type=token_type, extra_data=extra_data, )
[docs]def token_setter(remote, token, secret='', token_type='', extra_data=None, user=None): """Set token for user. :param remote: The remote application. :param token: The token to set. :param token_type: The token type. (Default: ``''``) :param extra_data: Extra information. (Default: ``None``) :param user: The user owner of the remote token. If it's not defined, the current user is used automatically. (Default: ``None``) :returns: A :class:`invenio_oauthclient.models.RemoteToken` instance or ``None``. """ session[token_session_key(remote.name)] = (token, secret) user = user or current_user # Save token if user is not anonymous (user exists but can be not active at # this moment) if not user.is_anonymous: uid = user.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
[docs]def token_getter(remote, token=''): """Retrieve OAuth access token. Used by flask-oauthlib to get the access token when making requests. :param remote: The remote application. :param token: Type of token to get. Data passed from ``oauth.request()`` to identify which token to retrieve. (Default: ``''``) :returns: The token. """ 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)
[docs]def token_delete(remote, token=''): """Remove OAuth access tokens from session. :param remote: The remote application. :param token: Type of token to get. Data passed from ``oauth.request()`` to identify which token to retrieve. (Default: ``''``) :returns: The token. """ session_key = token_session_key(remote.name) return session.pop(session_key, None)
# # Error handling decorators #
[docs]def oauth_error_handler(f): """Decorator to handle exceptions.""" @wraps(f) def inner(*args, **kwargs): # OAuthErrors should not happen, so they are not caught here. Hence # they will result in a 500 Internal Server Error which is what we # are interested in. try: return f(*args, **kwargs) except OAuthClientError as e: current_app.logger.warning(e.message, exc_info=True) return oauth2_handle_error( e.remote, e.response, e.code, e.uri, e.description ) except OAuthRejectedRequestError: flash(_('You rejected the authentication request.'), category='info') return redirect('/') except AlreadyLinkedError: flash(_('External service is already linked to another account.'), category='danger') return redirect(url_for('invenio_oauthclient_settings.index')) return inner
# # Handlers # @oauth_error_handler
[docs]def authorized_default_handler(resp, remote, *args, **kwargs): """Store access token in session. Default authorized handler. :param remote: The remote application. :param resp: The response. :returns: Redirect response. """ response_token_setter(remote, resp) db.session.commit() return redirect(url_for('invenio_oauthclient_settings.index'))
@oauth_error_handler
[docs]def authorized_signup_handler(resp, remote, *args, **kwargs): """Handle sign-in/up functionality. :param remote: The remote application. :param resp: The response. :returns: Redirect response. """ # Remove any previously stored auto register session key session.pop(token_session_key(remote.name) + '_autoregister', None) # Store token in session # ---------------------- # Set token in session - token object only returned if # current_user.is_autenticated(). token = response_token_setter(remote, resp) handlers = current_oauthclient.signup_handlers[remote.name] # Sign-in/up user # --------------- if not current_user.is_authenticated: account_info = handlers['info'](resp) account_info_received.send( remote, token=token, response=resp, account_info=account_info ) user = oauth_get_user( remote.consumer_key, account_info=account_info, access_token=token_getter(remote)[0], ) if user is None: # Auto sign-up if user not found form_cls = registrationform_cls() form = fill_form( disable_csrf(form_cls()), account_info['user'] ) user = oauth_register(form) # if registration fails ... if user is None: # requires extra information session[ token_session_key(remote.name) + '_autoregister'] = True session[token_session_key(remote.name) + '_account_info'] = account_info session[token_session_key(remote.name) + '_response'] = resp db.session.commit() return redirect(url_for( '.signup', remote_app=remote.name, )) # Authenticate user if not oauth_authenticate(remote.consumer_key, user, require_existing_link=False, remember=current_app.config[ 'OAUTHCLIENT_REMOTE_APPS'] [remote.name].get('remember', False)): return current_app.login_manager.unauthorized() # Link account # ------------ # Need to store token in database instead of only the session when # called first time. token = response_token_setter(remote, resp) # Setup account # ------------- if not token.remote_account.extra_data: account_setup = handlers['setup'](token, resp) account_setup_received.send( remote, token=token, response=resp, account_setup=account_setup ) db.session.commit() account_setup_committed.send(remote, token=token) else: db.session.commit() # Redirect to next next_url = get_session_next_url(remote.name) if next_url: return redirect(next_url) return redirect(url_for('invenio_oauthclient_settings.index'))
[docs]def disconnect_handler(remote, *args, **kwargs): """Handle unlinking of remote account. This default handler will just delete the remote account link. You may wish to extend this module to perform clean-up in the remote service before removing the link (e.g. removing install webhooks). :param remote: The remote application. :returns: Redirect response. """ if not current_user.is_authenticated: return current_app.login_manager.unauthorized() with db.session.begin_nested(): account = RemoteAccount.get( user_id=current_user.get_id(), client_id=remote.consumer_key ) if account: account.delete() db.session.commit() return redirect(url_for('invenio_oauthclient_settings.index'))
[docs]def signup_handler(remote, *args, **kwargs): """Handle extra signup information. :param remote: The remote application. :returns: Redirect response or the template rendered. """ # User already authenticated so move on if current_user.is_authenticated: return redirect('/') # Retrieve token from session oauth_token = token_getter(remote) if not oauth_token: return redirect('/') session_prefix = token_session_key(remote.name) # Test to see if this is coming from on authorized request if not session.get(session_prefix + '_autoregister', False): return redirect(url_for('.login', remote_app=remote.name)) form = registrationform_cls()(request.form) if form.validate_on_submit(): account_info = session.get(session_prefix + '_account_info') response = session.get(session_prefix + '_response') # Register user user = oauth_register(form) if user is None: raise OAuthError('Could not create user.', remote) # Remove session key session.pop(session_prefix + '_autoregister', None) # Link account and set session data token = token_setter(remote, oauth_token[0], secret=oauth_token[1], user=user) handlers = current_oauthclient.signup_handlers[remote.name] if token is None: raise OAuthError('Could not create token for user.', remote) if not token.remote_account.extra_data: account_setup = handlers['setup'](token, response) account_setup_received.send( remote, token=token, response=response, account_setup=account_setup ) # Registration has been finished db.session.commit() account_setup_committed.send(remote, token=token) else: # Registration has been finished db.session.commit() # Authenticate the user if not oauth_authenticate(remote.consumer_key, user, require_existing_link=False, remember=current_app.config[ 'OAUTHCLIENT_REMOTE_APPS'] [remote.name].get('remember', False)): # Redirect the user after registration (which doesn't include the # activation), waiting for user to confirm his email. return redirect(url_for('security.login')) # Remove account info from session session.pop(session_prefix + '_account_info', None) session.pop(session_prefix + '_response', None) # Redirect to next next_url = get_session_next_url(remote.name) if next_url: return redirect(next_url) else: return redirect('/') # Pre-fill form account_info = session.get(session_prefix + '_account_info') if not form.is_submitted(): form = fill_form(form, account_info['user']) return render_template( current_app.config['OAUTHCLIENT_SIGNUP_TEMPLATE'], form=form, remote=remote, app_title=current_app.config['OAUTHCLIENT_REMOTE_APPS'][ remote.name].get('title', ''), app_description=current_app.config['OAUTHCLIENT_REMOTE_APPS'][ remote.name].get('description', ''), app_icon=current_app.config['OAUTHCLIENT_REMOTE_APPS'][ remote.name].get('icon', None), )
[docs]def oauth_logout_handler(sender_app, user=None): """Remove all access tokens from session on logout.""" oauth = current_app.extensions['oauthlib.client'] for remote in oauth.remote_apps.values(): token_delete(remote) db.session.commit()
# # Helpers #
[docs]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.string_types): 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
[docs]def make_token_getter(remote): """Make a token getter for a remote application.""" return partial(token_getter, remote)
[docs]def oauth2_handle_error(remote, resp, error_code, error_uri, error_description): """Handle errors during exchange of one-time code for an access tokens.""" flash(_('Authorization with remote service failed.')) return redirect('/')