Oauth2 with edu-id

Hello,

I would like to implement oauth authentication with edu-id. edu-id supports openIDConnect OpenID Connect - For services - Documentation - SWITCH edu-ID - SWITCH Help


I was wondering where I can find more information and an example of such a custom oauth implementation. The only thing I could find was a slide from one of the jupyterhub presentations by mnirk (screenshot attached). I asked chatgpt to complete this example and it provided the below code. Would you have any comments? I appreciate it if you can provide me an example or more information on this.

from tornado.auth import OAuth2Mixin
from jupyterhub.handlers import BaseHandler
from jupyterhub.auth import Authenticator
from tornado import gen
import json

class MyServiceMixin(OAuth2Mixin):
    _OAUTH_AUTHORIZE_URL = "https://myservice.horse/login/oauth/authorize"
    _OAUTH_ACCESS_TOKEN_URL = "https://myservice.horse/login/oauth/access_token"

class MyServiceLoginHandler(BaseHandler, MyServiceMixin):
    @gen.coroutine
    def get(self):
        if self.get_argument("code", False):
            user = yield self.get_authenticated_user(
                redirect_uri=self.settings["oauth_redirect_uri"],
                code=self.get_argument("code"))
            
            # You might want to store the user data in a more robust way
            self.set_login_cookie(user)
            self.redirect(self.get_next_url())
        else:
            yield self.authorize_redirect(
                redirect_uri=self.settings["oauth_redirect_uri"],
                client_id=self.settings["my_service_client_id"],
                scope=["openid", "profile"],
                response_type="code",
                extra_params={"approval_prompt": "auto"}) 

class MyServiceOAuthenticator(Authenticator):
    login_service = "My Service"
    login_handler = MyServiceLoginHandler

    @gen.coroutine
    def authenticate(self, handler, data=None):
        code = handler.get_argument("code", False)
        info = yield self.ask_my_service_about(code)
        return info['username']

    @gen.coroutine
    def ask_my_service_about(self, code):
        # Here you should implement the logic to exchange code for user info
        # and return the user info as a dictionary
        # You might use Tornado's HTTP client to make requests
        # to the my service's endpoints
        
        # For example:
        token_response = yield self.fetch(
            self._OAUTH_ACCESS_TOKEN_URL,
            method="POST",
            body="grant_type=authorization_code&"
                 f"code={code}&"
                 f"client_id={self.settings['my_service_client_id']}&"
                 f"client_secret={self.settings['my_service_client_secret']}&"
                 f"redirect_uri={self.settings['oauth_redirect_uri']}"
        )
        
        access_token = json.loads(token_response.body)["access_token"]

        user_info_response = yield self.fetch(
            "https://myservice.horse/api/userinfo",
            headers={"Authorization": f"Bearer {access_token}"}
        )

        user_info = json.loads(user_info_response.body)
        return user_info

Here is the answer from Erik Sundell oauth with edu-id · Issue #672 · jupyterhub/oauthenticator · GitHub but I would still need help (if possible a similar example with custom oauth authentication).

best
B

It seems the answer should be closely related to the below part in Authentication — Zero to JupyterHub with Kubernetes 0.0.1-set.by.chartpress documentation

but how can I reformat the below code snippet and add it to the jupyterhub_config.py?

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
auth:
  type: custom
  custom:
    className: oauthenticator.generic.GenericOAuthenticator
    config:
      login_service: "keycloak"
      client_id: "y0urc1logonc1ient1d"
      client_secret: "an0ther1ongs3cretstr1ng"
      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

Have a look at the OAuthenticator docs, which includes an OIDC example:
https://oauthenticator.readthedocs.io/en/latest/tutorials/provider-specific-setup/providers/generic.html#tutorials-provider-specific-generic

Hi! I’m doing something similar, you can use the OIDC examples. In my case I am using UNIGE rather than Switch Edu-ID but the principle is the same. However I am having problems because ADFS returns mismatched UserInfo endpoints, which means you need to set the resource parameter as seen here proxmox - Setting up OIDC with ADFS - Invalid UserInfo Request - Server Fault

But I don’t see yet how to do this because I can’t even find out where authorize_url is used.

Update: I figured out that you need to define resource=urn:microsoft:userinfo for the inital authorize request (not the token request). I found the (somewhat hidden) extra_authorize_params setting so now I have c.GenericOAuthenticator.extra_authorize_params = {"resource": "urn:microsoft:userinfo"} and it works!