Is there a recommended facility for routing users to services?

We have several JupyterServer services running; they have been configured to authorize access to their respective group of users. The users are able to go directly to their service if they have the service URL; however, I would like provide all users with a single URL and route users to their assigned service after they have authenticated.

The only way I can think of doing it at present is to write a spawner that redirects the user to their assigned service. However, I was wondering if there is a designated routing facility for this. In other words, perhaps there is some way to say in the config file or some other means that userA should get routed to a particular URL for serviceA.

There is JupyterHub.default_url that lets you pick the URL users are sent to by default after login. This can be a fixed string, or a callable that takes the user into account and returns a string.

For example:

def service_url(handler):
    user = handler.current_user
    groups = { for group in user.groups }
    if "group-x" in groups:
        return "/services/x"
        return "/hub/home" # home page, not launching their default server

c.JupyterHub.default_url = service_url

This does not modify things like ‘start my server’. If you want to remove the notion of users having their own servers, that’s going far enough from JupyterHub’s standard behavior that I think it starts to make sense to use api only mode where you present your own UI and only use JupyterHub via the API. This is what Binder does, for instance.


@minrk When I try to get the current user from the handler object (RootHandler) that is passed to service_url, it is set to None. Below I have pasted my service_url function, which prints out various objects that I can get to from within the service_url function and I have pasted the output of those print statements. Do you know where else I could get the current user from within the service_url function?

def service_url(handler):
        async def fn():
                value = await handler.get_current_user()
                print("get_current_user", value)
            except Exception as e:

        io_loop = tornado.ioloop.IOLoop.current()

    except Exception as e:

    service_name = 'shared-notebook'
    url = f'{service_name}/lab'
    return url
{'_auto_finish': True,
 '_finished': False,
 '_headers': <tornado.httputil.HTTPHeaders object at 0x7fa1ca9faf20>,
 '_headers_written': False,
 '_jupyterhub_user': None,
 '_new_cookie': <SimpleCookie: jupyterhub-hub-login=''>,
 '_prepared_future': None,
 '_reason': 'OK',
 '_status_code': 200,
 '_transforms': [],
 '_write_buffer': [],
 'application': <tornado.web.Application object at 0x7fa1cabb9720>,
 'expanded_scopes': set(),
 'parsed_scopes': {},
 'path_args': [],
 'path_kwargs': {},
 'request': HTTPServerRequest(protocol='http', host='', method='GET', uri='/hub/', version='HTTP/1.1', remote_ip=''),
 'ui': {'_tt_modules': <tornado.web._UIModuleNamespace object at 0x7fa1ca9f9db0>,
        'modules': <tornado.web._UIModuleNamespace object at 0x7fa1ca9f9db0>}}
{'default_host': None,
 'default_router': <tornado.web._ApplicationRouter object at 0x7fa1ca9eda80>,
 'settings': {'active_server_limit': 0,
              'activity_resolution': 30,
              'admin_access': False,
              'admin_users': {'adpatter'},
              'allow_named_servers': False,
              'api_page_default_limit': 50,
              'api_page_max_limit': 200,
              'app': < object at 0x7fa1cb99bf10>,
              'authenticate_prometheus': True,
              'authenticator': <oauthenticator.github.GitHubOAuthenticator object at 0x7fa1caabe2c0>,
              'base_url': '/',
              'concurrent_spawn_limit': 100,
              'config': {'Authenticator': {'admin_users': {'adpatter'},
                                           'allowed_users': {'adpatter',
                         'ConfigurableHTTPProxy': {'auth_token': '8af5b4e9ca1b3f845da1298d35a02ed5'},
                         'GitHubOAuthenticator': {'client_id': '519a817b6bb39563b210',
                                                  'client_secret': 'efbbb02526c9d5a9d3f48dfab7d7d8b91225b3b8',
                                                  'oauth_callback_url': ''},
                         'JupyterHub': {'authenticator_class': <class 'oauthenticator.github.GitHubOAuthenticator'>,
                                        'bind_url': '',
                                        'config_file': '/etc/jupyterhub/',
                                        'default_url': <function service_url at 0x7fa1cb8279a0>,
                                        'hub_bind_url': '',
                                        'hub_connect_ip': '',
                                        'load_groups': {'shared-notebook': ['faranalytics',
                                        'load_roles': [{'groups': ['shared-notebook'],
                                                        'name': 'shared-notebook',
                                                        'scopes': ['access:services!service=shared-notebook'],
                                                        'services': ['shared-notebook']}],
                                        'log_level': 10,
                                        'services': [{'api_token': '823249464ad5466196a12703bb8aafd7',
                                                      'name': 'shared-notebook',
                                                      'url': ''}]}},
              'cookie_max_age_days': 14.0,
              'cookie_secret': b'\x85/\xa2\x06\x81]\xa7\xc9\x7f#!\x8a'
              'db': <sqlalchemy.orm.session.Session object at 0x7fa1caeda500>,
              'default_server_name': '',
              'default_url': <function service_url at 0x7fa1cb8279a0>,
              'domain': '',
              'eventlog': <jupyter_telemetry.eventlog.EventLog object at 0x7fa1cb9d52d0>,
              'hub': <Hub>,
              'implicit_spawn_seconds': 0.0,
              'internal_authorities': {'hub-ca': None,
                                       'notebooks-ca': None,
                                       'proxy-api-ca': None,
                                       'proxy-client-ca': None,
                                       'services-ca': None},
              'internal_certs_location': 'internal-ssl',
              'internal_ssl': False,
              'internal_ssl_ca': '',
              'internal_ssl_cert': '',
              'internal_ssl_key': '',
              'internal_trust_bundles': {},
              'jinja2_env': <jinja2.environment.Environment object at 0x7fa1cab98610>,
              'jinja2_env_sync': <jinja2.environment.Environment object at 0x7fa1cab9ba60>,
              'log': <Logger JupyterHub (DEBUG)>,
              'log_function': <function log_request at 0x7fa1cb95e320>,
              'login_url': '/hub/login',
              'logout_url': '/hub/logout',
              'named_server_limit_per_user': 0,
              'oauth_no_confirm_list': set(),
              'oauth_provider': <jupyterhub.oauth.provider.JupyterHubOAuthServer object at 0x7fa1caabd0f0>,
              'proxy': <jupyterhub.proxy.ConfigurableHTTPProxy object at 0x7fa1cac8bfd0>,
              'redirect_to_server': True,
              'services': {'shared-notebook': <Service(name=shared-notebook)>},
              'shutdown_on_logout': False,
              'spawn_throttle_retry_range': (30, 60),
              'spawner_class': <class 'jupyterhub.spawner.LocalProcessSpawner'>,
              'static_handler_class': <class 'jupyterhub.handlers.static.CacheControlStaticFilesHandler'>,
              'static_path': '/usr/local/share/jupyterhub/static',
              'static_url_prefix': '/hub/static/',
              'statsd': <jupyterhub.emptyclass.EmptyClass object at 0x7fa1cab73880>,
              'subdomain_host': '',
              'template_path': ['/usr/local/share/jupyterhub/templates'],
              'template_vars': {},
              'trusted_alt_names': [],
              'users': {},
              'version_hash': '20220817183744'},
 'transforms': [],
 'ui_methods': {},
 'ui_modules': {'Template': <class 'tornado.web.TemplateModule'>,
                'linkify': <class 'tornado.web._linkify'>,
                'xsrf_form_html': <class 'tornado.web._xsrf_form_html'>},
 'wildcard_router': <tornado.web._ApplicationRouter object at 0x7fa1cabb92d0>}
{'_cookies': <SimpleCookie: jupyterhub-hub-login='2|1:0|10:1660761111|20:jupyterhub-hub-login|44:MjEwMmYyZDdlNjNmNGFkZTg0YWU2N2NkNDA2NWUzOTg=|c6bd0e642f83a39fdcf2934d9540aab859beac9d354170eb1f3b1b5bea4a60d9' jupyterhub-session-id='4f2b0f22e6ca4bec9d806d9a108352f6'>,
 '_finish_time': None,
 '_start_time': 1660761472.4006698,
 'arguments': {},
 'body': b'',
 'body_arguments': {},
 'connection': <tornado.http1connection.HTTP1Connection object at 0x7fa1ca9fa650>,
 'files': {},
 'headers': <tornado.httputil.HTTPHeaders object at 0x7fa1ca9fa830>,
 'host': '',
 'host_name': '',
 'method': 'GET',
 'path': '/hub/',
 'protocol': 'http',
 'query': '',
 'query_arguments': {},
 'remote_ip': '',
 'server_connection': <tornado.http1connection.HTTP1ServerConnection object at 0x7fa1ca9fa1a0>,
 'uri': '/hub/',
 'version': 'HTTP/1.1'}
[I 2022-08-17 18:37:52.413 JupyterHub log:189] 302 GET /hub/ -> (@ 12.96ms
get_current_user None

I’ve discovered that c.JupyterHub.default_url actually gets called before the user has authenticated. I guess it makes sense that it wouldn’t contain the user’s information if that is the case.

handler.current_user is a synchronous reference to the current authenticated user, you shouldn’t need to call the async get_current_user.

@minrk The current_user property is also None. I tried calling the asyn function in case it might yield something different. The default_url function is getting called prior to the user completing authentication; I can see that in the logs. We are using the GitHub Authenticator.

Please let me know if you have any ideas.

Thank you.

Try returning handler.base_url if the user is None. I think it will get called again when the user’s authenticated.

handler.base_url is / in our case. If I return handler.base_url if handler.current_user is None, it causes the server to do numerous 302 redirects; it doesn’t get to the authentication page. We are using version 2.3.1.

Sorry, this behavior is a bit confusing and not quite what I expected. default_url is both the unconditional redirect for /hub/ and the default destination from the login page. So if you point it to itself, it’s going to redirect without logging in. Return /hub/login when there’s no user logged in and it should work, sending to your default page after login.