import logging from collections import namedtuple from flask import request, redirect, url_for, Blueprint from peewee import IntegrityError import features from app import app, analytics, get_app_url, oauth_login, authentication from auth.auth_context import get_authenticated_user from auth.process import require_session_login from data import model from endpoints.common import common_login from endpoints.web import index from endpoints.csrf import csrf_protect, OAUTH_CSRF_TOKEN_NAME from oauth.login import OAuthLoginException from util.validation import generate_valid_usernames logger = logging.getLogger(__name__) client = app.config['HTTPCLIENT'] oauthlogin = Blueprint('oauthlogin', __name__) oauthlogin_csrf_protect = csrf_protect(OAUTH_CSRF_TOKEN_NAME, 'state', all_methods=True) OAuthResult = namedtuple('oauthresult', ['to_login', 'service_name', 'error_message', 'register_redirect']) def _oauthresult(to_login=None, service_name=None, error_message=None, register_redirect=False): return OAuthResult(to_login, service_name, error_message, register_redirect) def _get_response(result): if result.error_message is not None: return _render_ologin_error(result.service_name, result.error_message, result.register_redirect) return _perform_login(result.to_login, result.service_name) def _conduct_oauth_login(auth_system, login_service, lid, lusername, lemail, metadata=None): """ Conducts login from the result of an OAuth service's login flow and returns the status of the login, as well as the followup step. """ service_id = login_service.service_id() service_name = login_service.service_name() # Check for an existing account *bound to this service*. If found, conduct login of that account # and redirect. to_login = model.user.verify_federated_login(service_id, lid) if to_login is not None: return _oauthresult(to_login=to_login, service_name=service_name) # If the login service has a bound field name, and we have a defined internal auth type that is # not the database, then search for an existing account with that matching field. This allows # users to setup SSO while also being backed by something like LDAP. bound_field_name = login_service.login_binding_field() if auth_system.federated_service is not None and bound_field_name is not None: # Perform lookup. logger.debug('Got oauth bind field name of "%s"', bound_field_name) lookup_value = None if bound_field_name == 'username': lookup_value = lusername elif bound_field_name == 'email': lookup_value = lemail if lookup_value is None: logger.error('Missing lookup value for OAuth login') return _oauthresult(service_name=service_name, error_message='Configuration error in this provider') (user_obj, err) = auth_system.link_user(lookup_value) if err is not None: logger.debug('%s %s not found: %s', bound_field_name, lookup_value, err) msg = '%s %s not found in backing auth system' % (bound_field_name, lookup_value) return _oauthresult(service_name=service_name, error_message=msg) # Found an existing user. Bind their internal auth account to this service as well. result = _attach_service(login_service, user_obj, lid, lusername) if result.error_message is not None: return result return _oauthresult(to_login=user_obj, service_name=service_name) # Otherwise, we need to create a new user account. if not features.USER_CREATION: error_message = 'User creation is disabled. Please contact your administrator' return _oauthresult(service_name=service_name, error_message=error_message) # Try to create the user try: # Generate a valid username. new_username = None for valid in generate_valid_usernames(lusername): if model.user.get_user_or_org(valid): continue new_username = valid break prompts = model.user.get_default_user_prompts(features) to_login = model.user.create_federated_user(new_username, lemail, service_id, lid, set_password_notification=True, metadata=metadata or {}, prompts=prompts, email_required=features.MAILING) # Success, tell analytics analytics.track(to_login.username, 'register', {'service': service_name.lower()}) return _oauthresult(to_login=to_login, service_name=service_name) except model.InvalidEmailAddressException: message = "The e-mail address %s is already associated " % (lemail, ) message = message + "with an existing %s account." % (app.config['REGISTRY_TITLE_SHORT'], ) message = message + "\nPlease log in with your username and password and " message = message + "associate your %s account to use it in the future." % (service_name, ) return _oauthresult(service_name=service_name, error_message=message, register_redirect=True) except model.DataModelException as ex: return _oauthresult(service_name=service_name, error_message=ex.message) def _render_ologin_error(service_name, error_message=None, register_redirect=False): """ Returns a Flask response indicating an OAuth error. """ user_creation = bool(features.USER_CREATION and features.DIRECT_LOGIN) error_info = { 'reason': 'ologinerror', 'service_name': service_name, 'error_message': error_message or 'Could not load user data. The token may have expired', 'service_url': get_app_url(), 'user_creation': user_creation, 'register_redirect': register_redirect, } resp = index('', error_info=error_info) resp.status_code = 400 return resp def _perform_login(to_login, service_name): """ Attempts to login the given user, returning the Flask result of whether the login succeeded. """ if common_login(to_login): if model.user.has_user_prompts(to_login): return redirect(url_for('web.updateuser')) else: return redirect(url_for('web.index')) else: return _render_ologin_error(service_name, 'Could not login. Account may be disabled') def _attach_service(login_service, user_obj, lid, lusername): """ Attaches the given user account to the given service, with the given service user ID and service username. """ metadata = { 'service_username': lusername } try: model.user.attach_federated_login(user_obj, login_service.service_id(), lid, metadata=metadata) return _oauthresult(to_login=user_obj) except IntegrityError: err = '%s account %s is already attached to a %s account' % ( login_service.service_name(), lusername, app.config['REGISTRY_TITLE_SHORT']) return _oauthresult(service_name=login_service.service_name(), error_message=err) def _register_service(login_service): """ Registers the given login service, adding its callback and attach routes to the blueprint. """ @oauthlogin_csrf_protect def callback_func(): # Check for a callback error. error = request.args.get('error', None) if error: return _render_ologin_error(login_service.service_name(), error) # Exchange the OAuth code for login information. code = request.args.get('code') try: lid, lusername, lemail = login_service.exchange_code_for_login(app.config, client, code, '') except OAuthLoginException as ole: logger.exception('Got login exception') return _render_ologin_error(login_service.service_name(), ole.message) # Conduct login. metadata = { 'service_username': lusername } result = _conduct_oauth_login(authentication, login_service, lid, lusername, lemail, metadata=metadata) return _get_response(result) @require_session_login @oauthlogin_csrf_protect def attach_func(): # Check for a callback error. error = request.args.get('error', None) if error: return _render_ologin_error(login_service.service_name(), error) # Exchange the OAuth code for login information. code = request.args.get('code') try: lid, lusername, _ = login_service.exchange_code_for_login(app.config, client, code, '/attach') except OAuthLoginException as ole: return _render_ologin_error(login_service.service_name(), ole.message) # Conduct attach. user_obj = get_authenticated_user() result = _attach_service(login_service, user_obj, lid, lusername) if result.error_message is not None: return _get_response(result) return redirect(url_for('web.user_view', path=user_obj.username, tab='external')) oauthlogin.add_url_rule('/%s/callback' % login_service.service_id(), '%s_oauth_callback' % login_service.service_id(), callback_func, methods=['GET']) oauthlogin.add_url_rule('/%s/callback/attach' % login_service.service_id(), '%s_oauth_attach' % login_service.service_id(), attach_func, methods=['GET']) # Register the routes for each of the login services. for current_service in oauth_login.services: _register_service(current_service)