I’m not sure how to debug this, but I am using Keycloak with OAuthenticator integration and Generic OAuth endpoint connections.
I had to do some configuration via environment variables instead of the OAuthenticator documentation with TLJH setup steps, but effectively I have this:
ENV VARS (defined in SystemD unit overrides):
OAUTH2_AUTHORIZE_URL=https://auth.REDACTED/realms/jupyter/protocol/openid-connect/auth
OAUTH2_TOKEN_URL=https://auth.REDACTED/realms/jupyter/protocol/openid-connect/token
OAUTH2_TOKEN_URL=https://auth.REDACTED/realms/jupyter/protocol/openid-connect/userinfo
OAUTH2_USERNAME_KEY=email
OAUTH_LOGOUT_REDIRECT_URL=https://auth.REDACTED/realms/jupyter/protocol/openid-connect/logout
OAUTH_CLIENT_ID=REDACTED
OAUTH_CLIENT_SECRET=REDACTED
The TLJH config YAML contains:
(NOTE: it did not work without the aforementioned env vars being set so I think there’s something odd going on there)
users:
admin:
- sysadmin
https:
enabled: true
tls:
cert: /etc/ssl/certs/ssl-cert-snakeoil.pem
key: /etc/ssl/private/ssl-cert-snakeoil.key
JupyterHub:
authenticator_class: generic-oauth
GenericOAuthenticator:
client_id: REDACTED
client_secret: REDACTED
authorize_url:
https://auth.REDACTED/realms/jupyter/protocol/openid-connect/auth
token_url:
https://auth.REDACTED/realms/jupyter/protocol/openid-connect/token
userdata_url:
https://auth.dark-chaos.net/realms/jupyter/protocol/openid-connect/userinfo
scope: ["openid", "email", "groups"]
username_claim: email
auth_state_groups_key: oauth_user.groups
admin_groups: {"administrator"}
oauth_callback_url: https://jupyter.REDACTED/hub/oauth_callback
auth:
type: generic-oauth
During authentication, the handoff from TLJH to Keycloak works fine, and it hands back a code/token for exchange. However, during the exchange going back from Keycloak to TLJH, we get a 500 Internal Server Error, with this traceback:
Oct 19 00:00:31 jupyter python3[6829]: [E 2025-10-19 00:00:31.933 JupyterHub oauth2:857] Error fetching 401 POST https://auth.REDACTED/realms/jupyter/protocol/openid-connect/userinfo:
Oct 19 00:00:31 jupyter python3[6829]: [E 2025-10-19 00:00:31.933 JupyterHub web:1934] Uncaught exception GET /hub/oauth_callback?state=REDACTED&session_state=REDACTED&iss=https%3A%2F%2Fauth.REDACTED%2Frealms%2Fjupyter&code=REDACTED (10.200.5.20)
Oct 19 00:00:31 jupyter python3[6829]: HTTPServerRequest(protocol='https', host='jupyter.REDACTED', method='GET', uri='/hub/oauth_callback?state=REDACTED&session_state=REDACTED&iss=https%3A%2F%2Fauth.REDACTED%2Frealms%2Fjupyter&code=REDACTED', version='HTTP/1.1', remote_ip='10.200.5.20')
Oct 19 00:00:31 jupyter python3[6829]: Traceback (most recent call last):
...
Oct 19 00:00:31 jupyter python3[6829]: tornado.httpclient.HTTPClientError: HTTP 401: Unauthorized
I’m unable to determine why it’s doing a 401 Unauthorized response. Though, it’s behaving almost like it isn’t properly authenticating to the userinfo endpoint.
Is there a way to increase the log verbosity to see what exactly is being sent (headers included), or am I missing something in my configuration to make this work? It’s behaving as if the system isn’t providing an Authorization header with an access token in the header to the backend. If you do that it works.
(And before you address the use of the snakeoil cert on a system being selfsigned / insecure, jupyter is served to the world with an NGINX proxy in front of it with proper SSL, so the snakeoil cert is to just let the SSL handoff to the backend work. It hasn’t given any problems here so far)