mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-17 15:03:02 +00:00
use OAuth in single-user server
This commit is contained in:
@@ -45,7 +45,6 @@ class TokenAPIHandler(APIHandler):
|
|||||||
self.write(json.dumps({'token': api_token}))
|
self.write(json.dumps({'token': api_token}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CookieAPIHandler(APIHandler):
|
class CookieAPIHandler(APIHandler):
|
||||||
@token_authenticated
|
@token_authenticated
|
||||||
def get(self, cookie_name, cookie_value=None):
|
def get(self, cookie_name, cookie_value=None):
|
||||||
|
@@ -1167,7 +1167,7 @@ class JupyterHub(Application):
|
|||||||
self.oauth_provider = make_provider(
|
self.oauth_provider = make_provider(
|
||||||
self.session_factory,
|
self.session_factory,
|
||||||
url_prefix=url_path_join(self.hub.server.base_url, 'api/oauth2'),
|
url_prefix=url_path_join(self.hub.server.base_url, 'api/oauth2'),
|
||||||
login_url=self.authenticator.login_url(self.hub.server.base_url)
|
login_url=self.authenticator.login_url(self.hub.server.base_url),
|
||||||
)
|
)
|
||||||
|
|
||||||
client_store = self.oauth_provider.client_authenticator.client_store
|
client_store = self.oauth_provider.client_authenticator.client_store
|
||||||
|
@@ -87,6 +87,10 @@ class BaseHandler(RequestHandler):
|
|||||||
def authenticator(self):
|
def authenticator(self):
|
||||||
return self.settings.get('authenticator', None)
|
return self.settings.get('authenticator', None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def oauth_provider(self):
|
||||||
|
return self.settings['oauth_provider']
|
||||||
|
|
||||||
def finish(self, *args, **kwargs):
|
def finish(self, *args, **kwargs):
|
||||||
"""Roll back any uncommitted transactions from the handler."""
|
"""Roll back any uncommitted transactions from the handler."""
|
||||||
self.db.rollback()
|
self.db.rollback()
|
||||||
|
@@ -7,20 +7,24 @@ HubAuth can be used in any application, even outside tornado.
|
|||||||
HubAuthenticated is a mixin class for tornado handlers that should authenticate with the Hub.
|
HubAuthenticated is a mixin class for tornado handlers that should authenticate with the Hub.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote, urlencode
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from tornado.gen import coroutine
|
||||||
from tornado.log import app_log
|
from tornado.log import app_log
|
||||||
from tornado.web import HTTPError
|
from tornado.httputil import url_concat
|
||||||
|
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
||||||
|
from tornado.web import HTTPError, RequestHandler
|
||||||
|
|
||||||
from traitlets.config import Configurable
|
from traitlets.config import Configurable
|
||||||
from traitlets import Unicode, Integer, Instance, default, observe
|
from traitlets import Unicode, Integer, Instance, default, observe, validate
|
||||||
|
|
||||||
from ..utils import url_path_join
|
from ..utils import url_path_join
|
||||||
|
|
||||||
@@ -103,21 +107,109 @@ class HubAuth(Configurable):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# where is the hub
|
# where is the hub
|
||||||
api_url = Unicode(os.environ.get('JUPYTERHUB_API_URL') or 'http://127.0.0.1:8081/hub/api',
|
api_url = Unicode(os.getenv('JUPYTERHUB_API_URL') or 'http://127.0.0.1:8081/hub/api',
|
||||||
help="""The base API URL of the Hub.
|
help="""The base API URL of the Hub.
|
||||||
|
|
||||||
Typically http://hub-ip:hub-port/hub/api
|
Typically http://hub-ip:hub-port/hub/api
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
login_url = Unicode('/hub/login',
|
host = Unicode('',
|
||||||
help="""The login URL of the Hub
|
help="""The public host of this server.
|
||||||
|
|
||||||
Typically /hub/login
|
Only used if working with subdomains.
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
api_token = Unicode(os.environ.get('JUPYTERHUB_API_TOKEN', ''),
|
base_url = Unicode(os.getenv('JUPYTERHUB_SERVICE_PREFIX') or '/',
|
||||||
|
help="""The base URL prefix of this
|
||||||
|
|
||||||
|
added on to host
|
||||||
|
|
||||||
|
e.g. /services/service-name/ or /user/name/
|
||||||
|
|
||||||
|
Default: get from JUPYTERHUB_SERVICE_PREFIX
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
@validate('base_url')
|
||||||
|
def _add_slash(self, proposal):
|
||||||
|
"""Ensure base_url starts and ends with /"""
|
||||||
|
value = proposal['value']
|
||||||
|
if not value.startswith('/'):
|
||||||
|
value = '/' + value
|
||||||
|
if not value.endswith('/'):
|
||||||
|
value = value + '/'
|
||||||
|
return value
|
||||||
|
|
||||||
|
login_url = Unicode('/hub/login',
|
||||||
|
help="""The login URL of the Hub
|
||||||
|
|
||||||
|
Typically /hub/login
|
||||||
|
|
||||||
|
If using OAuth, will the OAuth redirect URL
|
||||||
|
including client_id, redirect_uri params.
|
||||||
|
"""
|
||||||
|
).tag(config=True)
|
||||||
|
@default('login_url')
|
||||||
|
def _login_url(self):
|
||||||
|
if self.using_oauth:
|
||||||
|
return url_concat(self.oauth_authorization_url, {
|
||||||
|
'client_id': self.oauth_client_id,
|
||||||
|
'redirect_uri': self.oauth_redirect_uri,
|
||||||
|
'response_type': 'code',
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return '/hub/login'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def using_oauth(self):
|
||||||
|
"""Am I using OAuth?"""
|
||||||
|
return bool(self.oauth_client_id)
|
||||||
|
|
||||||
|
oauth_client_id = Unicode(os.getenv('JUPYTERHUB_CLIENT_ID', ''),
|
||||||
|
help="""The OAuth client ID for this application.
|
||||||
|
|
||||||
|
Use JUPYTERHUB_CLIENT_ID by default.
|
||||||
|
"""
|
||||||
|
).tag(config=True)
|
||||||
|
oauth_client_secret = Unicode(os.getenv('JUPYTERHUB_CLIENT_SECRET', ''),
|
||||||
|
help="""The OAuth client secret for this application.
|
||||||
|
|
||||||
|
Use JUPYTERHUB_CLIENT_SECRET by default.
|
||||||
|
"""
|
||||||
|
).tag(config=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def oauth_cookie_name(self):
|
||||||
|
"""Use OAuth client_id for cookie name
|
||||||
|
|
||||||
|
because we don't want to use the same cookie name
|
||||||
|
across OAuth clients.
|
||||||
|
"""
|
||||||
|
return self.oauth_client_id
|
||||||
|
|
||||||
|
oauth_redirect_uri = Unicode(
|
||||||
|
help="""OAuth redirect URI
|
||||||
|
|
||||||
|
Should generally be /base_url/oauth_callback
|
||||||
|
"""
|
||||||
|
).tag(config=True)
|
||||||
|
@default('oauth_redirect_uri')
|
||||||
|
def _default_redirect(self):
|
||||||
|
return self.host + url_path_join(self.base_url, 'oauth_callback')
|
||||||
|
|
||||||
|
oauth_authorization_url = Unicode('/hub/api/oauth2/authorize',
|
||||||
|
help="The URL to redirect to when starting the OAuth process",
|
||||||
|
).tag(config=True)
|
||||||
|
|
||||||
|
oauth_token_url = Unicode(
|
||||||
|
help="""The URL for requesting an OAuth token from JupyterHub"""
|
||||||
|
).tag(config=True)
|
||||||
|
@default('oauth_token_url')
|
||||||
|
def _token_url(self):
|
||||||
|
return url_path_join(self.api_url, 'oauth2/token')
|
||||||
|
|
||||||
|
api_token = Unicode(os.getenv('JUPYTERHUB_API_TOKEN', ''),
|
||||||
help="""API key for accessing Hub API.
|
help="""API key for accessing Hub API.
|
||||||
|
|
||||||
Generate with `jupyterhub token [username]` or add to JupyterHub.services config.
|
Generate with `jupyterhub token [username]` or add to JupyterHub.services config.
|
||||||
@@ -169,12 +261,23 @@ class HubAuth(Configurable):
|
|||||||
if cached is not None:
|
if cached is not None:
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
|
data = self._api_request('GET', url, allow_404=True)
|
||||||
|
if data is None:
|
||||||
|
app_log.warning("No Hub user identified for request")
|
||||||
|
else:
|
||||||
|
app_log.debug("Received request from Hub user %s", data)
|
||||||
|
if use_cache:
|
||||||
|
# cache result
|
||||||
|
self.cache[cache_key] = data
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _api_request(self, method, url, **kwargs):
|
||||||
|
"""Make an API request"""
|
||||||
|
allow_404 = kwargs.pop('allow_404', False)
|
||||||
|
headers = kwargs.setdefault('headers', {})
|
||||||
|
headers.setdefault('Authorization', 'token %s' % self.api_token)
|
||||||
try:
|
try:
|
||||||
r = requests.get(url,
|
r = requests.request(method, url, **kwargs)
|
||||||
headers = {
|
|
||||||
'Authorization' : 'token %s' % self.api_token,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except requests.ConnectionError:
|
except requests.ConnectionError:
|
||||||
msg = "Failed to connect to Hub API at %r." % self.api_url
|
msg = "Failed to connect to Hub API at %r." % self.api_url
|
||||||
msg += " Is the Hub accessible at this URL (from host: %s)?" % socket.gethostname()
|
msg += " Is the Hub accessible at this URL (from host: %s)?" % socket.gethostname()
|
||||||
@@ -184,26 +287,49 @@ class HubAuth(Configurable):
|
|||||||
raise HTTPError(500, msg)
|
raise HTTPError(500, msg)
|
||||||
|
|
||||||
data = None
|
data = None
|
||||||
if r.status_code == 404:
|
if r.status_code == 404 and allow_404:
|
||||||
app_log.warning("No Hub user identified for request")
|
pass
|
||||||
elif r.status_code == 403:
|
elif r.status_code == 403:
|
||||||
app_log.error("I don't have permission to check authorization with JupyterHub, my auth token may have expired: [%i] %s", r.status_code, r.reason)
|
app_log.error("I don't have permission to check authorization with JupyterHub, my auth token may have expired: [%i] %s", r.status_code, r.reason)
|
||||||
|
app_log.error(r.text)
|
||||||
raise HTTPError(500, "Permission failure checking authorization, I may need a new token")
|
raise HTTPError(500, "Permission failure checking authorization, I may need a new token")
|
||||||
elif r.status_code >= 500:
|
elif r.status_code >= 500:
|
||||||
app_log.error("Upstream failure verifying auth token: [%i] %s", r.status_code, r.reason)
|
app_log.error("Upstream failure verifying auth token: [%i] %s", r.status_code, r.reason)
|
||||||
|
app_log.error(r.text)
|
||||||
raise HTTPError(502, "Failed to check authorization (upstream problem)")
|
raise HTTPError(502, "Failed to check authorization (upstream problem)")
|
||||||
elif r.status_code >= 400:
|
elif r.status_code >= 400:
|
||||||
app_log.warning("Failed to check authorization: [%i] %s", r.status_code, r.reason)
|
app_log.warning("Failed to check authorization: [%i] %s", r.status_code, r.reason)
|
||||||
|
app_log.warning(r.text)
|
||||||
raise HTTPError(500, "Failed to check authorization")
|
raise HTTPError(500, "Failed to check authorization")
|
||||||
else:
|
else:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
app_log.debug("Received request from Hub user %s", data)
|
|
||||||
|
|
||||||
if use_cache:
|
|
||||||
# cache result
|
|
||||||
self.cache[cache_key] = data
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def oauth_token_for_code(self, code):
|
||||||
|
"""Get OAuth token for code
|
||||||
|
|
||||||
|
Finish OAuth handshake. Should be called in OAuth Callback handler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: oauth code for finishing OAuth login
|
||||||
|
"""
|
||||||
|
# GitHub specifies a POST request yet requires URL parameters
|
||||||
|
params = dict(
|
||||||
|
client_id=self.oauth_client_id,
|
||||||
|
client_secret=self.oauth_client_secret,
|
||||||
|
grant_type='authorization_code',
|
||||||
|
code=code,
|
||||||
|
redirect_uri=self.oauth_redirect_uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
token_reply = self._api_request('POST', self.oauth_token_url,
|
||||||
|
data=urlencode(params).encode('utf8'),
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
})
|
||||||
|
|
||||||
|
return token_reply['access_token']
|
||||||
|
|
||||||
def user_for_cookie(self, encrypted_cookie, use_cache=True):
|
def user_for_cookie(self, encrypted_cookie, use_cache=True):
|
||||||
"""Ask the Hub to identify the user for a given cookie.
|
"""Ask the Hub to identify the user for a given cookie.
|
||||||
@@ -264,6 +390,24 @@ class HubAuth(Configurable):
|
|||||||
user_token = m.group(1)
|
user_token = m.group(1)
|
||||||
return user_token
|
return user_token
|
||||||
|
|
||||||
|
def set_cookie(self, handler, user_model):
|
||||||
|
"""Set a cookie recording OAuth result"""
|
||||||
|
kwargs = {
|
||||||
|
'path': self.base_url,
|
||||||
|
}
|
||||||
|
if handler.request.protocol == 'https':
|
||||||
|
kwargs['secure'] = True
|
||||||
|
# if self.subdomain_host:
|
||||||
|
# kwargs['domain'] = self.domain
|
||||||
|
cookie_value = json.dumps({'name': user_model['name']})
|
||||||
|
app_log.debug("Setting oauth cookie for %s: %s, %s",
|
||||||
|
handler.request.remote_ip, self.oauth_cookie_name, kwargs)
|
||||||
|
handler.set_secure_cookie(
|
||||||
|
self.oauth_cookie_name,
|
||||||
|
cookie_value,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
def get_user(self, handler):
|
def get_user(self, handler):
|
||||||
"""Get the Hub user for a given tornado handler.
|
"""Get the Hub user for a given tornado handler.
|
||||||
|
|
||||||
@@ -295,9 +439,14 @@ class HubAuth(Configurable):
|
|||||||
|
|
||||||
# no token, check cookie
|
# no token, check cookie
|
||||||
if user_model is None:
|
if user_model is None:
|
||||||
encrypted_cookie = handler.get_cookie(self.cookie_name)
|
if self.using_oauth:
|
||||||
if encrypted_cookie:
|
user_model_json = handler.get_secure_cookie(self.oauth_cookie_name)
|
||||||
user_model = self.user_for_cookie(encrypted_cookie)
|
if user_model_json:
|
||||||
|
user_model = json.loads(user_model_json.decode('utf8', 'replace'))
|
||||||
|
else:
|
||||||
|
encrypted_cookie = handler.get_cookie(self.cookie_name)
|
||||||
|
if encrypted_cookie:
|
||||||
|
user_model = self.user_for_cookie(encrypted_cookie)
|
||||||
|
|
||||||
# cache result
|
# cache result
|
||||||
handler._cached_hub_user = user_model
|
handler._cached_hub_user = user_model
|
||||||
@@ -360,6 +509,7 @@ class HubAuthenticated(object):
|
|||||||
|
|
||||||
def get_login_url(self):
|
def get_login_url(self):
|
||||||
"""Return the Hub's login URL"""
|
"""Return the Hub's login URL"""
|
||||||
|
app_log.debug("Redirecting to login url: %s" % self.hub_auth.login_url)
|
||||||
return self.hub_auth.login_url
|
return self.hub_auth.login_url
|
||||||
|
|
||||||
def check_hub_user(self, model):
|
def check_hub_user(self, model):
|
||||||
@@ -418,3 +568,53 @@ class HubAuthenticated(object):
|
|||||||
self._hub_auth_user_cache = self.check_hub_user(user_model)
|
self._hub_auth_user_cache = self.check_hub_user(user_model)
|
||||||
return self._hub_auth_user_cache
|
return self._hub_auth_user_cache
|
||||||
|
|
||||||
|
|
||||||
|
class JupyterHubOAuthCallbackHandler(HubAuthenticated, RequestHandler):
|
||||||
|
"""OAuth Callback handler"""
|
||||||
|
|
||||||
|
@coroutine
|
||||||
|
def get(self):
|
||||||
|
code = self.get_argument("code", False)
|
||||||
|
if not code:
|
||||||
|
raise HTTPError(400, "oauth callback made without a token")
|
||||||
|
# TODO: make async (in a Thread?)
|
||||||
|
token_reply = self.hub_auth.oauth_token_for_code(code)
|
||||||
|
|
||||||
|
# TODO: Configure the curl_httpclient for tornado
|
||||||
|
|
||||||
|
# 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.hub_auth.oauth_client_id,
|
||||||
|
client_secret=self.hub_auth.oauth_client_secret,
|
||||||
|
code=code
|
||||||
|
)
|
||||||
|
|
||||||
|
url = self.hub_auth.oauth_token_url
|
||||||
|
|
||||||
|
req = HTTPRequest(url,
|
||||||
|
method="POST",
|
||||||
|
headers={"Accept": "application/json"},
|
||||||
|
body=''
|
||||||
|
)
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
"User-Agent": "JupyterHub OAuth Client",
|
||||||
|
"Authorization": "token {}".format(access_token)
|
||||||
|
}
|
||||||
|
req = HTTPRequest(self.hub_auth.oauth_token_url,
|
||||||
|
method="GET",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
resp = yield http_client.fetch(req)
|
||||||
|
user_model = json.loads(resp.body.decode('utf8', 'replace'))
|
||||||
|
self.hub_auth.set_cookie(self, user_model)
|
||||||
|
|
||||||
|
@@ -34,9 +34,10 @@ from notebook.notebookapp import (
|
|||||||
)
|
)
|
||||||
from notebook.auth.login import LoginHandler
|
from notebook.auth.login import LoginHandler
|
||||||
from notebook.auth.logout import LogoutHandler
|
from notebook.auth.logout import LogoutHandler
|
||||||
|
from notebook.base.handlers import IPythonHandler
|
||||||
|
|
||||||
from jupyterhub import __version__
|
from jupyterhub import __version__
|
||||||
from .services.auth import HubAuth, HubAuthenticated
|
from .services.auth import HubAuth, HubAuthenticated, JupyterHubOAuthCallbackHandler
|
||||||
from .utils import url_path_join
|
from .utils import url_path_join
|
||||||
|
|
||||||
|
|
||||||
@@ -96,6 +97,25 @@ class JupyterHubLogoutHandler(LogoutHandler):
|
|||||||
url_path_join(self.settings['hub_prefix'], 'logout'))
|
url_path_join(self.settings['hub_prefix'], 'logout'))
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthCallbackHandler(IPythonHandler):
|
||||||
|
"""Mixin IPythonHandler to get the right error pages, etc."""
|
||||||
|
@property
|
||||||
|
def hub_auth(self):
|
||||||
|
return self.settings['hub_auth']
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
code = self.get_argument("code", False)
|
||||||
|
if not code:
|
||||||
|
raise HTTPError(400, "oauth callback made without a token")
|
||||||
|
# TODO: make async (in a Thread?)
|
||||||
|
token = self.hub_auth.oauth_token_for_code(code)
|
||||||
|
user_model = self.hub_auth.user_for_token(token)
|
||||||
|
self.log.info("Logged-in user %s", user_model)
|
||||||
|
self.hub_auth.set_cookie(self, user_model)
|
||||||
|
next_url = self.get_argument('next', '') or self.base_url
|
||||||
|
self.redirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
# register new hub related command-line aliases
|
# register new hub related command-line aliases
|
||||||
aliases = dict(notebook_aliases)
|
aliases = dict(notebook_aliases)
|
||||||
aliases.update({
|
aliases.update({
|
||||||
@@ -311,10 +331,11 @@ class SingleUserNotebookApp(NotebookApp):
|
|||||||
parent=self,
|
parent=self,
|
||||||
api_token=api_token,
|
api_token=api_token,
|
||||||
api_url=self.hub_api_url,
|
api_url=self.hub_api_url,
|
||||||
|
base_url=self.base_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
def init_webapp(self):
|
def init_webapp(self):
|
||||||
# load the hub related settings into the tornado settings dict
|
# load the hub-related settings into the tornado settings dict
|
||||||
self.init_hub_auth()
|
self.init_hub_auth()
|
||||||
s = self.tornado_settings
|
s = self.tornado_settings
|
||||||
s['user'] = self.user
|
s['user'] = self.user
|
||||||
@@ -322,11 +343,17 @@ class SingleUserNotebookApp(NotebookApp):
|
|||||||
s['hub_prefix'] = self.hub_prefix
|
s['hub_prefix'] = self.hub_prefix
|
||||||
s['hub_host'] = self.hub_host
|
s['hub_host'] = self.hub_host
|
||||||
s['hub_auth'] = self.hub_auth
|
s['hub_auth'] = self.hub_auth
|
||||||
self.hub_auth.login_url = self.hub_host + self.hub_prefix
|
|
||||||
s['csp_report_uri'] = self.hub_host + url_path_join(self.hub_prefix, 'security/csp-report')
|
s['csp_report_uri'] = self.hub_host + url_path_join(self.hub_prefix, 'security/csp-report')
|
||||||
super(SingleUserNotebookApp, self).init_webapp()
|
super(SingleUserNotebookApp, self).init_webapp()
|
||||||
self.patch_templates()
|
|
||||||
|
|
||||||
|
# add OAuth callback
|
||||||
|
self.web_app.add_handlers(r".*$", [(
|
||||||
|
urlparse(self.hub_auth.oauth_redirect_uri).path,
|
||||||
|
OAuthCallbackHandler
|
||||||
|
)])
|
||||||
|
|
||||||
|
self.patch_templates()
|
||||||
|
|
||||||
def patch_templates(self):
|
def patch_templates(self):
|
||||||
"""Patch page templates to add Hub-related buttons"""
|
"""Patch page templates to add Hub-related buttons"""
|
||||||
|
|
||||||
|
@@ -51,6 +51,8 @@ class Spawner(LoggingConfigurable):
|
|||||||
hub = Any()
|
hub = Any()
|
||||||
authenticator = Any()
|
authenticator = Any()
|
||||||
api_token = Unicode()
|
api_token = Unicode()
|
||||||
|
oauth_client_id = Unicode()
|
||||||
|
oauth_client_secret = Unicode()
|
||||||
|
|
||||||
will_resume = Bool(False,
|
will_resume = Bool(False,
|
||||||
help="""Whether the Spawner will resume on next start
|
help="""Whether the Spawner will resume on next start
|
||||||
@@ -391,6 +393,7 @@ class Spawner(LoggingConfigurable):
|
|||||||
Subclasses should call super, to ensure that state is properly cleared.
|
Subclasses should call super, to ensure that state is properly cleared.
|
||||||
"""
|
"""
|
||||||
self.api_token = ''
|
self.api_token = ''
|
||||||
|
self.oauth_client_secret = ''
|
||||||
|
|
||||||
def get_env(self):
|
def get_env(self):
|
||||||
"""Return the environment dict to use for the Spawner.
|
"""Return the environment dict to use for the Spawner.
|
||||||
@@ -425,6 +428,9 @@ class Spawner(LoggingConfigurable):
|
|||||||
env['JUPYTERHUB_API_TOKEN'] = self.api_token
|
env['JUPYTERHUB_API_TOKEN'] = self.api_token
|
||||||
# deprecated (as of 0.7.2), for old versions of singleuser
|
# deprecated (as of 0.7.2), for old versions of singleuser
|
||||||
env['JPY_API_TOKEN'] = self.api_token
|
env['JPY_API_TOKEN'] = self.api_token
|
||||||
|
# OAuth settings
|
||||||
|
env['JUPYTERHUB_CLIENT_ID'] = self.oauth_client_id
|
||||||
|
env['JUPYTERHUB_CLIENT_SECRET'] = self.oauth_client_secret
|
||||||
|
|
||||||
# Put in limit and guarantee info if they exist.
|
# Put in limit and guarantee info if they exist.
|
||||||
# Note that this is for use by the humans / notebook extensions in the
|
# Note that this is for use by the humans / notebook extensions in the
|
||||||
|
@@ -4,12 +4,12 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from urllib.parse import quote, urlparse
|
from urllib.parse import quote, urlparse
|
||||||
|
|
||||||
|
from oauth2.error import ClientNotFoundError
|
||||||
|
from sqlalchemy import inspect
|
||||||
from tornado import gen
|
from tornado import gen
|
||||||
from tornado.log import app_log
|
from tornado.log import app_log
|
||||||
|
|
||||||
from sqlalchemy import inspect
|
from .utils import url_path_join, default_server_name, new_token
|
||||||
|
|
||||||
from .utils import url_path_join, default_server_name
|
|
||||||
|
|
||||||
from . import orm
|
from . import orm
|
||||||
from traitlets import HasTraits, Any, Dict, observe, default
|
from traitlets import HasTraits, Any, Dict, observe, default
|
||||||
@@ -213,7 +213,6 @@ class User(HasTraits):
|
|||||||
url of the server will be /user/:name/:server_name
|
url of the server will be /user/:name/:server_name
|
||||||
"""
|
"""
|
||||||
db = self.db
|
db = self.db
|
||||||
|
|
||||||
if self.allow_named_servers:
|
if self.allow_named_servers:
|
||||||
if options is not None and 'server_name' in options:
|
if options is not None and 'server_name' in options:
|
||||||
server_name = options['server_name']
|
server_name = options['server_name']
|
||||||
@@ -242,7 +241,24 @@ class User(HasTraits):
|
|||||||
spawner.user_options = options or {}
|
spawner.user_options = options or {}
|
||||||
# we are starting a new server, make sure it doesn't restore state
|
# we are starting a new server, make sure it doesn't restore state
|
||||||
spawner.clear_state()
|
spawner.clear_state()
|
||||||
|
|
||||||
|
# create API and OAuth tokens
|
||||||
spawner.api_token = api_token
|
spawner.api_token = api_token
|
||||||
|
spawner.oauth_client_id = client_id = 'user-%s-%s' % (self.escaped_name, server_name)
|
||||||
|
client_store = self.settings['oauth_provider'].client_authenticator.client_store
|
||||||
|
try:
|
||||||
|
oauth_client = client_store.fetch_by_client_id(client_id)
|
||||||
|
except ClientNotFoundError:
|
||||||
|
oauth_client = None
|
||||||
|
# create a new OAuth client + secret on every launch,
|
||||||
|
# except for resuming containers.
|
||||||
|
if oauth_client is None or not spawner.will_resume:
|
||||||
|
spawner.oauth_client_secret = client_secret = new_token()
|
||||||
|
print(server.base_url)
|
||||||
|
client_store.add_client(client_id, client_secret,
|
||||||
|
url_path_join(server.base_url, 'oauth_callback'),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
# trigger pre-spawn hook on authenticator
|
# trigger pre-spawn hook on authenticator
|
||||||
authenticator = self.authenticator
|
authenticator = self.authenticator
|
||||||
|
Reference in New Issue
Block a user