mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 02:24:08 +00:00
131 lines
7.4 KiB
Markdown
131 lines
7.4 KiB
Markdown
# Logging users in via URL
|
|
|
|
Sometimes, JupyterHub is integrated into an existing application that has already handled user login, etc..
|
|
It is often preferable in these applications to be able to link users to their running JupyterHub server without _prompting_ the user to login again with the Hub when the Hub should really be an implementation detail,
|
|
and not part of the user experience.
|
|
|
|
One way to do this has been to use [API only mode](#howto:api-only), issue tokens for users, and redirect users to a URL like `/users/name/?token=abc123`.
|
|
This is [disabled by default](#HubAuth.allow_token_in_url) in JupyterHub 5, because it presents a vulnerability for users to craft links that let _other_ users login as them, which can lead to inter-user attacks.
|
|
|
|
But that leaves the question: how do I as an _application developer_ embedding JupyterHub link users to their own running server without triggering another login prompt?
|
|
|
|
The problem with `?token=...` in the URL is specifically that _users_ can get and create these tokens, and share URLs.
|
|
This wouldn't be an issue if only authorized applications could issue tokens that behave this way.
|
|
The single-user server doesn't exactly have the hooks to manage this easily, but the [Authenticator](#Authenticator) API does.
|
|
|
|
## Problem statement
|
|
|
|
We want our external application to be able to:
|
|
|
|
1. authenticate users
|
|
2. (maybe) create JupyterHub users
|
|
3. start JupyterHub servers
|
|
4. redirect users into running servers _without_ any login prompts/loading pages from JupyterHub, and without any prior JupyterHub credentials
|
|
|
|
Step 1 is up to the application and not JupyterHub's problem.
|
|
Step 2 and 3 use the JupyterHub [REST API](#jupyterhub-rest-API).
|
|
The service would need the scopes:
|
|
|
|
```
|
|
admin:users # creating users
|
|
servers # start/stop servers
|
|
```
|
|
|
|
That leaves the last step: sending users to their running server with credentials, without prompting login.
|
|
This is where things can get tricky!
|
|
|
|
### Ideal case: oauth
|
|
|
|
_Ideally_, the best way to set this up is with the external service as an OAuth provider,
|
|
though in some cases it works best to use proxy-based authentication like Shibboleth / [REMOTE_USER](https://github.com/cwaldbieser/jhub_remote_user_authenticator).
|
|
The main things to know are:
|
|
|
|
- Links to `/hub/user-redirect/some/path` will ultimately land users at `/users/theirserver/some/path` after completing login, ensuring the server is running, etc.
|
|
- Setting `Authenticator.auto_login = True` allows beginning the login process without JupyterHub's "Login with..." prompt
|
|
|
|
_If_ your OAuth provider allows logging in to external services via your oauth provider without prompting, this is enough.
|
|
Not all do, though.
|
|
|
|
If you've already ensured the server is running, this will _appear_ to the user as if they are being sent directly to their running server.
|
|
But what _actually_ happens is quite a series of redirects, state checks, and cookie-setting:
|
|
|
|
1. visiting `/hub/user-redirect/some/path` checks if the user is logged in
|
|
1. if not, begin the login process (`/hub/login?next=/hub/user-redirect/...`)
|
|
2. redirects to your oauth provider to authenticate the user
|
|
3. redirects back to `/hub/oauth_callback` to complete login
|
|
4. redirects back to `/hub/user-redirect/...`
|
|
2. once authenticated, checks that the user's server is running
|
|
1. if not running, begins launch of the server
|
|
2. redirects to `/hub/spawn-pending/?next=...`
|
|
3. once the server is running, redirects to the actual user server `/users/username/some/path`
|
|
|
|
Now we're done, right? Actually, no, because the browser doesn't have credentials for their user server!
|
|
This sequence of redirects happens all the time in JupyterHub launch, and is usually totally transparent.
|
|
|
|
4. at the user server, check for a token in cookie
|
|
1. if not present or not valid, begin oauth with the Hub (redirect to `/hub/api/oauth2/authorize/...`)
|
|
2. hub redirects back to `/users/user/oauth_callback` to complete oauth
|
|
3. redirect again to the URL that started this internal oauth
|
|
5. finally, arrive at `/users/username/some/path`, the ultimate destination, with valid JupyterHub credentials
|
|
|
|
The steps that will show users something other than the page you want them to are:
|
|
|
|
- Step 1.1 will be a prompt e.g. with "Login with..." unless you set `c.Authenticator.auto_login = True`
|
|
- Step 1.2 _may_ be a prompt from your oauth provider. This isn't controlled by JupyterHub, and may not be avoidable.
|
|
- Step 2.2 will show the spawn pending page only if the server is not already running
|
|
|
|
Otherwise, this is all transparent redirects to the final destination.
|
|
|
|
#### Using an authentication proxy (REMOTE_USER)
|
|
|
|
If you use an Authentication proxy like Shibboleth that sets e.g. the REMOTE_USER header,
|
|
you can use an Authenticator like [RemoteUserAuthenticator](https://github.com/cwaldbieser/jhub_remote_user_authenticator) to automatically login users based on headers in the request.
|
|
The same process will work, but instead of step 1.1 redirecting to the oauth provider, it logs in immediately.
|
|
If you do support an auth proxy, you also need to be extremely sure that requests only come from the auth proxy, and don't accept any requests setting the REMOTE_USER header coming from other sources.
|
|
|
|
### Custom case
|
|
|
|
But let's say you can't use OAuth or REMOTE_USER, and you still want to hide JupyterHub implementation details.
|
|
All you really want is a way to write a URL that will take users to their servers without any login prompts.
|
|
|
|
You can do this if you create an Authenticator with `auto_login=True` that logs users in based on something in the _request_, e.g. a query parameter.
|
|
|
|
We have an _example_ in the JupyterHub repo in `examples/forced-login` that does this.
|
|
It is a sample 'external service' where you type in a username and a destination path.
|
|
When you 'login' with this username:
|
|
|
|
1. a token is issued
|
|
2. the token is stored and associated with the username
|
|
3. redirect to `/hub/login?login_token=...&next=/hub/user-redirect/destination/path`
|
|
|
|
Then on the JupyterHub side, there is the `ForcedLoginAuthenticator`.
|
|
This class implements `authenticate`, which:
|
|
|
|
1. has `auto_login = True` so visiting `/hub/login` calls `authenticate()` directly instead of serving a page
|
|
2. gets the token from the `login_token` URL parameter
|
|
3. makes a POST request to the external application with the token, requesting a username
|
|
4. the external application returns the username and deletes the token, so it cannot be re-used
|
|
5. Authenticator returns the username
|
|
|
|
This doesn't _bypass_ JupyterHub authentication, as some deployments have done, but it does _hide_ it.
|
|
If your service launches servers via the API, you could run this in [API only mode](#howto:api-only) by adding `/hub/login` as well:
|
|
|
|
```python
|
|
c.JupyterHub.hub_routespec = "/hub/api/"
|
|
c.Proxy.additional_routes = {"/hub/login": "http://hub:8080"}
|
|
```
|
|
|
|
```{literalinclude} ../../../examples/forced-login/jupyterhub_config.py
|
|
:language: python
|
|
:start-at: class ForcedLoginAuthenticator
|
|
:end-before: c = get_config()
|
|
```
|
|
|
|
**Why does this work?**
|
|
|
|
This is still logging in with a token in the URL, right?
|
|
Yes, but the key difference is that users cannot issue these tokens.
|
|
The sample application is still technically vulnerable, because the token link should really be non-transferrable, even if it can only be used once.
|
|
The only defense the sample application has against this is rapidly expiring tokens (they expire after 30 seconds).
|
|
You can use state cookies, etc. to manage that more rigorously, as done in OAuth (at which point, maybe implement OAuth itself, why not?).
|