def testLoginForceAuthN(self): """ Tests the login method of the OneLogin_Saml2_Auth class Case Logout with no parameters. A AuthN Request is built with ForceAuthn and redirect executed """ settings_info = self.loadSettingsJSON() return_to = u"http://example.com/returnto" sso_url = settings_info["idp"]["singleSignOnService"]["url"] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url = auth.login(return_to) parsed_query = parse_qs(urlparse(target_url)[4]) sso_url = settings_info["idp"]["singleSignOnService"]["url"] self.assertIn(sso_url, target_url) self.assertIn("SAMLRequest", parsed_query) request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query["SAMLRequest"][0]) self.assertNotIn('ForceAuthn="true"', request) auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url_2 = auth_2.login(return_to, False, False) parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) self.assertIn(sso_url, target_url_2) self.assertIn("SAMLRequest", parsed_query_2) request_2 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2["SAMLRequest"][0]) self.assertNotIn('ForceAuthn="true"', request_2) auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url_3 = auth_3.login(return_to, True, False) parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) self.assertIn(sso_url, target_url_3) self.assertIn("SAMLRequest", parsed_query_3) request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3["SAMLRequest"][0]) self.assertIn('ForceAuthn="true"', request_3)
def testCreateRequest(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. The creation of a deflated SAML Request """ saml_settings = self.loadSettingsJSON() settings = OneLogin_Saml2_Settings(saml_settings) settings._OneLogin_Saml2_Settings__organization = { u'en-US': { u'url': u'http://sp.example.com', u'name': u'sp_test' } } authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertNotIn('ProviderName="SP test"', inflated) saml_settings['organization'] = {} settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertNotIn('ProviderName="SP test"', inflated)
def testLoginIsPassive(self): """ Tests the login method of the OneLogin_Saml2_Auth class Case Logout with no parameters. A AuthN Request is built with IsPassive and redirect executed """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' settings_info['idp']['singleSignOnService']['url'] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url = auth.login(return_to) parsed_query = parse_qs(urlparse(target_url)[4]) sso_url = settings_info['idp']['singleSignOnService']['url'] self.assertIn(sso_url, target_url) self.assertIn('SAMLRequest', parsed_query) request = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])) self.assertNotIn('IsPassive="true"', request) auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url_2 = auth_2.login(return_to, False, False) parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) self.assertIn(sso_url, target_url_2) self.assertIn('SAMLRequest', parsed_query_2) request_2 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0])) self.assertNotIn('IsPassive="true"', request_2) auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url_3 = auth_3.login(return_to, False, True) parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) self.assertIn(sso_url, target_url_3) self.assertIn('SAMLRequest', parsed_query_3) request_3 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])) self.assertIn('IsPassive="true"', request_3)
def testCreateRequestAuthContextComparision(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. The creation of a deflated SAML Request with defined AuthnContextComparison """ saml_settings = self.loadSettingsJSON() settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD, inflated) self.assertNotIn(OneLogin_Saml2_Constants.AC_X509, inflated) saml_settings['security']['requestedAuthnContext'] = True settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertIn('RequestedAuthnContext Comparison="exact"', inflated) saml_settings['security']['requestedAuthnContextComparison'] = 'minimun' settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertIn('RequestedAuthnContext Comparison="minimun"', inflated)
def testDeflateBase64Roundtrip(self): """ Tests deflate_and_base64_encode and decode_base64_and_inflate methods of OneLogin_Saml2_Utils """ body = 'Some random string.' encoded = OneLogin_Saml2_Utils.deflate_and_base64_encode(body) self.assertEqual(OneLogin_Saml2_Utils.decode_base64_and_inflate(encoded), body) unicode_body = u'Sömé rändöm nön-äsçïï strïng.' unicode_encoded = OneLogin_Saml2_Utils.deflate_and_base64_encode(unicode_body) self.assertEqual(OneLogin_Saml2_Utils.decode_base64_and_inflate(unicode_encoded), unicode_body)
def testCreateRequestAuthContext(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. The creation of a deflated SAML Request with defined AuthContext """ saml_settings = self.loadSettingsJSON() settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegex(inflated, '^<samlp:AuthnRequest') self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD, inflated) self.assertNotIn(OneLogin_Saml2_Constants.AC_X509, inflated) saml_settings['security']['requestedAuthnContext'] = True settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegex(inflated, '^<samlp:AuthnRequest') self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, inflated) self.assertNotIn(OneLogin_Saml2_Constants.AC_X509, inflated) del saml_settings['security']['requestedAuthnContext'] settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegex(inflated, '^<samlp:AuthnRequest') self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, inflated) self.assertNotIn(OneLogin_Saml2_Constants.AC_X509, inflated) saml_settings['security']['requestedAuthnContext'] = False settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegex(inflated, '^<samlp:AuthnRequest') self.assertNotIn(OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, inflated) self.assertNotIn(OneLogin_Saml2_Constants.AC_X509, inflated) saml_settings['security']['requestedAuthnContext'] = (OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, OneLogin_Saml2_Constants.AC_X509) settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegex(inflated, '^<samlp:AuthnRequest') self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, inflated) self.assertIn(OneLogin_Saml2_Constants.AC_X509, inflated)
def testProcessSLOResponseValidDeletingSession(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Valid Logout Response, validating deleting the local session """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) # FIXME # if (!isset($_SESSION)) { # $_SESSION = array(); # } # $_SESSION['samltest'] = true; # In order to avoid the destination problem plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLResponse'] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) auth.set_strict(True) auth.process_slo(False) self.assertEqual(len(auth.get_errors()), 0)
def testIsInValidIssuer(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse Case invalid Issuer """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) plain_message = plain_message.replace('http://idp.example.com/', 'http://invalid.issuer.example.com') message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) settings.set_strict(False) response = OneLogin_Saml2_Logout_Response(settings, message) self.assertTrue(response.is_valid(request_data)) settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) self.assertFalse(response_2.is_valid(request_data)) self.assertIn('Invalid issuer in the Logout Response', response_2.get_error())
def testProcessSLORequestSignedResponse(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Valid Logout Request, validating the relayState, a signed LogoutResponse is created and a redirection executed """ settings_info = self.loadSettingsJSON() settings_info["security"]["logoutResponseSigned"] = True request_data = self.get_request() message = self.file_contents(join(self.data_path, "logout_requests", "logout_request_deflated.xml.base64")) # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace("http://stuff.com/endpoints/endpoints/sls.php", current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data["get_data"]["SAMLRequest"] = message request_data["get_data"]["RelayState"] = "http://relaystate.com" auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) auth.set_strict(True) target_url = auth.process_slo(False) parsed_query = parse_qs(urlparse(target_url)[4]) slo_url = settings_info["idp"]["singleLogoutService"]["url"] self.assertIn(slo_url, target_url) self.assertIn("SAMLResponse", parsed_query) self.assertIn("RelayState", parsed_query) self.assertIn("SigAlg", parsed_query) self.assertIn("Signature", parsed_query) self.assertIn("http://relaystate.com", parsed_query["RelayState"]) self.assertIn(OneLogin_Saml2_Constants.RSA_SHA1, parsed_query["SigAlg"])
def testIsValid(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) response = OneLogin_Saml2_Logout_Response(settings, message) self.assertTrue(response.is_valid(request_data)) settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) try: valid = response_2.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('The LogoutRequest was received at', e.message) plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message_3 = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) self.assertTrue(response_3.is_valid(request_data))
def test_suomifi_logout_sp_request(django_client, django_user_model, fixed_saml_id): '''Suomi.fi use case #3: sending logout request''' oidc_client = create_oidc_client() user = create_user(django_user_model) create_social_user(user) create_oidc_token(user, oidc_client) django_client.login(username=TEST_USER, password=TEST_PASSWORD) args = { 'id_token_hint': ID_TOKEN_JWT, 'post_logout_redirect_uri': REDIRECT_URI, } logout_page_url = reverse('end-session') + '?{}'.format(urlencode(args)) logout_page_response = django_client.get(logout_page_url) # Logout request results in redirect to Suomi.fi with a SAML message in # query parameters. The OIDC token for the user is deleted. assert Token.objects.count() == 0 assert logout_page_response.status_code == 302 suomifi_redirect = urlparse(logout_page_response.url) suomifi_query_params = parse_qs(suomifi_redirect.query) suomifi_saml_request = SAMLUtils.decode_base64_and_inflate(suomifi_query_params['SAMLRequest'][0]) expected_slo_url = urlparse(getattr(settings, 'SOCIAL_AUTH_SUOMIFI_ENABLED_IDPS')['suomifi']['logout_url']) expected_logout_request = load_file('suomifi_logout_request.xml') expected_logout_signature = load_file('suomifi_logout_signature.b64').decode() assert suomifi_redirect[:3] == expected_slo_url[:3] assert suomifi_saml_request == expected_logout_request assert suomifi_query_params['RelayState'][0] == '{"cli": "test_client", "idx": 0}' assert suomifi_query_params['Signature'][0] == expected_logout_signature
def test_suomifi_logout_sp_request_invalid_token(django_client, django_user_model, fixed_saml_id): oidc_client = create_oidc_client() user = create_user(django_user_model) create_social_user(user) create_oidc_token(user, oidc_client) django_client.login(username=TEST_USER, password=TEST_PASSWORD) args = { 'id_token_hint': ID_TOKEN_JWT_INVALID, 'post_logout_redirect_uri': REDIRECT_URI, } logout_page_url = reverse('end-session') + '?{}'.format(urlencode(args)) logout_page_response = django_client.get(logout_page_url) # If the token hint is not recognized but the client has valid session we # still perform logout as we are able to deduce enough information to log # the client out. We are unable to remove the ID token and we do not have # a way to deduce the final redirect after Suomi.fi callback so the # RelayState parameter will be missing from the SAML request. assert Token.objects.count() == 1 assert logout_page_response.status_code == 302 suomifi_redirect = urlparse(logout_page_response.url) suomifi_query_params = parse_qs(suomifi_redirect.query) suomifi_saml_request = SAMLUtils.decode_base64_and_inflate(suomifi_query_params['SAMLRequest'][0]) expected_slo_url = urlparse(getattr(settings, 'SOCIAL_AUTH_SUOMIFI_ENABLED_IDPS')['suomifi']['logout_url']) expected_logout_request = load_file('suomifi_logout_request.xml') expected_logout_signature = load_file('suomifi_logout_without_relaystate_signature.b64').decode() assert suomifi_redirect[:3] == expected_slo_url[:3] assert suomifi_saml_request == expected_logout_request assert 'RelayState' not in suomifi_query_params assert suomifi_query_params['Signature'][0] == expected_logout_signature
def test_suomifi_idp_logout(django_client, fixed_saml_id): '''Suomi.fi use cases #4: receiving logout request, and #6: sending logout response''' create_oidc_client() args = { 'SAMLRequest': load_file('suomifi_idp_logout_request_encoded.b64').decode(), 'RelayState': RELAY_STATE, 'SigAlg': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', 'Signature': load_file('suomifi_idp_logout_signature.b64').decode() } callback_url = reverse('auth_backends:suomifi_logout_callback') + '?{}'.format(urlencode(args)) callback_response = django_client.get(callback_url) # IdP initiated logout request results in redirect to Suomi.fi SLO URL with # SAML response and RelayState from request. assert callback_response.status_code == 302 suomifi_redirect = urlparse(callback_response.url) suomifi_query_params = parse_qs(suomifi_redirect.query) suomifi_saml_response = SAMLUtils.decode_base64_and_inflate(suomifi_query_params['SAMLResponse'][0]) expected_slo_url = urlparse(getattr(settings, 'SOCIAL_AUTH_SUOMIFI_ENABLED_IDPS')['suomifi']['logout_url']) expected_logout_response = load_file('suomifi_idp_logout_response.xml') expected_logout_signature = load_file('suomifi_idp_logout_response_signature.b64').decode() assert suomifi_redirect[:3] == expected_slo_url[:3] assert suomifi_saml_response == expected_logout_response assert suomifi_query_params['RelayState'][0] == RELAY_STATE assert suomifi_query_params['Signature'][0] == expected_logout_signature
def testProcessSLORequestSignedResponse(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Valid Logout Request, validating the relayState, a signed LogoutResponse is created and a redirection executed """ settings_info = self.loadSettingsJSON() settings_info['security']['logoutResponseSigned'] = True request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) # In order to avoid the destination problem plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLRequest'] = message request_data['get_data']['RelayState'] = 'http://relaystate.com' auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) auth.set_strict(True) target_url = auth.process_slo(False) parsed_query = parse_qs(urlparse(target_url)[4]) slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url) self.assertIn('SAMLResponse', parsed_query) self.assertIn('RelayState', parsed_query) self.assertIn('SigAlg', parsed_query) self.assertIn('Signature', parsed_query) self.assertIn('http://relaystate.com', parsed_query['RelayState']) self.assertIn(OneLogin_Saml2_Constants.RSA_SHA1, parsed_query['SigAlg'])
def testCreateEncSAMLRequest(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. The creation of a deflated SAML Request """ settings = self.loadSettingsJSON() settings['organization'] = { 'es': { 'name': 'sp_prueba', 'displayname': 'SP prueba', 'url': 'http://sp.example.com' } } settings['security']['wantNameIdEncrypted'] = True settings = OneLogin_Saml2_Settings(settings) authn_request = OneLogin_Saml2_Authn_Request(settings) parameters = { 'SAMLRequest': authn_request.get_request() } auth_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SSOService.php', parameters, True) self.assertRegexpMatches(auth_url, '^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=') exploded = urlparse(auth_url) exploded = parse_qs(exploded[4]) payload = exploded['SAMLRequest'][0] inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertRegexpMatches(inflated, 'AssertionConsumerServiceURL="http://stuff.com/endpoints/endpoints/acs.php">') self.assertRegexpMatches(inflated, '<saml:Issuer>http://stuff.com/endpoints/metadata.php</saml:Issuer>') self.assertRegexpMatches(inflated, 'Format="urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"') self.assertRegexpMatches(inflated, 'ProviderName="SP prueba"')
def testGetIDFromDeflatedSAMLLogoutRequest(self): """ Tests the get_id method of the OneLogin_Saml2_LogoutRequest """ deflated_logout_request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(deflated_logout_request) id = OneLogin_Saml2_Logout_Request.get_id(logout_request) self.assertEqual('ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e', id)
def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None): """ Process the SAML Logout Response / Logout Request sent by the IdP. :param keep_local_session: When false will destroy the local session, otherwise will destroy it :type keep_local_session: bool :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string :returns: Redirection url """ self.__errors = [] if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']: logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) if not logout_response.is_valid(self.__request_data, request_id): self.__errors.append('invalid_logout_response') self.__error_reason = logout_response.get_error() elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS: self.__errors.append('logout_not_success') elif not keep_local_session: OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']: logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest']) if not logout_request.is_valid(self.__request_data): self.__errors.append('invalid_logout_request') self.__error_reason = logout_request.get_error() else: if not keep_local_session: OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) in_response_to = OneLogin_Saml2_Logout_Request.get_id(OneLogin_Saml2_Utils.decode_base64_and_inflate(self.__request_data['get_data']['SAMLRequest'])) response_builder = OneLogin_Saml2_Logout_Response(self.__settings) response_builder.build(in_response_to) logout_response = response_builder.get_response() parameters = {'SAMLResponse': logout_response} if 'RelayState' in self.__request_data['get_data']: parameters['RelayState'] = self.__request_data['get_data']['RelayState'] security = self.__settings.get_security_data() if 'logoutResponseSigned' in security and security['logoutResponseSigned']: parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None)) return self.redirect_to(self.get_slo_url(), parameters) else: self.__errors.append('invalid_binding') raise OneLogin_Saml2_Error( 'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding', OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND )
def testGetID(self): """ Tests the get_id method of the OneLogin_Saml2_Authn_Request. """ saml_settings = self.loadSettingsJSON() settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) document = OneLogin_Saml2_XML.to_etree(inflated) self.assertEqual(authn_request.get_id(), document.get('ID', None))
def testAttributeConsumingService(self): """ Tests that the attributeConsumingServiceIndex is present as an attribute """ saml_settings = self.loadSettingsJSON() settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertNotIn('AttributeConsumingServiceIndex="1"', inflated) saml_settings = self.loadSettingsJSON('settings4.json') settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegex(inflated, 'AttributeConsumingServiceIndex="1"')
def __init__(self, settings, response=None): """ Constructs a Logout Response object (Initialize params from settings and if provided load the Logout Response. Arguments are: * (OneLogin_Saml2_Settings) settings. Setting data * (string) response. An UUEncoded SAML Logout response from the IdP. """ self.__settings = settings if response is not None: self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response) self.document = parseString(self.__logout_response)
def testCreateRequestIsPassive(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. The creation of a deflated SAML Request with IsPassive="true" """ saml_settings = self.loadSettingsJSON() settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertNotIn('IsPassive="true"', inflated) authn_request_2 = OneLogin_Saml2_Authn_Request(settings, False, False) authn_request_encoded_2 = authn_request_2.get_request() inflated_2 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded_2)) self.assertRegexpMatches(inflated_2, '^<samlp:AuthnRequest') self.assertNotIn('IsPassive="true"', inflated_2) authn_request_3 = OneLogin_Saml2_Authn_Request(settings, False, True) authn_request_encoded_3 = authn_request_3.get_request() inflated_3 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded_3)) self.assertRegexpMatches(inflated_3, '^<samlp:AuthnRequest') self.assertIn('IsPassive="true"', inflated_3)
def testCreateDeflatedSAMLLogoutRequestURLParameter(self): """ Tests the OneLogin_Saml2_LogoutRequest Constructor. The creation of a deflated SAML Logout Request """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) logout_request = OneLogin_Saml2_Logout_Request(settings) parameters = {'SAMLRequest': logout_request.get_request()} logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True) self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=') url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) payload = exploded['SAMLRequest'][0] inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(payload) self.assertRegexpMatches(inflated, '^<samlp:LogoutRequest')
def testCreateDeflatedSAMLRequestURLParameter(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. The creation of a deflated SAML Request """ authn_request = OneLogin_Saml2_Authn_Request(self.__settings) parameters = { 'SAMLRequest': authn_request.get_request() } auth_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SSOService.php', parameters, True) self.assertRegexpMatches(auth_url, '^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=') exploded = urlparse(auth_url) exploded = parse_qs(exploded[4]) payload = exploded['SAMLRequest'][0] inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest')
def testConstructorWithoutNameIdFormat(self): """ Tests the OneLogin_Saml2_LogoutRequest Constructor. Case: Checks that NameIDFormat is not added """ settings_info = self.loadSettingsJSON() name_id = 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c' name_id_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' settings_info['sp']['NameIDFormat'] = name_id_format settings = OneLogin_Saml2_Settings(settings_info) logout_request = OneLogin_Saml2_Logout_Request(settings, name_id=name_id) logout_request_xml = OneLogin_Saml2_Utils.decode_base64_and_inflate(logout_request.get_request()) name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(logout_request_xml) expected_name_id_data = { 'Value': name_id } self.assertEqual(expected_name_id_data, name_id_data)
def testGetStatus(self): """ Tests the get_status method of the OneLogin_Saml2_LogoutResponse """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) response = OneLogin_Saml2_Logout_Response(settings, message) status = response.get_status() self.assertEquals(status, OneLogin_Saml2_Constants.STATUS_SUCCESS) dom = parseString(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) status_code_node = dom.getElementsByTagName('samlp:StatusCode')[0] status_code_node.parentNode.removeChild(status_code_node) xml = dom.toxml() message_2 = OneLogin_Saml2_Utils.deflate_and_base64_encode(xml) response_2 = OneLogin_Saml2_Logout_Response(settings, message_2) self.assertIsNone(response_2.get_status())
def testConstructor(self): """ Tests the OneLogin_Saml2_LogoutRequest Constructor. """ settings_info = self.loadSettingsJSON() settings_info['security']['nameIdEncrypted'] = True settings = OneLogin_Saml2_Settings(settings_info) logout_request = OneLogin_Saml2_Logout_Request(settings) parameters = {'SAMLRequest': logout_request.get_request()} logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True) self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=') url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) payload = exploded['SAMLRequest'][0] inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)) self.assertRegexpMatches(inflated, '^<samlp:LogoutRequest')
def testProcessSLORequestNotOnOrAfterFailed(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Logout Request NotOnOrAfter failed """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml.base64')) # In order to avoid the destination problem plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLRequest'] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) auth.set_strict(True) auth.process_slo(True) self.assertEqual(auth.get_errors(), ['invalid_logout_request'])
def testCreateDeflatedSAMLLogoutResponseURLParameter(self): """ Tests the OneLogin_Saml2_LogoutResponse Constructor. The creation of a deflated SAML Logout Response """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) in_response_to = 'ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e' response_builder = OneLogin_Saml2_Logout_Response(settings) response_builder.build(in_response_to) parameters = {'SAMLResponse': response_builder.get_response()} logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True) self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLResponse=') url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(exploded['SAMLResponse'][0]) self.assertRegexpMatches(inflated, '^<samlp:LogoutResponse')
def testConstructorWithNameIdFormatOnSettings(self): """ Tests the OneLogin_Saml2_LogoutRequest Constructor. Case: Defines NameIDFormat from settings """ settings_info = self.loadSettingsJSON() name_id = 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c' name_id_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' settings_info['sp']['NameIDFormat'] = name_id_format settings = OneLogin_Saml2_Settings(settings_info) logout_request = OneLogin_Saml2_Logout_Request(settings, name_id=name_id) logout_request_xml = OneLogin_Saml2_Utils.decode_base64_and_inflate(logout_request.get_request()) name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(logout_request_xml) expected_name_id_data = { 'Value': name_id, 'Format': name_id_format } self.assertEqual(expected_name_id_data, name_id_data)
def testGetIssuer(self): """ Tests the get_issuer of the OneLogin_Saml2_LogoutResponse """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) response = OneLogin_Saml2_Logout_Response(settings, message) issuer = response.get_issuer() self.assertEquals('http://idp.example.com/', issuer) dom = parseString(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) issuer_node = dom.getElementsByTagName('saml:Issuer')[0] issuer_node.parentNode.removeChild(issuer_node) xml = dom.toxml() message_2 = OneLogin_Saml2_Utils.deflate_and_base64_encode(xml) response_2 = OneLogin_Saml2_Logout_Response(settings, message_2) issuer_2 = response_2.get_issuer() self.assertIsNone(issuer_2)
def __init__(self, settings, response=None): """ Constructs a Logout Response object (Initialize params from settings and if provided load the Logout Response. Arguments are: * (OneLogin_Saml2_Settings) settings. Setting data * (string) response. An UUEncoded SAML Logout response from the IdP. """ self.__settings = settings self.__error = None self.id = None if response is not None: self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate( response) self.document = parseString(self.__logout_response) self.id = self.document.documentElement.getAttribute('ID')
def testIsInValidDestination(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse Case invalid Destination """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents( join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) settings.set_strict(False) response = OneLogin_Saml2_Logout_Response(settings, message) self.assertTrue(response.is_valid(request_data)) settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) try: valid = response_2.is_valid(request_data) self.assertFalse(valid) except Exception as e: self.assertIn('The LogoutRequest was received at', str(e)) # Empty destination dom = parseString( OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) dom.firstChild.setAttribute('Destination', '') xml = dom.toxml() message_3 = OneLogin_Saml2_Utils.deflate_and_base64_encode(xml) response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) self.assertTrue(response_3.is_valid(request_data)) # No destination dom.firstChild.removeAttribute('Destination') xml = dom.toxml() message_4 = OneLogin_Saml2_Utils.deflate_and_base64_encode(xml) response_4 = OneLogin_Saml2_Logout_Response(settings, message_4) self.assertTrue(response_4.is_valid(request_data))
def testCreateDeflatedSAMLLogoutRequestURLParameter(self): """ Tests the OneLogin_Saml2_LogoutRequest Constructor. The creation of a deflated SAML Logout Request """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) logout_request = OneLogin_Saml2_Logout_Request(settings) parameters = {'SAMLRequest': logout_request.get_request()} logout_url = OneLogin_Saml2_Utils.redirect( 'http://idp.example.com/SingleLogoutService.php', parameters, True) self.assertRegexpMatches( logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=' ) url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) payload = exploded['SAMLRequest'][0] inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(payload) self.assertRegexpMatches(inflated, '^<samlp:LogoutRequest')
def __init__(self, settings, response=None): """ Constructs a Logout Response object (Initialize params from settings and if provided load the Logout Response. Arguments are: * (OneLogin_Saml2_Settings) settings. Setting data * (string) response. An UUEncoded SAML Logout response from the IdP. """ self.__settings = settings self.__error = None self.id = None if response is not None: self.__logout_response = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate( response, ignore_zip=True)) self.document = OneLogin_Saml2_XML.to_etree(self.__logout_response) self.id = self.document.get('ID', None)
def testGetStatus(self): """ Tests the get_status method of the OneLogin_Saml2_LogoutResponse """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents( join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) response = OneLogin_Saml2_Logout_Response(settings, message) status = response.get_status() self.assertEquals(status, OneLogin_Saml2_Constants.STATUS_SUCCESS) dom = parseString( OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) status_code_node = dom.getElementsByTagName('samlp:StatusCode')[0] status_code_node.parentNode.removeChild(status_code_node) xml = dom.toxml() message_2 = OneLogin_Saml2_Utils.deflate_and_base64_encode(xml) response_2 = OneLogin_Saml2_Logout_Response(settings, message_2) self.assertIsNone(response_2.get_status())
def testConstructorEncryptIdUsingX509certMulti(self): """ Tests the OneLogin_Saml2_LogoutRequest Constructor. Case: Able to generate encryptedID with MultiCert """ settings_info = self.loadSettingsJSON('settings8.json') settings_info['security']['nameIdEncrypted'] = True settings = OneLogin_Saml2_Settings(settings_info) logout_request = OneLogin_Saml2_Logout_Request(settings) parameters = {'SAMLRequest': logout_request.get_request()} logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True) self.assertRegex(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=') url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) payload = exploded['SAMLRequest'][0] inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)) self.assertRegex(inflated, '^<samlp:LogoutRequest') self.assertRegex(inflated, '<saml:EncryptedID>')
def modify_start_url(self, start_url): """ Given a SAML redirect URL, parse it and change the ID to a consistent value, so the request is always identical. """ # Parse the SAML Request URL to get the XML being sent to TestShib url_parts = urlparse(start_url) query = dict( (k, v[0]) for (k, v) in parse_qs(url_parts.query).iteritems()) xml = OneLogin_Saml2_Utils.decode_base64_and_inflate( query['SAMLRequest']) # Modify the XML: xml, changed = re.subn(r'ID="[^"]+"', 'ID="TEST_ID"', xml) self.assertEqual(changed, 1) # Update the URL to use the modified query string: query['SAMLRequest'] = OneLogin_Saml2_Utils.deflate_and_base64_encode( xml) url_parts = list(url_parts) url_parts[4] = urlencode(query) return urlunparse(url_parts)
def testCreateEncSAMLRequest(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. The creation of a deflated SAML Request """ settings = self.loadSettingsJSON() settings['organization'] = { 'es': { 'name': 'sp_prueba', 'displayname': 'SP prueba', 'url': 'http://sp.example.com' } } settings['security']['wantNameIdEncrypted'] = True settings = OneLogin_Saml2_Settings(settings) authn_request = OneLogin_Saml2_Authn_Request(settings) parameters = {'SAMLRequest': authn_request.get_request()} auth_url = OneLogin_Saml2_Utils.redirect( 'http://idp.example.com/SSOService.php', parameters, True) self.assertRegexpMatches( auth_url, '^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=') exploded = urlparse(auth_url) exploded = parse_qs(exploded[4]) payload = exploded['SAMLRequest'][0] inflated = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertRegexpMatches( inflated, 'AssertionConsumerServiceURL="http://stuff.com/endpoints/endpoints/acs.php">' ) self.assertRegexpMatches( inflated, '<saml:Issuer>http://stuff.com/endpoints/metadata.php</saml:Issuer>' ) self.assertRegexpMatches( inflated, 'Format="urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"') self.assertRegexpMatches(inflated, 'ProviderName="SP prueba"')
def logout(self, to=None): # pylint: disable=R0201,C0111,C0103 # Check and set return_to return_to = self.settings["auth"]["logout_default_redirect_url"] if to is not None and to in self.settings["auth"][ "logout_allowed_redirect_urls"]: return_to = to # Create logout request object saml_auth = OneLogin_Saml2_Auth( self._prepare_request_object(cherrypy.request), self.settings["saml"]) logout_redirect_url = saml_auth.logout(return_to=return_to) # Clear session now cherrypy.session.clear() cherrypy.session.regenerate() # Redirect in case of HTTP-REDIRECT binding if self.settings["saml"]["idp"]["singleLogoutService"]["binding"] != \ "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST": raise cherrypy.HTTPRedirect(logout_redirect_url) # Prepare request for HTTP-POST parsed_url = urlsplit(logout_redirect_url) saml_post_action = urlunsplit(parsed_url._replace(query="")) saml_post_parameters = parse_qs(parsed_url.query) saml_security = saml_auth.get_settings().get_security_data() request = OneLogin_Saml2_Utils.decode_base64_and_inflate( saml_post_parameters["SAMLRequest"][0]) # Prepare signed request if needed if saml_security.get("logoutRequestSigned", False): request = self._sign_saml_request(request, saml_auth, saml_security) # Perform HTTP-POST binding return [{ "action": saml_post_action, "parameters": [{ "name": "SAMLRequest", "value": request }, { "name": "RelayState", "value": saml_post_parameters["RelayState"][0] }] }]
def test_saml_request_omits_authentication_context(self): # Make sure RequestedAuthnContext doesn't show up in SAMLReuqest with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS): request = self._request('get', '/sso/redirect', HTTP_HOST='www.stuff.com') auth_backend = SAMLAuthentication(request) sso_url = urllib.parse.urlparse(auth_backend.sso_url()) query = urllib.parse.parse_qs(sso_url.query) self.assertEqual( urllib.parse.urlunparse((sso_url.scheme, sso_url.netloc, sso_url.path, None, None, None)), TOKEN_AUTH_SETTINGS['idp']['singleSignOnService']['url']) saml_request = query['SAMLRequest'][0] saml_xml = OneLogin_Saml2_Utils.decode_base64_and_inflate( saml_request) pre = {'samlp': "urn:oasis:names:tc:SAML:2.0:protocol"} tree = ET.fromstring(saml_xml) rac = tree.findall('samlp:RequestedAuthnContext', pre) self.assertEqual(len(rac), 0)
def testProcessSLORequestNotOnOrAfterFailed(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Logout Request NotOnOrAfter failed """ request_data = self.get_request() message = self.file_contents( join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml.base64')) # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLRequest'] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) auth.set_strict(True) auth.process_slo(True) self.assertEqual(auth.get_errors(), ['invalid_logout_request'])
def test_suomifi_login_request(django_client, fixed_saml_id): '''Suomi.fi use case #1: sending authentication request''' create_oidc_client() args = { 'client_id': CLIENT_ID, 'redirect_uri': REDIRECT_URI, 'response_type': 'code', } auth_page_url = reverse('authorize') + '?{}'.format(urlencode(args)) auth_page_response = django_client.get(auth_page_url, follow=True) # As we have configured only one login method Tunnistamo will automatically # redirect there. We'll verify the final redirect here. suomifi_redirect = urlparse(auth_page_response.redirect_chain[-1][0]) suomifi_query_params = parse_qs(suomifi_redirect.query) suomifi_saml_request = SAMLUtils.decode_base64_and_inflate(suomifi_query_params['SAMLRequest'][0]) expected_sso_url = urlparse(getattr(settings, 'SOCIAL_AUTH_SUOMIFI_ENABLED_IDPS')['suomifi']['url']) expected_login_request = load_file('suomifi_login_request.xml') expected_login_signature = load_file('suomifi_login_signature.b64').decode() assert suomifi_redirect[:3] == expected_sso_url[:3] assert suomifi_saml_request == expected_login_request assert suomifi_query_params['Signature'][0] == expected_login_signature
def testGetIssuer(self): """ Tests the get_issuer of the OneLogin_Saml2_LogoutResponse """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents( join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) response = OneLogin_Saml2_Logout_Response(settings, message) issuer = response.get_issuer() self.assertEquals('http://idp.example.com/', issuer) dom = parseString( OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) issuer_node = dom.getElementsByTagName('saml:Issuer')[0] issuer_node.parentNode.removeChild(issuer_node) xml = dom.toxml() message_2 = OneLogin_Saml2_Utils.deflate_and_base64_encode(xml) response_2 = OneLogin_Saml2_Logout_Response(settings, message_2) issuer_2 = response_2.get_issuer() self.assertIsNone(issuer_2)
def testIsInValidRequestId(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse Case invalid request Id """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents( join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) plain_message = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace( 'http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_id = 'invalid_request_id' settings.set_strict(False) response = OneLogin_Saml2_Logout_Response(settings, message) self.assertTrue(response.is_valid(request_data, request_id)) settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) self.assertFalse(response_2.is_valid(request_data, request_id)) self.assertIn('The InResponseTo of the Logout Response:', response_2.get_error()) with self.assertRaisesRegexp( Exception, 'The InResponseTo of the Logout Response:'): response_2.is_valid(request_data, request_id, raise_exceptions=True)
def logout_view(request, *args, **kwargs): backend = request.backend logout(request) # Пользователя разлогинили, теперь нужно ответить провайдеру. # Для начала нужно узнать, от какого именно IdentityProvider'а пришёл запрос. # Как минимму OneLogin не присылает в запросе никаких опознавательных знаков кроме атрибута Issuer. # В Issuer у него находится путь к его метадате. В конфиге тот же путь надо указать в entity_id. # вынимаем значение Issuer request_str = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate( request.GET['SAMLRequest'])) xml_doc = OneLogin_Saml2_XML.to_etree(request_str) issuer_nodes = OneLogin_Saml2_XML.query( xml_doc, '/samlp:LogoutRequest/saml:Issuer') if len(issuer_nodes) == 0: raise ValueError('no Issuer in logout request: ' + request_str) issuer = issuer_nodes[0].text # ищем имя IDP idp_configs = backend.setting('ENABLED_IDPS') idp_name = next((name for name, cfg in idp_configs.items() if cfg.get('entity_id') == issuer), None) if idp_name is None: raise ValueError( f"IDP not found for Issuer '{issuer}' in logout request: " + request_str) # формируем адрес редиректа idp = backend.get_idp(idp_name) if idp.slo_config == {}: # если конфига slo нет url = settings.LOGOUT_REDIRECT_URL else: url = backend._create_saml_auth( idp, remove_signature_from_get=True).process_slo() return HttpResponseRedirect(url)
def testCreateDeflatedSAMLLogoutResponseURLParameter(self): """ Tests the OneLogin_Saml2_LogoutResponse Constructor. The creation of a deflated SAML Logout Response """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) in_response_to = 'ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e' response_builder = OneLogin_Saml2_Logout_Response(settings) response_builder.build(in_response_to) parameters = {'SAMLResponse': response_builder.get_response()} logout_url = OneLogin_Saml2_Utils.redirect( 'http://idp.example.com/SingleLogoutService.php', parameters, True) self.assertRegexpMatches( logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLResponse=' ) url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate( exploded['SAMLResponse'][0]) self.assertRegexpMatches(inflated, '^<samlp:LogoutResponse')
def testProcessSLOResponseRequestId(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Logout Response with valid and invalid Request ID """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLResponse'] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) request_id = 'wrongID' auth.set_strict(True) auth.process_slo(True, request_id) self.assertEqual(auth.get_errors(), ['invalid_logout_response']) request_id = 'ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e' auth.process_slo(True, request_id) self.assertEqual(len(auth.get_errors()), 0)
def testProcessSLOResponseValid(self): """ Tests the process_slo method of the OneLogin_Saml2_Auth class Case Valid Logout Response """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) # In order to avoid the destination problem plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) request_data['get_data']['SAMLResponse'] = message auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) # FIXME # if (!isset($_SESSION)) { # $_SESSION = array(); # } # $_SESSION['samltest'] = true; auth.set_strict(True) auth.process_slo(True) self.assertEqual(len(auth.get_errors()), 0)
def test_suomifi_logout_sp_request_invalid_token(django_client, django_user_model, fixed_saml_id): oidc_client = create_oidc_client() user = create_user(django_user_model) create_social_user(user) create_oidc_token(user, oidc_client) django_client.login(username=TEST_USER, password=TEST_PASSWORD) args = { 'id_token_hint': ID_TOKEN_JWT_INVALID, 'post_logout_redirect_uri': REDIRECT_URI, } logout_page_url = reverse('end-session') + '?{}'.format(urlencode(args)) logout_page_response = django_client.get(logout_page_url) # If the token hint is not recognized but the client has valid session we # still perform logout as we are able to deduce enough information to log # the client out. We are unable to remove the ID token and we do not have # a way to deduce the final redirect after Suomi.fi callback so the # RelayState parameter will be missing from the SAML request. assert Token.objects.count() == 1 assert logout_page_response.status_code == 302 suomifi_redirect = urlparse(logout_page_response.url) suomifi_query_params = parse_qs(suomifi_redirect.query) suomifi_saml_request = SAMLUtils.decode_base64_and_inflate( suomifi_query_params['SAMLRequest'][0]) expected_slo_url = urlparse( getattr(settings, 'SOCIAL_AUTH_SUOMIFI_ENABLED_IDPS')['suomifi']['logout_url']) expected_logout_request = load_file('suomifi_logout_request.xml') expected_logout_signature = load_file( 'suomifi_logout_without_relaystate_signature.b64').decode() assert suomifi_redirect[:3] == expected_slo_url[:3] assert suomifi_saml_request == expected_logout_request assert 'RelayState' not in suomifi_query_params assert suomifi_query_params['Signature'][0] == expected_logout_signature
def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None): """ Constructs the Logout Request object. :param settings: Setting data :type settings: OneLogin_Saml2_Settings :param request: Optional. A LogoutRequest to be loaded instead build one. :type request: string :param name_id: The NameID that will be set in the LogoutRequest. :type name_id: string :param session_index: SessionIndex that identifies the session of the user. :type session_index: string :param nq: IDP Name Qualifier :type: string """ self.__settings = settings self.__error = None self.id = None if request is None: sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() security = self.__settings.get_security_data() uid = OneLogin_Saml2_Utils.generate_unique_id() self.id = uid issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML( OneLogin_Saml2_Utils.now()) cert = None if security['nameIdEncrypted']: cert = idp_data['x509cert'] if name_id is not None: name_id_format = sp_data['NameIDFormat'] sp_name_qualifier = None else: name_id = idp_data['entityId'] name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY sp_name_qualifier = sp_data['entityId'] name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, sp_name_qualifier, name_id_format, cert, nq=nq, ) if session_index: session_index_str = '<samlp:SessionIndex>%s</samlp:SessionIndex>' % session_index else: session_index_str = '' logout_request = OneLogin_Saml2_Templates.LOGOUT_REQUEST % \ { 'id': uid, 'issue_instant': issue_instant, 'single_logout_url': idp_data['singleLogoutService']['url'], 'entity_id': sp_data['entityId'], 'name_id': name_id_obj, 'session_index': session_index_str, } else: logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate( request, ignore_zip=True) self.id = self.get_id(logout_request) self.__logout_request = logout_request
def testIsValidSign(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': { 'SAMLRequest': 'lVLBitswEP0Vo7tjeWzJtki8LIRCYLvbNksPewmyPc6K2pJqyXQ/v1LSQlroQi/DMJr33rwZbZ2cJysezNms/gt+X9H55G2etBOXlx1ZFy2MdMoJLWd0wvfieP/xQcCGCrsYb3ozkRvI+wjpHC5eGU2Sw35HTg3lA8hqZFwWFcMKsStpxbEsxoLXeQN9OdY1VAgk+YqLC8gdCUQB7tyKB+281D6UaF6mtEiBPudcABcMXkiyD26Ulv6CevXeOpFlVvlunb5ttEmV3ZjlnGn8YTRO5qx0NuBs8kzpAd829tXeucmR5NH4J/203I8el6gFRUqbFPJnyEV51Wq30by4TLW0/9ZyarYTxt4sBsjUYLMZvRykl1Fxm90SXVkfwx4P++T4KSafVzmpUcVJ/sfSrQZJPphllv79W8WKGtLx0ir8IrVTqD1pT2MH3QAMSs4KTvui71jeFFiwirOmprwPkYW063+5uRq4urHiiC4e8hCX3J5wqAEGaPpw9XB5JmkBdeDqSlkz6CmUXdl0Qae5kv2F/1384wu3PwE=', 'RelayState': '_1037fbc88ec82ce8e770b2bed1119747bb812a07e6', 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 'Signature': 'XCwCyI5cs7WhiJlB5ktSlWxSBxv+6q2xT3c8L7dLV6NQG9LHWhN7gf8qNsahSXfCzA0Ey9dp5BQ0EdRvAk2DIzKmJY6e3hvAIEp1zglHNjzkgcQmZCcrkK9Czi2Y1WkjOwR/WgUTUWsGJAVqVvlRZuS3zk3nxMrLH6f7toyvuJc=' } } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) request = OneLogin_Saml2_Utils.decode_base64_and_inflate( request_data['get_data']['SAMLRequest']) settings.set_strict(False) logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) relayState = request_data['get_data']['RelayState'] del request_data['get_data']['RelayState'] self.assertFalse(logout_request.is_valid(request_data)) request_data['get_data']['RelayState'] = relayState settings.set_strict(True) logout_request2 = OneLogin_Saml2_Logout_Request( settings, b64encode(request)) self.assertFalse(logout_request2.is_valid(request_data)) self.assertIn('The LogoutRequest was received at', logout_request2.get_error()) settings.set_strict(False) old_signature = request_data['get_data']['Signature'] request_data['get_data'][ 'Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333=' logout_request3 = OneLogin_Saml2_Logout_Request( settings, b64encode(request)) self.assertFalse(logout_request3.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Request rejected', logout_request3.get_error()) request_data['get_data']['Signature'] = old_signature old_signature_algorithm = request_data['get_data']['SigAlg'] del request_data['get_data']['SigAlg'] self.assertTrue(logout_request3.is_valid(request_data)) request_data['get_data'][ 'RelayState'] = 'http://example.com/relaystate' self.assertFalse(logout_request3.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Request rejected', logout_request3.get_error()) settings.set_strict(True) request_2 = request.replace( 'https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url) request_2 = request_2.replace( 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/') request_data['get_data'][ 'SAMLRequest'] = OneLogin_Saml2_Utils.deflate_and_base64_encode( request_2) logout_request4 = OneLogin_Saml2_Logout_Request( settings, b64encode(request_2)) self.assertFalse(logout_request4.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Request rejected', logout_request4.get_error()) settings.set_strict(False) logout_request5 = OneLogin_Saml2_Logout_Request( settings, b64encode(request_2)) self.assertFalse(logout_request5.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Request rejected', logout_request5.get_error()) request_data['get_data'][ 'SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' self.assertFalse(logout_request5.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Request rejected', logout_request5.get_error()) settings_info = self.loadSettingsJSON() settings_info['strict'] = True settings_info['security']['wantMessagesSigned'] = True settings = OneLogin_Saml2_Settings(settings_info) request_data['get_data']['SigAlg'] = old_signature_algorithm old_signature = request_data['get_data']['Signature'] del request_data['get_data']['Signature'] logout_request6 = OneLogin_Saml2_Logout_Request( settings, b64encode(request_2)) self.assertFalse(logout_request6.is_valid(request_data)) self.assertIn( 'The Message of the Logout Request is not signed and the SP require it', logout_request6.get_error()) request_data['get_data']['Signature'] = old_signature settings_info['idp'][ 'certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9' del settings_info['idp']['x509cert'] settings_2 = OneLogin_Saml2_Settings(settings_info) logout_request7 = OneLogin_Saml2_Logout_Request( settings_2, b64encode(request_2)) self.assertFalse(logout_request7.is_valid(request_data)) self.assertEqual( 'In order to validate the sign on the Logout Request, the x509cert of the IdP is required', logout_request7.get_error())
def decode_saml_request(self, encoded_saml_request): return str( OneLogin_Saml2_Utils.decode_base64_and_inflate( encoded_saml_request))
def testIsInValidSign(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse """ request_data = { 'http_host': 'example.com', 'script_name': 'index.html', 'get_data': {} } settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) settings.set_strict(False) request_data['get_data'] = { 'SAMLResponse': 'fZJva8IwEMa/Ssl7TZrW/gnqGHMMwSlM8cXeyLU9NaxNQi9lfvxVZczB5ptwSe733MPdjQma2qmFPdjOvyE5awiDU1MbUpevCetaoyyQJmWgQVK+VOvH14WSQ6Fca70tbc1ukPsEEGHrtTUsmM8mbDfKUhnFci8gliGINI/yXIAAiYnsw6JIRgWWAKlkwRZb6skJ64V6nKjDuSEPxvdPIowHIhpIsQkTFaYqSt9ZMEPy2oC/UEfvHSnOnfZFV38MjR1oN7TtgRv8tAZre9CGV9jYkGtT4Wnoju6Bauprme/ebOyErZbPi9XLfLnDoohwhHGc5WVSVhjCKM6rBMpYQpWJrIizfZ4IZNPxuTPqYrmd/m+EdONqPOfy8yG5rhxv0EMFHs52xvxWaHyd3tqD7+j37clWGGyh7vD+POiSrdZdWSIR49NrhR9R/teGTL8A', 'RelayState': 'https://pitbulk.no-ip.org/newonelogin/demo1/index.php', 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 'Signature': 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVfNKGA=' } response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertTrue(response.is_valid(request_data)) relayState = request_data['get_data']['RelayState'] del request_data['get_data']['RelayState'] inv_response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(inv_response.is_valid(request_data)) request_data['get_data']['RelayState'] = relayState settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_2.is_valid(request_data)) self.assertIn('Invalid issuer in the Logout Response', response_2.get_error()) settings.set_strict(False) old_signature = request_data['get_data']['Signature'] request_data['get_data']['Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333=' response_3 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_3.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Response rejected', response_3.get_error()) request_data['get_data']['Signature'] = old_signature old_signature_algorithm = request_data['get_data']['SigAlg'] del request_data['get_data']['SigAlg'] response_4 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertTrue(response_4.is_valid(request_data)) request_data['get_data']['RelayState'] = 'http://example.com/relaystate' response_5 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_5.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Response rejected', response_5.get_error()) settings.set_strict(True) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message_6 = OneLogin_Saml2_Utils.decode_base64_and_inflate(request_data['get_data']['SAMLResponse']) plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url) plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/') request_data['get_data']['SAMLResponse'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6) response_6 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_6.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Response rejected', response_6.get_error()) settings.set_strict(False) response_7 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_7.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Response rejected', response_7.get_error()) request_data['get_data']['SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' response_8 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_8.is_valid(request_data)) self.assertIn('Signature validation failed. Logout Response rejected', response_8.get_error()) settings_info = self.loadSettingsJSON() settings_info['strict'] = True settings_info['security']['wantMessagesSigned'] = True settings = OneLogin_Saml2_Settings(settings_info) request_data['get_data']['SigAlg'] = old_signature_algorithm old_signature = request_data['get_data']['Signature'] del request_data['get_data']['Signature'] request_data['get_data']['SAMLResponse'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6) response_9 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_9.is_valid(request_data)) self.assertIn('The Message of the Logout Response is not signed and the SP require it', response_9.get_error()) request_data['get_data']['Signature'] = old_signature settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9' del settings_info['idp']['x509cert'] settings_2 = OneLogin_Saml2_Settings(settings_info) response_10 = OneLogin_Saml2_Logout_Response(settings_2, request_data['get_data']['SAMLResponse']) self.assertFalse(response_10.is_valid(request_data)) self.assertIn('In order to validate the sign on the Logout Response, the x509cert of the IdP is required', response_10.get_error())
def testCreateRequestAuthContext(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. The creation of a deflated SAML Request with defined AuthContext """ saml_settings = self.loadSettingsJSON() settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate( authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD, inflated) self.assertNotIn(OneLogin_Saml2_Constants.AC_X509, inflated) saml_settings['security']['requestedAuthnContext'] = True settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate( authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, inflated) self.assertNotIn(OneLogin_Saml2_Constants.AC_X509, inflated) del saml_settings['security']['requestedAuthnContext'] settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate( authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, inflated) self.assertNotIn(OneLogin_Saml2_Constants.AC_X509, inflated) saml_settings['security']['requestedAuthnContext'] = False settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate( authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertNotIn(OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, inflated) self.assertNotIn(OneLogin_Saml2_Constants.AC_X509, inflated) saml_settings['security']['requestedAuthnContext'] = ( OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, OneLogin_Saml2_Constants.AC_X509) settings = OneLogin_Saml2_Settings(saml_settings) authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() inflated = compat.to_string( OneLogin_Saml2_Utils.decode_base64_and_inflate( authn_request_encoded)) self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest') self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, inflated) self.assertIn(OneLogin_Saml2_Constants.AC_X509, inflated)
def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None): """ Constructs the Logout Request object. :param settings: Setting data :type settings: OneLogin_Saml2_Settings :param request: Optional. A LogoutRequest to be loaded instead build one. :type request: string :param name_id: The NameID that will be set in the LogoutRequest. :type name_id: string :param session_index: SessionIndex that identifies the session of the user. :type session_index: string :param nq: IDP Name Qualifier :type: string :param name_id_format: The NameID Format that will be set in the LogoutRequest. :type: string :param spnq: SP Name Qualifier :type: string """ self.__settings = settings self.__error = None self.id = None if request is None: sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() security = self.__settings.get_security_data() uid = OneLogin_Saml2_Utils.generate_unique_id() self.id = uid issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) cert = None if security['nameIdEncrypted']: exists_multix509enc = 'x509certMulti' in idp_data and \ 'encryption' in idp_data['x509certMulti'] and \ idp_data['x509certMulti']['encryption'] if exists_multix509enc: cert = idp_data['x509certMulti']['encryption'][0] else: cert = idp_data['x509cert'] if name_id is not None: if not name_id_format and sp_data['NameIDFormat'] != OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED: name_id_format = sp_data['NameIDFormat'] else: name_id = idp_data['entityId'] name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY # From saml-core-2.0-os 8.3.6, when the entity Format is used: # "The NameQualifier, SPNameQualifier, and SPProvidedID attributes # MUST be omitted. if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY: nq = None spnq = None # NameID Format UNSPECIFIED omitted if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED: name_id_format = None name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, spnq, name_id_format, cert, False, nq ) if session_index: session_index_str = '<samlp:SessionIndex>%s</samlp:SessionIndex>' % session_index else: session_index_str = '' logout_request = OneLogin_Saml2_Templates.LOGOUT_REQUEST % \ { 'id': uid, 'issue_instant': issue_instant, 'single_logout_url': idp_data['singleLogoutService']['url'], 'entity_id': sp_data['entityId'], 'name_id': name_id_obj, 'session_index': session_index_str, } else: logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(request, ignore_zip=True) self.id = self.get_id(logout_request) self.__logout_request = compat.to_string(logout_request)
def test_full_login_process(self): """Asserts the nominal login process works.""" sso_location = "http://testserver/account/saml/local-accepting-idp/sso/" entity_descriptor_list = [ generate_idp_metadata( entity_id=sso_location, sso_location=sso_location, ui_info_display_names=format_mdui_display_name( "Local accepting IdP"), ), ] # 1/ Select Idp in the provider list with mock.patch("urllib.request.urlopen") as urlopen_mock: class UrlOpenMock: """Mockin object for the urlopen""" def read(self): """Allow object to be read several times.""" return generate_idp_federation_metadata( entity_descriptor_list=entity_descriptor_list, ).encode("utf-8") urlopen_mock.return_value = UrlOpenMock() response = self.client.get( reverse("account:saml_fer_idp_choice"), ) self.assertContains( response, f'action="{reverse("account:social:begin", args=("saml_fer",))}"', ) self.assertContains(response, "local-accepting-idp") response = self.client.get( f'{reverse("account:social:begin", args=("saml_fer",))}?idp=local-accepting-idp', ) self.assertEqual(response.status_code, 302) self.assertTrue(response["Location"].startswith( "http://testserver/account/saml/local-accepting-idp/sso/?SAMLRequest=" )) # 2/ Fake the redirection to the SSO response = self.client.get( f'{reverse("account:social:begin", args=("saml_fer",))}?idp=local-accepting-idp', follow=False, ) # 3/ Generate SAML response using SAML request query_values = parse_qs(urlparse(response["Location"]).query) saml_request = query_values["SAMLRequest"] saml_relay_state = query_values["RelayState"] readable_saml_request = OneLogin_Saml2_Utils.decode_base64_and_inflate( saml_request, ) saml_request = OneLogin_Saml2_XML.to_etree(readable_saml_request) saml_acs_url = saml_request.get("AssertionConsumerServiceURL") request_id = saml_request.get("ID") auth_response = OneLogin_Saml2_Utils.b64encode( generate_auth_response( request_id, saml_acs_url, issuer= "http://testserver/account/saml/local-accepting-idp/sso/", )) # 4/ POST the data to our endpoint response = self.client.post( saml_acs_url, data={ "RelayState": saml_relay_state, "SAMLResponse": auth_response, }, ) self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "/") # Assert the user is authenticated user = auth_get_user(self.client) self.assertTrue(user.is_authenticated) # Assert the user has an organization organization_access = user.organization_accesses.select_related( "organization").get() # also assert there is only one organization self.assertEqual(organization_access.role, STUDENT) self.assertEqual(organization_access.organization.name, "OrganizationDName")
def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None): """ Process the SAML Logout Response / Logout Request sent by the IdP. :param keep_local_session: When false will destroy the local session, otherwise will destroy it :type keep_local_session: bool :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string :returns: Redirection url """ self.__errors = [] if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data[ 'get_data']: logout_response = OneLogin_Saml2_Logout_Response( self.__settings, self.__request_data['get_data']['SAMLResponse']) if not logout_response.is_valid(self.__request_data, request_id): self.__errors.append('invalid_logout_response') self.__error_reason = logout_response.get_error() elif logout_response.get_status( ) != OneLogin_Saml2_Constants.STATUS_SUCCESS: self.__errors.append('logout_not_success') elif not keep_local_session: OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data[ 'get_data']: logout_request = OneLogin_Saml2_Logout_Request( self.__settings, self.__request_data['get_data']['SAMLRequest']) if not logout_request.is_valid(self.__request_data): self.__errors.append('invalid_logout_request') self.__error_reason = logout_request.get_error() else: if not keep_local_session: OneLogin_Saml2_Utils.delete_local_session( delete_session_cb) in_response_to = OneLogin_Saml2_Logout_Request.get_id( OneLogin_Saml2_Utils.decode_base64_and_inflate( self.__request_data['get_data']['SAMLRequest'])) response_builder = OneLogin_Saml2_Logout_Response( self.__settings) response_builder.build(in_response_to) logout_response = response_builder.get_response() parameters = {'SAMLResponse': logout_response} if 'RelayState' in self.__request_data['get_data']: parameters['RelayState'] = self.__request_data['get_data'][ 'RelayState'] security = self.__settings.get_security_data() if 'logoutResponseSigned' in security and security[ 'logoutResponseSigned']: parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 parameters['Signature'] = self.build_response_signature( logout_response, parameters.get('RelayState', None)) return self.redirect_to(self.get_slo_url(), parameters) else: self.__errors.append('invalid_binding') raise OneLogin_Saml2_Error( 'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding', OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND)
def processResponse(self, chkTime=True, checkInResponseTo=True): try: # get response and Relay state responsePost = self.get_argument('SAMLResponse') srelayPost = self.get_argument('RelayState') # decode saml response #response = responsePost self.response = responsePost try: self.response = OneLogin_Saml2_Utils.decode_base64_and_inflate( responsePost) except Exception: try: self.response = OneLogin_Saml2_Utils.b64decode( responsePost) except Exception: pass # try: # #response = OneLogin_Saml2_Utils.b64decode(responsePost) # self.response = OneLogin_Saml2_Utils.b64decode(responsePost) # except Exception: # pass ## parse XML and make some check ns = { 'md0': OneLogin_Saml2_Constants.NS_SAMLP, 'md1': OneLogin_Saml2_Constants.NS_SAML } parsedResponse = xml.etree.ElementTree.fromstring(self.response) self.inResponseTo = parsedResponse.get('InResponseTo') self.ResponseID = parsedResponse.get('ID') issuer = self.issuer = parsedResponse.find("md1:Issuer", ns) if issuer is None: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid118') return response_obj #spByInResponseTo = None # try to get sp searching a corresponding request and raise error if checkInResponseTo is True # inResponseChk = waitFuture(asyncio.run_coroutine_threadsafe(self.dbobjSaml.execute_statment("chk_idAssertion('%s')" % # inResponseTo), globalsObj.ioloop)) # if inResponseChk['error'] == 0 and inResponseChk['result'] is not None: # spByInResponseTo = inResponseChk['result'][0]['cod_sp'] # # elif checkInResponseTo and inResponseChk['error'] == 0 and inResponseChk['result'] == None: # response_obj = ResponseObj(httpcode=404, ID = ResponseID) # response_obj.setError('easyspid120') # response_obj.setResult(response = str(response, 'utf-8')) # return response_obj # # elif inResponseChk['error'] > 0: # response_obj = ResponseObj(httpcode=500) # response_obj.setError('easyspid105') # response_obj.setResult(inResponseChk['result']) # return response_obj # try to get sp searching a corresponding request and raise error if checkInResponseTo is True spByInResponseTo = self.chkExistsReq(checkInResponseTo) ### check StatusCode to find errors firstChk = easyspid.lib.utils.validateAssertion( str(self.response, 'utf-8'), None, None) if not firstChk['chkStatus']: #get errors codes samlErrors = waitFuture( asyncio.run_coroutine_threadsafe( getResponseError(parsedResponse, sp=spByInResponseTo, namespace=ns), globalsObj.ioloop)) if samlErrors['error'] == '0': response_obj = ResponseObj(httpcode=400, ID=self.ResponseID) response_obj.setError('easyspid121') response_obj.setResult(response=str( self.response, 'utf-8'), format='json', samlErrors=samlErrors['status']) return self.formatError(response_obj, srelayPost, samlErrors['service']) elif samlErrors['error'] == 'easyspid114': response_obj = ResponseObj(httpcode=404) response_obj.setError('easyspid114') return response_obj else: response_obj = ResponseObj(httpcode=500) response_obj.setError('500') response_obj.setResult(samlErrors['error']) return response_obj #decode Relay state #srelay = srelayPost self.srelay = srelayPost try: self.srelay = OneLogin_Saml2_Utils.decode_base64_and_inflate( srelayPost) except Exception: try: self.srelay = OneLogin_Saml2_Utils.b64decode(srelayPost) except Exception: pass #self.srelay = OneLogin_Saml2_Utils.b64decode(srelayPost) #pass # try: # #srelay = OneLogin_Saml2_Utils.b64decode(srelayPost) # self.srelay = OneLogin_Saml2_Utils.b64decode(srelayPost) # except Exception: # pass ## get sp by ID #ns = {'md0': OneLogin_Saml2_Constants.NS_SAMLP, 'md1': OneLogin_Saml2_Constants.NS_SAML} #parsedResponse = xml.etree.ElementTree.fromstring(response) #issuer = self.issuer = parsedResponse.find("md1:Issuer", ns) #inResponseTo = parsedResponse.get('InResponseTo') #get audience audience = self.audience = parsedResponse.find( 'md1:Assertion/md1:Conditions/md1:AudienceRestriction/md1:Audience', ns) if audience is None: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid118') return response_obj # if issuer is None or audience is None: # response_obj = ResponseObj(httpcode=401) # response_obj.setError('easyspid118') # return response_obj #task1 = asyncio.run_coroutine_threadsafe(self.dbobjSaml.execute_statment("chk_idAssertion('%s')" % # inResponseTo), globalsObj.ioloop) # task2 = asyncio.run_coroutine_threadsafe(self.dbobjSaml.execute_statment("get_provider_byentityid(%s, '%s')" % # ('True', '{'+(self.issuer.text.strip())+'}')), globalsObj.ioloop) #task3 = asyncio.run_coroutine_threadsafe(self.dbobjSaml.execute_statment("get_provider_byentityid(%s, '%s')" % # ('True', '{'+(audience.text.strip())+'}')), globalsObj.ioloop) #assert not task1.done() #inResponseChk = task1.result() #inResponseChk = waitFuture(task1) #audienceChk = waitFuture(task3) #spByAudience = None #spByInResponseTo = None #if inResponseChk['error'] == 0 and inResponseChk['result'] is not None: # spByInResponseTo = inResponseChk['result'][0]['cod_sp'] # if audienceChk['error'] == 0 and audienceChk['result'] is not None: # spByAudience = audienceChk['result'][0]['cod_provider'] #check audinece # if spByAudience is None: # response_obj = ResponseObj(httpcode=404) # response_obj.setError('easyspid115') # return response_obj # get sp by audience spByAudience = self.getSpByAudience() #check inresponseTo and spByAudience == spByInResponseTo if checkInResponseTo and spByAudience == spByInResponseTo: sp = spByAudience elif checkInResponseTo and spByAudience != spByInResponseTo: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid110') return response_obj sp = spByAudience # get service by sp and relay_state try: task = asyncio.run_coroutine_threadsafe( self.dbobjSaml.execute_statment( "get_service(%s, '%s', '%s')" % ('True', str(self.srelay), sp)), globalsObj.ioloop) #assert not task.done() #service = task.result() service = waitFuture(task) if service['error'] == 0 and service['result'] is not None: # costruisci il routing self.routing = dict() self.routing['url'] = service['result'][0]['url'] self.routing['relaystate'] = self.srelay self.routing['format'] = service['result'][0]['format'] elif service['error'] > 0 or service['result'] is None: response_obj = ResponseObj(httpcode=500, debugMessage=service['result']) response_obj.setError("easyspid111") return response_obj except Exception: pass # get IdP # idpEntityId = waitFuture(task2) # # if idpEntityId['error'] == 0 and idpEntityId['result'] is not None: # idp_metadata = idpEntityId['result'][0]['xml'] # idp = idpEntityId['result'][0]['cod_provider'] # # elif idpEntityId['error'] == 0 and idpEntityId['result'] is None: # response_obj = ResponseObj(httpcode=404) # response_obj.setError('easyspid103') # return response_obj # # elif idpEntityId['error'] > 0: # response_obj = ResponseObj(httpcode=500, debugMessage=idpEntityId['result']) # response_obj.setError("easyspid105") # return response_obj # get IdP and metadata (idp_metadata, idp) = self.getIdentyIdp() # get sp settings task = asyncio.run_coroutine_threadsafe( easyspid.lib.easyspid.spSettings(sp, idp, close=True), globalsObj.ioloop) sp_settings = waitFuture(task) if sp_settings['error'] == 0 and sp_settings['result'] != None: ## insert response into DB task = asyncio.run_coroutine_threadsafe( self.dbobjSaml.execute_statment( "write_assertion('%s', '%s', '%s', '%s')" % (str(self.response, 'utf-8').replace( "'", "''"), sp, idp, self.remote_ip)), globalsObj.ioloop) wrtAuthn = waitFuture(task) if wrtAuthn['error'] == 0: if self.routing['format'] == 'saml': return self.passthrough() task = asyncio.run_coroutine_threadsafe( self.dbobjJwt.execute_statment( "get_token_by_cod('%s')" % (wrtAuthn['result'][0]['cod_token'])), globalsObj.ioloop) #assert not task.done() #jwt = task.result() jwt = waitFuture(task) else: response_obj = ResponseObj(httpcode=500, debugMessage=wrtAuthn['result']) response_obj.setError("easyspid105") logging.getLogger( type(self).__module__ + "." + type(self).__qualname__).error('Exception', exc_info=True) return response_obj # create settings OneLogin dict #settings = sp_settings['result'] prvdSettings = Saml2_Settings(sp_settings['result']) chk = easyspid.lib.utils.validateAssertion( str(self.response, 'utf-8'), sp_settings['result']['idp']['x509cert_fingerprint'], sp_settings['result']['idp']['x509cert_fingerprintalg']) chk['issuer'] = issuer.text.strip() chk['audience'] = audience.text.strip() if not chk['chkStatus']: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid107') return response_obj elif not chk['schemaValidate']: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid104') response_obj.setResult(responseValidate=chk) return response_obj elif not chk['signCheck']: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid106') response_obj.setResult(responseValidate=chk) return response_obj elif not chk[ 'certAllowed'] and globalsObj.easyspid_checkCertificateAllowed: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid116') response_obj.setResult(responseValidate=chk) return response_obj elif not chk[ 'certValidity'] and globalsObj.easyspid_checkCertificateValidity: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid117') response_obj.setResult(responseValidate=chk) return response_obj elif chkTime and not chk['chkTime']: response_obj = ResponseObj(httpcode=401) response_obj.setError('easyspid108') return response_obj #get all attributes attributes = chk['serviceAttributes'] attributes_tmp = dict() for key in attributes: attributes_tmp[key] = attributes[key][0] attributes = attributes_tmp # build response form try: with open( os.path.join(globalsObj.modules_basedir, globalsObj.easyspid_responseFormPath), 'rb') as myfile: response_form = myfile.read().decode("utf-8") except: with open(globalsObj.easyspid_responseFormPath, 'rb') as myfile: response_form = myfile.read().decode("utf-8") response_obj = ResponseObj( httpcode=200, ID=wrtAuthn['result'][0]['ID_assertion']) response_obj.setError('200') response_obj.setResult(attributes=attributes, jwt=jwt['result'][0]['token'], responseValidate=chk, response=str(self.response, 'utf-8'), format='json') response_form = response_form.replace("%URLTARGET%", self.routing['url']) response_form = response_form.replace("%RELAYSTATE%", srelayPost) response_form = response_form.replace( "%RESPONSE%", OneLogin_Saml2_Utils.b64encode(response_obj.jsonWrite())) self.postTo = response_form elif sp_settings['error'] == 0 and sp_settings['result'] == None: response_obj = ResponseObj(httpcode=404) response_obj.setError('easyspid114') elif sp_settings['error'] > 0: #response_obj = sp_settings['result'] response_obj = sp_settings except goExit as e: return e.expression except tornado.web.MissingArgumentError as error: response_obj = ResponseObj(debugMessage=error.log_message, httpcode=error.status_code, devMessage=error.log_message) response_obj.setError(str(error.status_code)) logging.getLogger( type(self).__module__ + "." + type(self).__qualname__).error( '%s' % error, exc_info=True) except Exception as inst: response_obj = ResponseObj(httpcode=500) response_obj.setError('500') logging.getLogger( type(self).__module__ + "." + type(self).__qualname__).error( 'Exception', exc_info=True) return response_obj