-
Notifications
You must be signed in to change notification settings - Fork 367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add how-to doc on refresh tokens #778
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
# Refreshing user authentication | ||
|
||
JupyterHub has a mechanism called [`refresh_user`](inv:jupyterhub:py:method#jupyterhub.auth.Authenticator.refresh_user) that is meant to _refresh_ information from the Authentication provider periodically. | ||
This allows you to make sure things like group membership or other authorization info is up-to-date. | ||
In OAuth, this can also mean making sure the access token has not expired. | ||
This is particularly useful in deployments where an access token from the oauth provider is passed to the Server environment, | ||
e.g. for access to data sources, git repos, etc.. | ||
You don't want to start a server passing an expired token, do you? | ||
|
||
OAuthenticator 17.2 introduces support in all OAuthenticator classes for refreshing user info via this mechanism, including requesting new access tokens if a `refresh_token` is available from the oauth provider. | ||
|
||
```{seealso} | ||
- [More about refresh tokens](https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token/) | ||
``` | ||
|
||
How it works: | ||
|
||
- Every time a user takes an authenticated action with JupyterHub | ||
(making an API request, launching a server, visiting a page, etc.), | ||
JupyterHub checks when the last time auth info was loaded from the provider. | ||
- If the auth info is older than [Authenticator.auth_refresh_age](inv:jupyterhub:py:attribute#jupyterhub.auth.Authenticator.auth_refresh_age), the auth info is refreshed, | ||
i.e. the user model is retrieved anew with the current access token, and any changes are applied (usually there aren't any). | ||
The default value for this age is five minutes. | ||
You can consider it an expiring cache of the information we retrieved from the OAuth provider. | ||
- If the access token is expired and a refresh token is a available, | ||
a new access token is retrieved via the [refresh_token grant](https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token/) | ||
- If no auth info is retrievable (e.g. no refresh token and access token is expired or both are expired or revoked), | ||
then the user must login again before they are able to take actions in JupyterHub | ||
because at this point their authorization state is unknown and could no longer be valid. | ||
|
||
There is also an option [Authenticator.refresh_pre_spawn](inv:jupyterhub:py:attribute#jupyterhub.auth.Authenticator.refresh_pre_spawn) which can be enabled: | ||
|
||
```python | ||
c.Authenticator.refresh_pre_spawn = True | ||
``` | ||
|
||
to ensure auth is up-to-date before launching a server. | ||
This is most useful when the server is being passed an access token | ||
because it ensures the token is valid when the server starts. | ||
|
||
## Refreshing tokens from user sessions | ||
|
||
```{warning} | ||
This example requires granting users read access to their own `auth_state`. | ||
If you plan to provide users with access tokens, | ||
`auth_state` does not typically include information your users won't have access to with the token itself, | ||
but it is worth making sure that your Authenticator configuration doesn't put anything in `auth_state` | ||
that you do not want users to be able to see. | ||
``` | ||
|
||
If your user sessions use access tokens from your oauth provider and those tokens may expire during user sessions, | ||
you can rely on this mechanism to get fresh access tokens from JupyterHub. | ||
|
||
The first step is to grant the _server_ token access to read auth state for its owner. | ||
Users do not have permission to read their own auth state by default, | ||
but `auth_state` is where the `access_token` is stored. | ||
We need to grant the `admin:auth_state!user` scope to both the `user` and `server` roles, | ||
so that requests with `$JUPYTERHUB_API_TOKEN` will have permission to read the access token: | ||
|
||
```python | ||
c.JupyterHub.load_roles = [ | ||
{ | ||
"name": "user", | ||
"scopes": [ | ||
"self", | ||
"admin:auth_state!user", | ||
], | ||
}, | ||
{ | ||
"name": "server", | ||
"scopes": [ | ||
"users:activity!user", | ||
"access:servers!server", | ||
"admin:auth_state!user", | ||
], | ||
}, | ||
] | ||
``` | ||
|
||
We then also need to make sure "auth state" is enabled | ||
(it is enabled by default in the jupyterhub helm chart): | ||
|
||
```python | ||
c.Authenticator.enable_auth_state = True | ||
# also set $JUPYTERHUB_CRYPT_KEY env to 32-byte string | ||
# e.g. with `openssl rand -hex 32` | ||
``` | ||
|
||
At this point: | ||
|
||
1. When a user logs in, the OAuth user info and access token are encrypted and persisted in the Hub database. | ||
2. When the server token requests the user model at `/hub/api/user`, an `auth_state` field will be present, containing the current auth state. | ||
3. Further, when accessing `/hub/api/user` the `refresh_user` logic is triggered if `auth_refresh_age` has elapsed since the last refresh. | ||
|
||
This means that you can access `/hub/api/user` with `$JUPYTERHUB_API_TOKEN` and it will **always return a valid access token**, | ||
even if the currently stored token has expired when the request is made. | ||
|
||
To retrieve the access token, make a request to `${JUPYTERHUB_API_URL}/hub/user` with `${JUPYTERHUB_API_TOKEN}`, e.g. from Python: | ||
|
||
```python | ||
import os | ||
import requests | ||
|
||
hub_token = os.environ["JUPYTERHUB_API_TOKEN"] | ||
hub_api_url = os.environ["JUPYTERHUB_API_URL"] | ||
user_url = hub_api_url + "/user" | ||
|
||
r = requests.get(user_url, headers={"Authorization": f"Bearer {hub_token}"}) | ||
user = r.json() | ||
access_token = user["auth_state"]["access_token"] | ||
``` | ||
|
||
The `access_token` retrieved here should always be a fresh, valid access token, | ||
and will be updated by the `refresh_user` functionality when it expires. | ||
|
||
```{note} | ||
If you get a KeyError on `auth_state`, it means the request does not have the `admin:auth_state!user` permission. | ||
Check your `load_roles` config, relaunch the user server, and try again. | ||
``` | ||
|
||
## Disabling refresh | ||
|
||
The time-based refresh_user trigger is enabled by default in JupyterHub if `auth_state` is enabled. | ||
It can be disabled by setting: | ||
|
||
```python | ||
c.Authenticator.auth_refresh_age = 0 | ||
``` | ||
|
||
in which case the new `refresh_user` method will not be called. | ||
This is equivalent to the behavior of OAuthenticator 17.1 and earlier, | ||
where the default `refresh_user` was called, but did nothing. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there likely to be anything in
auth_state
that an admin wouldn't want a user to see?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's worth mentioning this, because it is possible in theory and there are not currently more fine-grained controls. I don't believe so with the current OAuthenticators, at least, which have the userinfo and token responses. Essentially, all the information OAuthenticator puts in auth state is retrievable with the access token we are after, so if you are doing this to pass the access token, access to auth state isn't giving access to any more info than you are already meaning to give by relaying the access token itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a caveat to the top of this section