Disable authorize access page (hub/api/oauth2/authorize)

When connecting to a server for the first time, users must go through an “authorise access page”:

This is presumably authorizing the authenticator to see your credentials.

I’m not sure this page makes sense for PAM setups. Is there a way to disable it?

This happens only when you are either accessing an external service or admin (or users with enough privileges) access other user’s servers. This does not happen when you spawn a regular JupyterLab/notebook instances.

When do you get this dialog?

1 Like

When do you get this dialog?

When accessing other user’s servers.


Some info on the use case:

I have a Jupyter Hub deployment where users are able to access each other’s servers.

This is intended to:

  • Open up Jupyter Hub use for “shared” user accounts (accounts that users of a group have sudo permissions for).
  • Allow restricted access for regular users to each other’s servers.

The authorize page is a bit of an encumbrance where cross-user activity is common.

We’re using the PAM authenticator (which already knows user credentials) so we are authorizing the authenticator to see information that it already possesses.

1 Like

This is more of a OAuth2 thing than PAM. As accessing other users servers is equivalent to letting a third-party app to access the user data, the user needs to perform this authorization action.

For external services, you can skip it using oauth_no_cofirm config parameter in service definition. But I am not sure if it is configurable for the single user servers spawned by JupyterHub.

1 Like

Thanks for pointing me in the right direction.

The oauth_no_confirm config doesn’t appear to be intended to disable this authorization step for single-user servers as you suggested. Although, I think you can use if for this purpose if you generate an entry in oauth_no_cofirm_list for every user on the hub, although there is no configuration to do this and you will get a warning in the log for each user on hub startup.

Overriding the needs_oauth_confirm method bypasses the configuration allowing us to turn this feature off completely, but requires a shim to JupyterHub.init_handlers to work:

import jupyterhub.app
import jupyterhub.apihandlers.auth


class DisableConfirmationOAuthAuthorizeHandler(
    jupyterhub.apihandlers.auth.OAuthAuthorizeHandler
):

    def needs_oauth_confirm(self, *args, **kwargs):
        # never show the authorize page (i.e. implicitly authorize all
        # authenticators to see user's credentials)
        return False


class DisableConfirmationJupyterHub(jupyterhub.app.JupyterHub):

    def init_handlers(self):
        # modify the authorize handler to use the patched class
        jupyterhub.app.apihandlers.default_handlers = [
            (
                route,
                (
                    DisableConfirmationOAuthAuthorizeHandler
                    if route == '/api/oauth2/authorize'
                    else handler
                ),
            )
            for route, handler in jupyterhub.app.apihandlers.default_handlers
        ]
        super().init_handlers()


jupyterhub.app.JupyterHub = DisableConfirmationJupyterHub

I understand why this page is there, it just doesn’t make much sense for my particular deployment and becomes a bit of an encumbrance as this authorization has to be performed for every server you connect to and cross-user activity is very common for our use case.

Would be happy to contribute a disable_oauth_confirm configuration if this is a feature the maintainers would be happy to accept.

1 Like

I’d be open to a global oauth_no_confirm flag, if you want to make a PR, thanks for offering! I wouldn’t necessarily recommend it since it means users can share links that will grant themselves impersonation permissions without any confirmation from their target user beyond clicking a link, but I know every deployment is different, and there isn’t always anything sensitive accessible with those credentials (in the default config, these credentials can’t do much for a reason).

Was anything like a global oauth_no_confirm flag ever added?

I am wondering what the recommended way is to use the realtime collaboration extension with JupyterHub, without running into the ‘Authorize access’ page. I suppose there could be a shared notebook running as a service, with oauth_no_confirm set to true?

Was anything like a global oauth_no_confirm flag ever added?

I didn’t get around to adding this feature, our users just got used to pressing the authorize button so it drifted onto my rainy day list.

what the recommended way is to use the realtime collaboration extension with JupyterHub, without running into the ‘Authorize access’ page

The recommended way is for users to go through the authorize step :frowning:

In the context of external authentication providers, I think this makes good sense. You are giving data to a third party service, this should not be allowed without your consent! However, I’m not sure whether this makes sense in the context of PAM authentication where the authentication service is internal and already holds this info. I wonder if it would make sense to turn off this authorisation step by default for setups “where PAM authentication is configured” or should that be “where OAUTH is not configured”?

This authorisation step happens every time you connect to another user’s server for the first time. I’m not sure but that might be overkill, perhaps we could get away with performing this once per authentication service rather than once per user server?

oauth_no_confirm has been added. It cannot be disabled when accessing another user’s server, though, only for services.

The oauth confirmation does not really have to do with the external provider, it has to do with internal JupyterHub authentication (which may or may not have implications beyond the boundaries of JupyterHub, depending on what permissions are requested). When you visit another user’s server, an oauth token is issued, which has permission to take certain actions as you. You are entrusting that user’s server with your credentials, which warrants confirmation. Now, most of the time, those credentials can’t be used for much other than identifying the user and talking to the server itself.

