First iteration of sign-in with gihub.
This commit is contained in:
parent
5627dfc0c6
commit
3d89227752
6 changed files with 153 additions and 8 deletions
21
config.py
21
config.py
|
@ -72,8 +72,21 @@ class MixpanelProdConfig(object):
|
|||
MIXPANEL_KEY = '50ff2b2569faa3a51c8f5724922ffb7e'
|
||||
|
||||
|
||||
class GitHubTestConfig(object):
|
||||
GITHUB_CLIENT_ID = 'cfbc4aca88e5c1b40679'
|
||||
GITHUB_CLIENT_SECRET = '7d1cc21e17e10cd8168410e2cd1e4561cb854ff9'
|
||||
GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'
|
||||
GITHUB_USER_URL = 'https://api.github.com/user'
|
||||
GITHUB_USER_EMAILS = GITHUB_USER_URL + '/emails'
|
||||
|
||||
|
||||
class GitHubProdConfig(GitHubTestConfig):
|
||||
GITHUB_CLIENT_ID = '5a8c08b06c48d89d4d1e'
|
||||
GITHUB_CLIENT_SECRET = 'f89d8bb28ea3bd4e1c68808500d185a816be53b1'
|
||||
|
||||
|
||||
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
||||
StripeTestConfig, MixpanelTestConfig):
|
||||
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig):
|
||||
REGISTRY_SERVER = 'localhost:5000'
|
||||
LOGGING_CONFIG = {
|
||||
'level': logging.DEBUG,
|
||||
|
@ -83,7 +96,8 @@ class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
|||
|
||||
|
||||
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||
StripeLiveConfig, MixpanelTestConfig):
|
||||
StripeLiveConfig, MixpanelTestConfig,
|
||||
GitHubProdConfig):
|
||||
REGISTRY_SERVER = 'localhost:5000'
|
||||
LOGGING_CONFIG = {
|
||||
'level': logging.DEBUG,
|
||||
|
@ -93,7 +107,8 @@ class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
|||
|
||||
|
||||
class ProductionConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||
StripeLiveConfig, MixpanelProdConfig):
|
||||
StripeLiveConfig, MixpanelProdConfig,
|
||||
GitHubProdConfig):
|
||||
REGISTRY_SERVER = 'quay.io'
|
||||
LOGGING_CONFIG = {
|
||||
'stream': sys.stderr,
|
||||
|
|
|
@ -36,12 +36,32 @@ class BaseModel(Model):
|
|||
|
||||
class User(BaseModel):
|
||||
username = CharField(unique=True, index=True)
|
||||
password_hash = CharField()
|
||||
password_hash = CharField(null=True)
|
||||
email = CharField(unique=True, index=True)
|
||||
verified = BooleanField(default=False)
|
||||
stripe_id = CharField(index=True, null=True)
|
||||
|
||||
|
||||
class LoginService(BaseModel):
|
||||
name = CharField(unique=True, index=True)
|
||||
|
||||
|
||||
class FederatedLogin(BaseModel):
|
||||
user = ForeignKeyField(User, index=True)
|
||||
service = ForeignKeyField(LoginService, index=True)
|
||||
service_ident = CharField()
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
indexes = (
|
||||
# create a unique index on service and the local service id
|
||||
(('service', 'service_ident'), True),
|
||||
|
||||
# a user may only have one federated login per service
|
||||
(('service', 'user'), True),
|
||||
)
|
||||
|
||||
|
||||
class Visibility(BaseModel):
|
||||
name = CharField(index=True)
|
||||
|
||||
|
@ -136,9 +156,10 @@ class RepositoryTag(BaseModel):
|
|||
def initialize_db():
|
||||
create_model_tables([User, Repository, Image, AccessToken, Role,
|
||||
RepositoryPermission, Visibility, RepositoryTag,
|
||||
EmailConfirmation])
|
||||
EmailConfirmation, FederatedLogin, LoginService])
|
||||
Role.create(name='admin')
|
||||
Role.create(name='write')
|
||||
Role.create(name='read')
|
||||
Visibility.create(name='public')
|
||||
Visibility.create(name='private')
|
||||
LoginService.create(name='github')
|
||||
|
|
|
@ -34,6 +34,34 @@ def create_user(username, password, email):
|
|||
raise DataModelException(ex.message)
|
||||
|
||||
|
||||
def create_federated_user(username, email, service_name, service_id):
|
||||
try:
|
||||
new_user = User.create(username=username, email=email, verified=True)
|
||||
service = LoginService.get(LoginService.name == service_name)
|
||||
federated_user = FederatedLogin.create(user=new_user, service=service,
|
||||
service_ident=service_id)
|
||||
|
||||
return new_user
|
||||
|
||||
except Exception as ex:
|
||||
raise DataModelException(ex.message)
|
||||
|
||||
|
||||
def verify_federated_login(service_name, service_id):
|
||||
selected = FederatedLogin.select(FederatedLogin, User)
|
||||
with_service = selected.join(LoginService)
|
||||
with_user = with_service.switch(FederatedLogin).join(User)
|
||||
found = with_user.where(FederatedLogin.service_ident == service_id,
|
||||
LoginService.name == service_name)
|
||||
|
||||
found_list = list(found)
|
||||
|
||||
if found_list:
|
||||
return found_list[0].user
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def create_confirm_email_code(user):
|
||||
code = EmailConfirmation.create(user=user, email_confirm=True)
|
||||
return code
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import requests
|
||||
|
||||
from flask import (abort, send_file, redirect, request, url_for,
|
||||
render_template, make_response)
|
||||
|
@ -66,7 +67,8 @@ def common_login(db_user):
|
|||
|
||||
@app.route('/signin', methods=['GET'])
|
||||
def render_signin_page():
|
||||
return render_template('signin.html')
|
||||
return render_template('signin.html',
|
||||
github_client_id=app.config['GITHUB_CLIENT_ID'])
|
||||
|
||||
|
||||
@app.route('/signin', methods=['POST'])
|
||||
|
@ -81,12 +83,66 @@ def signin():
|
|||
return redirect(request.args.get('next') or url_for('index'))
|
||||
else:
|
||||
return render_template('signin.html',
|
||||
needs_email_verification=True)
|
||||
needs_email_verification=True,
|
||||
github_client_id=app.config['GITHUB_CLIENT_ID'])
|
||||
|
||||
else:
|
||||
return render_template('signin.html',
|
||||
username=username,
|
||||
invalid_credentials=True)
|
||||
invalid_credentials=True,
|
||||
github_client_id=app.config['GITHUB_CLIENT_ID'])
|
||||
|
||||
|
||||
@app.route('/oauth2/github/callback', methods=['GET'])
|
||||
def github_oauth_callback():
|
||||
code = request.args.get('code')
|
||||
payload = {
|
||||
'client_id': app.config['GITHUB_CLIENT_ID'],
|
||||
'client_secret': app.config['GITHUB_CLIENT_SECRET'],
|
||||
'code': code,
|
||||
}
|
||||
headers = {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
get_access_token = requests.post(app.config['GITHUB_TOKEN_URL'],
|
||||
params=payload, headers=headers)
|
||||
|
||||
token = get_access_token.json()['access_token']
|
||||
|
||||
token_param = {
|
||||
'access_token': token,
|
||||
}
|
||||
get_user = requests.get(app.config['GITHUB_USER_URL'], params=token_param)
|
||||
|
||||
user_data = get_user.json()
|
||||
username = user_data['login']
|
||||
github_id = user_data['id']
|
||||
|
||||
v3_media_type = {
|
||||
'Accept': 'application/vnd.github.v3'
|
||||
}
|
||||
get_email = requests.get(app.config['GITHUB_USER_EMAILS'],
|
||||
params=token_param, headers=v3_media_type)
|
||||
|
||||
# We will accept any email, but we prefer the primary
|
||||
found_email = None
|
||||
for user_email in get_email.json():
|
||||
found_email = user_email['email']
|
||||
if user_email['primary']:
|
||||
break
|
||||
|
||||
to_login = model.verify_federated_login('github', github_id)
|
||||
if not to_login:
|
||||
# try to create the user
|
||||
to_login = model.create_federated_user(username, found_email, 'github',
|
||||
github_id)
|
||||
|
||||
if common_login(to_login):
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# TODO something bad happened, we need to tell the user somehow
|
||||
return redirect(url_for('signin'))
|
||||
|
||||
|
||||
@app.route('/confirm', methods=['GET'])
|
||||
|
|
|
@ -8,6 +8,7 @@ body {
|
|||
max-width: 330px;
|
||||
padding: 15px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
.form-signin .form-signin-heading,
|
||||
.form-signin .checkbox {
|
||||
|
@ -43,3 +44,19 @@ body {
|
|||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.social-alternate {
|
||||
color: #777;
|
||||
font-size: 3em;
|
||||
margin-left: 43px;
|
||||
}
|
||||
|
||||
.social-alternate .inner-text {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
color: white;
|
||||
left: -43px;
|
||||
top: -9px;
|
||||
font-weight: bold;
|
||||
font-size: .4em;
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
<title>Sign In - Quay</title>
|
||||
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
|
||||
|
||||
<link rel="stylesheet" href="static/css/signin.css">
|
||||
</head>
|
||||
|
@ -13,6 +14,13 @@
|
|||
<input type="text" class="form-control" placeholder="Username" name="username" value="{{ username }}"autofocus>
|
||||
<input type="password" class="form-control" placeholder="Password" name="password">
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
|
||||
|
||||
<span class="social-alternate">
|
||||
<i class="icon-circle"></i>
|
||||
<span class="inner-text">OR</i>
|
||||
</span>
|
||||
|
||||
<a href="https://github.com/login/oauth/authorize?client_id={{ github_client_id }}&scope=user:email" class="btn btn-primary btn-lg btn-block"><i class="icon-github icon-large"></i> Sign In with GitHub</a>
|
||||
</form>
|
||||
|
||||
{% if invalid_credentials %}
|
||||
|
|
Reference in a new issue