diff --git a/examples/external-oauth/README.md b/examples/external-oauth/README.md new file mode 100644 index 00000000..5fa47c12 --- /dev/null +++ b/examples/external-oauth/README.md @@ -0,0 +1,74 @@ +# Using JupyterHub as an OAuth provider + +JupyterHub 0.9 introduces +Uses `jupyterhub.services.HubAuthenticated` to authenticate requests with the Hub. + +There is an implementation each of cookie-based `HubAuthenticated` and OAuth-based `HubOAuthenticated`. + +## Run + +1. generate an API token: + + export JUPYTERHUB_API_TOKEN=`openssl rand -hex 32` + +2. launch the whoami service: + + bash launch-service.sh & + +3. Launch JupyterHub: + +4. Visit http://127.0.0.1:5555/ + +After logging in with your local-system credentials, you should see a JSON dump of your user info: + +```json +{ + "admin": false, + "last_activity": "2016-05-27T14:05:18.016372", + "name": "queequeg", + "pending": null, + "server": "/user/queequeg" +} +``` + + +The essential pieces for using JupyterHub as an OAuth provider are: + +1. registering your service with jupyterhub: + + ```python + c.JupyterHub.services = [ + { + # the name of your service + # should be simple and unique. + # mostly used to identify your service in logging + "name": "my-service", + # the oauth client id of your service + # must be unique but isn't private + # can be randomly generated or hand-written + "oauth_client_id": "abc123", + # the API token and client secret of the service + # should be generated securely, + # e.g. via `openssl rand -hex 32` + "api_token": "abc123...", + # the redirect target for jupyterhub to send users + # after successful authentication + "oauth_redirect_uri": "https://service-host/oauth_callback" + } + ] + ``` + +2. Telling your service how to authenticate with JupyterHub. + +The relevant OAuth URLs for working with JupyterHub are: + +1. the client_id, used in oauth requests +2. the api token registered with jupyterhub is the client_secret for oauth requests +3. oauth url of the Hub, which is "/hub/api/oauth2/authorize", e.g. `https://myhub.horse/hub/api/oauth2/authorize` +4. a redirect handler to receive the authenticated response + (at `oauth_redirect_uri` registered in jupyterhub config) +5. the token URL for completing the oauth process is "/hub/api/oauth2/token", + e.g. `https://myhub.horse/hub/api/oauth2/token`. + The reply is JSON and the token is in the field `access_token`. +6. Users can be identified by oauth token by making a request to `/hub/api/user` + with the new token in the `Authorization` header. diff --git a/examples/external-oauth/jupyterhub_config.py b/examples/external-oauth/jupyterhub_config.py new file mode 100644 index 00000000..7c5311ea --- /dev/null +++ b/examples/external-oauth/jupyterhub_config.py @@ -0,0 +1,17 @@ +import os + +# get the oauth client's API token. +# this could come from anywhere +api_token = os.getenv("JUPYTERHUB_API_TOKEN") +if not api_token: + raise ValueError("Make sure to `export JUPYTERHUB_API_TOKEN=$(openssl rand -hex 32)`") + +# tell JupyterHub to register the service as an external oauth client +c.JupyterHub.services = [ + { + 'name': 'external-oauth', + 'oauth_client_id': "whoami-oauth-client-test", + 'api_token': api_token, + 'oauth_redirect_uri': 'http://127.0.0.1:5555/oauth_callback', + }, +] diff --git a/examples/external-oauth/launch-service.sh b/examples/external-oauth/launch-service.sh new file mode 100644 index 00000000..6edde66d --- /dev/null +++ b/examples/external-oauth/launch-service.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# the service needs to know: +# 1. API token +if [[ -z "${JUPYTERHUB_API_TOKEN}" ]]; then + echo 'set API token with export JUPYTERHUB_API_TOKEN=$(openssl rand -hex 32)' +fi + +# 2. oauth client ID +export JUPYTERHUB_CLIENT_ID="whoami-oauth-client-test" +# 3. what URL to run on +export JUPYTERHUB_SERVICE_PREFIX='/' +export JUPYTERHUB_SERVICE_URL='http://127.0.0.1:5555' +export JUPYTERHUB_OAUTH_CALLBACK_URL="$JUPYTERHUB_SERVICE_URL/oauth_callback" +# 4. where the Hub is +export JUPYTERHUB_HOST='http://127.0.0.1:8000' + +# launch the service +exec python3 whoami-oauth.py diff --git a/examples/external-oauth/whoami-oauth.py b/examples/external-oauth/whoami-oauth.py new file mode 100644 index 00000000..8d26dc4d --- /dev/null +++ b/examples/external-oauth/whoami-oauth.py @@ -0,0 +1,46 @@ +"""An example service authenticating with the Hub. + +This example service serves `/services/whoami/`, +authenticated with the Hub, +showing the user their own info. +""" +from getpass import getuser +import json +import os +from urllib.parse import urlparse + +from tornado.ioloop import IOLoop +from tornado import log +from tornado.httpserver import HTTPServer +from tornado.web import RequestHandler, Application, authenticated + +from jupyterhub.services.auth import HubOAuthenticated, HubOAuthCallbackHandler +from jupyterhub.utils import url_path_join + +class WhoAmIHandler(HubOAuthenticated, RequestHandler): + hub_users = {getuser()} # the users allowed to access this service + + @authenticated + def get(self): + user_model = self.get_current_user() + self.set_header('content-type', 'application/json') + self.write(json.dumps(user_model, indent=1, sort_keys=True)) + +def main(): + log.enable_pretty_logging() + app = Application([ + (os.environ['JUPYTERHUB_SERVICE_PREFIX'], WhoAmIHandler), + (url_path_join(os.environ['JUPYTERHUB_SERVICE_PREFIX'], 'oauth_callback'), HubOAuthCallbackHandler), + (r'.*', WhoAmIHandler), + ], cookie_secret=os.urandom(32)) + + http_server = HTTPServer(app) + url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL']) + log.app_log.info("Running whoami service on %s", os.environ['JUPYTERHUB_SERVICE_URL']) + + http_server.listen(url.port, url.hostname) + + IOLoop.current().start() + +if __name__ == '__main__': + main() diff --git a/examples/external-oauth/whoami.png b/examples/external-oauth/whoami.png new file mode 100644 index 00000000..b7a33584 Binary files /dev/null and b/examples/external-oauth/whoami.png differ