Add support for login with Google. Note that this CL is not complete

This commit is contained in:
Joseph Schorr 2014-08-11 15:47:44 -04:00
parent b9c6c4c2f2
commit 2597bcef3f
10 changed files with 231 additions and 83 deletions

View file

@ -19,7 +19,7 @@ def build_requests_session():
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'GITHUB_CLIENT_ID',
'GITHUB_LOGIN_CLIENT_ID', 'MIXPANEL_KEY', 'STRIPE_PUBLISHABLE_KEY',
'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN', 'AUTHENTICATION_TYPE',
'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT']
'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT', 'GOOGLE_LOGIN_CLIENT_ID']
def getFrontendVisibleConfig(config_dict):
@ -115,6 +115,13 @@ class DefaultConfig(object):
GITHUB_LOGIN_CLIENT_ID = ''
GITHUB_LOGIN_CLIENT_SECRET = ''
# Google Config.
GOOGLE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
GOOGLE_USER_URL = 'https://www.googleapis.com/oauth2/v1/userinfo'
GOOGLE_LOGIN_CLIENT_ID = ''
GOOGLE_LOGIN_CLIENT_SECRET = ''
# Requests based HTTP client with a large request pool
HTTPCLIENT = build_requests_session()
@ -144,6 +151,9 @@ class DefaultConfig(object):
# Feature Flag: Whether GitHub login is supported.
FEATURE_GITHUB_LOGIN = False
# Feature Flag: Whether Google login is supported.
FEATURE_GOOGLE_LOGIN = False
# Feature flag, whether to enable olark chat
FEATURE_OLARK_CHAT = False

View file

