How to correctly access named servers via browser in API mode? (original) (raw)

April 17, 2025, 12:50pm 1

Hey everyone,

I’m looking for a way to painlessly allow users from another service to spawn and access a named server. Ideally, a user already authenticated with my service could just click a link to access their notebook and are authenticated behind the scenes via some sort of token. The topic How to access single user notebook in a web browser in API only mode? is the closest I could find to what I want to achieve. However, following those steps (in a local docker/non-kubernetes setup) I’ve only managed to receive 403 pages so far.

My Hub runs in a docker container in the docker network my-network. Containers also spawn in this network. my-service runs in another container in the same network. I communicate successfully with the Hub’s REST API using a predefined service and API token.

After starting the hub and service I do the following:

  1. Create new user me via POST HUB_URL/hub/api/users/me
    • Can confirm user creation via GET HUB_URL/hub/api/users
  2. Create new named server named via POST HUB_URL/hub/api/users/me/servers/named
    • Can confirm server created and running via GET HUB_URL/hub/api/users and docker ps
  3. Create new API token for me/named via POST HUB_URL/hub/api/users/me/tokens
    • Scopes:
      * access:servers!server=me/named
      * read:users:activity!user=me
      * users:activity!user=me
      * read:users:name!user=me
      * read:users:groups!user=me
  4. Use the token as mentioned in the link above in the browser: HUB_URL/user/me/named/?token={user_api_token}

I always receive 403 Forbidden.

What happens in the browser is that I am redirected several times:

  1. 302 GET HUB_URL/user/me/named/?token=<token_from_step_4>
  2. 302 GET HUB_URL/user/me/named/lab?token=<token_from_step_4>
  3. 302 GET HUB_URL/hub/api/oauth2/authorize?client_id=jupyterhub-user-me-named&redirect_uri=%2Fuser%2Fme%2Fnamed%2Foauth_callback&response_type=code&state=<some_value>
  4. 403 GET HUB_URL/hub/login?next=%2Fhub%2Fapi%2Foauth2%2Fauthorize%3Fclient_id%3Djupyterhub-user-me-named%26redirect_uri%3D%252Fuser%252Fme%252Fnamed%252Foauth_callback%26response_type%3Dcode%26state%3D<some_value>

What happens in the logs is:

2025-04-17 12:13:18 [I 2025-04-17 10:13:18.538 JupyterHub log:192] 302 GET /user/me/named/?token=[secret] -> /hub/user/me/named/?token=[secret] (@::ffff:192.168.65.1) 0.47ms
2025-04-17 12:13:18 [I 2025-04-17 10:13:18.545 JupyterHub log:192] 302 GET /hub/user/me/named/?token=[secret] -> /hub/login?next=%2Fhub%2Fuser%2Fme%2Fnamed%2F%3Ftoken%3D<token_from_step_4> (@::ffff:192.168.65.1) 0.44ms
2025-04-17 12:13:18 [W 2025-04-17 10:13:18.551 JupyterHub base:979] Failed login for unknown user
2025-04-17 12:13:18 [D 2025-04-17 10:13:18.551 JupyterHub base:1542] Using default error template for 403
2025-04-17 12:13:18 [W 2025-04-17 10:13:18.570 JupyterHub log:192] 403 GET /hub/login?next=%2Fhub%2Fuser%2Fme%2Fnamed%2F%3Ftoken%3D<token_from_step_4> (@::ffff:192.168.65.1) 19.54ms

Since the Hub/Authenticator apparently does not know about the user’s existence I’ve tried variations of the config (below) with c.Authenticator.allow_all = True as suggested in the logs. However, the problem remains the same. I also tried the user server variant instead of the named servers, but also to no avail.

jupyterhub_config.py

c = get_config()
c.Application.log_level = 10
c.JupyterHub.allow_named_servers = True
c.JupyterHub.hub_ip = "docker-hub-hostname"
c.JupyterHub.hub_port = 8080
c.JupyterHub.load_roles = [
    {
        "name": "my-service-role",
        "scopes": [
            "admin:users",
            "admin:servers",
            "tokens",
            "shares"
        ],
        "services": ["my-service"]
    }
]
c.JupyterHub.services = [
    {
        "name": "my-service",
        "api_token": "some_api_token"
    }
]
c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
c.Spawner.mem_limit = '8G'
c.DockerSpawner.network_name = 'my-network'
c.DockerSpawner.use_internal_ip = True
c.DockerSpawner.debug = False
c.JupyterHub.authenticator_class = "null"
# c.hub_routespec = '/hub/api/'

