From 27cb56429b5563d83c3e604e4ad884f651c31d7f Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 1 Mar 2022 09:43:01 +0100 Subject: [PATCH 1/3] HubAuth.get_token returns oauth token stored in cookie Useful for backend services that want to use the user's token. Added `in_cookie` bool argument to exclude cookies (previous behavior), since notebook servers do some things differently when auth is in query param or header vs cookies --- jupyterhub/services/auth.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/jupyterhub/services/auth.py b/jupyterhub/services/auth.py index 9315564d..716dfaac 100644 --- a/jupyterhub/services/auth.py +++ b/jupyterhub/services/auth.py @@ -501,11 +501,17 @@ class HubAuth(SingletonConfigurable): auth_header_name = 'Authorization' auth_header_pat = re.compile(r'(?:token|bearer)\s+(.+)', re.IGNORECASE) - def get_token(self, handler): - """Get the user token from a request + def get_token(self, handler, in_cookie=True): + """Get the token authenticating a request + + .. versionchanged:: 2.2 + in_cookie added. + Previously, only URL params and header were considered. + Pass `in_cookie=False` to preserve that behavior. - in URL parameters: ?token= - in header: Authorization: token + - in cookie (stored after oauth), if in_cookie is True """ user_token = handler.get_argument('token', '') @@ -516,8 +522,14 @@ class HubAuth(SingletonConfigurable): ) if m: user_token = m.group(1) + if not user_token and in_cookie: + user_token = self._get_token_cookie(handler) return user_token + def _get_token_cookie(self, handler): + """Base class doesn't store tokens in cookies""" + return None + def _get_user_cookie(self, handler): """Get the user model from a cookie""" # overridden in HubOAuth to store the access token after oauth @@ -553,8 +565,10 @@ class HubAuth(SingletonConfigurable): handler._cached_hub_user = user_model = None session_id = self.get_session_id(handler) - # check token first - token = self.get_token(handler) + # check token first, ignoring cookies + # because some checks are different when a request + # is token-authenticated (CORS-related) + token = self.get_token(handler, in_cookie=False) if token: user_model = self.user_for_token(token, session_id=session_id) if user_model: @@ -614,11 +628,18 @@ class HubOAuth(HubAuth): """ return self.cookie_name + '-oauth-state' - def _get_user_cookie(self, handler): + def _get_token_cookie(self, handler): + """Base class doesn't store tokens in cookies""" token = handler.get_secure_cookie(self.cookie_name) + if token: + # decode cookie bytes + token = token.decode('ascii', 'replace') + return token + + def _get_user_cookie(self, handler): + token = self._get_token_cookie(handler) session_id = self.get_session_id(handler) if token: - token = token.decode('ascii', 'replace') user_model = self.user_for_token(token, session_id=session_id) if user_model is None: app_log.warning("Token stored in cookie may have expired") From a0b60f911870389d5291356dae9020dccd5d2957 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 1 Mar 2022 09:45:14 +0100 Subject: [PATCH 2/3] place JupyterHub token in JupyterLab PageConfig restores token field useful for javascript-originating API requests, removed in 1.5 / 2.0 for security reasons because it was the wrong token. This places the _user's_ token in PageConfig, so it should have the right permissions. requires jupyterlab_server 2.9, has no effect on earlier versions. --- jupyterhub/singleuser/mixins.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/jupyterhub/singleuser/mixins.py b/jupyterhub/singleuser/mixins.py index 857de247..d2a5ec6a 100755 --- a/jupyterhub/singleuser/mixins.py +++ b/jupyterhub/singleuser/mixins.py @@ -16,7 +16,6 @@ import random import secrets import sys import warnings -from datetime import datetime from datetime import timezone from importlib import import_module from textwrap import dedent @@ -680,6 +679,7 @@ class SingleUserNotebookAppMixin(Configurable): s['hub_prefix'] = self.hub_prefix s['hub_host'] = self.hub_host s['hub_auth'] = self.hub_auth + s['page_config_hook'] = self.page_config_hook csp_report_uri = s['csp_report_uri'] = self.hub_host + url_path_join( self.hub_prefix, 'security/csp-report' ) @@ -707,6 +707,18 @@ class SingleUserNotebookAppMixin(Configurable): self.patch_default_headers() self.patch_templates() + def page_config_hook(self, handler, page_config): + """JupyterLab page config hook + + Adds JupyterHub info to page config. + + Places the JupyterHub API token in PageConfig.token. + + Only has effect on jupyterlab_server >=2.9 + """ + page_config["token"] = self.hub_auth.get_token(handler) or "" + return page_config + def patch_default_headers(self): if hasattr(RequestHandler, '_orig_set_default_headers'): return From 9c498aa5d4dec09758ff4c1c5c43da6074a0d0f1 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 1 Mar 2022 10:03:15 +0100 Subject: [PATCH 3/3] Document HubOAuth.get_token for requests on behalf of users --- docs/source/reference/services.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/source/reference/services.md b/docs/source/reference/services.md index 611ed603..00c8446a 100644 --- a/docs/source/reference/services.md +++ b/docs/source/reference/services.md @@ -246,6 +246,19 @@ action. HubAuth also caches the Hub's response for a number of seconds, configurable by the `cookie_cache_max_age` setting (default: five minutes). +If your service would like to make further requests _on behalf of users_, +it should use the token issued by this OAuth process. +If you are using tornado, +you can access the token authenticating the current request with {meth}`.HubAuth.get_token`. + +:::{versionchanged} 2.2 + +{meth}`.HubAuth.get_token` adds support for retrieving +tokens stored in tornado cookies after completion of OAuth. +Previously, it only retrieved tokens from URL parameters or the Authorization header. +Passing `get_token(handler, in_cookie=False)` preserves this behavior. +::: + ### Flask Example For example, you have a Flask service that returns information about a user.