Issue in custom OAuthenticator in jupyterhub

I’m using this guide to write oAuthenticator for jupyterhub. This is the code for that file:

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):
    _OAUTH_AUTHORIZE_URL = "https://<my_company>.com/adfs/oauth2/authorize/"
    _OAUTH_ACCESS_TOKEN_URL = "https://<my_company>.com/adfs/oauth2/token/"


class MyServiceLoginHandler(OAuthLoginHandler, MyServiceMixin):
    pass


class CustomOAuthenticator(OAuthenticator):

    login_service = "custom"

    login_handler = MyServiceLoginHandler

    async def authenticate(self, handler, data=None):
        code = handler.get_argument("code")
        http_client = AsyncHTTPClient()

        params = dict(
            client_id="<client_id>", 
            resource="resource_uri", 
            code=code ,
            redirect_uri="https://notebook.net/hub/oauth_callback"
        )

        url = url_concat("https://<mycompany>.com/adfs/oauth2/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))

        req = HTTPRequest(
            "https://<my_company>.com/adfs/userinfo",
            method="GET",
            headers={"Authorization": f"Bearer {access_token}"},
        )
        resp = await http_client.fetch(req)
        resp_json = json.loads(resp.body.decode('utf8', 'replace'))
        username = resp_json[""]

        if not username:
            return None

        user_info = {"name": username}
        user_info["auth_state"] = auth_state = {}
        auth_state['access_token'] = access_token
        auth_state['auth_reply'] = resp_json
        return user_info


class LocalCustomOAuthenticator(LocalAuthenticator, CustomOAuthenticator):
    pass

And this is the code in the configmap file:

auth:
      custom:
        className: CustomOAuth.CustomOAuthenticator
        config:
          login_service: "Custom"
          client_id: "<client_id"
          token_url: https://<my_company>.com/adfs/oauth2/token/
          userdata_url: https://<my_company>.com/adfs/userinfo
      admin:
        access: true
        users: null
      type: custom
      whitelist:
        users: null
    custom: {}
    debug:
      enabled: true
    hub:
      extraEnv:
        OAUTH2_AUTHORIZE_URL: https://<my_company>.com/adfs/oauth2/authorize/
        OAUTH2_TOKEN_URL: https://<my_company>/adfs/oauth2/token/
        OAUTH_CALLBACK_URL: https://notebook.net/hub/oauth_callback

When I click on the sign in button, it redirects me to the callback url, and some cookies are also set. BUT I get 500 Internal error.
This is the error log:

Traceback (most recent call last):
      File "/usr/local/lib64/python3.6/site-packages/tornado/web.py", line 1703, in _execute
        result = await result
      File "/usr/local/lib/python3.6/site-packages/oauthenticator/oauth2.py", line 182, in get
        user = yield self.login_user()
      File "/usr/local/lib/python3.6/site-packages/jupyterhub/handlers/base.py", line 483, in login_user
        authenticated = await self.authenticate(data)
      File "/usr/local/lib/python3.6/site-packages/jupyterhub/auth.py", line 257, in get_authenticated_user
        authenticated = await maybe_future(self.authenticate(handler, data))
      File "/usr/local/bin/IDAnywhereOAuth.py", line 84, in authenticate
        resp_json = json.loads(dataform)
      File "/usr/lib64/python3.6/json/__init__.py", line 354, in loads
        return _default_decoder.decode(s)
      File "/usr/lib64/python3.6/json/decoder.py", line 339, in decode
        obj, end = self.raw_decode(s, idx=_w(s, 0).end())
      File "/usr/lib64/python3.6/json/decoder.py", line 357, in raw_decode
        raise JSONDecodeError("Expecting value", s, err.value) from None
    json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

As the resp which I am getting is not a JSON but a html string.
Is there any problem in my code or setup?

I faced this issue as well.

You will need to remove the MyServiceMixin from MyServiceLoginHandler and directly pass in your URLs into it, as follows:

class MyServiceLoginHandler(OAuthLoginHandler):
     _OAUTH_AUTHORIZE_URL = "https://<my_company>.com/adfs/oauth2/authorize/"
    _OAUTH_ACCESS_TOKEN_URL = "https://<my_company>.com/adfs/oauth2/token/"

I spent about couple of hours debugging this, I think the issue was that even MyServiceLoginHandler extends OAuth2Mixin class hence it is setting the default URL for callback.

Let me know if that works!

Rohit

Hi rohit, Thanks for the reply.
I did what you suggested. But now I’m getting this error:

AttributeError: ‘MyServiceLoginHandler’ object has no attribute ‘authorize_redirect’

I’m not sure why that would happen.

Can you try extending GenericOAuthenticator instead of OAuthenticator

from oauthenticator.generic import GenericOAuthenticator
class CustomOAuthenticator(GenericOAuthenticator):
   ...

I have a similar issue but with .fetch method. If you are using TLJH deployment, I suspect this issue is caused by the older version of oauthenticator. For details refer to TLJH source code installer.py here. Honestly, I do not understand why they are still using such an old version(0.10.0) of
oauthenticator as the current version is 14.1.0.