jupyterhub / oauthenticator

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

[Auth0] Using Auth0 usernames instead of email addresses #266

Closed JuanCab closed 3 years ago

JuanCab commented 5 years ago

I noticed that https://github.com/jupyterhub/oauthenticator/blob/master/oauthenticator/auth0.py appears to hardcode use of email address as the username when using Auth0. Is there a way to allow Auth0 USERNAME as the JupyterHub username? It would make my life a lot easier to not have usernames locked to email addresses.

minrk commented 5 years ago

Definitely possible, just need to add a switch. It seems like this should have been the default, but it's hard to change defaults without causing pain for existing users. Check out username_key in generic.py for a configurable source of username from the OAuth response.

JuanCab commented 5 years ago

I should note I am dealing with this in the context of TLJH and not a full JupyterHub installation.

I am not an OAuth expert, just a professor trying to piece together a solution for a class server. As far as I can interpret, username_key is a traitlet (which I partly understand the concept of) which can contain a string, the default being 'username'. Is this supposed to allow me to change what part of the response JSON the generic oauthenticator interprets as the username?

I believe (if I understand the documentation well enough) the Auth0 OAuth authenticator responds with preferred_username as the username, so do I just have to set the username_key traitlet value in the Jupyter config and then I will get the proper username instead of email address as the username?

JuanCab commented 5 years ago

OK, I attempted to set up the generic Oauth authenticator, using the following settings (these are from TLJH config):

auth:
  type: oauthenticator.generic.GenericOAuthenticator
  GenericOAuthenticator:
    client_id: [REMOVED]
    client_secret: [REMOVED]
    oauth_callback_url: https://jupyter.mnstate.edu/hub/oauth_callback
    token_url: https://msumjupyter.auth0.com/oauth/token

and this just seems to generate a ERR_TOO_MANY_REDIRECTS in Chrome. :(

I also, as a brute force approach, tried to edit my auth0.py file and replaced 'name': resp_json["email”] with 'name': resp_json["username”] but that triggered a 500 Server error, the TLJH logs indicating that this is because

File "/opt/tljh/hub/lib/python3.6/site-packages/oauthenticator/auth0.py", line 99, in authenticate
 'name': resp_json["username"],
 KeyError: 'username'

I would really appreciate if someone can find some way to use Auth0 usernames as the account names for JupyterHub.

metasim commented 5 years ago

We're having the same problem as well.

jsjohnstone commented 5 years ago

Writing this out after working through this myself...

You'll need to check that your scope actually includes the username, and that auth0 is returning a username (which will depend on what you're using to authenticate with auth0 itself). A scope defines what information will be returned about the user.

There are three 'scopes' available with auth0: openid, email and profile. It's likely you're using the first two and need to add 'profile' by specifying this in your jupyterhub_config.py:

c.Auth0OAuthenticator.scope = ['openid', 'email', 'profile']

This will expose these fields in the auth0 userinfo response: name, family_name, given_name, middle_name, nickname, picture, and updated_at

From my brief testing with auth0's built-in user database (as opposed to connecting to a third-party auth platform), the 'username' field appears to be exposed as 'nickname' in the /userinfo response:

{"sub": "auth0|12345", "nickname": "myusernamehere", "name": "myemail@email.com", "picture": "", "updated_at": "2019-11-07T13:33:58.827Z", "email": "myemail@email.com", "email_verified": false}

...and so, after including the 'profile' scope in jupyterhub_config.py, I could reference this username in auth0.py by replacing email with 'nickname':

        return {
            'name': resp_json["nickname"],
            'auth_state': {
                'access_token': access_token,
                'auth0_user': resp_json,
            }
        }

If you're using a different source for authentication (e.g. Active Directory, Google, Facebook), it's possible you'll need to do some playing to see if the username is exposed in the 'profile' scope, or if you need to expose it manually using auth0 rules (which is a whole other game).

To help see the actual response JupyterHub is getting from auth0, I found it easy to just dump this to a file by adding this code...

        with open('/home/jamie/auth0data.json', 'w') as f:
             json.dump(resp_json, f)

...after:

        # Determine who the logged in user is
        headers={"Accept": "application/json",
                 "User-Agent": "JupyterHub",
                 "Authorization": "Bearer {}".format(access_token)
        }
    req = HTTPRequest("https://%s.auth0.com/userinfo" % AUTH0_SUBDOMAIN,
                          method="GET",
                          headers=headers
                          )
        resp = await http_client.fetch(req)
        resp_json = json.loads(resp.body.decode('utf8', 'replace'))

...which just dumps the json response into a file in my home directory.

More reading: https://auth0.com/docs/scopes/current/oidc-scopes

Best of luck!