jupyterhub / oauthenticator

OAuth + JupyterHub Authenticator = OAuthenticator
https://oauthenticator.readthedocs.io
BSD 3-Clause "New" or "Revised" License
413 stars 364 forks source link

Working configuration for generic authenticator with Keycloak #107

Closed pfisterer closed 4 years ago

pfisterer commented 7 years ago

I have tried to configure Jupyterhub to use the generic OAuth2 authentication mechanism with Keycloak as OAuth2 sever. However, it never redirects to it for authentication. Could you please support me and provide a sample configuration that is supposed to work? Or is there anything that I'm obviously doing wrong?

Thank you very much! Dennis

OAuthenticator configuration in jupyterhub_config.py:

from oauthenticator.generic import GenericOAuthenticator
c.JupyterHub.authenticator_class = GenericOAuthenticator

c.GenericOAuthenticator.oauth_callback_url = 'https://example.com:8443/auth/realms/testrealm/protocol/openid-connect/auth'
c.GenericOAuthenticator.client_id = 'oauth-secret'
c.GenericOAuthenticator.client_secret = 'asfdsdf-dfdf-fhhfh-aed7-asdafsdfsaf'
c.GenericOAuthenticator.token_url = 'https://example.com:8443/auth/realms/testrealm/protocol/openid-connect/token'
c.GenericOAuthenticator.userdata_url = 'https://141.72.18.234:8443/auth/realms/testrealm/protocol/openid-connect/userinfo'

Keycloak Endpoints:

{
    "issuer": "https://example.com:8443/auth/realms/testrealm",
    "authorization_endpoint": "https://example.com:8443/auth/realms/testrealm/protocol/openid-connect/auth",
    "token_endpoint": "https://example.com:8443/auth/realms/testrealm/protocol/openid-connect/token",
    "token_introspection_endpoint": "https://example.com:8443/auth/realms/testrealm/protocol/openid-connect/token/introspect",
    "userinfo_endpoint": "https://example.com:8443/auth/realms/testrealm/protocol/openid-connect/userinfo",
    "end_session_endpoint": "https://example.com:8443/auth/realms/testrealm/protocol/openid-connect/logout",
    "jwks_uri": "https://example.com:8443/auth/realms/testrealm/protocol/openid-connect/certs",
    "check_session_iframe": "https://example.com:8443/auth/realms/testrealm/protocol/openid-connect/login-status-iframe.html",
    "grant_types_supported": ["authorization_code", "implicit", "refresh_token", "password", "client_credentials"],
    "response_types_supported": ["code", "none", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"],
    "subject_types_supported": ["public", "pairwise"],
    "id_token_signing_alg_values_supported": ["RS256"],
    "userinfo_signing_alg_values_supported": ["RS256"],
    "request_object_signing_alg_values_supported": ["none", "RS256"],
    "response_modes_supported": ["query", "fragment", "form_post"],
    "registration_endpoint": "https://example.com:8443/auth/realms/testrealm/clients-registrations/openid-connect",
    "token_endpoint_auth_methods_supported": ["private_key_jwt", "client_secret_basic", "client_secret_post"],
    "token_endpoint_auth_signing_alg_values_supported": ["RS256"],
    "claims_supported": ["sub", "iss", "auth_time", "name", "given_name", "family_name", "preferred_username", "email"],
    "claim_types_supported": ["normal"],
    "claims_parameter_supported": false,
    "scopes_supported": ["openid", "offline_access"],
    "request_parameter_supported": true,
    "request_uri_parameter_supported": true
}
willingc commented 7 years ago

Hi @pfisterer, How are you generating the client_secret:

c.GenericOAuthenticator.client_secret = 'asfdsdf-dfdf-fhhfh-aed7-asdafsdfsaf'

pfisterer commented 7 years ago

@willingc I obtained the (original) client secret from Keycloak (and verified that it is correct using curl). However, the string above is a modified version (just as the domain name does not reflect my internal settings at the university).

My problem ist that the browser is never redirected to Keycloak for the user to authenticate.

pfisterer commented 7 years ago

Would it help if I supply a keycloak instance for testing?

willingc commented 7 years ago

@pfisterer Thanks for the testing offer. I'm out of the office for the next week so I may have a delayed response.

@minrk Have you worked with Keycloak? I'm thinking that we need to add to the generic auth to support Keycloak's use of Realm.

minrk commented 7 years ago

I haven't used the generic authenticator. @dmvieira do you have an example of a fully working generic oauth config?

@pfisterer what happens when you hit the 'Login with...' button exactly, if there is no redirect? Are there errors in the js console or the jupyterhub logs?

dmvieira commented 7 years ago

For OAuthenticator generic integration we need to add at jupyterhub_config.py: c.JupyterHub.authenticator_class = 'oauthenticator.generic.GenericOAuthenticator' c.OAuthenticator.client_id # oauth2 client id for your app c.OAuthenticator.client_secret # oauth2 client secret for your app c.GenericOAuthenticator.token_url # oauth2 provider's token url c.GenericOAuthenticator.userdata_url # oauth2 provider's endpoint with user data c.GenericOAuthenticator.userdata_method # method used to request user data endpoint c.GenericOAuthenticator.userdata_params # params to send for userdata endpoint c.GenericOAuthenticator.username_key # username key from json returned from user data endpoint

And export these environment variables: OAUTH2_TOKEN_URL # the same as token_url param OAUTH2_AUTHORIZE_URL # oauth2 provider's authorization andpoint

For more details I'll show how to integrate with generic authentication in https://conferences.oreilly.com/jupyter/jup-ny/public/schedule/detail/59400

dmvieira commented 7 years ago

Perhaps you forgot environment variables

patback66 commented 7 years ago

Was there any resolution to this? I'm attempting a similar setup and have been running into the same issues. The button for the login page still directs to example.com:8000/hub/login so it won't direct the user to the Keycloak login. If I manually navigate to example.com:8000/hub/oauth_login I end up with an infinite redirect to example.com:8000/hub/oauth_callback?... and don't actually hit Keycloak's login.

dmvieira commented 7 years ago

Are you using the environment variables?

OAUTH2_TOKEN_URL # the same as token_url param OAUTH2_AUTHORIZE_URL # oauth2 provider's authorization andpoint

patback66 commented 7 years ago

Fixing the env variables solved the infinite redirect after navigating to /hub/oauth_login. Unfortunately the login url for the button on /hub/login still doesn't get set properly.

dmvieira commented 7 years ago

You can use custom template paths and change login button template to redirect to correct login URL.

Here I'm using extra_template_path and template_paths at jupyter and jupyterhub configs. Then you can use your custom htmls

dmvieira commented 7 years ago

It's strange because you should never click on login button... Oauthenticator always redirects you to authorize URL when you try to access Notebook. Please, explain me a bit more and paste your Hub and Notebook configuration

patback66 commented 7 years ago

Censored some bits but here's the config for authentication:

from oauthenticator.generic import LocalGenericOAuthenticator
c.JupyterHub.authenticator_class = LocalGenericOAuthenticator
c.OAuthenticator.client_id = '{client_id}'
c.OAuthenticator.client_secret = '{client_secret}'
c.LocalGenericOAuthenticator.token_url = 'https://{domain}:{port}/auth/realms/{realm}/protocol/openid-connect/token'# oauth2 provider's token url
c.LocalGenericOAuthenticator.userdata_url = 'https://{domain}:{port}/auth/realms/{realm}/protocol/openid-connect/userinfo'
c.LocalGenericOAuthenticator.userdata_method = 'GET'
c.LocalGenericOAuthenticator.userdata_params = {"state": "state"}
c.LocalGenericOAuthenticator.username_key = "preferred_username"
c.LocalAuthenticator.create_system_users = True

and env vars

OAUTH2_AUTHORIZE_URL=https://{domain}:{port}/auth/realms/{realm}/protocol/openid-connect/auth
OAUTH2_TOKEN_URL=https:/{domain}:{port}/auth/realms/{realm}/protocol/openid-connect/token

Should I be setting something like c.Authenticator.auto_login = True to get the intended behavior?

dmvieira commented 7 years ago

I never used LocalGenericOAuthenticator only GenericOAuthenticator and it works great... LocalGenericOAuthenticator I don't know

SarunasG commented 7 years ago

Hi All,

I am able to log in to Jupyterhub successfully with configuration mentioned above. (using Keycloak for authentication and GenericOAuthenticator as Oauth class), but the problem is that I am not able to logout. The only thing that happens when I click on "logout" button - I am redirected to default login page with login "Sign in with GenericOAuth2" button again. And if I click on this button I am not prompted for any credentials. Log record below... [I 2017-10-25 13:33:23.105 JupyterHub log:122] 302 GET /hub/logout → /hub/login

It means that my session tokens/cookies are not removed, hence no call to Keycloack to end up the session is done - so I actually still remain logged in. Am I missing anything in my jupyterhub_config.py file, should I implement my own logout handler or etc. ?

dmvieira commented 7 years ago

Keycloak has logout callback? Use logout callback to call /logout on jupyterhub

niveau0 commented 6 years ago

Made a keycloak authenticator a while ago: https://github.com/matipp/oauthenticator/commit/326d2617ce09749ca0c4d9a19e9651fe437e1755 but never had time to write tests for a PR, sorry

SofianeB commented 6 years ago

Finally, I got the integration working. Maybe a question for those who had a successful set up. How can we restrict the access to jupyterhub after a successful login through keycloak? I have been experementing with the Group function in kycloak, but it does not help. The whitelist in jupyterhub is not working. Any information?

Thank you.

bellackn commented 6 years ago

Hey @SofianeB, I stumbled upon the same problem a while ago and built something that works for us.

We are using a JupyterHub managed service that first checks the Keycloak API for users that are in a certain group (e.g. jupyterhub_user). Afterwards, it checks whether this user is already in JupyterHub's whitelist via the Hub's API. If not, it creates this user. It also works the other way round, i.e. deleting users that are in the whitelist but not in the correct Keycloak group anymore.

This approach feels pretty hacky, but I also haven't found a neat way to use Keycloak's roles/groups with JupyterHub. If somebody has a better solution, I'm happy to see it!

Anyway, here's the code:

1) get_members_jupyterhub_user.sh – Checks Keycloak for users in the jupyterhub_user group.