I’m happy to hear suggestions for ways to streamline this process, but I don’t think that generally we should disable a confirmation step before passing one user’s credentials to something controlled by another user. With RTC around, I do think we should make it as not-annoying as possible, especially when little permissions is granted, as in the default case.

Yes, so it should really only show up once per user+user combination, so I don’t think it should be too annoying. It’s a “first-time trust” confirmation that should last. I’m not sure that’s quite the case, as it may be that this “already authorized” information gets cleared when the authorized server. If true, that’s something we can work on improving.

1 Like

Thanks for this explanation, I now get why this is a related to the server not the authenticator. I do think PAM is a bit special though, because the information it holds on a user has already been provided to the system and may be openly accessible to processes running on it (e.g via getent passwd).

You are entrusting that user’s server with your credentials, which warrants confirmation.

Makes perfect sense.

In my setup, there is a lot of cross-user access, so the number of user-user pairs is quite high. They also have to repeat this authorisation step for each user-user pair every time the server is restarted, which is inconvenient for them. It’s a bit frustrating from their perspective as authorising one user’s server is much the same as far as they are concerned (they’re always going to press the “Authorize” button blindly).

In this setup, all servers are spawned locally and auth is provided via PAM (i.e, all authenticated users have accounts on the system). The only scope requested is Identify the owner of the requesting entity, which I think is just granting access to their username, display name, etc, which is information the server already has access to. If that’s the case, I think the authorise step might be redundant for this particular setup / use case (but not in general obviously).

It would be convenient for us to be able to turn off the authorise step for this particular setup, so a disable_oauth_confirm flag for servers would make sense.

I’m happy to hear suggestions for ways to streamline this process

Best ideas so far in order from easy to hard:

  • Add an “always accept” button to the authorise page:

    • I.e, allow users to opt into disable_oauth_confirm for themselves (for all user’s servers).
    • Could be restricted to “always accept for these scopes” to prevent escalation.
  • Preserve prior scope authorisation for future oauth tokens:

    • This would save users having to re-authorise the same scopes when the server & hub are restarted.
    • This is tricky as the oauth scopes (and configuration in general) may have changed since the server / hub were shutdown.
    • So we would need to check the previously authorised scopes against the newly requested ones.
    • And possibly check the authenticator hasn’t changed?
  • Disable the check in setups where it doesn’t make sense:

    • If it is the case that this authorisation isn’t necessary for some setups (which it might not be).
    • And it is easy to detect these setups from the config (which it might not be).
    • Then there may be the potential for a dynamic default (which might not be a good idea).

I wonder if we could make it a callable instead. That makes it clear to the admin a lot more consideration is required than a simple boolean, and if we pass enough information into the callable it’ll be possible to make fine grained decisions. It also avoids needing to store a user’s preferences in the database.

I really don’t think the authenticator enters into this at all. It’s not that usernames are accessible via pam, it’s really all about browser access and JupyterHub token access. There is a meaningful difference between looking up the existence of usernames in passwd and knowing that you have a connection to a browser session an authenticated as a specific user.

I do think there are certain cases (such as JupyterHub’s original target audience of trusted users on a shared system) where it really is low-risk and a simple boolean would suffice and not be too much. But a callable makes sense in general. Need to think about what exactly to pass.

I also think that if we fixed the fact that the “remember authorized scopes” state gets cleared across authorized server restarts, the experience would be greatly improved, since in most cases this would be prompted very rarely.

We do already preserve scope authorization precisely as @oliver-sanders suggests (including specific checks for authorized scopes), just with the wrong lifetime. We don’t separately store that a user has authorized scopes for a server, we use the existence of already-approved tokens for this. This is insufficient for two reasons:

  1. we delete oauth clients ant server shutdown (we don’t have to do this), and
  2. oauth tokens expire

so it’s possible for a previously authorized oauth client to no longer have any tokens for a given user, resulting in a re-prompt for the same scopes.

So I think there’s two separate but related issues:

  1. allow opt-out for deployments where admins want to force users to trust each other
  2. improve persistence of authorized oauth client scopes so we don’t re-prompt users with the same authorization as often as we do today

If we fixed this lifetime of “already authorized scopes” so that it persists across server restarts and token expiry (a table of user, oauth client, authorized_scopes), I think that would improve things a lot and is something we should do.

I don’t think we need to automatically disable the check based on criteria, as long as we allow deployments to opt-out appropriately. I see 3 main options:

  1. boolean (e.g. c.Spawner.oauth_no_confirm)
  2. set of scopes (e.g. allow skipping if scopes are subset of a specific set, c.Spawner.oauth_no_confirm_scopes = ["read:users", "read:users:name!user"])
  3. callable with (user, oauth client, [owner?], requested scopes)

We can also add the spawner oauth_no_confirm opt-out