Skip to content

Instantly share code, notes, and snippets.

@minrk
Created March 2, 2022 10:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save minrk/68bf1f2d0162ecf8ce910f500d74f750 to your computer and use it in GitHub Desktop.
Save minrk/68bf1f2d0162ecf8ce910f500d74f750 to your computer and use it in GitHub Desktop.

Using an authenticated service in a custom template

This example embeds an authenticated request to a service into the template for /hub/home. The user may not be logged in to the service when the page is requested, so we must process the oauth login to the service in that event.

To avoid the user having to experience any of the login process, this requires:

  1. attempting to fetch the endpoint
  2. if it fails with a login redirect, send the viewport to the service to start login, with ?next=current page
  3. on completion of oauth with the service, the service redirects back to the originating page to try again
  4. on the next attempt, the page will be able to make requests to the service

Uses jupyterhub.services.HubOAuthenticated to authenticate requests with the Hub.

There is an implementation each of api-token-based HubAuthenticated and OAuth-based HubOAuthenticated.

Run

  1. Launch JupyterHub and the whoami services with

    jupyterhub
    
  2. Visit http://127.0.0.1:8000/hub/home

After logging in with any username and password, you should see a JSON dump of your user info at the bottom of the hub home page:

{
  "admin": false,
  "groups": [],
  "kind": "user",
  "name": "queequeg",
  "scopes": ["access:services!service=whoami-oauth"],
  "session_id": "5a2164273a7346728873bcc2e3c26415"
}

Relevant config

The javascript added to /hub/home:

<pre id="whoami-out"></pre>
<script type="text/javascript">
async function fetchService() {
  const serviceUrl = "{{base_url}}../services/whoami/"
  console.log("fetching", serviceUrl);
  try {
    resp = await fetch(serviceUrl, {redirect: "manual"});
  } catch (e) {
    console.log(e);
    return;
  }
  if (resp.type === "opaqueredirect") {
    // not logged in to service,
    // construct local absolute path URI
    const here = location.pathname + location.search + location.hash;
    // login to service and trust it to redirect back here
    window.location = serviceUrl + "?next=" + encodeURIComponent(here);
    return;
  }
  // logged in, put the service response on the page
  const text = await resp.text();
  document.getElementById("whoami-out").appendChild(document.createTextNode(text));
}
fetchService();
</script>

The service definition:

c.JupyterHub.services = [
    {
        'name': 'whoami',
        'url': 'http://127.0.0.1:10102',
        'command': [sys.executable, './service.py'],
        # oauth_no_confirm skips the confirmation page
        # this is required for the login to the service to be fully transparent
        # note: you are taking away your users' ability to consent to login to this service,
        # so use with caution.
        'oauth_no_confirm': True,
    },
]

Grant users access to services:

c.JupyterHub.load_roles = [
    {
        "name": "user",
        # grant all users access to all services
        "scopes": ["access:services", "self"],
    }
]

Register custom template:

from pathlib import Path

here = Path(__file__).parent
c.JupyterHub.template_paths = [str(here.joinpath("templates"))]
{% extends "templates/home.html" %}
{% block main %}
{{ super() }}
<pre id="whoami-out"></pre>
<script type="text/javascript">
async function fetchService() {
const serviceUrl = "{{base_url}}../services/whoami/"
console.log("fetching", serviceUrl);
try {
resp = await fetch(serviceUrl, {redirect: "manual"});
} catch (e) {
console.log(e);
return;
}
if (resp.type === "opaqueredirect") {
// not logged in to service,
// login and redirect back here
// construct local absolute path URI
const here = location.pathname + location.search + location.hash;
window.location = serviceUrl + "?next=" + encodeURIComponent(here);
return;
}
console.log(resp);
const text = await resp.text();
document.getElementById("whoami-out").appendChild(document.createTextNode(text));
}
fetchService();
</script>
{% endblock main %}
import sys
c = get_config() # noqa
c.JupyterHub.services = [
{
'name': 'whoami',
'url': 'http://127.0.0.1:10102',
'command': [sys.executable, './service.py'],
# oauth_no_confirm skips the confirmation page
# this is required for the login to the service to be fully transparent
# note: you are taking away your users' ability to consent to login to this service,
# so use with caution.
'oauth_no_confirm': True,
},
]
c.JupyterHub.load_roles = [
{
"name": "user",
# grant all users access to all services
"scopes": ["access:services", "self"],
}
]
# register our custom template
from pathlib import Path
here = Path(__file__).parent
c.JupyterHub.template_paths = [str(here.joinpath("templates"))]
# dummy spawner and authenticator for testing, don't actually use these!
c.JupyterHub.authenticator_class = 'dummy'
c.JupyterHub.spawner_class = 'simple'
c.JupyterHub.ip = '127.0.0.1' # let's just run on localhost while dummy auth is enabled
"""An example service authenticating with the Hub.
This example service serves `/services/whoami/`,
authenticated with the Hub,
showing the user their own info.
"""
import json
import os
from urllib.parse import urlparse
from tornado import log
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.web import Application
from tornado.web import authenticated
from tornado.web import RequestHandler
from jupyterhub.services.auth import HubOAuthCallbackHandler
from jupyterhub.services.auth import HubOAuthenticated
from jupyterhub.utils import url_path_join
class WhoAmIHandler(HubOAuthenticated, RequestHandler):
@authenticated
def get(self):
next_url = self.get_argument("next", None)
if next_url:
# if visited with ?next=...
# redirect instead of rendering the current page
# only allow absolute path redirects
# no proto://host, no //host
next_url = "/" + next_url.lstrip("/")
self.redirect(next_url)
return
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'])
http_server.listen(url.port, url.hostname)
IOLoop.current().start()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment