There are two ways to write your own OAuthenticator.
The first and simplest is to use GenericOAuthenticator and configuration to set the necessary configuration variables.
GenericOAuthenticator
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.
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.