Skip to content

Commit c027208

Browse files
authored
Merge pull request #778 from minrk/how-to-refresh
Add how-to doc on refresh tokens
2 parents 0d1ffa4 + 5c657e5 commit c027208

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

docs/source/how-to/refresh.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Refreshing user authentication
2+
3+
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.
4+
This allows you to make sure things like group membership or other authorization info is up-to-date.
5+
In OAuth, this can also mean making sure the access token has not expired.
6+
This is particularly useful in deployments where an access token from the oauth provider is passed to the Server environment,
7+
e.g. for access to data sources, git repos, etc..
8+
You don't want to start a server passing an expired token, do you?
9+
10+
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.
11+
12+
```{seealso}
13+
- [More about refresh tokens](https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token/)
14+
```
15+
16+
How it works:
17+
18+
- Every time a user takes an authenticated action with JupyterHub
19+
(making an API request, launching a server, visiting a page, etc.),
20+
JupyterHub checks when the last time auth info was loaded from the provider.
21+
- 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,
22+
i.e. the user model is retrieved anew with the current access token, and any changes are applied (usually there aren't any).
23+
The default value for this age is five minutes.
24+
You can consider it an expiring cache of the information we retrieved from the OAuth provider.
25+
- If the access token is expired and a refresh token is a available,
26+
a new access token is retrieved via the [refresh_token grant](https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token/)
27+
- If no auth info is retrievable (e.g. no refresh token and access token is expired or both are expired or revoked),
28+
then the user must login again before they are able to take actions in JupyterHub
29+
because at this point their authorization state is unknown and could no longer be valid.
30+
31+
There is also an option [Authenticator.refresh_pre_spawn](inv:jupyterhub:py:attribute#jupyterhub.auth.Authenticator.refresh_pre_spawn) which can be enabled:
32+
33+
```python
34+
c.Authenticator.refresh_pre_spawn = True
35+
```
36+
37+
to ensure auth is up-to-date before launching a server.
38+
This is most useful when the server is being passed an access token
39+
because it ensures the token is valid when the server starts.
40+
41+
## Refreshing tokens from user sessions
42+
43+
```{warning}
44+
This example requires granting users read access to their own `auth_state`.
45+
If you plan to provide users with access tokens,
46+
`auth_state` does not typically include information your users won't have access to with the token itself,
47+
but it is worth making sure that your Authenticator configuration doesn't put anything in `auth_state`
48+
that you do not want users to be able to see.
49+
```
50+
51+
If your user sessions use access tokens from your oauth provider and those tokens may expire during user sessions,
52+
you can rely on this mechanism to get fresh access tokens from JupyterHub.
53+
54+
The first step is to grant the _server_ token access to read auth state for its owner.
55+
Users do not have permission to read their own auth state by default,
56+
but `auth_state` is where the `access_token` is stored.
57+
We need to grant the `admin:auth_state!user` scope to both the `user` and `server` roles,
58+
so that requests with `$JUPYTERHUB_API_TOKEN` will have permission to read the access token:
59+
60+
```python
61+
c.JupyterHub.load_roles = [
62+
{
63+
"name": "user",
64+
"scopes": [
65+
"self",
66+
"admin:auth_state!user",
67+
],
68+
},
69+
{
70+
"name": "server",
71+
"scopes": [
72+
"users:activity!user",
73+
"access:servers!server",
74+
"admin:auth_state!user",
75+
],
76+
},
77+
]
78+
```
79+
80+
We then also need to make sure "auth state" is enabled
81+
(it is enabled by default in the jupyterhub helm chart):
82+
83+
```python
84+
c.Authenticator.enable_auth_state = True
85+
# also set $JUPYTERHUB_CRYPT_KEY env to 32-byte string
86+
# e.g. with `openssl rand -hex 32`
87+
```
88+
89+
At this point:
90+
91+
1. When a user logs in, the OAuth user info and access token are encrypted and persisted in the Hub database.
92+
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.
93+
3. Further, when accessing `/hub/api/user` the `refresh_user` logic is triggered if `auth_refresh_age` has elapsed since the last refresh.
94+
95+
This means that you can access `/hub/api/user` with `$JUPYTERHUB_API_TOKEN` and it will **always return a valid access token**,
96+
even if the currently stored token has expired when the request is made.
97+
98+
To retrieve the access token, make a request to `${JUPYTERHUB_API_URL}/hub/user` with `${JUPYTERHUB_API_TOKEN}`, e.g. from Python:
99+
100+
```python
101+
import os
102+
import requests
103+
104+
hub_token = os.environ["JUPYTERHUB_API_TOKEN"]
105+
hub_api_url = os.environ["JUPYTERHUB_API_URL"]
106+
user_url = hub_api_url + "/user"
107+
108+
r = requests.get(user_url, headers={"Authorization": f"Bearer {hub_token}"})
109+
user = r.json()
110+
access_token = user["auth_state"]["access_token"]
111+
```
112+
113+
The `access_token` retrieved here should always be a fresh, valid access token,
114+
and will be updated by the `refresh_user` functionality when it expires.
115+
116+
```{note}
117+
If you get a KeyError on `auth_state`, it means the request does not have the `admin:auth_state!user` permission.
118+
Check your `load_roles` config, relaunch the user server, and try again.
119+
```
120+
121+
## Disabling refresh
122+
123+
The time-based refresh_user trigger is enabled by default in JupyterHub if `auth_state` is enabled.
124+
It can be disabled by setting:
125+
126+
```python
127+
c.Authenticator.auth_refresh_age = 0
128+
```
129+
130+
in which case the new `refresh_user` method will not be called.
131+
This is equivalent to the behavior of OAuthenticator 17.1 and earlier,
132+
where the default `refresh_user` was called, but did nothing.

docs/source/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Things like how to write your own `oauthenticator` or how to migrate to a newer
3636
3737
how-to/custom-403
3838
how-to/writing-an-oauthenticator
39+
how-to/refresh
3940
how-to/migrations/upgrade-to-15
4041
```
4142

0 commit comments

Comments
 (0)