From aec73a90f9bcda100ea482f4032c7bb72e92fdbe Mon Sep 17 00:00:00 2001 From: tomcatling Date: Fri, 28 Jan 2022 22:40:54 +0000 Subject: [PATCH 1/6] Minimal working AuthnRequest for AWS SSO. Needs paramaterising. Tested (and currently hardcoded to) a local instance of jupyterhub --- samlauthenticator/samlauthenticator.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/samlauthenticator/samlauthenticator.py b/samlauthenticator/samlauthenticator.py index 05b99ed..afdc016 100644 --- a/samlauthenticator/samlauthenticator.py +++ b/samlauthenticator/samlauthenticator.py @@ -21,13 +21,15 @@ ''' # Imports from python standard library -from base64 import b64decode +from base64 import b64decode, b64encode from datetime import datetime, timezone from urllib.request import urlopen +from urllib.parse import quote_plus import asyncio import pwd import subprocess +import zlib # Imports to work with JupyterHub from jupyterhub.auth import Authenticator @@ -724,9 +726,18 @@ def _get_redirect_from_metadata_and_redirect(authenticator_self, element_name, h redirect_link_getter = xpath_with_namespaces(final_xpath) + saml_request=quote_plus(b64encode(zlib.compress(f""" + + http://localhost:8000/hub + + """.encode('utf8'))[2:-4])) + + # see https://developers.onelogin.com/saml/examples/authnrequest + # https://stackoverflow.com/questions/30388926/http-redirect-binding-saml-request + # Here permanent MUST BE False - otherwise the /hub/logout GET will not be fired # by the user's browser. - handler_self.redirect(redirect_link_getter(saml_metadata_etree)[0], permanent=False) + handler_self.redirect(f"{redirect_link_getter(saml_metadata_etree)[0]}?SAMLRequest={saml_request}", permanent=False) def _make_org_metadata(self): if self.organization_name or \ From d94a7391cf90194ef8a421d986b5830181e8d278 Mon Sep 17 00:00:00 2001 From: tomcatling Date: Fri, 28 Jan 2022 23:39:50 +0000 Subject: [PATCH 2/6] Adding paramaterisation --- samlauthenticator/samlauthenticator.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/samlauthenticator/samlauthenticator.py b/samlauthenticator/samlauthenticator.py index afdc016..a7fee7b 100644 --- a/samlauthenticator/samlauthenticator.py +++ b/samlauthenticator/samlauthenticator.py @@ -725,19 +725,18 @@ def _get_redirect_from_metadata_and_redirect(authenticator_self, element_name, h handler_self.log.debug('Final xpath is: ' + final_xpath) redirect_link_getter = xpath_with_namespaces(final_xpath) + sso_login_url = redirect_link_getter(saml_metadata_etree)[0] + # AWS SSO does not require a signed request so this is fairly simple. saml_request=quote_plus(b64encode(zlib.compress(f""" - - http://localhost:8000/hub + + {authenticator_self.audience} """.encode('utf8'))[2:-4])) - # see https://developers.onelogin.com/saml/examples/authnrequest - # https://stackoverflow.com/questions/30388926/http-redirect-binding-saml-request - # Here permanent MUST BE False - otherwise the /hub/logout GET will not be fired # by the user's browser. - handler_self.redirect(f"{redirect_link_getter(saml_metadata_etree)[0]}?SAMLRequest={saml_request}", permanent=False) + handler_self.redirect(f"{sso_login_url}?SAMLRequest={saml_request}", permanent=False) def _make_org_metadata(self): if self.organization_name or \ From 64046712e99b694b3271c3aa3a6aa573f9145d18 Mon Sep 17 00:00:00 2001 From: tomcatling Date: Sat, 29 Jan 2022 09:02:55 +0000 Subject: [PATCH 3/6] NameIDPolicy can be omitted, this just allows the IDP to choose the format. See https://stackoverflow.com/questions/10099828/saml-nameid-policy --- samlauthenticator/samlauthenticator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/samlauthenticator/samlauthenticator.py b/samlauthenticator/samlauthenticator.py index a7fee7b..09b4b8b 100644 --- a/samlauthenticator/samlauthenticator.py +++ b/samlauthenticator/samlauthenticator.py @@ -731,7 +731,6 @@ def _get_redirect_from_metadata_and_redirect(authenticator_self, element_name, h saml_request=quote_plus(b64encode(zlib.compress(f""" {authenticator_self.audience} - """.encode('utf8'))[2:-4])) # Here permanent MUST BE False - otherwise the /hub/logout GET will not be fired From 7350cce03ada659c32c0fd6faeec1a00bd27a334 Mon Sep 17 00:00:00 2001 From: tomcatling Date: Sat, 29 Jan 2022 10:08:04 +0000 Subject: [PATCH 4/6] Refactor --- samlauthenticator/samlauthenticator.py | 61 +++++++++++++++++++++----- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/samlauthenticator/samlauthenticator.py b/samlauthenticator/samlauthenticator.py index 09b4b8b..3a477eb 100644 --- a/samlauthenticator/samlauthenticator.py +++ b/samlauthenticator/samlauthenticator.py @@ -707,7 +707,7 @@ def _authenticate(self, handler, data): def authenticate(self, handler, data): return self._authenticate(handler, data) - def _get_redirect_from_metadata_and_redirect(authenticator_self, element_name, handler_self): + def _get_redirect_from_metadata(authenticator_self, element_name, handler_self): saml_metadata_etree = authenticator_self._get_saml_metadata_etree() handler_self.log.debug('Got metadata etree') @@ -719,23 +719,36 @@ def _get_redirect_from_metadata_and_redirect(authenticator_self, element_name, h handler_self.log.debug('Got valid metadata etree') xpath_with_namespaces = authenticator_self._make_xpath_builder() - binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' final_xpath = '//' + element_name + '[@Binding=\'' + binding + '\']/@Location' - handler_self.log.debug('Final xpath is: ' + final_xpath) + handler_self.log.debug('Final xpath is: ' + final_xpath) + redirect_link_getter = xpath_with_namespaces(final_xpath) sso_login_url = redirect_link_getter(saml_metadata_etree)[0] + + return sso_login_url + + def _get_redirect_from_metadata_and_redirect(authenticator_self, element_name, handler_self, add_authn_request=False): - # AWS SSO does not require a signed request so this is fairly simple. - saml_request=quote_plus(b64encode(zlib.compress(f""" - - {authenticator_self.audience} - """.encode('utf8'))[2:-4])) + redirect_url = authenticator_self._get_redirect_from_metadata(element_name, handler_self) # Here permanent MUST BE False - otherwise the /hub/logout GET will not be fired # by the user's browser. - handler_self.redirect(f"{sso_login_url}?SAMLRequest={saml_request}", permanent=False) + + if add_authn_request: + authn_requst = quote_plus(b64encode(zlib.compress( + authenticator_self._make_authn_request(element_name, handler_self).encode('utf8') + )[2:-4])) + handler_self.redirect( + f"{redirect_url}?SAMLRequest={authn_requst}", + permanent=False + ) + else: + handler_self.redirect( + redirect_url, + permanent=False + ) def _make_org_metadata(self): if self.organization_name or \ @@ -772,6 +785,28 @@ def _make_org_metadata(self): return '' + def _make_authn_request(authenticator_self, element_name, handler_self): + authn_request_text = ''' + + {{ audience }} +''' + + xml_template = Template(authn_request_text) + return xml_template.render( + issue_time = datetime.now().strftime(authenticator_self.time_format_string), + sso_login_url = authenticator_self._get_redirect_from_metadata(element_name, handler_self), + acs_url = authenticator_self.acs_endpoint_url, + audience = authenticator_self.audience + ) + def _make_sp_metadata(authenticator_self, meta_handler_self): metadata_text = ''' Date: Sat, 29 Jan 2022 10:11:10 +0000 Subject: [PATCH 5/6] Refactor --- samlauthenticator/samlauthenticator.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/samlauthenticator/samlauthenticator.py b/samlauthenticator/samlauthenticator.py index 3a477eb..7d5e041 100644 --- a/samlauthenticator/samlauthenticator.py +++ b/samlauthenticator/samlauthenticator.py @@ -719,15 +719,14 @@ def _get_redirect_from_metadata(authenticator_self, element_name, handler_self): handler_self.log.debug('Got valid metadata etree') xpath_with_namespaces = authenticator_self._make_xpath_builder() + binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' final_xpath = '//' + element_name + '[@Binding=\'' + binding + '\']/@Location' - handler_self.log.debug('Final xpath is: ' + final_xpath) redirect_link_getter = xpath_with_namespaces(final_xpath) - sso_login_url = redirect_link_getter(saml_metadata_etree)[0] - - return sso_login_url + + return redirect_link_getter(saml_metadata_etree)[0] def _get_redirect_from_metadata_and_redirect(authenticator_self, element_name, handler_self, add_authn_request=False): @@ -735,7 +734,6 @@ def _get_redirect_from_metadata_and_redirect(authenticator_self, element_name, h # Here permanent MUST BE False - otherwise the /hub/logout GET will not be fired # by the user's browser. - if add_authn_request: authn_requst = quote_plus(b64encode(zlib.compress( authenticator_self._make_authn_request(element_name, handler_self).encode('utf8') From 8711c3dbb10c26d14b135ec9ebdbed02fe4b351c Mon Sep 17 00:00:00 2001 From: tomcatling Date: Sat, 29 Jan 2022 10:14:25 +0000 Subject: [PATCH 6/6] Tidying whitespace --- samlauthenticator/samlauthenticator.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/samlauthenticator/samlauthenticator.py b/samlauthenticator/samlauthenticator.py index 7d5e041..be9c4b6 100644 --- a/samlauthenticator/samlauthenticator.py +++ b/samlauthenticator/samlauthenticator.py @@ -723,7 +723,7 @@ def _get_redirect_from_metadata(authenticator_self, element_name, handler_self): binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' final_xpath = '//' + element_name + '[@Binding=\'' + binding + '\']/@Location' handler_self.log.debug('Final xpath is: ' + final_xpath) - + redirect_link_getter = xpath_with_namespaces(final_xpath) return redirect_link_getter(saml_metadata_etree)[0] @@ -846,10 +846,11 @@ class SAMLLoginHandler(LoginHandler): async def get(login_handler_self): login_handler_self.log.info('Starting SP-initiated SAML Login') - authenticator_self._get_redirect_from_metadata_and_redirect('md:SingleSignOnService', - login_handler_self, - add_authn_request=True - ) + authenticator_self._get_redirect_from_metadata_and_redirect( + 'md:SingleSignOnService', + login_handler_self, + add_authn_request=True + ) class SAMLLogoutHandler(LogoutHandler): # TODO: When the time is right to force users onto JupyterHub 1.0.0, @@ -892,10 +893,11 @@ async def get(logout_handler_self): forward_on_logout = True if authenticator_self.slo_forward_on_logout else False forwad_on_logout = True if authenticator_self.slo_forwad_on_logout else False if forward_on_logout or forwad_on_logout: - authenticator_self._get_redirect_from_metadata_and_redirect('md:SingleLogoutService', - logout_handler_self, - add_authn_request=False - ) + authenticator_self._get_redirect_from_metadata_and_redirect( + 'md:SingleLogoutService', + logout_handler_self, + add_authn_request=False + ) else: html = logout_handler_self.render_template('logout.html') logout_handler_self.finish(html)