@ -7,6 +7,7 @@ from endpoints.common import render_page_template, common_login, route_show_if
from app import app, analytics
from data import model
from util.names import parse_repository_name
from util.validation import generate_valid_usernames
from util.http import abort
from auth.permissions import AdministerRepositoryPermission
from auth.auth import require_session_login
@ -21,19 +22,31 @@ client = app.config['HTTPCLIENT']
callback = Blueprint('callback', __name__)
def exchange_github_code_for_token(code, for_login=True):
def exchange_code_for_token(code, service_name='GITHUB', for_login=True, form_encode=False):
code = request.args.get('code')
id_config = service_name + '_LOGIN_CLIENT_ID' if for_login else service_name + '_CLIENT_ID'
secret_config = service_name + '_LOGIN_CLIENT_SECRET' if for_login else service_name + '_CLIENT_SECRET'
payload = {
'client_id': app.config['GITHUB_LOGIN_CLIENT_ID' if for_login else 'GITHUB_CLIENT_ID'],
'client_secret': app.config['GITHUB_LOGIN_CLIENT_SECRET' if for_login else 'GITHUB_CLIENT_SECRET'],
'client_id': app.config[id_config],
'client_secret': app.config[secret_config],
'code': code,
'grant_type': 'authorization_code',
'redirect_uri': '%s://%s/oauth2/%s/callback' % (app.config['PREFERRED_URL_SCHEME'],
app.config['SERVER_HOSTNAME'],
service_name.lower())
}
headers = {
'Accept': 'application/json'
}
get_access_token = client.post(app.config['GITHUB_TOKEN_URL'],
params=payload, headers=headers)
if form_encode:
get_access_token = client.post(app.config[service_name + '_TOKEN_URL'],
data=payload, headers=headers)
else:
get_access_token = client.post(app.config[service_name + '_TOKEN_URL'],
params=payload, headers=headers)
json_data = get_access_token.json()
if not json_data:
@ -52,17 +65,76 @@ def get_github_user(token):
return get_user.json()
def get_google_user(token):
token_param = {
'access_token': token,
'alt': 'json',
}
get_user = client.get(app.config['GOOGLE_USER_URL'], params=token_param)
return get_user.json()
def conduct_oauth_login(service_name, user_id, username, email):
to_login = model.verify_federated_login(service_name.lower(), user_id)
if not to_login:
# try to create the user
try:
valid = next(generate_valid_usernames(username))
to_login = model.create_federated_user(valid, email, service_name.lower(),
user_id, set_password_notification=True)
# Success, tell analytics
analytics.track(to_login.username, 'register', {'service': service_name.lower()})
state = request.args.get('state', None)
if state:
logger.debug('Aliasing with state: %s' % state)
analytics.alias(to_login.username, state)
except model.DataModelException, ex:
return render_page_template('ologinerror.html', service_name=service_name,
error_message=ex.message)
if common_login(to_login):
return redirect(url_for('web.index'))
return render_page_template('ologinerror.html', service_name=service_name,
error_message='Unknown error')
@callback.route('/google/callback', methods=['GET'])
@route_show_if(features.GOOGLE_LOGIN)
def google_oauth_callback():
error = request.args.get('error', None)
if error:
return render_page_template('ologinerror.html', service_name='Google', error_message=error)
token = exchange_code_for_token(request.args.get('code'), service_name='GOOGLE', form_encode=True)
user_data = get_google_user(token)
if not user_data or not user_data.get('id', None) or not user_data.get('email', None):
return render_page_template('ologinerror.html', service_name = 'Google',
error_message='Could not load user data')
username = user_data['email']
at = username.find('@')
if at > 0:
username = username[0:at]
return conduct_oauth_login('Google', user_data['id'], username, user_data['email'])
@callback.route('/github/callback', methods=['GET'])
@route_show_if(features.GITHUB_LOGIN)
def github_oauth_callback():
error = request.args.get('error', None)
if error:
return render_page_template('githuberror.html', error_message=error)
return render_page_template('ologinerror.html', service_name = 'GitHub', error_message=error)
token = exchange_github_code_for_token(request.args.get('code'))
token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB')
user_data = get_github_user(token)
if not user_data:
return render_page_template('githuberror.html', error_message='Could not load user data')
return render_page_template('ologinerror.html', service_name = 'GitHub',
error_message='Could not load user data')
username = user_data['login']
github_id = user_data['id']
@ -84,38 +156,34 @@ def github_oauth_callback():
if user_email['primary']:
break
to_login = model.verify_federated_login('github', github_id)
if not to_login:
# try to create the user
try:
to_login = model.create_federated_user(username, found_email, 'github',
github_id, set_password_notification=True)
return conduct_oauth_login('github', github_id, username, found_email)
# Success, tell analytics
analytics.track(to_login.username, 'register', {'service': 'github'})
state = request.args.get('state', None)
if state:
logger.debug('Aliasing with state: %s' % state)
analytics.alias(to_login.username, state)
@callback.route('/google/callback/attach', methods=['GET'])
@route_show_if(features.GOOGLE_LOGIN)
@require_session_login
def google_oauth_attach():
token = exchange_code_for_token(request.args.get('code'), service_name='GOOGLE')
user_data = get_google_user(token)
if not user_data or not user_data.get('id', None):
return render_page_template('ologinerror.html', service_name = 'Google',
error_message='Could not load user data')
except model.DataModelException, ex:
return render_page_template('githuberror.html', error_message=ex.message)
if common_login(to_login):
return redirect(url_for('web.index'))
return render_page_template('githuberror.html')
google_id = user_data['id']
user_obj = current_user.db_user()
model.attach_federated_login(user_obj, 'google', google_id)
return redirect(url_for('web.user'))
@callback.route('/github/callback/attach', methods=['GET'])
@route_show_if(features.GITHUB_LOGIN)
@require_session_login
def github_oauth_attach():
token = exchange_github_code_for_token(request.args.get('code'))
token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB')
user_data = get_github_user(token)
if not user_data:
return render_page_template('githuberror.html', error_message='Could not load user data')
return render_page_template('ologinerror.html', service_name = 'GitHub',
error_message='Could not load user data')
github_id = user_data['id']
user_obj = current_user.db_user()
@ -130,7 +198,7 @@ def github_oauth_attach():
def attach_github_build_trigger(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
token = exchange_github_code_for_token(request.args.get('code'), for_login=False)
token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB', for_login=False)
repo = model.get_repository(namespace, repository)
if not repo:
msg = 'Invalid repository: %s/%s' % (namespace, repository)

View file

@ -179,6 +179,8 @@ def initialize_database():
TeamRole.create(name='member')
Visibility.create(name='public')
Visibility.create(name='private')
LoginService.create(name='google')
LoginService.create(name='github')
LoginService.create(name='quayrobot')
LoginService.create(name='ldap')

View file

@ -0,0 +1,17 @@
<span class="external-login-button-element">
<span ng-if="provider == 'github'">
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']" ng-click="startSignin('github')" style="margin-bottom: 10px">
<i class="fa fa-github fa-lg"></i>
<span ng-if="action != 'attach'">Sign In with GitHub</span>
<span ng-if="action == 'attach'">Attach to GitHub Account</span>
</a>
</span>
<span ng-if="provider == 'google'">
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GOOGLE_LOGIN']" ng-click="startSignin('google')">
<i class="fa fa-google fa-lg"></i>
<span ng-if="action != 'attach'">Sign In with Google</span>
<span ng-if="action == 'attach'">Attach to Google Account</span>
</a>
</span>
</span>

View file

@ -11,10 +11,8 @@
<span class="inner-text">OR</span>
</span>
<a id="github-signin-link" class="btn btn-primary btn-lg btn-block" href="javascript:void(0)" ng-click="showGithub()"
quay-require="['GITHUB_LOGIN']">
<i class="fa fa-github fa-lg"></i> Sign In with GitHub
</a>
<div class="external-login-button" provider="github" redirect-url="redirectUrl" sign-in-started="markStarted()"></div>
<div class="external-login-button" provider="google" redirect-url="redirectUrl" sign-in-started="markStarted()"></div>
</form>
<div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div>

View file

@ -18,10 +18,8 @@
<i class="fa fa-circle"></i>
<span class="inner-text">OR</span>
</span>
<a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ github_state_clause }}"
class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']">
<i class="fa fa-github fa-lg"></i> Sign In with GitHub
</a>
<div class="external-login-button" provider="github"></div>
<div class="external-login-button" provider="google"></div>
<p class="help-block" quay-require="['BILLING']">No credit card required.</p>
</div>
</form>

