Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
adapt OAuth toy into JupyterHub Authenticator
- Loading branch information
Showing
3 changed files
with
128 additions
and
130 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
""" | ||
Custom Authenticator to use GitHub OAuth with JupyterHub | ||
Most of the code c/o Kyle Kelley (@rgbkrk) | ||
""" | ||
|
||
|
||
import json | ||
import os | ||
|
||
from tornado.auth import OAuth2Mixin | ||
from tornado import gen, web | ||
|
||
from tornado.httputil import url_concat | ||
from tornado.httpclient import HTTPRequest, AsyncHTTPClient | ||
|
||
from jupyterhub.handlers import BaseHandler | ||
from jupyterhub.auth import Authenticator | ||
from jupyterhub.utils import url_path_join | ||
|
||
from IPython.utils.traitlets import Unicode | ||
|
||
class GitHubMixin(OAuth2Mixin): | ||
_OAUTH_AUTHORIZE_URL = "https://github.com/login/oauth/authorize" | ||
_OAUTH_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token" | ||
|
||
|
||
class GitHubLoginHandler(BaseHandler, GitHubMixin): | ||
def get(self): | ||
redirect_uri='{proto}://{host}{path}'.format( | ||
proto=self.request.protocol, | ||
host=self.request.host, | ||
path=url_path_join( | ||
self.hub.server.base_url, | ||
'oauth_callback' | ||
), | ||
) | ||
self.log.warn('oauth redirect: %r', redirect_uri) | ||
|
||
self.authorize_redirect( | ||
redirect_uri=redirect_uri, | ||
client_id=self.authenticator.github_client_id, | ||
scope=[], | ||
response_type='code') | ||
|
||
|
||
class GitHubOAuthHandler(BaseHandler): | ||
@gen.coroutine | ||
def get(self): | ||
# TODO: Check if state argument needs to be checked | ||
username = yield self.authenticator.authenticate(self) | ||
if username: | ||
user = self.user_from_username(username) | ||
self.set_login_cookie(user) | ||
self.redirect(url_path_join(self.hub.server.base_url, 'home')) | ||
else: | ||
# todo: custom error page? | ||
raise web.HTTPError(403) | ||
|
||
|
||
class GitHubOAuthenticator(Authenticator): | ||
|
||
github_client_id = Unicode(os.environ.get('GITHUB_CLIENT_ID', ''), | ||
config=True) | ||
github_client_secret = Unicode(os.environ.get('GITHUB_CLIENT_SECRET', ''), | ||
config=True) | ||
|
||
def login_url(self, base_url): | ||
return url_path_join(base_url, 'oauth_login') | ||
|
||
def get_handlers(self, app): | ||
return [ | ||
(r'/oauth_login', GitHubLoginHandler), | ||
(r'/oauth_callback', GitHubOAuthHandler), | ||
] | ||
|
||
@gen.coroutine | ||
def authenticate(self, handler): | ||
code = handler.get_argument("code", False) | ||
if not code: | ||
raise web.HTTPError(400, "oauth callback made without a token") | ||
# TODO: Configure the curl_httpclient for tornado | ||
http_client = AsyncHTTPClient() | ||
|
||
# Exchange the OAuth code for a GitHub Access Token | ||
# | ||
# See: https://developer.github.com/v3/oauth/ | ||
|
||
# GitHub specifies a POST request yet requires URL parameters | ||
params = dict( | ||
client_id=self.github_client_id, | ||
client_secret=self.github_client_secret, | ||
code=code | ||
) | ||
|
||
url = url_concat("https://github.com/login/oauth/access_token", | ||
params) | ||
|
||
req = HTTPRequest(url, | ||
method="POST", | ||
headers={"Accept": "application/json"}, | ||
body='' # Body is required for a POST... | ||
) | ||
|
||
resp = yield http_client.fetch(req) | ||
resp_json = json.loads(resp.body.decode('utf8', 'replace')) | ||
|
||
access_token = resp_json['access_token'] | ||
|
||
# Determine who the logged in user is | ||
headers={"Accept": "application/json", | ||
"User-Agent": "JupyterHub", | ||
"Authorization": "token {}".format(access_token) | ||
} | ||
req = HTTPRequest("https://api.github.com/user", | ||
method="GET", | ||
headers=headers | ||
) | ||
resp = yield http_client.fetch(req) | ||
resp_json = json.loads(resp.body.decode('utf8', 'replace')) | ||
|
||
username = resp_json["login"] | ||
if self.whitelist and username not in self.whitelist: | ||
username = None | ||
raise gen.Return(username) | ||
|
||
|