Writing your own OAuthenticator

There are two ways to write your own OAuthenticator.

Using GenericOAuthenticator

The first and simplest is to use GenericOAuthenticator and configuration to set the necessary configuration variables.

  • client_id

  • client_secret

  • login_service

  • userdata_url

  • token_url

  • username_key

Example config:

c.JupyterHub.authenticator_class = "generic"

c.GenericOAuthenticator.oauth_callback_url = 'https://{host}/hub/oauth_callback'
c.GenericOAuthenticator.client_id = 'OAUTH-CLIENT-ID'
c.GenericOAuthenticator.client_secret = 'OAUTH-CLIENT-SECRET-KEY'
c.GenericOAuthenticator.login_service = 'name-of-service-provider'
c.GenericOAuthenticator.userdata_url = 'url-retrieving-user-data-with-access-token'
c.GenericOAuthenticator.token_url = 'url-retrieving-access-token-oauth-completion'
c.GenericOAuthenticator.username_key = 'username-key-for-USERDATA-URL'

Checkout Moodle Setup and Yandex Setup for how to configure GenericOAuthenticator for Moodle and Yandex.

Writing your own OAuthenticator class

If you want more advanced features and customization beyond the basics of OAuth, you can write your own full OAuthenticator subclass, which enables more detailed customization login and logout actions.

The skeleton of an OAuthenticator looks like this:

"""
Example OAuthenticator to use with My Service
"""


import json

from jupyterhub.auth import LocalAuthenticator
from oauthenticator.oauth2 import OAuthLoginHandler, OAuthenticator
from tornado.auth import OAuth2Mixin
from tornado.httputil import url_concat
from tornado.httpclient import HTTPRequest, AsyncHTTPClient, HTTPError


class MyServiceMixin(OAuth2Mixin):
    # authorize is the URL users are redirected to to authorize your service
    _OAUTH_AUTHORIZE_URL = "https://myservice.biz/login/oauth/authorize"
    # token is the URL JupyterHub accesses to finish the OAuth process
    _OAUTH_ACCESS_TOKEN_URL = "https://myservice.biz/login/oauth/access_token"


class MyServiceLoginHandler(OAuthLoginHandler, MyServiceMixin):
    pass


class GitHubOAuthenticator(OAuthenticator):

    # login_service is the text displayed on the "Login with..." button
    login_service = "My Service"

    login_handler = MyServiceLoginHandler

    async def authenticate(self, handler, data=None):
        """We set up auth_state based on additional GitHub info if we
        receive it.
        """
        code = handler.get_argument("code")
        # TODO: Configure the curl_httpclient for tornado
        http_client = AsyncHTTPClient()

        # Exchange the OAuth code for an Access Token
        # this is the TOKEN URL in your provider

        params = dict(
            client_id=self.client_id, client_secret=self.client_secret, code=code
        )

        url = url_concat("https://myservice.biz/login/oauth/access_token", params)

        req = HTTPRequest(
            url, method="POST", headers={"Accept": "application/json"}, body=''
        )

        resp = await http_client.fetch(req)
        resp_json = json.loads(resp.body.decode('utf8', 'replace'))

        if 'access_token' in resp_json:
            access_token = resp_json['access_token']
        elif 'error_description' in resp_json:
            raise HTTPError(
                403,
                "An access token was not returned: {}".format(
                    resp_json['error_description']
                ),
            )
        else:
            raise HTTPError(500, "Bad response: %s".format(resp))

        # Determine who the logged in user is
        # by using the new access token to make a request
        # check with your OAuth provider for this URL.
        # it could also be in the response to the token request,
        # making this request unnecessary.

        req = HTTPRequest(
            "https://myservice.biz/api/user",
            method="GET",
            headers={"Authorization": f"Bearer {access_token}"},
        )
        resp = await http_client.fetch(req)
        resp_json = json.loads(resp.body.decode('utf8', 'replace'))

        # check the documentation for what field contains a unique username
        # it might not be the 'username'!
        username = resp_json["username"]

        if not username:
            # return None means that no user is authenticated
            # and login has failed
            return None

        # here we can add additional checks such as against team whitelists
        # if the OAuth provider has such a concept

        # 'name' is the JupyterHub username
        user_info = {"name": username}

        # We can also persist auth state,
        # which is information encrypted in the Jupyter database
        # and can be passed to the Spawner for e.g. authenticated data access
        # these fields are up to you, and not interpreted by JupyterHub
        # see Authenticator.pre_spawn_start for how to use this information
        user_info["auth_state"] = auth_state = {}
        auth_state['access_token'] = access_token
        auth_state['auth_reply'] = resp_json

        return user_info


class LocalGitHubOAuthenticator(LocalAuthenticator, GitHubOAuthenticator):
    """A version that mixes in local system user creation"""

    pass

where you will need to find and define the URLs and requests necessary to complete OAuth with your provider.