(c.hub_routespec deactivated for the moment to make sure that all routes are accessible)

JuypterHub Version Info from the logs

2025-04-17 11:58:35 [I 2025-04-17 09:58:35.412 JupyterHub app:3354] Running JupyterHub version 5.3.0
2025-04-17 11:58:35 [I 2025-04-17 09:58:35.412 JupyterHub app:3384] Using Authenticator: jupyterhub.auth.NullAuthenticator-5.3.0
2025-04-17 11:58:35 [I 2025-04-17 09:58:35.412 JupyterHub app:3384] Using Spawner: dockerspawner.dockerspawner.DockerSpawner-14.0.0
2025-04-17 11:58:35 [I 2025-04-17 09:58:35.412 JupyterHub app:3384] Using Proxy: jupyterhub.proxy.ConfigurableHTTPProxy-5.3.0

Limitations:

Unfortunately, I seem to be at my wit’s (and Google-fu’s) end. Probably I am fundamentally misunderstanding something and actually need to implement a custom authenticator instead of using the Null-Authenticator. However, if that is the case I also have no clue how to approach that for my case. Any advice is greatly appreciated!

minrk April 23, 2025, 8:19am 2

Forcing user login with a token in the URL is disabled by default starting in JupyterHub 5, since it allows users to share fully authenticated links to their own servers, as a potential avenue of XSRF attacks. You can enable it by setting c.HubAuth.allow_token_in_url = True in your single-user environment (e.g. /etc/jupyter/jupyter_server_config.py in a user image or container).

luzhifer April 24, 2025, 7:06am 3

Thank you so much minrk!

Updating the environment variable via c.DockerSpawner.environment.update({"JUPYTERHUB_ALLOW_TOKEN_IN_URL": "1"}) did the trick for me.

It’s crazy that literal days of googling, searching forums and discussing with LLMs did not lead me to Tokens in URLs, Changelog or URL Token parameter now disabled?. Either I messed up big time regarding my search terms or search engine degradation is very real.

Since the token option has been disabled for security reasons:
Are there recommended best practices that I should use instead?

I am not strictly required to authenticate users via token. I just don’t want to force users to interact with two different interfaces to manage servers or log in multiple times.

minrk April 24, 2025, 9:11am 4

I don’t actually have a great answer for this, but we should. The real problem here is that users can get and create API tokens themselves, so they can share links that force other users to login to their server. If, instead, tokens in the URL only accepted tokens you can create (and get consumed so they can’t be re-used), then it would be okay.

Authenticators have the hooks needed to work this way, but the single-user server doesn’t on its own. So a custom Authenticator (not NullAuthenticator) that:

  1. sets auto_login = True
  2. takes a (not JupyterHub) token from a URL parameter
  3. exchanges that for a username with your external service (which consumes the token so it can’t be used twice)

should work for this use case. I’ll see if I can whip up an example.

minrk April 24, 2025, 12:56pm 5

I added an example using an Authenticator that’s almost Null (users can’t try to login), but allows true jupyterhub login via token in URL, but only using tokens issued by your external tool, not any JupyterHub API token.

minrk April 24, 2025, 12:57pm 6

Specifically, this new doc

luzhifer April 24, 2025, 3:58pm 7

Wow, that’s great! It’s amazing how you created such a useful resource for this use case in just a few hours.

Again, thank you so much for this incredibly helpful explanation!

I had a rough idea of what you meant after your reply and had set up the corresponding API endpoints/logic in my service, but I got a little stuck implementing a custom Authenticator for the first time. I’ll try out your implementation first thing tomorrow!

That said, even though I read JupyterHub and OAuth during my research on my issue, your explanation helped me a lot to understand what happens behind the scenes in this specific use case and what I need to set up on both sides when trying to archive a seamless login with an OAuth provider that I run myself. So I might give this variant another shot as well.

I hope that the new doc will soon be available on the official website and that this post will be easy to find on Google until then. 🙂