Skip to content

Commit

Permalink
Merge pull request jupyter#134 from davidedelvento/master
Browse files Browse the repository at this point in the history
Making possible to specify custom claims for username in jupytherhub_config.py
  • Loading branch information
minrk authored Oct 13, 2017
2 parents a357db1 + e227ee7 commit 1168eb3
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 44 deletions.
69 changes: 36 additions & 33 deletions oauthenticator/cilogon.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,23 @@
Uses OAuth 2.0 with cilogon.org (override with CILOGON_HOST)
Based on the GitHub plugin.
Most of the code c/o Kyle Kelley (@rgbkrk)
CILogon support by Adam Thornton (athornton@lsst.org)
Caveats:
- For user whitelist/admin purposes, username will be the sub claim. This
is unlikely to work as a Unix userid. Typically an actual implementation
will specify the identity provider and scopes sufficient to retrieve an
ePPN or other unique identifier more amenable to being used as a username.
- For user whitelist/admin purposes, username will be the ePPN by default.
This is typically an email address and may not work as a Unix userid.
Normalization may be required to turn the JupyterHub username into a Unix username.
- Default username_claim of ePPN does not work for all providers,
e.g. generic OAuth such as Google.
Use `c.CILogonOAuthenticator.username_claim = 'email'` to use
email instead of ePPN as the JupyterHub username.
"""


import json
import os

from tornado.auth import OAuth2Mixin
from tornado import gen
from tornado import gen, web

from tornado.httputil import url_concat
from tornado.httpclient import HTTPRequest, AsyncHTTPClient
Expand All @@ -35,16 +32,6 @@
CILOGON_HOST = os.environ.get('CILOGON_HOST') or 'cilogon.org'


def _api_headers():
return {"Accept": "application/json",
"User-Agent": "JupyterHub",
}


def _add_access_token(access_token, params):
params["access_token"] = access_token


class CILogonMixin(OAuth2Mixin):
_OAUTH_AUTHORIZE_URL = "https://%s/authorize" % CILOGON_HOST
_OAUTH_TOKEN_URL = "https://%s/oauth2/token" % CILOGON_HOST
Expand All @@ -59,6 +46,7 @@ def authorize_redirect(self, *args, **kwargs):
extra_params["selected_idp"] = self.authenticator.idp
if self.authenticator.skin:
extra_params["skin"] = self.authenticator.skin

return super().authorize_redirect(*args, **kwargs)


Expand All @@ -69,10 +57,10 @@ class CILogonOAuthenticator(OAuthenticator):
client_secret_env = 'CILOGON_CLIENT_SECRET'
login_handler = CILogonLoginHandler

scope = List(Unicode(), default_value=['openid'],
scope = List(Unicode(), default_value=['openid', 'email', 'org.cilogon.userinfo'],
config=True,
help="""The OAuth scopes to request.
See cilogon_scope.md for details.
At least 'openid' is required.
""",
Expand All @@ -83,7 +71,7 @@ def _validate_scope(self, proposal):
if 'openid' not in proposal.value:
return ['openid'] + proposal.value
return proposal.value

idp = Unicode(
config=True,
help="""The `idp` attribute is the SAML Entity ID of the user's selected
Expand All @@ -101,6 +89,18 @@ def _validate_scope(self, proposal):
Contact help@cilogon.org to request a custom skin.
""",
)
username_claim = Unicode(
"eppn",
config=True,
help="""The claim in the userinfo response from which to get the JupyterHub username
Examples include: eppn, email
What keys are available will depend on the scopes requested.
See http://www.cilogon.org/oidc for details.
""",
)

@gen.coroutine
def authenticate(self, handler, data=None):
Expand All @@ -113,7 +113,11 @@ def authenticate(self, handler, data=None):

# Exchange the OAuth code for a CILogon Access Token
# See: http://www.cilogon.org/oidc
headers = _api_headers()
headers = {
"Accept": "application/json",
"User-Agent": "JupyterHub",
}

params = dict(
client_id=self.client_id,
client_secret=self.client_secret,
Expand All @@ -140,16 +144,15 @@ def authenticate(self, handler, data=None):
CILOGON_HOST, params),
headers=headers
)
self.log.info("REQ: %s / %r" % (str(req), req))
resp = yield http_client.fetch(req)
resp_json = json.loads(resp.body.decode('utf8', 'replace'))

self.log.info(json.dumps(resp_json, sort_keys=True, indent=4))

if "sub" not in resp_json or not resp_json["sub"]:
return None
username = resp_json["sub"]
# username is now the CILogon "sub" claim. This is not ideal.
username = resp_json.get(self.username_claim)
if not username:
self.log.error("Username claim %s not found in the response: %s",
self.username_claim, sorted(resp_json.keys())
)
raise web.HTTPError(500, "Failed to get username from CILogon")
userdict = {"name": username}
# Now we set up auth_state
userdict["auth_state"] = auth_state = {}
Expand All @@ -162,7 +165,7 @@ def authenticate(self, handler, data=None):
return userdict


class LocalGitHubOAuthenticator(LocalAuthenticator, CILogonOAuthenticator):
class LocalCILogonOAuthenticator(LocalAuthenticator, CILogonOAuthenticator):

"""A version that mixes in local system user creation"""
pass
14 changes: 3 additions & 11 deletions oauthenticator/tests/test_cilogon.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import re
import functools
import json
from io import BytesIO

from pytest import fixture, mark
from urllib.parse import urlparse, parse_qs
from tornado.httpclient import HTTPRequest, HTTPResponse
from tornado.httputil import HTTPHeaders

from ..cilogon import CILogonOAuthenticator

Expand All @@ -16,7 +10,7 @@
def user_model(username):
"""Return a user model"""
return {
'sub': 'http://cilogon.org/dinosaurs/users/%s/1729' % username,
'eppn': username + '@serenity.space',
}


Expand All @@ -38,12 +32,10 @@ def test_cilogon(cilogon_client):
user_info = yield authenticator.authenticate(handler)
print(json.dumps(user_info, sort_keys=True, indent=4))
name = user_info['name']
assert name == 'http://cilogon.org/dinosaurs/users/wash/1729'
assert name == 'wash@serenity.space'
auth_state = user_info['auth_state']
assert 'access_token' in auth_state
assert auth_state == {
'access_token': auth_state['access_token'],
'cilogon_user': {
'sub': name
}
'cilogon_user': user_model('wash'),
}

0 comments on commit 1168eb3

Please sign in to comment.