-
Notifications
You must be signed in to change notification settings - Fork 367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Would it be possible to have multiple authentication methods simultaneously? #136
Comments
This should be possible, but it is not ready to go at the moment. A |
Thank you very much for your suggestion :) |
I would like to have this feature as well, even just for the authentication classes in |
Same here. Our project also needs more than one ways to do the authentication in different situations. Thanks! |
I'd like to second this... We use FreeIPA for our team, and LDAP works fine there, but our parent company uses Google Apps. Being able to auth with either of these would be awesome. I'm looking at how it might work, but has anyone else made any progress with this? Cheers! |
Might be worth checking out https://www.keycloak.org/ as well as investigating a |
In addition to Keycloak, there is also Gluu (http://gluu.org). Also, "a quick and ready-to-use solution off the top of my head seems to be using JupyterHub Globus authenticator, which offers Globus, OpenID, Google, SAML/CILogon and E-mail authentication in a single package. Unfortunately, it is far from ideal due to missing GitHub/GitLab integration and adding Globus infrastructure dependency" (from my post on Gitter last night). @betatim For |
+1 for this |
we extended the snippet @ablekh shared above |
@zhiyuli Nice work, though a more detailed README would be appreciated. However, it seems that your approach only covers OAuth-based authenticators, whereas the original code (that I have linked above) is more generic, allowing for any authenticators (assuming it's relevantly updated). Am I correct on this? |
@ablekh sorry for the rushed readme. For the project I am working on, we only need to use OAuthenticators for now... and Yes, the snippet you shared seems to support any type of authenticator as the basic idea is to relay the call to the actual authenticators underneath the multiauthenticator. |
@zhiyuli No need to apologize - your work is much appreciated (but please ping me, if you update the description). Thank you for prompt clarifications. I just wanted to make sure that my understanding of your code is correct and I'm not missing anything relevant. I still plan to further review both the original and your code for better understanding. Thank you! |
@ablekh enriched the readme a little bit |
@zhiyuli Thank you very much for letting me know. Nice work. I still think that using Keycloak would be the optimal approach, since (based on my limited research) it is both comprehensive and flexible. I still plan to explore this path further. Have you had a chance to look at / play with / explore Keycloak? |
I got this work with the latest jupyterhub, not sure if it is the right way to do it but it just worked for me. from traitlets import List
from jupyterhub.auth import Authenticator
class PackedAuthenticator(Authenticator):
authenticators = List(help="The subauthenticators to use", config=True)
def __init__(self, *arg, **kwargs):
super().__init__(*arg, **kwargs)
self._authenticators = []
for authenticator_klass, url_scope, configs in self.authenticators:
self._authenticators.append({
'instance': authenticator_klass(**configs),
'url_scope': url_scope
})
async def authenticate(self, handler, data):
"""Using the url of the request to decide which authenticator
is responsible for this task.
"""
return self._get_responsible_authenticator(handler).authenticate(handler, data)
def get_callback_url(self, handler):
return self._get_responsible_authenticator(handler).get_callback_url()
def _get_responsible_authenticator(self, handler):
responsible_authenticator = None
for authenticator in self._authenticators:
if handler.request.path.find(authenticator['url_scope']) != -1:
responsible_authenticator = authenticator
break
return responsible_authenticator['instance']
def get_handlers(self, app):
routes = []
for authenticator in self._authenticators:
handlers = authenticator['instance'].get_handlers(app)
handlers = list(map(lambda route: (f'{authenticator["url_scope"]}{route[0]}', route[1]), handlers))
for path, handler in handlers:
setattr(handler, 'authenticator', authenticator['instance'])
routes.extend(handlers)
return routes And used it like: from oauthenticator.github import GitHubOAuthenticator
from oauthenticator.google import GoogleOAuthenticator
c.PackedAuthenticator.authenticators = [
(GitHubOAuthenticator, '/github', {
'client_id': 'xxxx',
'client_secret': 'xxxx',
'oauth_callback_url': 'http://example.com/hub/github/oauth_callback'
}),
(GoogleOAuthenticator, '/google', {
'client_id': 'xxxx',
'client_secret': 'xxxx',
'oauth_callback_url': 'http://example.com/hub/google/oauth_callback'
})
]
c.JupyterHub.authenticator_class = PackedAuthenticator Now, using the following path will redirect you to the right login page:
|
@louis-she That's awesome! Thank you very much for sharing. I will give it a try when I get a chance. By the way, unless I'm missing something, I'm not seeing any code responsible for displaying relevant UI options (as buttons) for a user. How did you handle that? Please clarify. |
Currently I'm only using the OAuth authenticator(in the above case, Github and Google), so there is no new pages(the login page is provided by Google and Github). But the login button (which may be just a I just tested the code with Github and Google yet, not sure if the others Authenticator will work. But the |
@louis-she I appreciate your clarifications and updates. I understand that OAuth flow redirects to relevant IdP providers. My confusion is about whether your approach is compatible with JupyterHub's default login page ( |
The code above just added the routes and handlers. I didn't change any frontend code of jupyterhub. I didn't use the default login page of jupyterhub in my project( I write my own ), so the buttons are in my own login page. |
@louis-she Understood. Thank you for clarifying. And Happy New Year! :-) |
I'm closing this as a multi-authenticator wouldn't be specific to oauthenticator. Please feel free to continue discussion on the Jupyter Community Forum. Thanks! |
This issue has been mentioned on Jupyter Community Forum. There might be relevant details there: https://discourse.jupyter.org/t/multiple-authentication-sources-for-zero-to-jupyterhub/6461/2 |
Could you tell me where to put the first part of the code please?
and every link goes to github. |
Re: @nijisakai, I had a similar issue. The problem is both Google and Github's authenticator use the exact same handlers (OAuthLoginHandler and OAuthCallbackHandler, both in oauthenticator.oauth2). Both OAuthenticator's def get_handlers(self, app):
routes = []
for authenticator in self._authenticators:
handlers = []
for path, handler in authenticator['instance'].get_handlers(app):
class SubHandler(handler):
authenticator = authenticator['instance']
handlers.append((f'{authenticator["url_scope"]}{path}', SubHandler))
routes.extend(handlers)
return routes (untested code, I'm doing something a bit different, but the logic should be the same). Hopefully this helps. |
Just an update on the code written by @ louis-she, it has a couple of weird quirks that others might not expect. Other than the problem above, the This is especially a problem if you want to override functions and just do it in class HorribleObjectAmalgam:
def __init__(self, base, override):
self.__base = base
self.__override = override
def __getattr__(self, name):
try:
return getattr(self.__override, name)
except AttributeError:
return getattr(self.__base, name) And later, in |
Thank you @louis-she @rkevin-arch Now, I made it work with the following code. class MultiOAuthenticator(Authenticator):
authenticators = List(help="The subauthenticators to use", config=True)
def __init__(self, *arg, **kwargs):
super().__init__(*arg, **kwargs)
self._authenticators = []
for authenticator_klass, url_scope, configs in self.authenticators:
c = self.trait_values()
c.update(configs)
self._authenticators.append({"instance": authenticator_klass(**c), "url_scope": url_scope})
def get_custom_html(self, base_url):
html = []
for authenticator in self._authenticators:
login_service = authenticator["instance"].login_service
url = url_path_join(base_url, authenticator["url_scope"], "oauth_login")
html.append(
f"""
<div class="service-login">
<a role="button" class='btn btn-jupyter btn-lg' href='{url}'>
Sign in with {login_service}
</a>
</div>
"""
)
return "\n".join(html)
def get_handlers(self, app):
routes = []
for _authenticator in self._authenticators:
for path, handler in _authenticator["instance"].get_handlers(app):
class SubHandler(handler):
authenticator = _authenticator["instance"]
routes.append((f'{_authenticator["url_scope"]}{path}', SubHandler))
return routes In from oauthenticator.github import GitHubOAuthenticator
from oauthenticator.google import GoogleOAuthenticator
c.MultiOAuthenticator.authenticators = [
(GitHubOAuthenticator, '/google', {
'client_id': 'xxxx',
'client_secret': 'xxxx',
'oauth_callback_url': 'http://example.com/hub/google/oauth_callback'
}),
(GoogleOAuthenticator, '/github', {
'client_id': 'xxxx',
'client_secret': 'xxxx',
'oauth_callback_url': 'http://example.com/hub/github/oauth_callback'
})
]
c.JupyterHub.authenticator_class = MultiOAuthenticator This code works w/ my PR of JupyterHub. |
This issue has been mentioned on Jupyter Community Forum. There might be relevant details there: https://discourse.jupyter.org/t/make-jupyterhub-authentication-pluggable/10122/1 |
This issue has been mentioned on Jupyter Community Forum. There might be relevant details there: https://discourse.jupyter.org/t/make-jupyterhub-authentication-pluggable/10122/2 |
This issue has been mentioned on Jupyter Community Forum. There might be relevant details there: https://discourse.jupyter.org/t/multiple-authentication-options/12711/2 |
Thanks all, this thread was a big help in getting it to work with the helm chart too. I did have to fix the hub:
config:
Authenticator:
allowed_users:
- [email protected]
- [email protected]
admin_users:
- [email protected]
AzureAdOAuthenticator:
client_id: ""
client_secret: ""
tenant_id: ""
oauth_callback_url: https://<domain>/hub/azuread/oauth_callback
username_claim: unique_name
scope:
- openid
- email
GoogleOAuthenticator:
client_id: ""
client_secret: ""
oauth_callback_url: https://<domain>/hub/google/oauth_callback
hosted_domain:
- abc.com
extraConfig:
PackedAuthenticator: |-
from traitlets import List
from jupyterhub.auth import Authenticator
from oauthenticator.google import GoogleOAuthenticator
from oauthenticator.azuread import AzureAdOAuthenticator
class PackedAuthenticator(Authenticator):
authenticators = List(help="The sub-authenticators to use", config=True)
def __init__(self, *arg, **kwargs):
super().__init__(*arg, **kwargs)
self._authenticators = []
for auth_class, url_scope, configs in self.authenticators:
instance = auth_class(**configs)
# get the login url for this authenticator, e.g. 'login' for PAM, 'oauth_login' for Google
login_url = instance.login_url('')
# update the login_url function on the instance to fix it as we are adding url_scopes
instance._url_scope = url_scope
instance._login_url = login_url
def custom_login_url(self, base_url):
return url_path_join(base_url, self._url_scope, self._login_url)
instance.login_url = custom_login_url.__get__(instance, auth_class)
self._authenticators.append({
'instance': instance,
'url_scope': url_scope,
})
def get_handlers(self, app):
routes = []
for _auth in self._authenticators:
for path, handler in _auth['instance'].get_handlers(app):
class SubHandler(handler):
authenticator = _auth['instance']
routes.append((f'{_auth["url_scope"]}{path}', SubHandler))
print("routes", routes)
return routes
def get_custom_html(self, base_url):
html = [
'<div class="service-login">',
'<h2>Please sign in below</h2>',
]
for authenticator in self._authenticators:
login_service = authenticator['instance'].login_service or "Local User"
url = authenticator['instance'].login_url(base_url)
html.append(
f"""
<div style="margin-bottom:10px;">
<a style="width:20%;" role="button" class='btn btn-jupyter btn-lg' href='{url}'>
Sign in with {login_service}
</a>
</div>
"""
)
footer_html = [
'</div>',
]
return '\n'.join(html + footer_html)
c.PackedAuthenticator.authenticators = [
(GoogleOAuthenticator, '/google', c['GoogleOAuthenticator']),
(AzureAdOAuthenticator, '/azuread', c['AzureAdOAuthenticator']),
]
c.JupyterHub.authenticator_class = PackedAuthenticator |
For people who get here after searching for similar solutions: the current work seems to be https://github.com/idiap/multiauthenticator which originated from #459 |
Hi,
is it possible to setup multiple authentication methods simultaneously?
For example, I would like my JupyterHub instance to have Google, GitHub and Local authentication method on the same login page: is it possible?
Many thanks
The text was updated successfully, but these errors were encountered: