Skip to content

Instantly share code, notes, and snippets.

@danizen
Created March 15, 2018 23:06
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save danizen/78111676530738fcbca8d8ad87c56690 to your computer and use it in GitHub Desktop.
Save danizen/78111676530738fcbca8d8ad87c56690 to your computer and use it in GitHub Desktop.
MultiAuthenticator for jupyterhub
from urllib.parse import urlsplit
from tornado import gen
from tornado.escape import url_escape
from tornado.httputil import url_concat
from traitlets import (
Unicode, Integer, Dict, TraitError, List, Bool, Any,
Type, Set, Instance, Bytes, Float,
observe, default,
)
from jupyterhub.auth import Authenticator
from jupyterhub.handlers.login import LoginHandler, LogoutHandler
from oauthenticator.google import GoogleOAuthenticator, GoogleLoginHandler, GoogleOAuthHandler
from oauthenticator.github import GitHubOAuthenticator, GitHubLoginHandler
from jhub_cas_authenticator.cas_auth import CASAuthenticator, CASLoginHandler
import pdb
class MultiLoginHandler(LoginHandler):
def _render(self, login_error=None):
"""
Mainly changes the template, also simplify a bit
"""
context = {
'google': {
'enabled': self.authenticator.enable_google,
},
'github': {
'enabled': self.authenticator.enable_github,
},
'nih': {
'enabled': self.authenticator.enable_nih,
},
}
nextval = self.get_argument('next', default='')
return self.render_template('nlm/login.html',
next=url_escape(nextval),
multiauth=context,
login_error=login_error,
authenticator_login_url=url_concat(
self.authenticator.login_url(self.hub.base_url),
{'next': nextval},
),
)
@gen.coroutine
def get(self):
"""
Simplify rendering as there is no username
"""
self.statsd.incr('login.request')
user = self.get_current_user()
if user:
# set new login cookie
# because single-user cookie may have been cleared or incorrect
self.set_login_cookie(self.get_current_user())
self.redirect(self.get_next_url(user), permanent=False)
else:
self.finish(self._render())
@gen.coroutine
def post(self):
"""
Redirect to the handler for the appropriate oauth selected
"""
concat_data = {
'next': self.get_argument('next', ''),
}
if self.authenticator.enable_google and self.get_argument('login_google', None):
login_url = '{}://{}{}google/login'.format(self.request.protocol, self.request.host, self.hub.base_url)
self.redirect(url_concat(login_url, concat_data))
elif self.authenticator.enable_github and self.get_argument('login_github', None):
login_url = '{}://{}{}github/login'.format(self.request.protocol, self.request.host, self.hub.base_url)
self.redirect(url_concat(login_url, concat_data))
elif self.authenticator.enable_nih and self.get_argument('login_nih', None):
login_url = '{}://{}{}nih/login'.format(self.request.protocol, self.request.host, self.hub.base_url)
self.redirect(url_concat(login_url, concat_data))
else:
html = self._render(login_error='Unknown or missing authenticator')
self.finish(html)
class MultiLogoutHandler(LogoutHandler):
pass
class MultiAuthenticator(Authenticator):
enable_google = Bool(True, help='Enable oauth2 authorization to google').tag(config=True)
enable_github = Bool(True, help='Enable oauth2 authorization to github').tag(config=True)
enable_nih = Bool(True, help='Enable CAS/SAML authentication to NIH Login').tag(config=True)
google_class = Type(GoogleOAuthenticator, Authenticator, help='Must be an authenticator').tag(config=True)
github_class = Type(GitHubOAuthenticator, Authenticator, help='Must be an authenticator').tag(config=True)
nih_class = Type(CASAuthenticator, Authenticator, help='Must be an authenticator').tag(config=True)
google_authenticator = Instance(Authenticator)
github_authenticator = Instance(Authenticator)
nih_authenticator = Instance(Authenticator)
@default('google_authenticator')
def _default_google_authenticator(self):
return self.google_class()
@default('github_authenticator')
def _default_github_authenticator(self):
return self.github_class()
@default('nih_authenticator')
def _default_nih_authenticator(self):
return self.nih_class()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__client_id = None
self.__client_secret = None
self.__scope = None
@property
def client_id(self):
return self.__client_id
@property
def client_secret(self):
return self.__client_secret
@property
def scope(self):
return self.__scope
def set_oauth_tokens(self, subauth):
"""
Caches configured information from the subauthenticator in properties
"""
self.__client_id = subauth.client_id
self.__client_secret = subauth.client_secret
self.__scope = subauth.scope
def get_callback_url(self, handler=None):
"""
This is called by oauth2, it thinks that there will just be one
"""
pdb.set_trace()
if handler is None:
raise ValueError("MultiAuthenticator only works with a handler")
if isinstance(handler, GoogleLoginHandler):
self.set_oauth_tokens(self.google_authenticator)
if self.google_authenticator.oauth_callback_url:
return self.google_authenticator.oauth_callback_url
if isinstance(handler, GitHubLoginHandler):
self.set_oauth_tokens(self.github_authenticator)
if self.config.github_authenticator.oauth_callback_url:
return self.config.github_authenticator.oauth_callback_url
parts = urlsplit(handler.request.uri)
callback_url = '{}://{}{}'.format(
handler.request.protocol,
handler.request.host,
parts.path.replace('/login', '/callback')
)
return callback_url
def validate_username(self, username):
return super().validate_username(username)
def normalize_username(self, username):
return super().normalize_username(username)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get_handlers(self, app):
h = [
('/login', MultiLoginHandler),
('/logout', MultiLogoutHandler),
]
if self.enable_google:
handlers = dict(self.google_authenticator.get_handlers(app))
h.extend([
('/google/login', handlers['/oauth_login']),
('/google/callback', handlers['/oauth_callback'])
])
if self.enable_github:
handlers = dict(self.github_authenticator.get_handlers(app))
h.extend([
('/github/login', handlers['/oauth_login']),
('/github/callback', handlers['/oauth_callback']),
])
if self.enable_nih:
handlers = dict(self.nih_authenticator.get_handlers(app))
h.extend([
('/nih/login', handlers['/login']),
])
return h
@gen.coroutine
def authenticate(self, handler, data):
"""
Delegate authentication to the appropriate authenticator
"""
pdb.set_trace()
if isinstance(handler, GoogleOAuthHandler):
return self.google_authenticator.authenticate(handler, data)
elif isinstance(handler, CASLoginHandler):
return self.nih_authenticator.authenticate(handler, data)
else:
return self.github_authenticator.authenticate(handler, data)
@ponomarevsy
Copy link

Hi @danizen! How do I combine LDAP authentication with local /etc/password accounts in Jupyterhub? Is there a solution out there, do you know? Thank you!

@MRichards99
Copy link

@danizen how do you configure this in the JupyterHub config file?

@danizen
Copy link
Author

danizen commented Sep 19, 2019

@danizen how do you configure this in the JupyterHub config file?

I don't remember exactly, but you need to create an App registration for whichever OAuth authenticators you want to create, and copy the application client id and secret into the config file.

@danizen
Copy link
Author

danizen commented Sep 19, 2019

Hi @danizen! How do I combine LDAP authentication with local /etc/password accounts in Jupyterhub? Is there a solution out there, do you know? Thank you!

A better solution will be to use the LDAP authentication indirectly, through some sort of OAuth2 or Saml configuration. LDAP Authentication directly is no longer considered as secure.

I would avoid combining local /etc/passwd authentication with anything else - if you are going to do that, then you are going to be adding the other things through PAM (Pluggable Authentication Module) at the Linux system admin. level, and that is not what I would recommend here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment