Skip to content

Commit

Permalink
add github!
Browse files Browse the repository at this point in the history
  • Loading branch information
snarfed committed Jan 30, 2018
1 parent 7641605 commit b8e6007
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 14 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ facebook_app_id_local
facebook_app_secret_local
flickr_app_key
flickr_app_secret
github_client_id
github_client_secret
github_client_id_local
github_client_secret_local
google_client_id
google_client_secret
instagram_client_id
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ you have it as a relative directory. pip requires fully qualified directories.
Changelog
---
### 1.11 - unreleased
* Add GitHub!
* Facebook
* Pass `state` to the initial OAuth endpoint directly, instead of encoding it into the redirect URL, so the redirect can [match the Strict Mode whitelist](https://developers.facebook.com/blog/post/2017/12/18/strict-uri-matching/).
* Add humanize dependency for webutil.logs.
Expand Down
29 changes: 17 additions & 12 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@

from oauth_dropins.webutil import handlers

from oauth_dropins import blogger_v2
from oauth_dropins import disqus
from oauth_dropins import dropbox
from oauth_dropins import facebook
from oauth_dropins import flickr
from oauth_dropins import googleplus
from oauth_dropins import indieauth
from oauth_dropins import instagram
from oauth_dropins import medium
from oauth_dropins import tumblr
from oauth_dropins import twitter
from oauth_dropins import wordpress_rest
from oauth_dropins import (
blogger_v2,
disqus,
dropbox,
facebook,
flickr,
github,
googleplus,
indieauth,
instagram,
medium,
tumblr,
twitter,
wordpress_rest,
)


class FrontPageHandler(handlers.ModernHandler):
Expand Down Expand Up @@ -50,6 +53,8 @@ def get(self):
('/facebook/oauth_callback', facebook.CallbackHandler.to('/')),
('/flickr/start', flickr.StartHandler.to('/flickr/oauth_callback')),
('/flickr/oauth_callback', flickr.CallbackHandler.to('/')),
('/github/start', github.StartHandler.to('/github/oauth_callback')),
('/github/oauth_callback', github.CallbackHandler.to('/')),
('/googleplus/start', googleplus.StartHandler.to('/googleplus/oauth_callback')),
('/googleplus/oauth_callback', googleplus.CallbackHandler.to('/')),
('/indieauth/start', indieauth.StartHandler.to('/indieauth/oauth_callback')),
Expand Down
4 changes: 4 additions & 0 deletions oauth_dropins/appengine_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def read(filename):
read('facebook_app_id_local'))
FACEBOOK_APP_SECRET = (os.getenv('FACEBOOK_APP_SECRET') or
read('facebook_app_secret_local'))
GITHUB_CLIENT_ID = read('github_client_id_local')
GITHUB_CLIENT_SECRET = read('github_client_secret_local')
INSTAGRAM_CLIENT_ID = read('instagram_client_id_local')
INSTAGRAM_CLIENT_SECRET = read('instagram_client_secret_local')
WORDPRESS_CLIENT_ID = read('wordpress.com_client_id_local')
Expand All @@ -80,6 +82,8 @@ def read(filename):
read('facebook_app_id'))
FACEBOOK_APP_SECRET = (os.getenv('FACEBOOK_APP_SECRET') or
read('facebook_app_secret'))
GITHUB_CLIENT_ID = read('github_client_id')
GITHUB_CLIENT_SECRET = read('github_client_secret')
INSTAGRAM_CLIENT_ID = read('instagram_client_id')
INSTAGRAM_CLIENT_SECRET = read('instagram_client_secret')
WORDPRESS_CLIENT_ID = read('wordpress.com_client_id')
Expand Down
161 changes: 161 additions & 0 deletions oauth_dropins/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""GitHub OAuth drop-in.
API docs:
https://developer.github.com/v4/
https://developer.github.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/#web-application-flow
"""
import json
import logging
import urllib
import urlparse

import appengine_config
import handlers
from models import BaseAuth
from webutil import util

from google.appengine.ext import ndb
from webob import exc

# URL templates. Can't (easily) use urllib.urlencode() because I want to keep
# the %(...)s placeholders as is and fill them in later in code.
GET_AUTH_CODE_URL = str('&'.join((
'https://github.com/login/oauth/authorize?'
'client_id=%(client_id)s',
# https://developer.github.com/apps/building-oauth-apps/scopes-for-oauth-apps/
'scope=%(scope)s',
# if provided, must be the same in the access token request, or a subpath!
'redirect_uri=%(redirect_uri)s',
'state=%(state)s',
)))

GET_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'

API_GRAPHQL = 'https://api.github.com/graphql'
# https://developer.github.com/v4/object/user/
GRAPHQL_USER = {
'query': 'query { viewer { id login name location websiteUrl } }',
}


class GitHubAuth(BaseAuth):
"""An authenticated GitHub user.
Provides methods that return information about this user and make OAuth-signed
requests to the GitHub REST API. Stores OAuth credentials in the datastore.
See models.BaseAuth for usage details.
GitHub-specific details: implements get() but not urlopen(), http(), or api().
The key name is the username.
"""
access_token_str = ndb.StringProperty(required=True)
user_json = ndb.TextProperty()

def site_name(self):
return 'GitHub'

def user_display_name(self):
"""Returns the user's full name or username.
"""
return self.key.string_id()

def access_token(self):
"""Returns the OAuth access token string.
"""
return self.access_token_str

def get(self, *args, **kwargs):
"""Wraps requests.get() and adds the Bearer token header.
TODO: unify with medium.py.
"""
return self._requests_call(util.requests_get, *args, **kwargs)

def post(self, *args, **kwargs):
"""Wraps requests.post() and adds the Bearer token header.
TODO: unify with medium.py.
"""
return self._requests_call(util.requests_post, *args, **kwargs)

def _requests_call(self, fn, *args, **kwargs):
headers = kwargs.setdefault('headers', {})
headers['Authorization'] = 'Bearer ' + self.access_token_str

resp = fn(*args, **kwargs)
try:
resp.raise_for_status()
except BaseException, e:
util.interpret_http_exception(e)
raise
return resp


class StartHandler(handlers.StartHandler):
"""Starts GitHub auth. Requests an auth code and expects a redirect back.
"""
DEFAULT_SCOPE = ''

def redirect_url(self, state=None):
assert (appengine_config.GITHUB_CLIENT_ID and
appengine_config.GITHUB_CLIENT_SECRET), (
"Please fill in the github_client_id and "
"github_client_secret files in your app's root directory.")
return str(GET_AUTH_CODE_URL % {
'client_id': appengine_config.GITHUB_CLIENT_ID,
'redirect_uri': urllib.quote_plus(self.to_url()),
# TODO: does GitHub require non-empty state?
'state': urllib.quote_plus(state if state else ''),
'scope': self.scope,
})


class CallbackHandler(handlers.CallbackHandler):
"""The OAuth callback. Fetches an access token and stores it.
"""

def get(self):
# handle errors
error = self.request.get('error')
if error:
if error == 'access_denied':
logging.info('User declined')
self.finish(None, state=self.request.get('state'))
return
else:
msg = 'Error: %s: %s' % (error, self.request.get('error_description'))
logging.info(msg)
raise exc.HTTPBadRequest(msg)

# extract auth code and request access token
auth_code = util.get_required_param(self, 'code')
data = {
'code': auth_code,
'client_id': appengine_config.GITHUB_CLIENT_ID,
'client_secret': appengine_config.GITHUB_CLIENT_SECRET,
# redirect_uri here must be the same in the oauth code request!
# (the value here doesn't actually matter since it's requested server side.)
'redirect_uri': self.request.path_url,
}
resp = util.requests_post(GET_ACCESS_TOKEN_URL,
data=urllib.urlencode(data)).text
logging.debug('Access token response: %s', resp)

resp = urlparse.parse_qs(resp)

error = resp.get('error')
if error:
msg = 'Error: %s: %s' % (error[0], resp.get('error_description'))
logging.info(msg)
raise exc.HTTPBadRequest(msg)

access_token = resp['access_token'][0]
resp = GitHubAuth(access_token_str=access_token).post(
API_GRAPHQL, json=GRAPHQL_USER).json()
logging.debug('GraphQL data.viewer response: %s', resp)
user_json = resp['data']['viewer']
auth = GitHubAuth(id=user_json['login'], access_token_str=access_token,
user_json=json.dumps(user_json))
auth.put()

self.finish(auth, state=self.request.get('state'))
2 changes: 1 addition & 1 deletion oauth_dropins/medium.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
https://github.com/Medium/medium-api-docs#contents
https://medium.com/developers/welcome-to-the-medium-api-3418f956552
Medium doesn't let you use a localhost redirect URL. : / A common workaround is
Medium doesn't let you use a localhost redirect URL. :/ A common workaround is
to map an arbitrary host to localhost in your /etc/hosts, e.g.:
127.0.0.1 my.dev.com
Expand Down
Binary file added oauth_dropins/static/github_button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added oauth_dropins/static/github_button_large.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion oauth_dropins/webutil
Submodule webutil updated 2 files
+1 −0 testutil.py
+3 −1 util.py
7 changes: 7 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ <h1>
<input type="image" class="shadow" alt="WordPress.com" src="/static/wordpress_com.png" />
</form></div>
</div>
<br />

<div class="row">
<div class="col-md-3 col-sm-6"><form method="post" action="/github/start">
<input type="image" class="shadow" alt="GitHub" src="/static/github_button.png" />
</form></div>
</div>

</div>
</main>
Expand Down

0 comments on commit b8e6007

Please sign in to comment.