Merge pull request #2300 from coreos-inc/openid-connect
OpenID Connect support and OAuth login refactoring
This commit is contained in:
commit
01ec22b362
36 changed files with 1623 additions and 983 deletions
|
@ -11,7 +11,9 @@ from peewee import IntegrityError
|
|||
|
||||
import features
|
||||
|
||||
from app import app, billing as stripe, authentication, avatar, user_analytics, all_queues
|
||||
from app import (app, billing as stripe, authentication, avatar, user_analytics, all_queues,
|
||||
oauth_login)
|
||||
|
||||
from auth import scopes
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
|
||||
|
@ -24,11 +26,12 @@ from endpoints.api import (ApiResource, nickname, resource, validate_json_reques
|
|||
query_param, require_scope, format_date, show_if,
|
||||
require_fresh_login, path_param, define_json_response,
|
||||
RepositoryParamResource, page_support)
|
||||
from endpoints.exception import NotFound, InvalidToken
|
||||
from endpoints.exception import NotFound, InvalidToken, InvalidRequest, DownstreamIssue
|
||||
from endpoints.api.subscribe import subscribe
|
||||
from endpoints.common import common_login
|
||||
from endpoints.csrf import generate_csrf_token, OAUTH_CSRF_TOKEN_NAME
|
||||
from endpoints.decorators import anon_allowed
|
||||
from oauth.oidc import DiscoveryFailureException
|
||||
from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email,
|
||||
send_password_changed, send_org_recovery_email)
|
||||
from util.names import parse_single_urn
|
||||
|
@ -692,26 +695,62 @@ class Signout(ApiResource):
|
|||
return {'success': True}
|
||||
|
||||
|
||||
@resource('/v1/externaltoken')
|
||||
@resource('/v1/externallogin/<service_id>')
|
||||
@internal_only
|
||||
class GenerateExternalToken(ApiResource):
|
||||
""" Resource for generating a token for external login. """
|
||||
@nickname('generateExternalLoginToken')
|
||||
def post(self):
|
||||
""" Generates a CSRF token explicitly for OIDC/OAuth-associated login. """
|
||||
return {'token': generate_csrf_token(OAUTH_CSRF_TOKEN_NAME)}
|
||||
class ExternalLoginInformation(ApiResource):
|
||||
""" Resource for both setting a token for external login and returning its authorization
|
||||
url.
|
||||
"""
|
||||
schemas = {
|
||||
'GetLogin': {
|
||||
'type': 'object',
|
||||
'description': 'Information required to an retrieve external login URL.',
|
||||
'required': [
|
||||
'kind',
|
||||
],
|
||||
'properties': {
|
||||
'kind': {
|
||||
'type': 'string',
|
||||
'description': 'The kind of URL',
|
||||
'enum': ['login', 'attach'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@resource('/v1/detachexternal/<servicename>')
|
||||
@nickname('retrieveExternalLoginAuthorizationUrl')
|
||||
@anon_allowed
|
||||
@validate_json_request('GetLogin')
|
||||
def post(self, service_id):
|
||||
""" Generates the auth URL and CSRF token explicitly for OIDC/OAuth-associated login. """
|
||||
login_service = oauth_login.get_service(service_id)
|
||||
if login_service is None:
|
||||
raise InvalidRequest()
|
||||
|
||||
csrf_token = generate_csrf_token(OAUTH_CSRF_TOKEN_NAME)
|
||||
kind = request.get_json()['kind']
|
||||
redirect_suffix = '/attach' if kind == 'attach' else ''
|
||||
|
||||
try:
|
||||
login_scopes = login_service.get_login_scopes()
|
||||
auth_url = login_service.get_auth_url(app.config, redirect_suffix, csrf_token, login_scopes)
|
||||
return {'auth_url': auth_url}
|
||||
except DiscoveryFailureException as dfe:
|
||||
logger.exception('Could not discovery OAuth endpoint information')
|
||||
raise DownstreamIssue(dfe.message)
|
||||
|
||||
|
||||
@resource('/v1/detachexternal/<service_id>')
|
||||
@show_if(features.DIRECT_LOGIN)
|
||||
@internal_only
|
||||
class DetachExternal(ApiResource):
|
||||
""" Resource for detaching an external login. """
|
||||
@require_user_admin
|
||||
@nickname('detachExternalLogin')
|
||||
def post(self, servicename):
|
||||
def post(self, service_id):
|
||||
""" Request that the current user be detached from the external login service. """
|
||||
model.user.detach_external_login(get_authenticated_user(), servicename)
|
||||
model.user.detach_external_login(get_authenticated_user(), service_id)
|
||||
return {'success': True}
|
||||
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from flask_principal import identity_changed
|
|||
import endpoints.decorated # Register the various exceptions via decorators.
|
||||
import features
|
||||
|
||||
from app import app, oauth_apps, LoginWrappedDBUser, user_analytics, license_validator
|
||||
from app import app, oauth_apps, oauth_login, LoginWrappedDBUser, user_analytics, license_validator
|
||||
from auth import scopes
|
||||
from auth.permissions import QuayDeferredPermissionUser
|
||||
from config import frontend_visible_config
|
||||
|
@ -189,6 +189,18 @@ def render_page_template(name, route_data=None, **kwargs):
|
|||
cache_buster = cachebusters.get(filename, random_string()) if not debugging else 'debugging'
|
||||
yield (filename, cache_buster)
|
||||
|
||||
def get_external_login_config():
|
||||
login_config = []
|
||||
for login_service in oauth_login.services:
|
||||
login_config.append({
|
||||
'id': login_service.service_id(),
|
||||
'title': login_service.service_name(),
|
||||
'config': login_service.get_public_config(),
|
||||
'icon': login_service.get_icon(),
|
||||
})
|
||||
|
||||
return login_config
|
||||
|
||||
def get_oauth_config():
|
||||
oauth_config = {}
|
||||
for oauth_app in oauth_apps:
|
||||
|
@ -215,6 +227,7 @@ def render_page_template(name, route_data=None, **kwargs):
|
|||
feature_set=features.get_features(),
|
||||
config_set=frontend_visible_config(app.config),
|
||||
oauth_set=get_oauth_config(),
|
||||
external_login_set=get_external_login_config(),
|
||||
scope_set=scopes.app_scopes(app.config),
|
||||
vuln_priority_set=PRIORITY_LEVELS,
|
||||
enterprise_logo=app.config.get('ENTERPRISE_LOGO_URL', ''),
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import logging
|
||||
import requests
|
||||
import uuid
|
||||
|
||||
from flask import request, redirect, url_for, Blueprint
|
||||
from flask_login import current_user
|
||||
from peewee import IntegrityError
|
||||
|
||||
import features
|
||||
|
||||
from app import app, analytics, get_app_url, github_login, google_login, dex_login
|
||||
from app import app, analytics, get_app_url, oauth_login
|
||||
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, route_show_if
|
||||
from endpoints.common import common_login
|
||||
from endpoints.web import index
|
||||
from endpoints.csrf import csrf_protect, OAUTH_CSRF_TOKEN_NAME
|
||||
from util.security.jwtutil import decode, InvalidTokenError
|
||||
from oauth.login import OAuthLoginException
|
||||
from util.validation import generate_valid_usernames
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -22,7 +22,9 @@ oauthlogin = Blueprint('oauthlogin', __name__)
|
|||
|
||||
oauthlogin_csrf_protect = csrf_protect(OAUTH_CSRF_TOKEN_NAME, 'state', all_methods=True)
|
||||
|
||||
def render_ologin_error(service_name, error_message=None, register_redirect=False):
|
||||
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',
|
||||
|
@ -37,30 +39,19 @@ def render_ologin_error(service_name, error_message=None, register_redirect=Fals
|
|||
resp.status_code = 400
|
||||
return resp
|
||||
|
||||
def _conduct_oauth_login(service_id, service_name, user_id, username, email, metadata=None):
|
||||
""" Conducts login from the result of an OAuth service's login flow. """
|
||||
|
||||
def get_user(service, token):
|
||||
token_param = {
|
||||
'access_token': token,
|
||||
'alt': 'json',
|
||||
}
|
||||
got_user = client.get(service.user_endpoint(), params=token_param)
|
||||
if got_user.status_code != requests.codes.ok:
|
||||
return {}
|
||||
|
||||
return got_user.json()
|
||||
|
||||
|
||||
def conduct_oauth_login(service, user_id, username, email, metadata=None):
|
||||
service_name = service.service_name()
|
||||
to_login = model.user.verify_federated_login(service_name.lower(), user_id)
|
||||
to_login = model.user.verify_federated_login(service_id, user_id)
|
||||
if not to_login:
|
||||
# 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)
|
||||
return _render_ologin_error(service_name, error_message)
|
||||
|
||||
# Try to create the user
|
||||
try:
|
||||
# Generate a valid username.
|
||||
new_username = None
|
||||
for valid in generate_valid_usernames(username):
|
||||
if model.user.get_user_or_org(valid):
|
||||
|
@ -69,8 +60,13 @@ def conduct_oauth_login(service, user_id, username, email, metadata=None):
|
|||
new_username = valid
|
||||
break
|
||||
|
||||
# Generate a valid email. If the email is None and the MAILING feature is turned
|
||||
# off, simply place in a fake email address.
|
||||
if email is None and not features.MAILING:
|
||||
email = '%s@fake.example.com' % (str(uuid.uuid4()))
|
||||
|
||||
prompts = model.user.get_default_user_prompts(features)
|
||||
to_login = model.user.create_federated_user(new_username, email, service_name.lower(),
|
||||
to_login = model.user.create_federated_user(new_username, email, service_id,
|
||||
user_id, set_password_notification=True,
|
||||
metadata=metadata or {},
|
||||
prompts=prompts)
|
||||
|
@ -84,10 +80,10 @@ def conduct_oauth_login(service, user_id, username, email, metadata=None):
|
|||
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, register_redirect=True)
|
||||
return _render_ologin_error(service_name, message, register_redirect=True)
|
||||
|
||||
except model.DataModelException as ex:
|
||||
return render_ologin_error(service_name, ex.message)
|
||||
return _render_ologin_error(service_name, ex.message)
|
||||
|
||||
if common_login(to_login):
|
||||
if model.user.has_user_prompts(to_login):
|
||||
|
@ -95,277 +91,79 @@ def conduct_oauth_login(service, user_id, username, email, metadata=None):
|
|||
else:
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
return render_ologin_error(service_name)
|
||||
return _render_ologin_error(service_name)
|
||||
|
||||
|
||||
def get_email_username(user_data):
|
||||
username = user_data['email']
|
||||
at = username.find('@')
|
||||
if at > 0:
|
||||
username = username[0:at]
|
||||
def _register_service(login_service):
|
||||
""" Registers the given login service, adding its callback and attach routes to the blueprint. """
|
||||
|
||||
return username
|
||||
@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
|
||||
}
|
||||
|
||||
return _conduct_oauth_login(login_service.service_id(), login_service.service_name(), lid,
|
||||
lusername, lemail, metadata=metadata)
|
||||
|
||||
|
||||
@oauthlogin.route('/google/callback', methods=['GET'])
|
||||
@route_show_if(features.GOOGLE_LOGIN)
|
||||
@oauthlogin_csrf_protect
|
||||
def google_oauth_callback():
|
||||
error = request.args.get('error', None)
|
||||
if error:
|
||||
return render_ologin_error('Google', error)
|
||||
@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)
|
||||
|
||||
code = request.args.get('code')
|
||||
token = google_login.exchange_code_for_token(app.config, client, code, form_encode=True)
|
||||
if token is None:
|
||||
return render_ologin_error('Google')
|
||||
# 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)
|
||||
|
||||
user_data = get_user(google_login, token)
|
||||
if not user_data or not user_data.get('id', None) or not user_data.get('email', None):
|
||||
return render_ologin_error('Google')
|
||||
# Conduct attach.
|
||||
metadata = {
|
||||
'service_username': lusername
|
||||
}
|
||||
|
||||
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.',
|
||||
)
|
||||
user_obj = get_authenticated_user()
|
||||
|
||||
username = get_email_username(user_data)
|
||||
metadata = {
|
||||
'service_username': user_data['email']
|
||||
}
|
||||
try:
|
||||
model.user.attach_federated_login(user_obj, login_service.service_id(), lid,
|
||||
metadata=metadata)
|
||||
except IntegrityError:
|
||||
err = '%s account %s is already attached to a %s account' % (
|
||||
login_service.service_name(), lusername, app.config['REGISTRY_TITLE_SHORT'])
|
||||
return _render_ologin_error(login_service.service_name(), err)
|
||||
|
||||
return conduct_oauth_login(google_login, user_data['id'], username, user_data['email'],
|
||||
metadata=metadata)
|
||||
return redirect(url_for('web.user_view', path=user_obj.username, tab='external'))
|
||||
|
||||
|
||||
@oauthlogin.route('/github/callback', methods=['GET'])
|
||||
@route_show_if(features.GITHUB_LOGIN)
|
||||
@oauthlogin_csrf_protect
|
||||
def github_oauth_callback():
|
||||
error = request.args.get('error', None)
|
||||
if error:
|
||||
return render_ologin_error('GitHub', error)
|
||||
oauthlogin.add_url_rule('/%s/callback' % login_service.service_id(),
|
||||
'%s_oauth_callback' % login_service.service_id(),
|
||||
callback_func,
|
||||
methods=['GET'])
|
||||
|
||||
# Exchange the OAuth code.
|
||||
code = request.args.get('code')
|
||||
token = github_login.exchange_code_for_token(app.config, client, code)
|
||||
if token is None:
|
||||
return render_ologin_error('GitHub')
|
||||
oauthlogin.add_url_rule('/%s/callback/attach' % login_service.service_id(),
|
||||
'%s_oauth_attach' % login_service.service_id(),
|
||||
attach_func,
|
||||
methods=['GET'])
|
||||
|
||||
# Retrieve the user's information.
|
||||
user_data = get_user(github_login, token)
|
||||
if not user_data or 'login' not in user_data:
|
||||
return render_ologin_error('GitHub')
|
||||
|
||||
username = user_data['login']
|
||||
github_id = user_data['id']
|
||||
|
||||
v3_media_type = {
|
||||
'Accept': 'application/vnd.github.v3'
|
||||
}
|
||||
|
||||
token_param = {
|
||||
'access_token': token,
|
||||
}
|
||||
|
||||
# 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,
|
||||
headers={'Accept': 'application/vnd.github.moondragon+json'})
|
||||
|
||||
organizations = set([org.get('login').lower() for org in get_orgs.json()])
|
||||
matching_organizations = organizations & set(github_login.allowed_organizations())
|
||||
if not matching_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
|
||||
get_email = client.get(github_login.email_endpoint(), params=token_param,
|
||||
headers=v3_media_type)
|
||||
if get_email.status_code / 100 != 2:
|
||||
return render_ologin_error('GitHub')
|
||||
|
||||
found_email = None
|
||||
for user_email in get_email.json():
|
||||
if not github_login.is_enterprise() and not user_email['verified']:
|
||||
continue
|
||||
|
||||
found_email = user_email['email']
|
||||
if user_email['primary']:
|
||||
break
|
||||
|
||||
if found_email is None:
|
||||
err = 'There is no verified e-mail address attached to the GitHub account.'
|
||||
return render_ologin_error('GitHub', err)
|
||||
|
||||
metadata = {
|
||||
'service_username': username
|
||||
}
|
||||
|
||||
return conduct_oauth_login(github_login, github_id, username, found_email, metadata=metadata)
|
||||
|
||||
|
||||
@oauthlogin.route('/google/callback/attach', methods=['GET'])
|
||||
@route_show_if(features.GOOGLE_LOGIN)
|
||||
@require_session_login
|
||||
@oauthlogin_csrf_protect
|
||||
def google_oauth_attach():
|
||||
code = request.args.get('code')
|
||||
token = google_login.exchange_code_for_token(app.config, client, code,
|
||||
redirect_suffix='/attach', form_encode=True)
|
||||
if token is None:
|
||||
return render_ologin_error('Google')
|
||||
|
||||
user_data = get_user(google_login, token)
|
||||
if not user_data or not user_data.get('id', None):
|
||||
return render_ologin_error('Google')
|
||||
|
||||
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.',
|
||||
)
|
||||
|
||||
google_id = user_data['id']
|
||||
user_obj = current_user.db_user()
|
||||
|
||||
username = get_email_username(user_data)
|
||||
metadata = {
|
||||
'service_username': user_data['email']
|
||||
}
|
||||
|
||||
try:
|
||||
model.user.attach_federated_login(user_obj, 'google', google_id, metadata=metadata)
|
||||
except IntegrityError:
|
||||
err = 'Google account %s is already attached to a %s account' % (
|
||||
username, app.config['REGISTRY_TITLE_SHORT'])
|
||||
return render_ologin_error('Google', err)
|
||||
|
||||
return redirect(url_for('web.user_view', path=user_obj.username, tab='external'))
|
||||
|
||||
|
||||
@oauthlogin.route('/github/callback/attach', methods=['GET'])
|
||||
@route_show_if(features.GITHUB_LOGIN)
|
||||
@require_session_login
|
||||
@oauthlogin_csrf_protect
|
||||
def github_oauth_attach():
|
||||
code = request.args.get('code')
|
||||
token = github_login.exchange_code_for_token(app.config, client, code)
|
||||
if token is None:
|
||||
return render_ologin_error('GitHub')
|
||||
|
||||
user_data = get_user(github_login, token)
|
||||
if not user_data:
|
||||
return render_ologin_error('GitHub')
|
||||
|
||||
github_id = user_data['id']
|
||||
user_obj = current_user.db_user()
|
||||
|
||||
username = user_data['login']
|
||||
metadata = {
|
||||
'service_username': username
|
||||
}
|
||||
|
||||
try:
|
||||
model.user.attach_federated_login(user_obj, 'github', github_id, metadata=metadata)
|
||||
except IntegrityError:
|
||||
err = 'Github account %s is already attached to a %s account' % (
|
||||
username, app.config['REGISTRY_TITLE_SHORT'])
|
||||
|
||||
return render_ologin_error('GitHub', err)
|
||||
|
||||
return redirect(url_for('web.user_view', path=user_obj.username, tab='external'))
|
||||
|
||||
|
||||
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)
|
||||
@oauthlogin_csrf_protect
|
||||
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)
|
||||
if token is None:
|
||||
return render_ologin_error(dex_login.public_title)
|
||||
|
||||
try:
|
||||
payload = decode_user_jwt(token, dex_login)
|
||||
except InvalidTokenError:
|
||||
logger.exception('Exception when decoding returned JWT')
|
||||
return render_ologin_error(
|
||||
dex_login.public_title,
|
||||
'Could not decode response. Please contact your system administrator about this error.',
|
||||
)
|
||||
|
||||
username = get_email_username(payload)
|
||||
metadata = {}
|
||||
|
||||
dex_id = payload['sub']
|
||||
email_address = payload['email']
|
||||
|
||||
if not payload.get('email_verified', False):
|
||||
return render_ologin_error(
|
||||
dex_login.public_title,
|
||||
'A verified e-mail address is required for login. Please verify your ' +
|
||||
'e-mail address in %s and try again.' % dex_login.public_title,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@oauthlogin_csrf_protect
|
||||
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 token is None:
|
||||
return render_ologin_error(dex_login.public_title)
|
||||
|
||||
try:
|
||||
payload = decode_user_jwt(token, dex_login)
|
||||
except InvalidTokenError:
|
||||
logger.exception('Exception when decoding returned JWT')
|
||||
return render_ologin_error(
|
||||
dex_login.public_title,
|
||||
'Could not decode response. Please contact your system administrator about this error.',
|
||||
)
|
||||
|
||||
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,
|
||||
app.config['REGISTRY_TITLE_SHORT'])
|
||||
return render_ologin_error(dex_login.public_title, err)
|
||||
|
||||
return redirect(url_for('web.user_view', path=user_obj.username, tab='external'))
|
||||
# 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