Move OAuth login into its own endpoints module
This commit is contained in:
parent
0167e1e7bf
commit
198bdf88bc
6 changed files with 32 additions and 31 deletions
231
endpoints/oauth/login.py
Normal file
231
endpoints/oauth/login.py
Normal file
|
@ -0,0 +1,231 @@
|
|||
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', ['user_obj', 'service_name', 'error_message',
|
||||
'register_redirect'])
|
||||
|
||||
def _oauthresult(user_obj=None, service_name=None, error_message=None, register_redirect=False):
|
||||
return OAuthResult(user_obj, 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.user_obj, 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.
|
||||
user_obj = model.user.verify_federated_login(service_id, lid)
|
||||
if user_obj is not None:
|
||||
return _oauthresult(user_obj=user_obj, 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 == 'sub':
|
||||
lookup_value = lid
|
||||
elif 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(user_obj=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)
|
||||
user_obj = 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(user_obj.username, 'register', {'service': service_name.lower()})
|
||||
return _oauthresult(user_obj=user_obj, service_name=service_name)
|
||||
|
||||
except model.InvalidEmailAddressException:
|
||||
message = ("The e-mail address {0} is already associated "
|
||||
"with an existing {1} account. \n"
|
||||
"Please log in with your username and password and "
|
||||
"associate your {2} account to use it in the future.")
|
||||
message = message.format(lemail, app.config['REGISTRY_TITLE_SHORT'], 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(user_obj, service_name):
|
||||
""" Attempts to login the given user, returning the Flask result of whether the login succeeded.
|
||||
"""
|
||||
if common_login(user_obj):
|
||||
if model.user.has_user_prompts(user_obj):
|
||||
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(user_obj=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)
|
Reference in a new issue