Making API requests to hub from browser from services

Hello,

I have the following scenario with the latest JupyterHub (4.1.5):

We created a JupyterHub service server manager that is based on JupyterHub Admin page. Essentially the service is a stripped down Admin page that lets our support team to access user’s servers when our users need help. The service renders the list of running servers using the React app like for the admin page (although lot of components from the original app are removed).

With the new reinforced auth flow, when users login into hub, there is a XSRF cookie set with path /hub/. And when users access the service, a new XSRF cookie is set with path /service/server-manager/, assuming server-manager is the name of the service. Just like in the admin page, the React App is making API requests to hub to get list of servers, users, etc.

The problem comes here: the browser is sending the _xsrf cookie for /hub/ as the requested API is /hub/api/users but the service has a different _xsrf token set and hence, we end up with following:

[W 2024-04-11 15:01:16.172 JupyterHub web:1873] 403 GET /hub/api/users?include_stopped_servers&offset=0&limit=10&name_filter=&_xsrf=[secret] (::ffff:127.0.0.1): XSRF cookie does not match GET argument

At least this what I understood. Please let me know if that is not the case.

My question is if it is possible to make API requests to hub from browser from within a service? Or is there a better way to solve this issue. It was working with the previous versions of JupyterHub which I guess due to usage of single _xsrf token everywhere?!

Cheers!

1 Like

Yes, all requests from a service or single-user server should use the API token issued during oauth in the Authorization header, whether originating from a browser or the backend. XSRF checks only apply to cookie-authenticated requests.

If you are using HubOAuthenticated, the token for each request is available as:

token = self.hub_auth.get_token(self)

Then you can make this token available to the page. JupyterLab, for example, creates a jupyter-config-data tag in the header, and then javascript extracts the token and other config from this tag and adds it to requests.

1 Like

Yeah, that is an elegant way to do it. Awesome. Cheers @minrk for the hint and the references. Appreciate it!

@minrk What are the default scopes that the API token issued in OAuth will have? In my example, we want the service to be able to make requests to hub API. Let’s do a simple example, I want my service to list users of hub and the service should be accessible for user foo. So, I have following role defined:

c.JupyterHub.load_roles = [
    {
      'name': 'server-manager-role',
      'users': ['foo'],
      'scopes': [
           # list all users
           "list:users",
           "access:services!service=server-manager",
      ],
    },
]

So, when I make the API requests to /hub/api/users using the token from self.hub_auth.get_token(self), I get following warning:

[W 2024-04-17 15:13:39.249 JupyterHub scopes:888] Not authorizing access to /hub/api/users. Requires any of [list:users], not derived from scopes [read:users:name!user=foo, read:users:groups!user=foo, access:services!service=server-manager]

I can verify these scopes by making a request to /hub/api/user using the same token and I get following response

{"kind": "user", "name": "foo", "admin": false, "groups": [], "session_id": "da274b0878714766b3e7f5877bbfce10", "scopes": ["access:services!service=server-manager", "read:users:groups!user=foo", "read:users:name!user=foo"]}

When I make the request without this token but with cookie, I get a very different response with all the scopes

{"name": "foo", "groups": [], "roles": ["user", "server-manager-role"], "server": null, "created": "2024-04-17T12:47:29.154483Z", "last_activity": "2024-04-17T13:23:39.365620Z", "pending": null, "admin": false, "kind": "user", "servers": {}, "session_id": null, "scopes": ["access:servers!user=foo", "access:services!service=server-manager", "delete:servers!user=foo", "list:users", "read:servers!user=foo", "read:tokens!user=foo", "read:users!user=foo", "read:users:activity!user=foo", "read:users:groups!user=foo", "read:users:name", "servers!user=foo", "tokens!user=foo", "users:activity!user=foo"]}

So from my understanding, the token is not inheriting the scopes of the user. Even when I add service server-manager to the role, I get the same behaviour.

Is there any way that I can ensure that the token issued from OAuth will have scopes from user roles? Cheers!!

I think I figured it out looking at source and docs. What I did is add the inherit scope to the oauth_client_allowed_scopes for the service. This way the OAuth tokens will inherit all user scopes. For the reference, this is my service def

c.JupyterHub.services = [
    {
        'name': 'server-manager',
        'url': 'http://localhost:8012',
        'api_token': 'somsecret',
        'oauth_no_confirm': True,
        'oauth_client_allowed_scopes': [
            'inherit'
        ],
    }
]

@minrk Is it a correct way to do it or am I doing something not very secure?

Correct, and it shouldn’t. It will get the requested scopes, which are a subset of the scopes held by the user. This allows tokens to only have access to what they need.

‘inherit’ is the way to specify that tokens should have the absolute maximum permissions, if that’s what you want. It’s the easiest way to make sure you have the permissions you need without looking up what permissions you are actually going to use.

It’s a fine place to start, but it would be more prudent to specify the scopes you will actually use, instead.

1 Like