How to force re-login for users


we’re using JupyterHub to spawn JupyterLab containers via the dockerspawner.SystemUserSpawner class. For authentication we are using a custom class extension of the PAMAuthenticator class which does some additional creation of kerberos tickets if the user is authenticated with the system.

The Kerberos tickets expire after 10 hours and we need to force the users to login with JH in order to create a new kerberos ticket.

We have tried with various approaches to force a redirect to JH from the JupyterLab servers.

e.g. in the

JupyterHub.cookie_max_age_days = 0.4125

as suggested by


c.JupyterHub.tornado_settings.cookie_options = dict(expires_days=0.41.25, max_age_days=0.4125)

as suggested by

Both of which seems to generate a log entry in jupyterhub in the lines of:

[...] JupyterHub base:350] Invalid or expired cookie token

but which doesn’t force the user to log out.

If the user clicks on the “Hub Control Panel” entry in the file menu, they are redirected to a login page on the JH side, but if they use a deep link to their JupyterLab instance (e.g. https://{server_name}/user/{username}/lab? ), they are not prompted for their credentials.

How do I force a redirection to the jupyterhub login page?

Update: this seems tangentially related to Culling JupyterLab and OAuth cookies causing errors when user returns days later to use system

1 Like

The new refresh_user API in 1.0 is meant to specifically enable this kind of thing, so it would be a great use case to test with. This is meant to refresh auth if possible (e.g. refresh tokens), or return None if a new login should be required (e.g. auth revoked or refresh token expired).

1 Like

Thanks for the answer and pointing me towards the new API feature. It looked promising, and the coroutine is also picked up as advertised. I have created the method refresh_user in my Authenticator class as such:

def refresh_user(self, user, handler=None):
    return None

and in the JH log I see that it is being hit and that returning None triggers the following message from

JupyterHub base:294] User smhe has stale auth info. Login is required to refresh.

However, when I then in a new tab in the browser use the deeplink https://{servername}/user/{username}/lab?, it opens the JupyterLab without requiring re-authentication from JH. Am I doing something wrong with the implementation?

Essentially a bump, but I would really like to see a working example of how to implement refresh_user to force jupyterhub to redirect the browser to the login screen.

I have tried returning both False and None which, as described above, is picked up to some degree, but not enough to have the JH server do anything about it. Any ideas appreciated.

I have still found no solution to this problem. However, I can see that a jupyterhub-session-id cookie is created upon successful login. This can be deleted without affecting the authentication status of the logged in user. Can someone point me in the direction of the documentation describing how cookies relate to authentication status in jupyterhub?

Thanks in advance

There aren’t detailed docs of the cookies (there should be). I’m going to write down a sketch here, if anyone would like to make a PR adding a formal description to the docs.

Cookies used by jupyterhub:

  • jupyterhub-hub-login
    • This is the login token when visiting hub-served pages (main login, spawn, etc.)
    • encrypted (resetting cookie secret effectively revokes this cookie)
    • restricted to path: /hub/ so that only the Hub process receives it.
    • If this cookie is set, the user is logged in.
  • jupyterhub-user-username
    • This is the cookie used for authenticating with a single-user server
    • encrypted (resetting cookie secret effectively revokes this cookie)
    • set by the single-user server after OAuth with the Hub
    • set on /users/name so that only the user’s server receives it
    • effectively the same as jupyterhub-hub-login, but for the single-user server instead of the Hub
    • contains an OAuth access token, which is checked with the Hub to authenticate the browser. Each OAuth access token has a session id (see below)
    • To avoid hitting the Hub on every request, the authentication response is cached. To avoid a stale cache, the cache key is both the token and session id.
  • jupyterhub-session-id
    • set on / so all endpoints receive it, can clear it, etc.
    • this is a random string, meaningless in itself, and the only cookie shared by the Hub and single-user servers
    • its sole purpose is to to coordinate logout of the multiple oauth cookies.
  • jupyterhub-user-name-oauth-state
    • a short-lived cookie, used solely to store and validate oauth state. Only set while oauth between the notebook and the Hub is processing

Logging in