#!/usr/bin/env bash

# Obtain a valid access token by passing the admin credentials
# (a token is valid just for 1 minute)
KC_TKN=`curl -s -k \
  -d "client_id=admin-cli" \
  -d "username=admin" \
  -d "password=$KC_ADM_PASSWORD" \
  -d "grant_type=password" \
  "$KC_ADM_API" \
  | jq -r ".access_token"`

if [ -z $KC_TKN ]; then echo "Warning: KC_TKN is empty"; fi

# Before getting the new usernames, put the old ones away in a 
# seperate file. This is necessary as you might want to delete
# users that lost their right to access the JHub.
if [ -f "./jsons/jhub_user.json" ]; then
  cp ./jsons/jhub_user.json ./jsons/OLD_jhub_user.json
fi

# Get the names of all users that are in the "jupyterhub_user" group
# and write it in a file named "jhub_user"
curl -s -k "$KC_ROOT/admin/realms/$KC_REALM/groups/$KC_GRP_ID/members" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer $KC_TKN" | jq "." \
  > ./jsons/jhub_user.json

2) update_whitelist.sh – Checks if the users obtained in step 1) should be whitelisted or not.

#!/usr/bin/env bash

# Adding users to JupyterHub that are in KC's "jupyterhub_user" group
# (See doc: https://jupyterhub.readthedocs.io/en/latest/_static/rest-api/)

JHUB_TKN=$(cd /srv/jupyterhub && jupyterhub token admin)

if [ -z $JHUB_TKN ]; then echo "Warning: JHUB_TKN is empty"; fi

# Save the allowed users (retrieved by "get_members_jhub_user.sh")
# in the variable NEW  
NEW=`cat ./jsons/jhub_user.json | jq -r ".[].username"`

# Also, save the "old" users from the get-script's previous fetch 
# (if existent) in the variable OLD in order to delete users that are not in KC's
# "jhub_user" group anymore
if [[ -f "./jsons/OLD_jhub_user.json" ]]; then
    OLD=`cat ./jsons/OLD_jhub_user.json | jq -r ".[].username"`
fi

# Put content of NEW and OLD (if existent)
# in array ARRNEW/ARROLD
read -ra ARRNEW -d '' <<<"$NEW"

if [[ ! -z " $OLD" ]]; then
    read -ra ARROLD -d '' <<<"$OLD"
fi

# UNIQ is a variable that contains the usernames that differ
# between the old and the freshly fetched file.
# By UNIQ, check if a new user has to be created or if an old
# user should not be on JHub's whitelist anymore.
UNIQ=`echo ${ARRNEW[@]} ${ARROLD[@]} | tr ' ' '\n' | sort | uniq -u`

# Put UNIQ in array as well
read -ra UNIQ -d '' <<<"$UNIQ"

# If UNIQ is empty, do nothing and quit
if [[ "${#UNIQ[@]}" -eq 0 ]]; then
    exit
fi

# For every user in the UNIQ array, check if he/she's in the
# ARRNEW (needs to be written to the whitelist) or not (needs to
# be deleted)
for user in "${UNIQ[@]}"
do
    if [[ "${ARRNEW[@]}" =~ "$user" ]]; then
        curl -s -X POST \
        -H "Authorization: token $JHUB_TKN" \
        -H "Accept: application/json" \
        "$JHUB_API/users/$user" \
        && echo -e "\nCreated user $user"
    else
        curl -s -X DELETE \
        -H "Authorization: token $JHUB_TKN" \
        -H "Accept: application/json" \
        "$JHUB_API/users/$user" \
        && echo -e "\Deleted user $user"
    fi
done

3) init_updating.sh – The actual service run by JupyterHub.

#!/bin/bash

# This script is automatically run by JupyterHub as a service.
# (See bottom of "jupyterhub_config.py")

# Set the working directory to the path where the script is
cd "${0%/*}"

# First, delete JSONs that might exist from previous JHub instances.
# This is done to replace users that might have been accidently 
# deleted via JHubs's admin console.
# This will result in some warnings when you start the JupyterHub
# saying that users already exist. You can safely ignore these.
if [ ! -z "$(ls -A ./jsons)" ]; then
  rm ./jsons/*
fi

# Sleep 10 secs to suppress initial warnings because of Keycloak
# which might not be ready to accept API connections yet.
sleep 10

# Check every 5 seconds if there are new users that have to
# be added to JHub's whitelist or old ones that have to be
# deleted.
while true; do
    ./get_members_jhub_user.sh 
    ./update_whitelist.sh   
    sleep 5
done

4) Put the following in the jupyterhub_config.py:

c.JupyterHub.services = [    
    {
        'name': 'update-whitelist',
        'admin': True,
        'environment': {
          'KC_ADM_PASSWORD': os.environ.get('KC_ADM_PASSWORD'),
          'KC_ADM_API': os.environ.get('KC_ADM_API'),
          'KC_ROOT': os.environ.get('KC_ROOT'),
          'KC_REALM': os.environ.get('KC_REALM'),
          'KC_GRP_ID': os.environ.get('KC_GRP_ID'),
          'JHUB_API': os.environ.get('JHUB_API'),
        },
        'command': './services/init_updating.sh'.split()        
    }
]

Hope this helps!

clkao commented 6 years ago

You can create a user realm role mapper in the keycloak client, and enable auth_state. You can then override OIDCAuthenticator's authenticate method and check for the roles that you want to whitelist.

bellackn commented 6 years ago

Hey @clkao, thanks for your answer, this does sound much more elegant indeed... Would you mind describing in greater detail how to achieve this? I and potentially billions of other people would really appreciate it. :slightly_smiling_face:

gweis commented 6 years ago

something like this? https://github.com/ausecocloud/keycloakauthenticator/blob/master/keycloakauthenticator/auth.py

Note, this code exploits the fact, that Keycloak issues JWT as access token.

FCtj commented 5 years ago

Is there anyone dealing with this issue..? I want to ask your favor with 3 questions..

  1. How can I logout by clicking jupyterhub logout button? (with cookie or session deleted)

  2. How can I access directly to keycloak login page not seeing this page (url: [host]/hub/login) image

  3. How can I apply @gweis 's extension solution..?

gweis commented 5 years ago

see https://jupyterhub.readthedocs.io/en/stable/api/auth.html

c.Authenticator.auto_login = Bool(False)

setting this option to True will skip the extra login page.

ysy92618 commented 5 years ago

Thanks for responding back to me. I have already set c.Authenticator.auto_login=True.

Anyhow, we finally escalate this to our internal Okta expert yesterday and it turns out the c.GenericOAuthenticator.userdata_url = 'https://$OktaServer/oauth2/v1/clients' they provided us was incorrect. It looks that generic.py forces Jupyterhub to call the userdata_endpoint (they said that is usually an optional step) and the correct userdata end point should be the my company’s Okta userinfo_endpoint which has a special string between oath2 and v1.

Thanks, Shawn

From: gweis [mailto:notifications@github.com] Sent: Tuesday, February 19, 2019 4:04 PM To: jupyterhub/oauthenticator oauthenticator@noreply.github.com Cc: Subscribed subscribed@noreply.github.com Subject: Re: [jupyterhub/oauthenticator] Working configuration for generic authenticator with Keycloak (#107)

see https://jupyterhub.readthedocs.io/en/stable/api/auth.html

c.Authenticator.auto_login = Bool(False)

setting this option to True will skip the extra login page.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHubhttps://github.com/jupyterhub/oauthenticator/issues/107#issuecomment-465363024, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AtO3fGl04BaKam1fG3pAdlfWiQf-XonKks5vPJDwgaJpZM4O7ZQg.

ibraheemsadiq commented 5 years ago

Request URL: http://x.y.z/hub/oauth_login?response_type=code&redirect_uri=https%3A%2F%2F3.19.59%3A8443%2Fauth%2Frealms%2Fmaster%2Fprotocol%2Fopenid-connect%2Fauth&client_id=jupyterconnection&state=eyJzdGF0ZV9pZCI6ICI4M2FiZDAzMWY3Y2E0ZWUwOTU1ZWNlZDEyOTBmMTg5MyIsICJuZXh0X3VybCI6IG51bGx9

Error 302 i am not hitting keycloak login

saurav-bhagat commented 4 years ago

For OAuthenticator generic integration we need to add at jupyterhub_config.py: c.JupyterHub.authenticator_class = 'oauthenticator.generic.GenericOAuthenticator' c.OAuthenticator.client_id # oauth2 client id for your app c.OAuthenticator.client_secret # oauth2 client secret for your app c.GenericOAuthenticator.token_url # oauth2 provider's token url c.GenericOAuthenticator.userdata_url # oauth2 provider's endpoint with user data c.GenericOAuthenticator.userdata_method # method used to request user data endpoint c.GenericOAuthenticator.userdata_params # params to send for userdata endpoint c.GenericOAuthenticator.username_key # username key from json returned from user data endpoint

And export these environment variables: OAUTH2_TOKEN_URL # the same as token_url param OAUTH2_AUTHORIZE_URL # oauth2 provider's authorization andpoint

For more details I'll show how to integrate with generic authentication in https://conferences.oreilly.com/jupyter/jup-ny/public/schedule/detail/59400

If I'm doing this in docker container, then the environment variables should be set using Dockerfile right?

dmvieira commented 4 years ago

For OAuthenticator generic integration we need to add at jupyterhub_config.py: c.JupyterHub.authenticator_class = 'oauthenticator.generic.GenericOAuthenticator' c.OAuthenticator.client_id # oauth2 client id for your app c.OAuthenticator.client_secret # oauth2 client secret for your app c.GenericOAuthenticator.token_url # oauth2 provider's token url c.GenericOAuthenticator.userdata_url # oauth2 provider's endpoint with user data c.GenericOAuthenticator.userdata_method # method used to request user data endpoint c.GenericOAuthenticator.userdata_params # params to send for userdata endpoint c.GenericOAuthenticator.username_key # username key from json returned from user data endpoint And export these environment variables: OAUTH2_TOKEN_URL # the same as token_url param OAUTH2_AUTHORIZE_URL # oauth2 provider's authorization andpoint For more details I'll show how to integrate with generic authentication in https://conferences.oreilly.com/jupyter/jup-ny/public/schedule/detail/59400

If I'm doing this in docker container, then the environment variables should be set using Dockerfile right?

Yes, but how is your infrastructure?

saurav-bhagat commented 4 years ago

For OAuthenticator generic integration we need to add at jupyterhub_config.py: c.JupyterHub.authenticator_class = 'oauthenticator.generic.GenericOAuthenticator' c.OAuthenticator.client_id # oauth2 client id for your app c.OAuthenticator.client_secret # oauth2 client secret for your app c.GenericOAuthenticator.token_url # oauth2 provider's token url c.GenericOAuthenticator.userdata_url # oauth2 provider's endpoint with user data c.GenericOAuthenticator.userdata_method # method used to request user data endpoint c.GenericOAuthenticator.userdata_params # params to send for userdata endpoint c.GenericOAuthenticator.username_key # username key from json returned from user data endpoint And export these environment variables: OAUTH2_TOKEN_URL # the same as token_url param OAUTH2_AUTHORIZE_URL # oauth2 provider's authorization andpoint For more details I'll show how to integrate with generic authentication in https://conferences.oreilly.com/jupyter/jup-ny/public/schedule/detail/59400

If I'm doing this in docker container, then the environment variables should be set using Dockerfile right?

Yes, but how is your infrastructure?

Hey! How can I set this environment variables? Using Dockerfile?

dmvieira commented 4 years ago

Or in Dockerfile with ENV OAUTH2_TOKEN_URL https://YOUR_URL or using docker command docker run -e OAUTH2_TOKEN_URL='https://YOUR_URL'

saurav-bhagat commented 4 years ago

Thanks @dmvieira . Actually, I'm little confused regarding the setup of authentication with jupyterhub and my on-premise Oauth service.

Can you please help me out with this? Thanks

subhojitpaul1995 commented 4 years ago

Getting the same issue as @pfisterer after I configured Jupyterhub with Keycloak. The login mechanism is working fine, however the logout button always redirect to login URI (/hub/logout -> /hub/login) and then on clicking "Sign in using Keycloak" it directly opens home page without any authentication. I found that the keycloak logout API is not getting called from GenericOauthenticator & that's why the SSO user session is not getting terminated. keycloak logout URL :https://{ip}:{port}/auth/realms/jhub_realm/protocol/openid-connect/logout?redirect_uri= OAuthenticator configuration in jupyterhub_config.py: from oauthenticator.generic import GenericOAuthenticator c.JupyterHub.authenticator_class = GenericOAuthenticator c.GenericOAuthenticator.login_service = 'keycloak' c.GenericOAuthenticator.userdata_params = {"state": "state"} c.GenericOAuthenticator.auto_login = False c.Authenticator.auto_login = False

and exporter this env variables,

export OAUTH2_AUTHORIZE_URL=https://{ip}:{port}/auth/realms/jhub_realm/protocol/openid-connect/auth export OAUTH2_TOKEN_URL=https://{ip}:{port}/auth/realms/jhub_realm/protocol/openid-connect/token export OAUTH_CLIENT_SECRET=***** export OAUTH_CALLBACK_URL=http://{ip}:{port}/hub/oauth_callback export OAUTH2_USERDATA_URL=https://{ip}:{port}/auth/realms/jhub_realm/protocol/openid-connect/userinfo export OAUTH_CLIENT_ID=jupyter-hub export OAUTH2_USERNAME_KEY=preferred_username

Can anyone help me regarding this, how to call keycloak user session end point from jupyterhub GenericOauthenticator ?????

meeseeksmachine commented 4 years ago

This issue has been mentioned on Jupyter Community Forum. There might be relevant details there:

https://discourse.jupyter.org/t/generic-oauthenticator-redirect-issue/4078/5

hbuttguavus commented 4 years ago

Hi, I am also facing an issue with logging out. Once I sign in I am unable to log out unless I go to keycloak and end the session. Only then do I get prompted to enter the username and password again. I am using the juyterhub helm chart and have the following values which work except that I can not logout. Can anyone help identify what I might be missing? Thanks.

hub:
  extraEnv:
    OAUTH2_AUTHORIZE_URL: https://${host}/auth/realms/${realm}/protocol/openid-connect/auth
    OAUTH2_TOKEN_URL: https://${host}/auth/realms/${realm}/protocol/openid-connect/token
    OAUTH_CALLBACK_URL: https://<your_jupyterhub_host>/hub/oauth_callback
    OAUTH2_TLS_VERIFY: false
auth:
  type: custom
  custom:
    className: oauthenticator.generic.GenericOAuthenticator
    config:
      login_service: "Keycloak"
      client_id: "XXX"
      client_secret: "XXX"
      token_url: https://${host}/auth/realms/${realm}/protocol/openid-connect/token
      userdata_url: https://${host}/auth/realms/${realm}/protocol/openid-connect/userinfo
      userdata_method: GET
      userdata_params: {'state': 'state'}
      username_key: preferred_username
t20100 commented 4 years ago

If that can help, I got logout with keycloak working with oauthenticator 0.11.0 with the following code:

An authenticator with a LogoutHandler since the default one does not handles logout against authentication service:

from traitlets import Unicode
from oauthenticator.generic import GenericOAuthenticator
from jupyterhub.utils import url_path_join
from jupyterhub.handlers.login import LogoutHandler
from tornado.httputil import url_concat

class KeycloakLogoutHandler(LogoutHandler):
    """Logout handler for keycloak"""

    async def render_logout_page(self):
        params = {
            'redirect_uri': '%s://%s%s' % (
                self.request.protocol,
                self.request.host,
                self.hub.server.base_url),
            }
        self.redirect(
            url_concat(self.authenticator.keycloak_logout_url, params),
            permanent=False
        )

class KeycloakAuthenticator(GenericOAuthenticator):
    """Authenticator handling keycloak logout"""

    keycloak_logout_url = Unicode(
        config=True,
        help="The keycloak logout URL"
    )

    def get_handlers(self, app):
        return super().get_handlers(app) + [(r'/logout', KeycloakLogoutHandler)]

And then use the authenticator and logout URL configuration:

c.JupyterHub.authenticator_class = KeycloakAuthenticator
c.KeycloakAuthenticator.keycloak_logout_url = "https://example.com/auth/realms/REALM/protocol/openid-connect/logout"

I don't know how much that can be generalized, but when using keycloak that would be great to have the logout handled by default.

hbuttguavus commented 4 years ago

@t20100 Thank you! Really appreciate it. The code helped me understand what I needed to do and I was able to use it in the helm chart for jupyterhub.

manics commented 4 years ago

Hi, I'm closing this as it seems to have ended up covering several issues. There's a recent example of a working config on the Jupyter community forum: https://discourse.jupyter.org/t/configuring-zero-to-jupyter-hub-to-use-an-arbitrary-oauth2-provider/5542/4

If anyone has a concrete idea for improving the docs please open a PR 😀

Freir96 commented 4 years ago

All those advices helped me a lot, but when I try to use OAuth 2.0 I get this message from keycloak:

We are sorry... Invalid parameter: redirect_uri

And this is the log from keyclock server:

2020-10-29 10:34:58,025 WARN [org.keycloak.events] (default task-5) type=LOGIN_ERROR, realmId=master, clientId=account, userId=null, ipAddress=10.0.2.2, error=invalid_redirect_uri, redirect_uri=http://192.168.100.1:8600/hub/oauth_callback

jupyterhub_config.py:

` host_ip = '192.168.100.1' ip = host_ip keycloak_guest_port = '8180' keycloak_host_port = '8586' keycloak_port = keycloak_host_port c.JupyterHub.authenticator_class = GenericOAuthenticator

c.OAuthenticator.client_id = 'account'

c.OAuthenticator.client_secret = '5110687d-5c91-406d-8721-2f09b11e21fe'

c.GenericOAuthenticator.token_url = "http://" + ip + ":" +keycloak_port + "/auth/realms/master/protocol/openid-connect/token"

c.GenericOAuthenticator.userdata_url = "http://" + ip + ":" +keycloak_port + "/auth/realms/master/protocol/openid-connect/userinfo"

c.GenericOAuthenticator.userdata_method = 'GET'

c.GenericOAuthenticator.userdata_params = {'state': 'state'}

c.GenericOAuthenticator.username_key = 'admin' `

Enviromental variables:

export OAUTH2_TOKEN_URL=http://192.168.100.1:8586/auth/realms/master/protocol/openid-connect/token export OAUTH2_AUTHORIZE_URL=http://192.168.100.1:8586/auth/realms/master/protocol/openid-connect/auth export OAUTH_CALLBACK_URL=http://192.168.100.1:8600/hub/oauth_callback

Ports: ` config.vm.network "forwarded_port", guest: 8000, host: 8600

config.vm.network "forwarded_port", guest: 8001, host: 8601

config.vm.network "forwarded_port", guest: 8081, host: 8681

config.vm.network "forwarded_port", guest: 8180, host: 8586 `

As you can probably see, I'm not using docker, but Vagrant. Nothing I've found helped me solve this issue. Edit: I had to configure the keycloak client

Freir96 commented 3 years ago

@t20100 What is that Unicode() function? I can't find a unicode function with config param, and the logout does not work when I replace it with a string.

t20100 commented 3 years ago

It looks like from traitlets import Unicode was missing, I've added it to the sample code.

See traitlets.Unicode

Freir96 commented 3 years ago

Are you sure that this is everything? It does not seem to be working:

[E 2021-02-03 17:30:44.074 JupyterHub app:2859]
    Traceback (most recent call last):
      File "/usr/local/lib/python3.6/site-packages/jupyterhub/app.py", line 2856, in launch_instance_async
        await self.initialize(argv)
      File "/usr/local/lib/python3.6/site-packages/jupyterhub/app.py", line 2347, in initialize
        self.load_config_file(self.config_file)
      File "<decorator-gen-5>", line 2, in load_config_file
      File "/usr/local/lib/python3.6/site-packages/traitlets/config/application.py", line 87, in catch_config_error
        return method(app, *args, **kwargs)
      File "/usr/local/lib/python3.6/site-packages/traitlets/config/application.py", line 602, in load_config_file
        raise_config_file_errors=self.raise_config_file_errors,
      File "/usr/local/lib/python3.6/site-packages/traitlets/config/application.py", line 563, in _load_config_files
        config = loader.load_config()
      File "/usr/local/lib/python3.6/site-packages/traitlets/config/loader.py", line 457, in load_config
        self._read_file_as_dict()
      File "/usr/local/lib/python3.6/site-packages/traitlets/config/loader.py", line 489, in _read_file_as_dict
        py3compat.execfile(conf_filename, namespace)
      File "/usr/local/lib/python3.6/site-packages/ipython_genutils/py3compat.py", line 198, in execfile
        exec(compiler(f.read(), fname, 'exec'), glob, loc)
      File "/vagrant/modules/jupyterhub/jupyterhub_config.py", line 36, in <module>
        class KeycloakAuthenticator(GenericOAuthenticator):
      File "/vagrant/modules/jupyterhub/jupyterhub_config.py", line 40, in KeycloakAuthenticator
        help="The keycloak logout URL"
    TypeError: 'config' is an invalid keyword argument for this function
ric-art-m commented 3 years ago

In my jupyterhub_config.py i put the below settings to resolve the redirect issue

import os

os.environ['OAUTH2_TOKEN_URL'] = 'https://{domain}:{port}/auth/realms/{realm}/protocol/openid-connect/token'
os.environ['OAUTH2_AUTHORIZE_URL'] = 'https://{domain}:{port}/auth/realms/{realm}/protocol/openid-connect/auth'
os.environ['OAUTH2_USERDATA_URL'] = 'https://{domain}:{port}/auth/realms/{realm}/protocol/openid-connect/userinfo'

from oauthenticator.generic import GenericOAuthenticator
c.JupyterHub.authenticator_class = 'oauthenticator.generic.GenericOAuthenticator'
c.GenericOAuthenticator.login_service = 'Keycloak'
c.OAuthenticator.client_id = '{client_id}'
c.OAuthenticator.client_secret = '{client_secret}'
c.GenericOAuthenticator.oauth_callback_url = 'https://{hub}/jupyter/hub/oauth_callback'
c.GenericOAuthenticator.userdata_url = 'https://{domain}:{port}/auth/realms/{realm}/protocol/openid-connect/userinfo'
c.GenericOAuthenticator.token_url = 'https://{domain}:{port}/auth/realms/{realm}/protocol/openid-connect/token'
c.GenericOAuthenticator.userdata_method = 'GET'
c.GenericOAuthenticator.userdata_params = {"state": "state"}
c.GenericOAuthenticator.username_key = "preferred_username"

However i after successfull authentication from keycloak i got SSL error below

ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:877)

to temporary resolve it i add this settings

c.OAuthenticator.tls_verify = False

hope this helps.

jgato commented 3 years ago

Hi, I am also facing an issue with logging out. Once I sign in I am unable to log out unless I go to keycloak and end the session. Only then do I get prompted to enter the username and password again. I am using the juyterhub helm chart and have the following values which work except that I can not logout. Can anyone help identify what I might be missing? Thanks.

hub:
  extraEnv:
    OAUTH2_AUTHORIZE_URL: https://${host}/auth/realms/${realm}/protocol/openid-connect/auth
    OAUTH2_TOKEN_URL: https://${host}/auth/realms/${realm}/protocol/openid-connect/token
    OAUTH_CALLBACK_URL: https://<your_jupyterhub_host>/hub/oauth_callback
    OAUTH2_TLS_VERIFY: false
auth:
  type: custom
  custom:
    className: oauthenticator.generic.GenericOAuthenticator
    config:
      login_service: "Keycloak"
      client_id: "XXX"
      client_secret: "XXX"
      token_url: https://${host}/auth/realms/${realm}/protocol/openid-connect/token
      userdata_url: https://${host}/auth/realms/${realm}/protocol/openid-connect/userinfo
      userdata_method: GET
      userdata_params: {'state': 'state'}
      username_key: preferred_username

using you cool exmaple it worked like a charm, but logout is not working :( Did you find the solution?

MaisamMD commented 3 years ago

Hi everyone,

I am using the generic authenticator to integrate jupyterhub with an external OIDC provider. When I press the login button I will be redirected to the OIDC provider login page. When I log in using my credentials, I don't redirect back to the jupyterhub and I will stay on my page on the OIDC provider side. Any hint to tackle this issue? This is the part of my config file related to authentication :

c = get_config()
import os
# use Generic OAuthenticator for local users
from oauthenticator.generic import GenericOAuthenticator
c.Application.log_level = 'DEBUG'

c.JupyterHub.authenticator_class = GenericOAuthenticator
c.GenericOAuthenticator.client_id =  os.environ['IAM_CLIENT_ID']
c.GenericOAuthenticator.client_secret = os.environ['IAM_CLIENT_SECRET']
c.GenericOAuthenticator.token_url = os.environ['OAUTH2_TOKEN_URL']
c.GenericOAuthenticator.userdata_method = 'GET'
c.GenericOAuthenticator.userdata_params = {"state": "state"}
c.GenericOAuthenticator.login_service = 'ESCAPE IAM'
c.GenericOAuthenticator.oauth_callback_url = os.environ['OAUTH_CALLBACK_URL']
navdeepbansal commented 3 years ago

i am able to integrate keycloak with jupyterhub but still not able to figure it out how can we restrict users with particular keycloak role to access jupyterhub. Any leads would be appreciated.

navdeepbansal commented 3 years ago

Hey @clkao, thanks for your answer, this does sound much more elegant indeed... Would you mind describing in greater detail how to achieve this? I and potentially billions of other people would really appreciate it. 🙂

Hi, could you please let me know if you have got the solution ?

vitas commented 3 years ago

i am able to integrate keycloak with jupyterhub but still not able to figure it out how can we restrict users with particular keycloak role to access jupyterhub. Any leads would be appreciated.

me too, very important feature

geoffo-dev commented 2 years ago

Hello @navdeepbansal - are you able to post the solution?

As for group restriction - have you seen this?

https://github.com/jupyterhub/oauthenticator/tree/main/examples/generic - there is a section at the bottom about groups