2014-02-18 15:50:15 -05:00
|
|
|
import logging
|
2014-11-05 16:43:37 -05:00
|
|
|
import requests
|
2014-02-18 15:50:15 -05:00
|
|
|
|
|
|
|
from flask import request, redirect, url_for, Blueprint
|
2016-09-28 20:17:14 -04:00
|
|
|
from flask_login import current_user
|
2016-03-09 16:20:28 -05:00
|
|
|
from peewee import IntegrityError
|
|
|
|
|
|
|
|
import features
|
2014-02-18 15:50:15 -05:00
|
|
|
|
2015-09-04 16:14:46 -04:00
|
|
|
from app import app, analytics, get_app_url, github_login, google_login, dex_login
|
2016-09-29 15:24:57 -04:00
|
|
|
from auth.process import require_session_login
|
2014-02-18 15:50:15 -05:00
|
|
|
from data import model
|
2016-03-09 16:20:28 -05:00
|
|
|
from endpoints.common import common_login, route_show_if
|
|
|
|
from endpoints.web import render_page_template_with_routedata
|
2016-05-31 16:48:19 -04:00
|
|
|
from util.security.jwtutil import decode, InvalidTokenError
|
2014-08-11 15:47:44 -04:00
|
|
|
from util.validation import generate_valid_usernames
|
2014-02-18 15:50:15 -05:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2014-02-18 18:09:14 -05:00
|
|
|
client = app.config['HTTPCLIENT']
|
2015-04-24 15:13:08 -04:00
|
|
|
oauthlogin = Blueprint('oauthlogin', __name__)
|
2014-02-18 15:50:15 -05:00
|
|
|
|
2016-09-28 20:17:14 -04:00
|
|
|
|
2014-09-04 17:54:51 -04:00
|
|
|
def render_ologin_error(service_name,
|
|
|
|
error_message='Could not load user data. The token may have expired.'):
|
2016-01-08 13:53:27 -05:00
|
|
|
user_creation = features.USER_CREATION and features.DIRECT_LOGIN
|
|
|
|
return render_page_template_with_routedata('ologinerror.html',
|
2016-03-09 16:20:28 -05:00
|
|
|
service_name=service_name,
|
|
|
|
error_message=error_message,
|
|
|
|
service_url=get_app_url(),
|
|
|
|
user_creation=user_creation)
|
2014-02-18 15:50:15 -05:00
|
|
|
|
|
|
|
|
2014-11-05 16:43:37 -05:00
|
|
|
def get_user(service, token):
|
2014-08-11 15:47:44 -04:00
|
|
|
token_param = {
|
|
|
|
'access_token': token,
|
|
|
|
'alt': 'json',
|
|
|
|
}
|
2016-09-08 18:43:50 -04:00
|
|
|
got_user = client.get(service.user_endpoint(), params=token_param)
|
|
|
|
if got_user.status_code != requests.codes.ok:
|
2014-11-05 16:43:37 -05:00
|
|
|
return {}
|
2014-08-11 15:47:44 -04:00
|
|
|
|
2016-09-08 18:43:50 -04:00
|
|
|
return got_user.json()
|
2014-08-11 15:47:44 -04:00
|
|
|
|
2014-11-05 16:43:37 -05:00
|
|
|
|
|
|
|
def conduct_oauth_login(service, user_id, username, email, metadata={}):
|
|
|
|
service_name = service.service_name()
|
2015-07-15 17:25:41 -04:00
|
|
|
to_login = model.user.verify_federated_login(service_name.lower(), user_id)
|
2014-08-11 15:47:44 -04:00
|
|
|
if not to_login:
|
2014-10-02 14:49:18 -04:00
|
|
|
# See if we can create a new user.
|
|
|
|
if not features.USER_CREATION:
|
|
|
|
error_message = 'User creation is disabled. Please contact your administrator'
|
|
|
|
return render_ologin_error(service_name, error_message)
|
|
|
|
|
|
|
|
# Try to create the user
|
2014-08-11 15:47:44 -04:00
|
|
|
try:
|
2014-11-10 11:30:47 -05:00
|
|
|
new_username = None
|
|
|
|
for valid in generate_valid_usernames(username):
|
2015-07-15 17:25:41 -04:00
|
|
|
if model.user.get_user_or_org(valid):
|
2014-11-10 11:30:47 -05:00
|
|
|
continue
|
|
|
|
|
|
|
|
new_username = valid
|
|
|
|
break
|
|
|
|
|
2016-11-04 17:57:55 -04:00
|
|
|
prompts = model.user.get_default_user_prompts(features)
|
2015-07-15 17:25:41 -04:00
|
|
|
to_login = model.user.create_federated_user(new_username, email, service_name.lower(),
|
|
|
|
user_id, set_password_notification=True,
|
2016-11-04 17:57:55 -04:00
|
|
|
metadata=metadata,
|
|
|
|
prompts=prompts)
|
2014-08-11 15:47:44 -04:00
|
|
|
|
|
|
|
# Success, tell analytics
|
|
|
|
analytics.track(to_login.username, 'register', {'service': service_name.lower()})
|
|
|
|
|
|
|
|
state = request.args.get('state', None)
|
|
|
|
if state:
|
2015-07-15 17:25:41 -04:00
|
|
|
logger.debug('Aliasing with state: %s', state)
|
2014-08-11 15:47:44 -04:00
|
|
|
analytics.alias(to_login.username, state)
|
|
|
|
|
2016-09-08 18:43:50 -04:00
|
|
|
except model.InvalidEmailAddressException:
|
2014-10-17 11:44:31 -04:00
|
|
|
message = "The e-mail address %s is already associated " % (email, )
|
|
|
|
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 render_ologin_error(service_name, message)
|
|
|
|
|
|
|
|
except model.DataModelException as ex:
|
2014-09-04 17:54:51 -04:00
|
|
|
return render_ologin_error(service_name, ex.message)
|
2014-08-11 15:47:44 -04:00
|
|
|
|
|
|
|
if common_login(to_login):
|
2016-09-08 18:43:50 -04:00
|
|
|
if model.user.has_user_prompts(to_login):
|
|
|
|
return redirect(url_for('web.updateuser'))
|
|
|
|
else:
|
|
|
|
return redirect(url_for('web.index'))
|
2014-08-11 15:47:44 -04:00
|
|
|
|
2014-09-04 17:54:51 -04:00
|
|
|
return render_ologin_error(service_name)
|
2014-08-11 15:47:44 -04:00
|
|
|
|
2015-09-04 16:14:46 -04:00
|
|
|
def get_email_username(user_data):
|
2014-08-11 18:25:01 -04:00
|
|
|
username = user_data['email']
|
|
|
|
at = username.find('@')
|
|
|
|
if at > 0:
|
|
|
|
username = username[0:at]
|
|
|
|
|
|
|
|
return username
|
|
|
|
|
|
|
|
|
2015-04-24 15:13:08 -04:00
|
|
|
@oauthlogin.route('/google/callback', methods=['GET'])
|
2014-08-11 15:47:44 -04:00
|
|
|
@route_show_if(features.GOOGLE_LOGIN)
|
|
|
|
def google_oauth_callback():
|
|
|
|
error = request.args.get('error', None)
|
|
|
|
if error:
|
2014-09-04 17:54:51 -04:00
|
|
|
return render_ologin_error('Google', error)
|
2014-08-11 15:47:44 -04:00
|
|
|
|
2015-04-24 15:13:08 -04:00
|
|
|
code = request.args.get('code')
|
|
|
|
token = google_login.exchange_code_for_token(app.config, client, code, form_encode=True)
|
2014-11-05 16:43:37 -05:00
|
|
|
user_data = get_user(google_login, token)
|
2014-08-11 15:47:44 -04:00
|
|
|
if not user_data or not user_data.get('id', None) or not user_data.get('email', None):
|
2014-09-04 17:54:51 -04:00
|
|
|
return render_ologin_error('Google')
|
|
|
|
|
2016-10-10 13:12:35 -04:00
|
|
|
if not user_data.get('verified_email', False):
|
|
|
|
return render_ologin_error(
|
|
|
|
'Google',
|
|
|
|
'A verified e-mail address is required for login. Please verify your ' +
|
|
|
|
'e-mail address in Google and try again.',
|
|
|
|
)
|
|
|
|
|
2015-09-04 16:14:46 -04:00
|
|
|
username = get_email_username(user_data)
|
2014-08-11 18:25:01 -04:00
|
|
|
metadata = {
|
2014-09-04 17:54:51 -04:00
|
|
|
'service_username': user_data['email']
|
2014-08-11 18:25:01 -04:00
|
|
|
}
|
2014-08-11 15:47:44 -04:00
|
|
|
|
2014-11-05 16:43:37 -05:00
|
|
|
return conduct_oauth_login(google_login, user_data['id'], username, user_data['email'],
|
2014-08-11 18:25:01 -04:00
|
|
|
metadata=metadata)
|
2014-08-11 15:47:44 -04:00
|
|
|
|
|
|
|
|
2015-04-24 15:13:08 -04:00
|
|
|
@oauthlogin.route('/github/callback', methods=['GET'])
|
2014-04-06 00:50:30 -04:00
|
|
|
@route_show_if(features.GITHUB_LOGIN)
|
2014-02-18 15:50:15 -05:00
|
|
|
def github_oauth_callback():
|
|
|
|
error = request.args.get('error', None)
|
|
|
|
if error:
|
2014-09-04 17:54:51 -04:00
|
|
|
return render_ologin_error('GitHub', error)
|
2014-02-18 15:50:15 -05:00
|
|
|
|
2015-03-03 19:58:42 -05:00
|
|
|
# Exchange the OAuth code.
|
2015-04-24 15:13:08 -04:00
|
|
|
code = request.args.get('code')
|
2015-05-01 12:58:50 -04:00
|
|
|
token = github_login.exchange_code_for_token(app.config, client, code)
|
2015-03-03 19:58:42 -05:00
|
|
|
|
|
|
|
# Retrieve the user's information.
|
2014-11-05 16:43:37 -05:00
|
|
|
user_data = get_user(github_login, token)
|
2014-10-02 14:49:18 -04:00
|
|
|
if not user_data or not 'login' in user_data:
|
2014-09-04 17:54:51 -04:00
|
|
|
return render_ologin_error('GitHub')
|
2014-02-18 15:50:15 -05:00
|
|
|
|
|
|
|
username = user_data['login']
|
|
|
|
github_id = user_data['id']
|
|
|
|
|
|
|
|
v3_media_type = {
|
|
|
|
'Accept': 'application/vnd.github.v3'
|
|
|
|
}
|
|
|
|
|
|
|
|
token_param = {
|
|
|
|
'access_token': token,
|
|
|
|
}
|
2015-03-03 19:58:42 -05:00
|
|
|
|
|
|
|
# Retrieve the user's orgnizations (if organization filtering is turned on)
|
|
|
|
if github_login.allowed_organizations() is not None:
|
|
|
|
get_orgs = client.get(github_login.orgs_endpoint(), params=token_param,
|
2016-03-09 16:20:28 -05:00
|
|
|
headers={'Accept': 'application/vnd.github.moondragon+json'})
|
2015-03-03 19:58:42 -05:00
|
|
|
|
2015-04-16 12:17:39 -04:00
|
|
|
organizations = set([org.get('login').lower() for org in get_orgs.json()])
|
2015-03-03 19:58:42 -05:00
|
|
|
if not (organizations & set(github_login.allowed_organizations())):
|
|
|
|
err = """You are not a member of an allowed GitHub organization.
|
|
|
|
Please contact your system administrator if you believe this is in error."""
|
|
|
|
return render_ologin_error('GitHub', err)
|
|
|
|
|
|
|
|
# Find the e-mail address for the user: we will accept any email, but we prefer the primary
|
2014-11-05 16:43:37 -05:00
|
|
|
get_email = client.get(github_login.email_endpoint(), params=token_param,
|
2014-02-18 18:09:14 -05:00
|
|
|
headers=v3_media_type)
|
2014-02-18 15:50:15 -05:00
|
|
|
|
|
|
|
found_email = None
|
|
|
|
for user_email in get_email.json():
|
2015-04-16 12:17:39 -04:00
|
|
|
if not github_login.is_enterprise() and not user_email['verified']:
|
2015-03-05 12:07:39 -05:00
|
|
|
continue
|
2014-02-18 15:50:15 -05:00
|
|
|
|
|
|
|
found_email = user_email['email']
|
|
|
|
if user_email['primary']:
|
|
|
|
break
|
|
|
|
|
2015-03-03 19:58:42 -05:00
|
|
|
if found_email is None:
|
|
|
|
err = 'There is no verified e-mail address attached to the GitHub account.'
|
|
|
|
return render_ologin_error('GitHub', err)
|
|
|
|
|
2014-08-11 18:25:01 -04:00
|
|
|
metadata = {
|
|
|
|
'service_username': username
|
|
|
|
}
|
|
|
|
|
2014-11-05 16:43:37 -05:00
|
|
|
return conduct_oauth_login(github_login, github_id, username, found_email, metadata=metadata)
|
2014-02-18 15:50:15 -05:00
|
|
|
|
|
|
|
|
2015-04-24 15:13:08 -04:00
|
|
|
@oauthlogin.route('/google/callback/attach', methods=['GET'])
|
2014-08-11 15:47:44 -04:00
|
|
|
@route_show_if(features.GOOGLE_LOGIN)
|
|
|
|
@require_session_login
|
|
|
|
def google_oauth_attach():
|
2015-04-24 15:13:08 -04:00
|
|
|
code = request.args.get('code')
|
|
|
|
token = google_login.exchange_code_for_token(app.config, client, code,
|
|
|
|
redirect_suffix='/attach', form_encode=True)
|
2014-08-11 18:25:01 -04:00
|
|
|
|
2014-11-05 16:43:37 -05:00
|
|
|
user_data = get_user(google_login, token)
|
2014-08-11 15:47:44 -04:00
|
|
|
if not user_data or not user_data.get('id', None):
|
2014-09-04 17:54:51 -04:00
|
|
|
return render_ologin_error('Google')
|
2014-08-11 15:47:44 -04:00
|
|
|
|
2016-10-10 13:12:35 -04:00
|
|
|
if not user_data.get('verified_email', False):
|
|
|
|
return render_ologin_error(
|
|
|
|
'Google',
|
|
|
|
'A verified e-mail address is required for login. Please verify your ' +
|
|
|
|
'e-mail address in Google and try again.',
|
|
|
|
)
|
|
|
|
|
2014-08-11 15:47:44 -04:00
|
|
|
google_id = user_data['id']
|
|
|
|
user_obj = current_user.db_user()
|
2014-08-11 18:25:01 -04:00
|
|
|
|
2015-09-04 16:14:46 -04:00
|
|
|
username = get_email_username(user_data)
|
2014-08-11 18:25:01 -04:00
|
|
|
metadata = {
|
2014-09-04 17:54:51 -04:00
|
|
|
'service_username': user_data['email']
|
2014-08-11 18:25:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
2015-07-15 17:25:41 -04:00
|
|
|
model.user.attach_federated_login(user_obj, 'google', google_id, metadata=metadata)
|
2014-08-11 18:25:01 -04:00
|
|
|
except IntegrityError:
|
|
|
|
err = 'Google account %s is already attached to a %s account' % (
|
|
|
|
username, app.config['REGISTRY_TITLE_SHORT'])
|
2014-09-04 17:54:51 -04:00
|
|
|
return render_ologin_error('Google', err)
|
2014-08-11 18:25:01 -04:00
|
|
|
|
2016-11-28 18:55:41 -05:00
|
|
|
return redirect(url_for('web.user_view', path=user_obj.username, tab='external'))
|
2014-02-18 15:50:15 -05:00
|
|
|
|
|
|
|
|
2015-04-24 15:13:08 -04:00
|
|
|
@oauthlogin.route('/github/callback/attach', methods=['GET'])
|
2014-04-06 00:50:30 -04:00
|
|
|
@route_show_if(features.GITHUB_LOGIN)
|
2014-03-26 15:52:24 -04:00
|
|
|
@require_session_login
|
2014-02-18 15:50:15 -05:00
|
|
|
def github_oauth_attach():
|
2015-04-24 15:13:08 -04:00
|
|
|
code = request.args.get('code')
|
2015-05-01 12:58:50 -04:00
|
|
|
token = github_login.exchange_code_for_token(app.config, client, code)
|
2014-11-05 16:43:37 -05:00
|
|
|
user_data = get_user(github_login, token)
|
2014-06-04 16:12:31 -04:00
|
|
|
if not user_data:
|
2014-09-04 17:54:51 -04:00
|
|
|
return render_ologin_error('GitHub')
|
2014-06-04 16:12:31 -04:00
|
|
|
|
2014-02-18 15:50:15 -05:00
|
|
|
github_id = user_data['id']
|
|
|
|
user_obj = current_user.db_user()
|
2014-08-11 18:25:01 -04:00
|
|
|
|
|
|
|
username = user_data['login']
|
|
|
|
metadata = {
|
|
|
|
'service_username': username
|
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
2015-07-15 17:25:41 -04:00
|
|
|
model.user.attach_federated_login(user_obj, 'github', github_id, metadata=metadata)
|
2014-08-11 18:25:01 -04:00
|
|
|
except IntegrityError:
|
|
|
|
err = 'Github account %s is already attached to a %s account' % (
|
|
|
|
username, app.config['REGISTRY_TITLE_SHORT'])
|
|
|
|
|
2014-09-04 17:54:51 -04:00
|
|
|
return render_ologin_error('GitHub', err)
|
2014-08-11 18:25:01 -04:00
|
|
|
|
2016-11-28 18:55:41 -05:00
|
|
|
return redirect(url_for('web.user_view', path=user_obj.username, tab='external'))
|
2015-09-04 16:14:46 -04:00
|
|
|
|
|
|
|
|
|
|
|
def decode_user_jwt(token, oidc_provider):
|
|
|
|
try:
|
|
|
|
return decode(token, oidc_provider.get_public_key(), algorithms=['RS256'],
|
|
|
|
audience=oidc_provider.client_id(),
|
|
|
|
issuer=oidc_provider.issuer)
|
|
|
|
except InvalidTokenError:
|
|
|
|
# Public key may have expired. Try to retrieve an updated public key and use it to decode.
|
|
|
|
return decode(token, oidc_provider.get_public_key(force_refresh=True), algorithms=['RS256'],
|
|
|
|
audience=oidc_provider.client_id(),
|
|
|
|
issuer=oidc_provider.issuer)
|
|
|
|
|
|
|
|
|
|
|
|
@oauthlogin.route('/dex/callback', methods=['GET', 'POST'])
|
|
|
|
@route_show_if(features.DEX_LOGIN)
|
|
|
|
def dex_oauth_callback():
|
|
|
|
error = request.values.get('error', None)
|
|
|
|
if error:
|
|
|
|
return render_ologin_error(dex_login.public_title, error)
|
|
|
|
|
|
|
|
code = request.values.get('code')
|
|
|
|
if not code:
|
|
|
|
return render_ologin_error(dex_login.public_title, 'Missing OAuth code')
|
|
|
|
|
|
|
|
token = dex_login.exchange_code_for_token(app.config, client, code, client_auth=True,
|
|
|
|
form_encode=True)
|
|
|
|
|
|
|
|
try:
|
|
|
|
payload = decode_user_jwt(token, dex_login)
|
|
|
|
except InvalidTokenError:
|
|
|
|
logger.exception('Exception when decoding returned JWT')
|
2016-03-09 16:20:28 -05:00
|
|
|
return render_ologin_error(
|
|
|
|
dex_login.public_title,
|
|
|
|
'Could not decode response. Please contact your system administrator about this error.',
|
|
|
|
)
|
2015-09-04 16:14:46 -04:00
|
|
|
|
|
|
|
username = get_email_username(payload)
|
|
|
|
metadata = {}
|
|
|
|
|
|
|
|
dex_id = payload['sub']
|
|
|
|
email_address = payload['email']
|
|
|
|
|
|
|
|
if not payload.get('email_verified', False):
|
2016-03-09 16:20:28 -05:00
|
|
|
return render_ologin_error(
|
|
|
|
dex_login.public_title,
|
2015-09-04 16:14:46 -04:00
|
|
|
'A verified e-mail address is required for login. Please verify your ' +
|
2016-03-09 16:20:28 -05:00
|
|
|
'e-mail address in %s and try again.' % dex_login.public_title,
|
|
|
|
)
|
2015-09-04 16:14:46 -04:00
|
|
|
|
|
|
|
|
|
|
|
return conduct_oauth_login(dex_login, dex_id, username, email_address,
|
|
|
|
metadata=metadata)
|
|
|
|
|
|
|
|
|
|
|
|
@oauthlogin.route('/dex/callback/attach', methods=['GET', 'POST'])
|
|
|
|
@route_show_if(features.DEX_LOGIN)
|
|
|
|
@require_session_login
|
|
|
|
def dex_oauth_attach():
|
|
|
|
code = request.args.get('code')
|
|
|
|
token = dex_login.exchange_code_for_token(app.config, client, code, redirect_suffix='/attach',
|
|
|
|
client_auth=True, form_encode=True)
|
|
|
|
if not token:
|
|
|
|
return render_ologin_error(dex_login.public_title)
|
|
|
|
|
|
|
|
try:
|
|
|
|
payload = decode_user_jwt(token, dex_login)
|
2016-11-28 18:55:51 -05:00
|
|
|
except InvalidTokenError:
|
2015-09-04 16:14:46 -04:00
|
|
|
logger.exception('Exception when decoding returned JWT')
|
2016-03-09 16:20:28 -05:00
|
|
|
return render_ologin_error(
|
|
|
|
dex_login.public_title,
|
|
|
|
'Could not decode response. Please contact your system administrator about this error.',
|
|
|
|
)
|
2015-09-04 16:14:46 -04:00
|
|
|
|
|
|
|
user_obj = current_user.db_user()
|
|
|
|
dex_id = payload['sub']
|
|
|
|
metadata = {}
|
|
|
|
|
|
|
|
try:
|
|
|
|
model.user.attach_federated_login(user_obj, 'dex', dex_id, metadata=metadata)
|
|
|
|
except IntegrityError:
|
|
|
|
err = '%s account is already attached to a %s account' % (dex_login.public_title,
|
2016-03-09 16:20:28 -05:00
|
|
|
app.config['REGISTRY_TITLE_SHORT'])
|
2015-09-04 16:14:46 -04:00
|
|
|
return render_ologin_error(dex_login.public_title, err)
|
|
|
|
|
2016-11-28 18:55:41 -05:00
|
|
|
return redirect(url_for('web.user_view', path=user_obj.username, tab='external'))
|
|
|
|
|