The login process, starting with no cookies and accessing an running server at /user/name (the most complicated case)

  • user visits /user/name (served by notebook)
  • jupyterhub-user-name is not set, login process begins. That’s OAuth with the Hub, redirect to /hub/api/oauth2/authorize
  • /hub/api/oauth2/authorize (served by hub) is an authenticated page. jupyterhub-hub-login is not set, redirect to /hub/login to begin login process
  • login process may be a form or external oauth, etc.
  • on successful login, jupyterhub-hub-login and jupyterhub-session-id are set, redirect back to /hub/api/oauth2/authorize
  • authenticated access to /hub/api/oauth2/authorize completes OAuth process with an oauth code and redirects back to /users/name/oauth_callback (will start here if already authenticated with the Hub at the time of requesting access to /user/name)
  • /users/name/oauth_callback (served by notebook) completes oauth with the Hub, retrieving an OAuth access token and storing it in jupyterhub-user-name
  • redirects back to /user/name, checks jupyterhub-user-name cookie
    • finds token
    • verifies token with the Hub via /hub/api/authorizations/token
    • caches response using jupyterhub-session-id and token together as key
    • allows access based on who is authenticated

Logging out

Clicking a logout button anywhere should:

  • redirect to /hub/logout (served by Hub)
  • which calls clear_login_cookie, which clears jupyterhub-hub-login
  • revokes all OAuth access tokens associated with jupyterhub-session-id
  • clears jupyterhub-session-id

After this, visiting /user/name will have jupyterhub-user-name set. This cookie is not cleared by the Hub. However, since jupyterhub-session-id is cleared, the cached authentication response will not be used. The Hub will be consulted again for the validity of the token in jupyterhub-user-name, which was revoked during logout due to the session id, so authentication will fail and the cookie will be cleared and login will be prompted to start again.


I have marked this post. If I have time, I can have a try.

We are using jupyterhub + jupyterhub configurable-http-proxy + jupyterlab.

The problem with this is that, once you are in the notebook, the hub never sees any requests from the user’s browser. The only requests are to the notebook server (kernelspecs, sessions, terminals, etc…) and other communication is done over websockets.

So I’m not sure quite sure how a redirect could happen?

1 Like

Any update on the original issue here? I have seen the same behavior using the off the shelf generic Oauth Authenticator (with Auth0)

1 Like

I wanted to achieve exactly the same; I think I did it.


async def refresh_user(self, user, handler, force=True):
    await handler.stop_single_user(user,

force=True is optional and i am not sure it makes any difference.
But yeah, that bit works. (although it doesn’t redirect the page in the browser, but it simply stops the server and throws a Service Unavailable/Server Error default message. Which is ok for my case.).
So far, tested on PAMAuthentication-default installation. I will need to try it with OAuth too.

Hi, I am using Auth0, added your functionality getting this error
“Page isn’t redirected properly - problem might be caused by disabling or refusing to accept cookies”

from oauthenticator.auth0 import Auth0OAuthenticator, LocalAuth0OAuthenticator

c.JupyterHub.authenticator_class = LocalAuth0OAuthenticator

c.LocalAuth0OAuthenticator.auth0_subdomain = ‘domain’

c.LocalAuthenticator.add_user_cmd = [‘adduser’, ‘-q’, ‘–gecos’, ‘’, ‘–disabled-password’, ‘–force-badname’]

c.LocalAuthenticator.create_system_users = True

c.Authenticator.delete_invalid_users = True

c.Auth0OAuthenticator.client_id = ‘id’

c.Auth0OAuthenticator.client_secret = ‘sec’

c.Auth0OAuthenticator.oauth_callback_url = ‘http://localhost:8000/hub/oauth_callback

c.Auth0OAuthenticator.scope = [‘openid’, ‘email’]

c.JupyterHub.cookie_max_age_days = 0.4125

c.Authenticator.refresh_pre_spawn = True

c.Authenticator.auth_refresh_age = 300

c.Authenticator.admin_users = {‘myemail’}

Any ideas what I could do to fix? My problem is that it keeps auto logging me in - I cannot test the actual signup/login page

Where did you put that snippet? I have it on /usr/local/lib/python3.6/dist-packages/jupyterhub/ which is the basic authentication class which also contains this method (as empty) by itself . (the method refresh_user() i mean.). Then any other Authenticator class will use it. This error you get, also, may not be related to this snippet. I don’t think it is. Maybe something in the configuration file is not correct. (haven’t used LocalAuth0 to be honest). Also, i would try to open the developer console (F12) and select all cookies of localhost and Delete them all .

Thank you for posting your solution.
However, I need a solution without shutting down the user server.

Does anyone has an idea, how the redirect to ‘/hub/logout/’ can be enabled?

I have implemented Pingfederate integration by extending class PingfedOAuthenticator(OAuthenticator):…

I have implemented the refresh_user in the same class like below:
def refresh_user(self, user, handler=None):
print(‘I am in refresh user’)
return False

I tried to return None.
It does work on staling the session and forcing user to login .
However this introduced the issue that even watcher is getting a 403 and the Culling feature also not working.

Can you please suggest/direct.