mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 21:43:01 +00:00
add whoami-oauth-basic example
implements oauth without inheriting from HubOAuthenticated should be easier to parse for users with alternate oauth implementations
This commit is contained in:
@@ -5,7 +5,14 @@ for external services that may not be otherwise integrated with JupyterHub.
|
|||||||
The main feature this enables is using JupyterHub like a 'regular' OAuth 2
|
The main feature this enables is using JupyterHub like a 'regular' OAuth 2
|
||||||
provider for services running anywhere.
|
provider for services running anywhere.
|
||||||
|
|
||||||
This example uses `jupyterhub.services.HubOAuthenticated` to authenticate requests with the Hub for a service run on its own host.
|
There are two examples here. `whoami-oauth` uses `jupyterhub.services.HubOAuthenticated`
|
||||||
|
to authenticate requests with the Hub for a service run on its own host.
|
||||||
|
This is an implementation of OAuth 2.0 provided by the jupyterhub package,
|
||||||
|
which configures all of the necessary URLs from environment variables.
|
||||||
|
|
||||||
|
The second is `whoami-oauth-basic`, which implements the full OAuth process
|
||||||
|
without any inheritance, so it can be used as a reference for other OAuth
|
||||||
|
implementations.
|
||||||
|
|
||||||
## Run the example
|
## Run the example
|
||||||
|
|
||||||
@@ -16,6 +23,8 @@ This example uses `jupyterhub.services.HubOAuthenticated` to authenticate reques
|
|||||||
2. launch the whoami service:
|
2. launch the whoami service:
|
||||||
|
|
||||||
bash launch-service.sh &
|
bash launch-service.sh &
|
||||||
|
# or
|
||||||
|
bash launch-service-basic.sh &
|
||||||
|
|
||||||
3. Launch JupyterHub:
|
3. Launch JupyterHub:
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@ if not api_token:
|
|||||||
raise ValueError("Make sure to `export JUPYTERHUB_API_TOKEN=$(openssl rand -hex 32)`")
|
raise ValueError("Make sure to `export JUPYTERHUB_API_TOKEN=$(openssl rand -hex 32)`")
|
||||||
|
|
||||||
# tell JupyterHub to register the service as an external oauth client
|
# tell JupyterHub to register the service as an external oauth client
|
||||||
|
|
||||||
c.JupyterHub.services = [
|
c.JupyterHub.services = [
|
||||||
{
|
{
|
||||||
'name': 'external-oauth',
|
'name': 'external-oauth',
|
||||||
|
19
examples/external-oauth/launch-service-basic.sh
Normal file
19
examples/external-oauth/launch-service-basic.sh
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/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. where the Hub is
|
||||||
|
export JUPYTERHUB_URL='http://127.0.0.1:8000'
|
||||||
|
|
||||||
|
# 4. where to run
|
||||||
|
export JUPYTERHUB_SERVICE_URL='http://127.0.0.1:5555'
|
||||||
|
|
||||||
|
# launch the service
|
||||||
|
exec python3 whoami-oauth-basic.py
|
131
examples/external-oauth/whoami-oauth-basic.py
Normal file
131
examples/external-oauth/whoami-oauth-basic.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"""Basic implementation of OAuth without any inheritance
|
||||||
|
|
||||||
|
Implements OAuth handshake directly
|
||||||
|
so all URLs and requests should be in one place
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
|
from tornado.auth import OAuth2Mixin
|
||||||
|
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
||||||
|
from tornado.httputil import url_concat
|
||||||
|
from tornado.ioloop import IOLoop
|
||||||
|
from tornado import log
|
||||||
|
from tornado import web
|
||||||
|
|
||||||
|
|
||||||
|
class JupyterHubLoginHandler(web.RequestHandler):
|
||||||
|
"""Login Handler
|
||||||
|
|
||||||
|
this handler both begins and ends the OAuth process
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def token_for_code(self, code):
|
||||||
|
"""Complete OAuth by requesting an access token for an oauth code"""
|
||||||
|
params = dict(
|
||||||
|
client_id=self.settings['client_id'],
|
||||||
|
client_secret=self.settings['api_token'],
|
||||||
|
grant_type='authorization_code',
|
||||||
|
code=code,
|
||||||
|
redirect_uri=self.settings['redirect_uri'],
|
||||||
|
)
|
||||||
|
req = HTTPRequest(self.settings['token_url'], method='POST',
|
||||||
|
body=urlencode(params).encode('utf8'),
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response = await AsyncHTTPClient().fetch(req)
|
||||||
|
data = json.loads(response.body.decode('utf8', 'replace'))
|
||||||
|
return data['access_token']
|
||||||
|
|
||||||
|
async def get(self):
|
||||||
|
code = self.get_argument('code', None)
|
||||||
|
if code:
|
||||||
|
# code is set, we are the oauth callback
|
||||||
|
# complete oauth
|
||||||
|
token = await self.token_for_code(code)
|
||||||
|
# login successful, set cookie and redirect back to home
|
||||||
|
self.set_secure_cookie('whoami-oauth-token', token)
|
||||||
|
self.redirect('/')
|
||||||
|
else:
|
||||||
|
# we are the login handler,
|
||||||
|
# begin oauth process which will come back later with an
|
||||||
|
# authorization_code
|
||||||
|
self.redirect(url_concat(
|
||||||
|
self.settings['authorize_url'],
|
||||||
|
dict(
|
||||||
|
redirect_uri=self.settings['redirect_uri'],
|
||||||
|
client_id=self.settings['client_id'],
|
||||||
|
response_type='code',
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class WhoAmIHandler(web.RequestHandler):
|
||||||
|
"""Serve the JSON model for the authenticated user"""
|
||||||
|
|
||||||
|
def get_current_user(self):
|
||||||
|
"""The login handler stored a jupyterhub API token
|
||||||
|
|
||||||
|
in a cookie
|
||||||
|
"""
|
||||||
|
btoken = self.get_secure_cookie('whoami-oauth-token')
|
||||||
|
if btoken:
|
||||||
|
return btoken.decode('ascii')
|
||||||
|
|
||||||
|
async def user_for_token(self, token):
|
||||||
|
"""Retrieve the user for a given token, via /hub/api/user"""
|
||||||
|
|
||||||
|
req = HTTPRequest(
|
||||||
|
self.settings['user_url'],
|
||||||
|
headers={
|
||||||
|
'Authorization': f'token {token}'
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response = await AsyncHTTPClient().fetch(req)
|
||||||
|
return json.loads(response.body.decode('utf8', 'replace'))
|
||||||
|
|
||||||
|
@web.authenticated
|
||||||
|
async def get(self):
|
||||||
|
user_token = self.get_current_user()
|
||||||
|
user_model = await self.user_for_token(user_token)
|
||||||
|
self.set_header('content-type', 'application/json')
|
||||||
|
self.write(json.dumps(user_model, indent=1, sort_keys=True))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log.enable_pretty_logging()
|
||||||
|
|
||||||
|
# construct OAuth URLs from jupyterhub base URL
|
||||||
|
hub_api = os.environ['JUPYTERHUB_URL'].rstrip('/') + '/hub/api'
|
||||||
|
authorize_url = hub_api + '/oauth2/authorize'
|
||||||
|
token_url = hub_api + '/oauth2/token'
|
||||||
|
user_url = hub_api + '/user'
|
||||||
|
|
||||||
|
app = web.Application([
|
||||||
|
('/oauth_callback', JupyterHubLoginHandler),
|
||||||
|
('/', WhoAmIHandler),
|
||||||
|
],
|
||||||
|
login_url='/oauth_callback',
|
||||||
|
cookie_secret=os.urandom(32),
|
||||||
|
api_token=os.environ['JUPYTERHUB_API_TOKEN'],
|
||||||
|
client_id=os.environ['JUPYTERHUB_CLIENT_ID'],
|
||||||
|
redirect_uri=os.environ['JUPYTERHUB_SERVICE_URL'].rstrip('/') + '/oauth_callback',
|
||||||
|
authorize_url=authorize_url,
|
||||||
|
token_url=token_url,
|
||||||
|
user_url=user_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||||
|
log.app_log.info("Running basic whoami service on %s",
|
||||||
|
os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||||
|
app.listen(url.port, url.hostname)
|
||||||
|
IOLoop.current().start()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Reference in New Issue
Block a user