mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-11 12:03:00 +00:00
support oauth in services
fix bugs caught by tests
This commit is contained in:
@@ -1041,6 +1041,7 @@ class JupyterHub(Application):
|
||||
host = '%s://services.%s' % (parsed.scheme, parsed.netloc)
|
||||
else:
|
||||
domain = host = ''
|
||||
client_store = self.oauth_provider.client_authenticator.client_store
|
||||
for spec in self.services:
|
||||
if 'name' not in spec:
|
||||
raise ValueError('service spec must have a name: %r' % spec)
|
||||
@@ -1082,6 +1083,12 @@ class JupyterHub(Application):
|
||||
base_url=service.prefix,
|
||||
)
|
||||
self.db.add(server)
|
||||
|
||||
client_store.add_client(
|
||||
client_id=service.oauth_client_id,
|
||||
client_secret=service.oauth_client_secret,
|
||||
redirect_uri=host + url_path_join(service.prefix, 'oauth_callback'),
|
||||
)
|
||||
else:
|
||||
service.orm.server = None
|
||||
|
||||
@@ -1378,11 +1385,11 @@ class JupyterHub(Application):
|
||||
self.init_db()
|
||||
self.init_hub()
|
||||
self.init_proxy()
|
||||
self.init_oauth()
|
||||
yield self.init_users()
|
||||
yield self.init_groups()
|
||||
self.init_services()
|
||||
yield self.init_api_tokens()
|
||||
self.init_oauth()
|
||||
self.init_tornado_settings()
|
||||
yield self.init_spawners()
|
||||
self.init_handlers()
|
||||
|
@@ -616,6 +616,7 @@ class APIToken(Base):
|
||||
|
||||
|
||||
class GrantType(enum.Enum):
|
||||
# we only use authorization_code for now
|
||||
authorization_code = 'authorization_code'
|
||||
implicit = 'implicit'
|
||||
password = 'password'
|
||||
@@ -656,7 +657,6 @@ class OAuthClient(Base):
|
||||
redirect_uri = Column(Unicode(1023))
|
||||
|
||||
|
||||
|
||||
def new_session_factory(url="sqlite:///:memory:", reset=False, **kwargs):
|
||||
"""Create a new session at url"""
|
||||
if url.startswith('sqlite'):
|
||||
|
@@ -643,11 +643,11 @@ class JupyterHubOAuthCallbackHandler(HubOAuthenticated, RequestHandler):
|
||||
if not code:
|
||||
raise HTTPError(400, "oauth callback made without a token")
|
||||
# TODO: make async (in a Thread?)
|
||||
token_reply = self.hub_auth.token_for_code(code)
|
||||
token = self.hub_auth.token_for_code(code)
|
||||
user_model = self.hub_auth.user_for_token(token)
|
||||
self.log.info("Logged-in user %s", user_model)
|
||||
app_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
|
||||
next_url = self.get_argument('next', '') or self.hub_auth.base_url
|
||||
self.redirect(next_url)
|
||||
|
||||
|
||||
|
@@ -57,7 +57,7 @@ from traitlets.config import LoggingConfigurable
|
||||
from .. import orm
|
||||
from ..traitlets import Command
|
||||
from ..spawner import LocalProcessSpawner, set_user_setuid
|
||||
from ..utils import url_path_join
|
||||
from ..utils import url_path_join, new_token
|
||||
|
||||
class _MockUser(HasTraits):
|
||||
name = Unicode()
|
||||
@@ -198,6 +198,30 @@ class Service(LoggingConfigurable):
|
||||
db = Any()
|
||||
orm = Any()
|
||||
|
||||
oauth_provider = Any()
|
||||
|
||||
oauth_client_id = Unicode(
|
||||
help="""OAuth client ID for this service.
|
||||
|
||||
You shouldn't generally need to change this.
|
||||
Default: `service-<name>`
|
||||
"""
|
||||
).tag(input=True)
|
||||
@default('oauth_client_id')
|
||||
def _default_client_id(self):
|
||||
return 'service-%s' % self.name
|
||||
|
||||
oauth_client_secret = Unicode(
|
||||
help="""OAuth client secret for this service.
|
||||
|
||||
Default: Generated on each launch.
|
||||
"""
|
||||
).tag(input=True)
|
||||
@default('oauth_client_secret')
|
||||
def _default_client_secret(self):
|
||||
self.log.debug("Generating new OAuth secret for service %s", self.name)
|
||||
return new_token()
|
||||
|
||||
@property
|
||||
def server(self):
|
||||
return self.orm.server
|
||||
@@ -242,6 +266,8 @@ class Service(LoggingConfigurable):
|
||||
cmd=self.command,
|
||||
environment=env,
|
||||
api_token=self.api_token,
|
||||
oauth_client_id=self.oauth_client_id,
|
||||
oauth_client_secret=self.oauth_client_secret,
|
||||
cwd=self.cwd,
|
||||
user=_MockUser(
|
||||
name=self.user,
|
||||
|
@@ -11,13 +11,15 @@ Handlers allow:
|
||||
"""
|
||||
|
||||
import json
|
||||
import pprint
|
||||
import os
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from tornado import web, httpserver, ioloop
|
||||
|
||||
from jupyterhub.services.auth import HubAuthenticated
|
||||
from jupyterhub.services.auth import HubAuthenticated, HubOAuthenticated, JupyterHubOAuthCallbackHandler
|
||||
|
||||
|
||||
class EchoHandler(web.RequestHandler):
|
||||
@@ -47,21 +49,37 @@ class APIHandler(web.RequestHandler):
|
||||
|
||||
|
||||
class WhoAmIHandler(HubAuthenticated, web.RequestHandler):
|
||||
"""Reply with the name of the user who made the request."""
|
||||
"""Reply with the name of the user who made the request.
|
||||
|
||||
Uses deprecated cookie login
|
||||
"""
|
||||
@web.authenticated
|
||||
def get(self):
|
||||
self.write(self.get_current_user())
|
||||
|
||||
class OWhoAmIHandler(HubOAuthenticated, web.RequestHandler):
|
||||
"""Reply with the name of the user who made the request.
|
||||
|
||||
Uses OAuth login flow
|
||||
"""
|
||||
@web.authenticated
|
||||
def get(self):
|
||||
self.write(self.get_current_user())
|
||||
|
||||
|
||||
def main():
|
||||
pprint.pprint(dict(os.environ), stream=sys.stderr)
|
||||
|
||||
if os.environ['JUPYTERHUB_SERVICE_URL']:
|
||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||
app = web.Application([
|
||||
(r'.*/env', EnvHandler),
|
||||
(r'.*/api/(.*)', APIHandler),
|
||||
(r'.*/whoami/?', WhoAmIHandler),
|
||||
(r'.*/owhoami/?', OWhoAmIHandler),
|
||||
(r'.*/oauth_callback', JupyterHubOAuthCallbackHandler),
|
||||
(r'.*', EchoHandler),
|
||||
])
|
||||
], cookie_secret=os.urandom(32))
|
||||
|
||||
server = httpserver.HTTPServer(app)
|
||||
server.listen(url.port, url.hostname)
|
||||
@@ -70,6 +88,7 @@ def main():
|
||||
except KeyboardInterrupt:
|
||||
print('\nInterrupted')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from tornado.options import parse_command_line
|
||||
parse_command_line()
|
||||
|
@@ -18,7 +18,7 @@ from tornado.web import RequestHandler, Application, authenticated, HTTPError
|
||||
|
||||
from ..services.auth import _ExpiringDict, HubAuth, HubAuthenticated
|
||||
from ..utils import url_path_join
|
||||
from .mocking import public_url
|
||||
from .mocking import public_url, public_host
|
||||
from .test_api import add_user
|
||||
|
||||
# mock for sending monotonic counter way into the future
|
||||
@@ -244,7 +244,6 @@ def test_hubauth_token(app, mockservice_url):
|
||||
headers={
|
||||
'Authorization': 'token %s' % token,
|
||||
})
|
||||
r.raise_for_status()
|
||||
reply = r.json()
|
||||
sub_reply = { key: reply.get(key, 'missing') for key in ['name', 'admin']}
|
||||
assert sub_reply == {
|
||||
@@ -312,3 +311,23 @@ def test_hubauth_service_token(app, mockservice_url, io_loop):
|
||||
path = urlparse(location).path
|
||||
assert path.endswith('/hub/login')
|
||||
|
||||
|
||||
def test_oauth_service(app, mockservice_url):
|
||||
url = url_path_join(public_url(app, mockservice_url) + 'owhoami/')
|
||||
# first request is only going to set login cookie
|
||||
# FIXME: redirect to originating URL (OAuth loses this info)
|
||||
s = requests.Session()
|
||||
s.cookies = app.login_user('link')
|
||||
r = s.get(url)
|
||||
r.raise_for_status()
|
||||
# second request should be authenticated
|
||||
r = s.get(url, allow_redirects=False)
|
||||
r.raise_for_status()
|
||||
assert r.status_code == 200
|
||||
reply = r.json()
|
||||
sub_reply = { key:reply.get(key, 'missing') for key in ('kind', 'name') }
|
||||
assert sub_reply == {
|
||||
'name': 'link',
|
||||
'kind': 'user',
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user