View file

@ -1278,10 +1278,41 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
var keyService = {}
keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY'];
keyService['githubClientId'] = Config['GITHUB_CLIENT_ID'];
keyService['githubLoginClientId'] = Config['GITHUB_LOGIN_CLIENT_ID'];
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
keyService['googleLoginClientId'] = Config['GOOGLE_LOGIN_CLIENT_ID'];
keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback');
keyService['googleLoginUrl'] = 'https://accounts.google.com/o/oauth2/auth?response_type=code&';
keyService['githubLoginUrl'] = 'https://github.com/login/oauth/authorize?';
keyService['googleLoginScope'] = 'openid email';
keyService['githubLoginScope'] = 'user:email';
keyService.getExternalLoginUrl = function(service, action) {
var state_clause = '';
if (Config.MIXPANEL_KEY && window.mixpanel) {
if (mixpanel.get_distinct_id !== undefined) {
state_clause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
}
}
var client_id = keyService[service + 'LoginClientId'];
var scope = keyService[service + 'LoginScope'];
var redirect_uri = keyService[service + 'RedirectUri'];
if (action == 'attach') {
redirect_uri += '/attach';
}
var url = keyService[service + 'LoginUrl'] + 'client_id=' + client_id + '&scope=' + scope +
'&redirect_uri=' + redirect_uri + state_clause;
return url;
};
return keyService;
}]);
@ -2150,6 +2181,41 @@ quayApp.directive('userSetup', function () {
});
quayApp.directive('externalLoginButton', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/external-login-button.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'signInStarted': '&signInStarted',
'redirectUrl': '=redirectUrl',
'provider': '@provider',
'action': '@action'
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) {
$scope.startSignin = function(service) {
$scope.signInStarted({'service': service});
var url = KeyService.getExternalLoginUrl(service, $scope.action || 'login');
// Save the redirect URL in a cookie so that we can redirect back after the service returns to us.
var redirectURL = $scope.redirectUrl || window.location.toString();
CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);
// Needed to ensure that UI work done by the started callback is finished before the location
// changes.
$timeout(function() {
document.location = url;
}, 250);
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('signinForm', function () {
var directiveDefinitionObject = {
priority: 0,
@ -2163,29 +2229,6 @@ quayApp.directive('signinForm', function () {
'signedIn': '&signedIn'
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) {
$scope.showGithub = function() {
if (!Features.GITHUB_LOGIN) { return; }
$scope.markStarted();
var mixpanelDistinctIdClause = '';
if (Config.MIXPANEL_KEY && mixpanel.get_distinct_id !== undefined) {
$scope.mixpanelDistinctIdClause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
}
// Save the redirect URL in a cookie so that we can redirect back after GitHub returns to us.
var redirectURL = $scope.redirectUrl || window.location.toString();
CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);
// Needed to ensure that UI work done by the started callback is finished before the location
// changes.
$timeout(function() {
var url = 'https://github.com/login/oauth/authorize?client_id=' + encodeURIComponent(KeyService.githubLoginClientId) +
'&scope=user:email' + mixpanelDistinctIdClause;
document.location = url;
}, 250);
};
$scope.markStarted = function() {
if ($scope.signInStarted != null) {
$scope.signInStarted();
@ -2235,18 +2278,9 @@ quayApp.directive('signupForm', function () {
scope: {
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
$('.form-signup').popover();
if (Config.MIXPANEL_KEY) {
angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) {
var mixpanelId = loadedMixpanel.get_distinct_id();
$scope.github_state_clause = '&state=' + mixpanelId;
});
}
$scope.githubClientId = KeyService.githubLoginClientId;
$scope.awaitingConfirmation = false;
$scope.registering = false;

View file

@ -1681,6 +1681,10 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.githubLogin = resp.login;
});
}
if ($scope.cuser.logins[i].service == 'google') {
$scope.hasGoogleLogin = true;
}
}
}
});
@ -1697,7 +1701,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.convertStep = 0;
$scope.org = {};
$scope.githubRedirectUri = KeyService.githubRedirectUri;
$scope.githubClientId = KeyService.githubLoginClientId;
$scope.authorizedApps = null;
$scope.logsShown = 0;

View file

@ -33,7 +33,7 @@
<li quay-classes="{'!Features.BILLING': 'active'}"><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#github" quay-require="['GITHUB_LOGIN']">GitHub Login</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#external" quay-show="Features.GITHUB_LOGIN || Features.GOOGLE_LOGIN">External Logins</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#authorized" ng-click="loadAuthedApps()">Authorized Applications</a></li>
<li quay-show="Features.USER_LOG_ACCESS || hasPaidBusinessPlan">
<a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a>
@ -162,12 +162,14 @@
</div>
</div>
<!-- Github tab -->
<div id="github" class="tab-pane" quay-require="['GITHUB_LOGIN']">
<!-- External Login tab -->
<div id="external" class="tab-pane" quay-show="Features.GITHUB_LOGIN || Features.GOOGLE_LOGIN">
<div class="loading" ng-show="!cuser">
<div class="quay-spinner 3x"></div>
</div>
<div class="row" ng-show="cuser">
<!-- Github -->
<div class="row" quay-show="cuser && Features.GITHUB_LOGIN">
<div class="panel">
<div class="panel-title">GitHub Login:</div>
<div class="panel-body">
@ -175,12 +177,28 @@
<i class="fa fa-github fa-lg" style="margin-right: 6px;" data-title="GitHub" bs-tooltip="tooltip.title"></i>
<b><a href="https://github.com/{{githubLogin}}" target="_blank">{{githubLogin}}</a></b>
</div>
<div ng-show="!githubLogin" class="col-md-8">
<a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ github_state_clause }}&redirect_uri={{ githubRedirectUri }}/attach" class="btn btn-primary"><i class="fa fa-github fa-lg"></i> Connect with GitHub</a>
<div ng-show="!githubLogin" class="col-md-4">
<span class="external-login-button" provider="github" action="attach"></span>
</div>
</div>
</div>
</div>
<!-- Google -->
<div class="row" quay-show="cuser && Features.GOOGLE_LOGIN">
<div class="panel">
<div class="panel-title">Google Login:</div>
<div class="panel-body">
<div ng-show="hasGoogleLogin" class="lead col-md-8">
Account tied to your Google account.
</div>
<div ng-show="!hasGoogleLogin" class="col-md-4">
<span class="external-login-button" provider="google" action="attach"></span>
</div>
</div>
</div>
</div>
</div>
<!-- Robot accounts tab -->

View file

@ -1,14 +1,14 @@
{% extends "base.html" %}
{% block title %}
<title>Error Logging in with GitHub · Quay.io</title>
<title>Error Logging in with {{ service_name }} · Quay.io</title>
{% endblock %}
{% block body_content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h2>There was an error logging in with GitHub.</h2>
<h2>There was an error logging in with {{ service_name }}.</h2>
{% if error_message %}
<div class="alert alert-danger">{{ error_message }}</div>
@ -16,11 +16,11 @@
<div>
Please register using the <a href="/">registration form</a> to continue.
You will be able to connect your github account to your Quay.io account
You will be able to connect your account to your Quay.io account
in the user settings.
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}