From a42554a640abb4cc57acb988a83444a616c1cba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pekka=20P=C3=B6yry?= Date: Tue, 15 Nov 2016 11:58:40 +0200 Subject: [PATCH 01/18] Rename deprecated assertations --- tests/src/OneLogin/saml2_tests/response_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index f8904d3c..e9246577 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -279,7 +279,7 @@ def testCheckOneCondition(self): settings.set_strict(True) response = OneLogin_Saml2_Response(settings, xml) self.assertFalse(response.is_valid(self.get_request_data())) - self.assertEquals('The Assertion must include a Conditions element', response.get_error()) + self.assertEqual('The Assertion must include a Conditions element', response.get_error()) xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) response_2 = OneLogin_Saml2_Response(settings, xml_2) @@ -298,7 +298,7 @@ def testCheckOneAuthnStatement(self): settings.set_strict(True) response = OneLogin_Saml2_Response(settings, xml) self.assertFalse(response.is_valid(self.get_request_data())) - self.assertEquals('The Assertion must include an AuthnStatement element', response.get_error()) + self.assertEqual('The Assertion must include an AuthnStatement element', response.get_error()) xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) response_2 = OneLogin_Saml2_Response(settings, xml_2) @@ -724,7 +724,7 @@ def testIsInValidDestination(self): message_3 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_destination.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, message_3) self.assertFalse(response_4.is_valid(self.get_request_data())) - self.assertEquals('The response has an empty Destination value', response_4.get_error()) + self.assertEqual('The response has an empty Destination value', response_4.get_error()) # No Destination dom.firstChild.removeAttribute('Destination') From e7b17a70dafc02447712c2bca1d9b2756533e25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pekka=20P=C3=B6yry?= Date: Thu, 17 Nov 2016 19:28:47 +0200 Subject: [PATCH 02/18] Make logout request function is_valid support raising exceptions --- src/onelogin/saml2/logout_request.py | 7 ++++++- .../OneLogin/saml2_tests/logout_request_test.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index c714bffa..6daafe9c 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -212,12 +212,15 @@ def get_session_indexes(request): session_indexes.append(session_index_node.text) return session_indexes - def is_valid(self, request_data): + def is_valid(self, request_data, raises=False): """ Checks if the Logout Request received is valid :param request_data: Request Data :type request_data: dict + :param raises: Optional argument. If true, the function will raise an exception as soon as first validation test fails + :type raises: bool + :return: If the Logout Request is or not valid :rtype: boolean """ @@ -274,6 +277,8 @@ def is_valid(self, request_data): debug = self.__settings.is_debug_active() if debug: print(err) + if raises: + raise return False def get_error(self): diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index e8f3b1b5..c30bd09d 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -336,3 +336,19 @@ def testIsValid(self): request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) logout_request5 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) self.assertTrue(logout_request5.is_valid(request_data)) + + def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): + request = OneLogin_Saml2_Utils.b64encode('invalid') + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings.set_strict(True) + + logout_request = OneLogin_Saml2_Logout_Request(settings, request) + + self.assertFalse(logout_request.is_valid(request_data)) + + with self.assertRaises(Exception): + logout_request.is_valid(request_data, raises=True) From d3549f9f469a748c4d92b453b2a757d5c7ee8f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pekka=20P=C3=B6yry?= Date: Thu, 17 Nov 2016 19:38:44 +0200 Subject: [PATCH 03/18] Improve logout request tests --- .../saml2_tests/logout_request_test.py | 43 ++++++------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index c30bd09d..7ab11346 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -121,10 +121,8 @@ def testGetNameIdData(self): self.assertEqual(expected_name_id_data, name_id_data_2) request_2 = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_encrypted_nameid.xml')) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(Exception, 'Key is required in order to decrypt the NameID'): OneLogin_Saml2_Logout_Request.get_nameid(request_2) - exception = context.exception - self.assertIn("Key is required in order to decrypt the NameID", str(exception)) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) key = settings.get_sp_key() @@ -140,16 +138,12 @@ def testGetNameIdData(self): encrypted_id_nodes = dom_2.getElementsByTagName('saml:EncryptedID') encrypted_data = encrypted_id_nodes[0].firstChild.nextSibling encrypted_id_nodes[0].removeChild(encrypted_data) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(Exception, 'Not NameID found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid(dom_2.toxml(), key) - exception = context.exception - self.assertIn("Not NameID found in the Logout Request", str(exception)) inv_request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'no_nameId.xml')) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(Exception, 'Not NameID found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid(inv_request) - exception = context.exception - self.assertIn("Not NameID found in the Logout Request", str(exception)) def testGetNameId(self): """ @@ -160,10 +154,8 @@ def testGetNameId(self): self.assertEqual(name_id, 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c') request_2 = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_encrypted_nameid.xml')) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(Exception, 'Key is required in order to decrypt the NameID'): OneLogin_Saml2_Logout_Request.get_nameid(request_2) - exception = context.exception - self.assertIn("Key is required in order to decrypt the NameID", str(exception)) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) key = settings.get_sp_key() @@ -242,12 +234,9 @@ def testIsInvalidIssuer(self): self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) - try: - logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) - valid = logout_request2.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Invalid issuer in the Logout Request', str(e)) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) + with self.assertRaisesRegexp(Exception, 'Invalid issuer in the Logout Request'): + logout_request2.is_valid(request_data, raises=True) def testIsInvalidDestination(self): """ @@ -264,12 +253,9 @@ def testIsInvalidDestination(self): self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) - try: - logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) - valid = logout_request2.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('The LogoutRequest was received at', str(e)) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) + with self.assertRaisesRegexp(Exception, 'The LogoutRequest was received at'): + logout_request2.is_valid(request_data, raises=True) dom = parseString(request) dom.documentElement.setAttribute('Destination', None) @@ -298,12 +284,9 @@ def testIsInvalidNotOnOrAfter(self): self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) - try: - logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) - valid = logout_request2.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Timing issues (please check your clock settings)', str(e)) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) + with self.assertRaisesRegexp(Exception, 'Timing issues \(please check your clock settings\)'): + logout_request2.is_valid(request_data, raises=True) def testIsValid(self): """ From 2965e0091c611edc42df86991f3455bbc8a65270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pekka=20P=C3=B6yry?= Date: Thu, 17 Nov 2016 19:34:41 +0200 Subject: [PATCH 04/18] Make logout response function is_valid support raising exceptions --- src/onelogin/saml2/logout_response.py | 8 +++++++- .../saml2_tests/logout_response_test.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 9a12beb8..dd7dc0ee 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -63,11 +63,15 @@ def get_status(self): status = entries[0].attrib['Value'] return status - def is_valid(self, request_data, request_id=None): + def is_valid(self, request_data, request_id=None, raises=False): """ Determines if the SAML LogoutResponse is valid :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string + + :param raises: Optional argument. If true, the function will raise an exception as soon as first validation test fails + :type raises: bool + :return: Returns if the SAML LogoutResponse is or not valid :rtype: boolean """ @@ -111,6 +115,8 @@ def is_valid(self, request_data, request_id=None): debug = self.__settings.is_debug_active() if debug: print(err) + if raises: + raise return False def __query(self, query): diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 51ee0f49..c95caf91 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -277,3 +277,20 @@ def testIsValid(self): response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) self.assertTrue(response_3.is_valid(request_data)) + + def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): + message = OneLogin_Saml2_Utils.deflate_and_base64_encode('invalid') + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings.set_strict(True) + + response = OneLogin_Saml2_Logout_Response(settings, message) + + self.assertFalse(response.is_valid(request_data)) + + with self.assertRaises(Exception): + response.is_valid(request_data, raises=True) From c3d92fbfe62873e0bfb1bd933c00db36ea82de92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pekka=20P=C3=B6yry?= Date: Thu, 17 Nov 2016 20:18:33 +0200 Subject: [PATCH 05/18] Improve logout response tests --- .../saml2_tests/logout_response_test.py | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index c95caf91..7ec22f8b 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -201,11 +201,8 @@ def testIsInValidIssuer(self): 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('Invalid issuer in the Logout Request', str(e)) + with self.assertRaisesRegexp(Exception, 'Invalid issuer in the Logout Request'): + response_2.is_valid(request_data, raises=True) def testIsInValidDestination(self): """ @@ -226,11 +223,8 @@ def testIsInValidDestination(self): 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)) + with self.assertRaisesRegexp(Exception, 'The LogoutRequest was received at'): + response_2.is_valid(request_data, raises=True) # Empty destination dom = parseString(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) @@ -264,11 +258,8 @@ def testIsValid(self): 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)) + with self.assertRaisesRegexp(Exception, 'The LogoutRequest was received at'): + response_2.is_valid(request_data, raises=True) 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) From 5545e21ad3a680cdcd2a6246ee6f7466e5ec7cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pekka=20P=C3=B6yry?= Date: Tue, 15 Nov 2016 13:05:23 +0200 Subject: [PATCH 06/18] Make response function is_valid support raising exceptions --- src/onelogin/saml2/response.py | 7 ++++++- tests/src/OneLogin/saml2_tests/response_test.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index bb6a9b44..c4b0751d 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -47,7 +47,7 @@ def __init__(self, settings, response): self.encrypted = True self.decrypted_document = self.__decrypt_assertion(decrypted_document) - def is_valid(self, request_data, request_id=None): + def is_valid(self, request_data, request_id=None, raises=False): """ Validates the response object. @@ -57,6 +57,9 @@ def is_valid(self, request_data, request_id=None): :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP :type request_id: string + :param raises: Optional argument. If true, the function will raise an exception as soon as first validation test fails + :type raises: bool + :returns: True if the SAML Response is valid, False if not :rtype: bool """ @@ -226,6 +229,8 @@ def is_valid(self, request_data, request_id=None): debug = self.__settings.is_debug_active() if debug: print(err) + if raises: + raise return False def check_status(self): diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index e9246577..06816d06 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1349,3 +1349,18 @@ def testIsValidWithoutInResponseTo(self): 'http_host': 'pitbulk.no-ip.org', 'script_name': 'newonelogin/demo1/index.php?acs' })) + + def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): + """ + Tests that the internal exception gets raised if the raise parameter + is True. + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings.set_strict(True) + xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_conditions.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + + self.assertFalse(response.is_valid(self.get_request_data())) + + with self.assertRaises(Exception): + response.is_valid(self.get_request_data(), raises=True) From 16e38bed9cb76777c12a310c8bddd634f4008a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pekka=20P=C3=B6yry?= Date: Tue, 15 Nov 2016 10:58:20 +0200 Subject: [PATCH 07/18] Improve response tests --- .../src/OneLogin/saml2_tests/response_test.py | 201 +++++------------- 1 file changed, 54 insertions(+), 147 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 06816d06..a2d8c65b 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -79,21 +79,15 @@ def testReturnNameId(self): xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, xml_4) - try: + with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): response_4.get_nameid() - self.assertTrue(False) - except Exception as e: - self.assertIn('Not NameID found in the assertion of the Response', str(e)) json_settings['security']['wantNameId'] = True settings = OneLogin_Saml2_Settings(json_settings) response_5 = OneLogin_Saml2_Response(settings, xml_4) - try: + with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): response_5.get_nameid() - self.assertTrue(False) - except Exception as e: - self.assertIn('Not NameID found in the assertion of the Response', str(e)) json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) @@ -106,30 +100,21 @@ def testReturnNameId(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - try: + with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): response_7.get_nameid() - self.assertTrue(False) - except Exception as e: - self.assertIn('Not NameID found in the assertion of the Response', str(e)) json_settings['strict'] = True settings = OneLogin_Saml2_Settings(json_settings) xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64')) response_8 = OneLogin_Saml2_Response(settings, xml_5) - try: + with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value.'): response_8.get_nameid() - self.assertTrue(False) - except Exception as e: - self.assertIn('The SPNameQualifier value mistmatch the SP entityID value.', str(e)) xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) response_9 = OneLogin_Saml2_Response(settings, xml_6) - try: + with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): response_9.get_nameid() - self.assertTrue(False) - except Exception as e: - self.assertIn('An empty NameID value found', str(e)) def testGetNameIdData(self): """ @@ -168,21 +153,15 @@ def testGetNameIdData(self): xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, xml_4) - try: + with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): response_4.get_nameid_data() - self.assertTrue(False) - except Exception as e: - self.assertIn('Not NameID found in the assertion of the Response', str(e)) json_settings['security']['wantNameId'] = True settings = OneLogin_Saml2_Settings(json_settings) response_5 = OneLogin_Saml2_Response(settings, xml_4) - try: + with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): response_5.get_nameid_data() - self.assertTrue(False) - except Exception as e: - self.assertIn('Not NameID found in the assertion of the Response', str(e)) json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) @@ -195,11 +174,8 @@ def testGetNameIdData(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - try: + with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): response_7.get_nameid_data() - self.assertTrue(False) - except Exception as e: - self.assertIn('Not NameID found in the assertion of the Response', str(e)) json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) @@ -212,30 +188,21 @@ def testGetNameIdData(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - try: + with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): response_7.get_nameid_data() - self.assertTrue(False) - except Exception as e: - self.assertIn('Not NameID found in the assertion of the Response', str(e)) json_settings['strict'] = True settings = OneLogin_Saml2_Settings(json_settings) xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64')) response_8 = OneLogin_Saml2_Response(settings, xml_5) - try: + with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value.'): response_8.get_nameid_data() - self.assertTrue(False) - except Exception as e: - self.assertIn('The SPNameQualifier value mistmatch the SP entityID value.', str(e)) xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) response_9 = OneLogin_Saml2_Response(settings, xml_6) - try: + with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): response_9.get_nameid_data() - self.assertTrue(False) - except Exception as e: - self.assertIn('An empty NameID value found', str(e)) def testCheckStatus(self): """ @@ -252,19 +219,13 @@ def testCheckStatus(self): xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64')) response_2 = OneLogin_Saml2_Response(settings, xml_2) - try: + with self.assertRaisesRegexp(Exception, 'The status code of the Response was not Success, was Responder'): response_2.check_status() - self.assertTrue(False) - except Exception as e: - self.assertIn('The status code of the Response was not Success, was Responder', str(e)) xml_3 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responer_and_msg.xml.base64')) response_3 = OneLogin_Saml2_Response(settings, xml_3) - try: + with self.assertRaisesRegexp(Exception, 'The status code of the Response was not Success, was Responder -> something_is_wrong'): response_3.check_status() - self.assertTrue(False) - except Exception as e: - self.assertIn('The status code of the Response was not Success, was Responder -> something_is_wrong', str(e)) def testCheckOneCondition(self): """ @@ -374,17 +335,13 @@ def testGetIssuers(self): xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_response.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, xml_4) - try: + with self.assertRaisesRegexp(Exception, 'Issuer of the Response not found or multiple.'): response_4.get_issuers() - except Exception as e: - self.assertIn('Issuer of the Response not found or multiple.', str(e)) xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_assertion.xml.base64')) response_5 = OneLogin_Saml2_Response(settings, xml_5) - try: + with self.assertRaisesRegexp(Exception, 'Issuer of the Assertion not found or multiple.'): response_5.get_issuers() - except Exception as e: - self.assertIn('Issuer of the Assertion not found or multiple.', str(e)) def testGetSessionIndex(self): """ @@ -535,11 +492,8 @@ def testValidateVersion(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_saml2.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - try: - valid = response.is_valid(self.get_request_data()) - self.assertFalse(valid) - except Exception as e: - self.assertEqual('Reference validation failed', str(e)) + with self.assertRaisesRegexp(Exception, 'Unsupported SAML version'): + response.is_valid(self.get_request_data(), raises=True) def testValidateID(self): """ @@ -549,11 +503,8 @@ def testValidateID(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_id.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - try: - valid = response.is_valid(self.get_request_data()) - self.assertFalse(valid) - except Exception as e: - self.assertEqual('Missing ID attribute on SAML Response', str(e)) + with self.assertRaisesRegexp(Exception, 'Missing ID attribute on SAML Response'): + response.is_valid(self.get_request_data(), raises=True) def testIsInValidReference(self): """ @@ -582,11 +533,8 @@ def testIsInValidExpired(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, xml) - try: - valid = response_2.is_valid(self.get_request_data()) - self.assertFalse(valid) - except Exception as e: - self.assertEqual('Timing issues (please check your clock settings)', str(e)) + with self.assertRaisesRegexp(Exception, 'Timing issues \(please check your clock settings\)'): + response_2.is_valid(self.get_request_data(), raises=True) def testIsInValidNoStatement(self): """ @@ -643,11 +591,8 @@ def testIsInValidNoKey(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - try: - valid = response.is_valid(self.get_request_data()) - self.assertFalse(valid) - except Exception as e: - self.assertEqual('Signature validation failed. SAML Response rejected', str(e)) + with self.assertRaisesRegexp(Exception, 'Signature validation failed. SAML Response rejected'): + response.is_valid(self.get_request_data(), raises=True) def testIsInValidMultipleAssertions(self): """ @@ -658,11 +603,8 @@ def testIsInValidMultipleAssertions(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'multiple_assertions.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - try: - valid = response.is_valid(self.get_request_data()) - self.assertFalse(valid) - except Exception as e: - self.assertEqual('SAML Response must contain 1 assertion', str(e)) + with self.assertRaisesRegexp(Exception, 'SAML Response must contain 1 assertion'): + response.is_valid(self.get_request_data(), raises=True) def testIsInValidEncAttrs(self): """ @@ -677,11 +619,8 @@ def testIsInValidEncAttrs(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, xml) - try: - valid = response_2.is_valid(self.get_request_data()) - self.assertFalse(valid) - except Exception as e: - self.assertEqual('There is an EncryptedAttribute in the Response and this SP not support them', str(e)) + with self.assertRaisesRegexp(Exception, 'There is an EncryptedAttribute in the Response and this SP not support them'): + response_2.is_valid(self.get_request_data(), raises=True) def testIsInValidDuplicatedAttrs(self): """ @@ -691,11 +630,8 @@ def testIsInValidDuplicatedAttrs(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'duplicated_attributes.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - try: + with self.assertRaisesRegexp(Exception, 'Found an Attribute element with duplicated Name'): response.get_attributes() - self.assertFalse(True) - except Exception as e: - self.assertEqual('Found an Attribute element with duplicated Name', str(e)) def testIsInValidDestination(self): """ @@ -786,18 +722,12 @@ def testIsInValidIssuer(self): settings.set_strict(True) response_3 = OneLogin_Saml2_Response(settings, message) - try: - valid = response_3.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertEqual('is not a valid audience for this Response', str(e)) + with self.assertRaisesRegexp(Exception, 'Invalid issuer in the Assertion/Response'): + response_3.is_valid(request_data, raises=True) response_4 = OneLogin_Saml2_Response(settings, message_2) - try: - valid = response_4.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertEqual('is not a valid audience for this Response', str(e)) + with self.assertRaisesRegexp(Exception, 'Invalid issuer in the Assertion/Response'): + response_4.is_valid(request_data, raises=True) def testIsInValidSessionIndex(self): """ @@ -821,11 +751,8 @@ def testIsInValidSessionIndex(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, message) - try: - valid = response_2.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertEqual('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response', str(e)) + with self.assertRaisesRegexp(Exception, 'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response'): + response_2.is_valid(request_data, raises=True) def testDatetimeWithMiliseconds(self): """ @@ -915,40 +842,28 @@ def testIsInValidSubjectConfirmation(self): settings.set_strict(True) response = OneLogin_Saml2_Response(settings, message) - try: - self.assertFalse(response.is_valid(request_data)) - except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) + with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + response.is_valid(request_data, raises=True) response_2 = OneLogin_Saml2_Response(settings, message_2) - try: - self.assertFalse(response_2.is_valid(request_data)) - except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) + with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + response_2.is_valid(request_data, raises=True) response_3 = OneLogin_Saml2_Response(settings, message_3) - try: - self.assertFalse(response_3.is_valid(request_data)) - except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) + with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + response_3.is_valid(request_data, raises=True) response_4 = OneLogin_Saml2_Response(settings, message_4) - try: - self.assertFalse(response_4.is_valid(request_data)) - except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) + with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + response_4.is_valid(request_data, raises=True) response_5 = OneLogin_Saml2_Response(settings, message_5) - try: - self.assertFalse(response_5.is_valid(request_data)) - except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) + with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + response_5.is_valid(request_data, raises=True) response_6 = OneLogin_Saml2_Response(settings, message_6) - try: - self.assertFalse(response_6.is_valid(request_data)) - except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) + with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + response_6.is_valid(request_data, raises=True) def testIsInValidRequestId(self): """ @@ -973,10 +888,8 @@ def testIsInValidRequestId(self): settings.set_strict(True) response = OneLogin_Saml2_Response(settings, message) - try: - self.assertFalse(response.is_valid(request_data, request_id)) - except Exception as e: - self.assertEqual('The InResponseTo of the Response', str(e)) + with self.assertRaisesRegexp(Exception, 'The InResponseTo of the Response'): + response.is_valid(request_data, request_id, raises=True) valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38' response.is_valid(request_data, valid_request_id) @@ -1020,10 +933,8 @@ def testIsInValidSignIssues(self): settings_info['security']['wantAssertionsSigned'] = True settings_4 = OneLogin_Saml2_Settings(settings_info) response_4 = OneLogin_Saml2_Response(settings_4, message) - try: - self.assertFalse(response_4.is_valid(request_data)) - except Exception as e: - self.assertEqual('The Assertion of the Response is not signed and the SP require it', str(e)) + with self.assertRaisesRegexp(Exception, 'The Assertion of the Response is not signed and the SP require it'): + response_4.is_valid(request_data, raises=True) settings_info['security']['wantAssertionsSigned'] = False settings_info['strict'] = False @@ -1050,10 +961,8 @@ def testIsInValidSignIssues(self): settings_info['security']['wantMessagesSigned'] = True settings_8 = OneLogin_Saml2_Settings(settings_info) response_8 = OneLogin_Saml2_Response(settings_8, message) - try: - self.assertFalse(response_8.is_valid(request_data)) - except Exception as e: - self.assertEqual('The Message of the Response is not signed and the SP require it', str(e)) + with self.assertRaisesRegexp(Exception, 'The Message of the Response is not signed and the SP require it'): + response_8.is_valid(request_data, raises=True) def testIsInValidEncIssues(self): """ @@ -1134,10 +1043,8 @@ def testIsInValidCert(self): xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - try: - self.assertFalse(response.is_valid(self.get_request_data())) - except Exception as e: - self.assertIn('openssl_x509_read(): supplied parameter cannot be', str(e)) + with self.assertRaisesRegexp(Exception, 'failed to load key'): + response.is_valid(self.get_request_data(), raises=True) def testIsInValidCert2(self): """ From 7ac264071e53a899b1b03975b0bd0960ca85b0e4 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 22 Dec 2016 12:07:15 +0100 Subject: [PATCH 08/18] Rename parameter --- src/onelogin/saml2/logout_request.py | 8 ++-- src/onelogin/saml2/logout_response.py | 8 ++-- src/onelogin/saml2/response.py | 8 ++-- .../saml2_tests/logout_request_test.py | 8 ++-- .../saml2_tests/logout_response_test.py | 8 ++-- .../src/OneLogin/saml2_tests/response_test.py | 40 +++++++++---------- 6 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 6daafe9c..d74993d3 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -212,14 +212,14 @@ def get_session_indexes(request): session_indexes.append(session_index_node.text) return session_indexes - def is_valid(self, request_data, raises=False): + def is_valid(self, request_data, raise_exceptions=False): """ Checks if the Logout Request received is valid :param request_data: Request Data :type request_data: dict - :param raises: Optional argument. If true, the function will raise an exception as soon as first validation test fails - :type raises: bool + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean :return: If the Logout Request is or not valid :rtype: boolean @@ -277,7 +277,7 @@ def is_valid(self, request_data, raises=False): debug = self.__settings.is_debug_active() if debug: print(err) - if raises: + if raise_exceptions: raise return False diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index dd7dc0ee..a9cdba8d 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -63,14 +63,14 @@ def get_status(self): status = entries[0].attrib['Value'] return status - def is_valid(self, request_data, request_id=None, raises=False): + def is_valid(self, request_data, request_id=None, raise_exceptions=False): """ Determines if the SAML LogoutResponse is valid :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string - :param raises: Optional argument. If true, the function will raise an exception as soon as first validation test fails - :type raises: bool + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean :return: Returns if the SAML LogoutResponse is or not valid :rtype: boolean @@ -115,7 +115,7 @@ def is_valid(self, request_data, request_id=None, raises=False): debug = self.__settings.is_debug_active() if debug: print(err) - if raises: + if raise_exceptions: raise return False diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index dc3b077b..16184336 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -47,7 +47,7 @@ def __init__(self, settings, response): self.encrypted = True self.decrypted_document = self.__decrypt_assertion(decrypted_document) - def is_valid(self, request_data, request_id=None, raises=False): + def is_valid(self, request_data, request_id=None, raise_exceptions=False): """ Validates the response object. @@ -57,8 +57,8 @@ def is_valid(self, request_data, request_id=None, raises=False): :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP :type request_id: string - :param raises: Optional argument. If true, the function will raise an exception as soon as first validation test fails - :type raises: bool + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean :returns: True if the SAML Response is valid, False if not :rtype: bool @@ -229,7 +229,7 @@ def is_valid(self, request_data, request_id=None, raises=False): debug = self.__settings.is_debug_active() if debug: print(err) - if raises: + if raise_exceptions: raise return False diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 7ab11346..3741db90 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -236,7 +236,7 @@ def testIsInvalidIssuer(self): settings.set_strict(True) logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) with self.assertRaisesRegexp(Exception, 'Invalid issuer in the Logout Request'): - logout_request2.is_valid(request_data, raises=True) + logout_request2.is_valid(request_data, raise_exceptions=True) def testIsInvalidDestination(self): """ @@ -255,7 +255,7 @@ def testIsInvalidDestination(self): settings.set_strict(True) logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) with self.assertRaisesRegexp(Exception, 'The LogoutRequest was received at'): - logout_request2.is_valid(request_data, raises=True) + logout_request2.is_valid(request_data, raise_exceptions=True) dom = parseString(request) dom.documentElement.setAttribute('Destination', None) @@ -286,7 +286,7 @@ def testIsInvalidNotOnOrAfter(self): settings.set_strict(True) logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) with self.assertRaisesRegexp(Exception, 'Timing issues \(please check your clock settings\)'): - logout_request2.is_valid(request_data, raises=True) + logout_request2.is_valid(request_data, raise_exceptions=True) def testIsValid(self): """ @@ -334,4 +334,4 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): self.assertFalse(logout_request.is_valid(request_data)) with self.assertRaises(Exception): - logout_request.is_valid(request_data, raises=True) + logout_request.is_valid(request_data, raise_exceptions=True) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 7ec22f8b..2cc1083b 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -202,7 +202,7 @@ def testIsInValidIssuer(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) with self.assertRaisesRegexp(Exception, 'Invalid issuer in the Logout Request'): - response_2.is_valid(request_data, raises=True) + response_2.is_valid(request_data, raise_exceptions=True) def testIsInValidDestination(self): """ @@ -224,7 +224,7 @@ def testIsInValidDestination(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) with self.assertRaisesRegexp(Exception, 'The LogoutRequest was received at'): - response_2.is_valid(request_data, raises=True) + response_2.is_valid(request_data, raise_exceptions=True) # Empty destination dom = parseString(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) @@ -259,7 +259,7 @@ def testIsValid(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) with self.assertRaisesRegexp(Exception, 'The LogoutRequest was received at'): - response_2.is_valid(request_data, raises=True) + response_2.is_valid(request_data, raise_exceptions=True) 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) @@ -284,4 +284,4 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): self.assertFalse(response.is_valid(request_data)) with self.assertRaises(Exception): - response.is_valid(request_data, raises=True) + response.is_valid(request_data, raise_exceptions=True) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index a2d8c65b..5639bd59 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -493,7 +493,7 @@ def testValidateVersion(self): xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_saml2.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) with self.assertRaisesRegexp(Exception, 'Unsupported SAML version'): - response.is_valid(self.get_request_data(), raises=True) + response.is_valid(self.get_request_data(), raise_exceptions=True) def testValidateID(self): """ @@ -504,7 +504,7 @@ def testValidateID(self): xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_id.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) with self.assertRaisesRegexp(Exception, 'Missing ID attribute on SAML Response'): - response.is_valid(self.get_request_data(), raises=True) + response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidReference(self): """ @@ -534,7 +534,7 @@ def testIsInValidExpired(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, xml) with self.assertRaisesRegexp(Exception, 'Timing issues \(please check your clock settings\)'): - response_2.is_valid(self.get_request_data(), raises=True) + response_2.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidNoStatement(self): """ @@ -592,7 +592,7 @@ def testIsInValidNoKey(self): xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) with self.assertRaisesRegexp(Exception, 'Signature validation failed. SAML Response rejected'): - response.is_valid(self.get_request_data(), raises=True) + response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidMultipleAssertions(self): """ @@ -604,7 +604,7 @@ def testIsInValidMultipleAssertions(self): xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'multiple_assertions.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) with self.assertRaisesRegexp(Exception, 'SAML Response must contain 1 assertion'): - response.is_valid(self.get_request_data(), raises=True) + response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidEncAttrs(self): """ @@ -620,7 +620,7 @@ def testIsInValidEncAttrs(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, xml) with self.assertRaisesRegexp(Exception, 'There is an EncryptedAttribute in the Response and this SP not support them'): - response_2.is_valid(self.get_request_data(), raises=True) + response_2.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidDuplicatedAttrs(self): """ @@ -723,11 +723,11 @@ def testIsInValidIssuer(self): settings.set_strict(True) response_3 = OneLogin_Saml2_Response(settings, message) with self.assertRaisesRegexp(Exception, 'Invalid issuer in the Assertion/Response'): - response_3.is_valid(request_data, raises=True) + response_3.is_valid(request_data, raise_exceptions=True) response_4 = OneLogin_Saml2_Response(settings, message_2) with self.assertRaisesRegexp(Exception, 'Invalid issuer in the Assertion/Response'): - response_4.is_valid(request_data, raises=True) + response_4.is_valid(request_data, raise_exceptions=True) def testIsInValidSessionIndex(self): """ @@ -752,7 +752,7 @@ def testIsInValidSessionIndex(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, message) with self.assertRaisesRegexp(Exception, 'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response'): - response_2.is_valid(request_data, raises=True) + response_2.is_valid(request_data, raise_exceptions=True) def testDatetimeWithMiliseconds(self): """ @@ -843,27 +843,27 @@ def testIsInValidSubjectConfirmation(self): settings.set_strict(True) response = OneLogin_Saml2_Response(settings, message) with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): - response.is_valid(request_data, raises=True) + response.is_valid(request_data, raise_exceptions=True) response_2 = OneLogin_Saml2_Response(settings, message_2) with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): - response_2.is_valid(request_data, raises=True) + response_2.is_valid(request_data, raise_exceptions=True) response_3 = OneLogin_Saml2_Response(settings, message_3) with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): - response_3.is_valid(request_data, raises=True) + response_3.is_valid(request_data, raise_exceptions=True) response_4 = OneLogin_Saml2_Response(settings, message_4) with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): - response_4.is_valid(request_data, raises=True) + response_4.is_valid(request_data, raise_exceptions=True) response_5 = OneLogin_Saml2_Response(settings, message_5) with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): - response_5.is_valid(request_data, raises=True) + response_5.is_valid(request_data, raise_exceptions=True) response_6 = OneLogin_Saml2_Response(settings, message_6) with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): - response_6.is_valid(request_data, raises=True) + response_6.is_valid(request_data, raise_exceptions=True) def testIsInValidRequestId(self): """ @@ -889,7 +889,7 @@ def testIsInValidRequestId(self): settings.set_strict(True) response = OneLogin_Saml2_Response(settings, message) with self.assertRaisesRegexp(Exception, 'The InResponseTo of the Response'): - response.is_valid(request_data, request_id, raises=True) + response.is_valid(request_data, request_id, raise_exceptions=True) valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38' response.is_valid(request_data, valid_request_id) @@ -934,7 +934,7 @@ def testIsInValidSignIssues(self): settings_4 = OneLogin_Saml2_Settings(settings_info) response_4 = OneLogin_Saml2_Response(settings_4, message) with self.assertRaisesRegexp(Exception, 'The Assertion of the Response is not signed and the SP require it'): - response_4.is_valid(request_data, raises=True) + response_4.is_valid(request_data, raise_exceptions=True) settings_info['security']['wantAssertionsSigned'] = False settings_info['strict'] = False @@ -962,7 +962,7 @@ def testIsInValidSignIssues(self): settings_8 = OneLogin_Saml2_Settings(settings_info) response_8 = OneLogin_Saml2_Response(settings_8, message) with self.assertRaisesRegexp(Exception, 'The Message of the Response is not signed and the SP require it'): - response_8.is_valid(request_data, raises=True) + response_8.is_valid(request_data, raise_exceptions=True) def testIsInValidEncIssues(self): """ @@ -1044,7 +1044,7 @@ def testIsInValidCert(self): response = OneLogin_Saml2_Response(settings, xml) with self.assertRaisesRegexp(Exception, 'failed to load key'): - response.is_valid(self.get_request_data(), raises=True) + response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidCert2(self): """ @@ -1270,4 +1270,4 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): self.assertFalse(response.is_valid(self.get_request_data())) with self.assertRaises(Exception): - response.is_valid(self.get_request_data(), raises=True) + response.is_valid(self.get_request_data(), raise_exceptions=True) From b144b06723c1d3a58993dff3e678ae39c7dfeebc Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 22 Dec 2016 12:46:20 +0100 Subject: [PATCH 09/18] Optionally raise detailed exceptions vs. returning False --- .travis.yml | 4 +- src/onelogin/saml2/logout_request.py | 4 +- src/onelogin/saml2/response.py | 14 +- src/onelogin/saml2/utils.py | 167 ++++++++++-------- .../saml2_tests/logout_request_test.py | 2 +- .../src/OneLogin/saml2_tests/response_test.py | 4 +- 6 files changed, 108 insertions(+), 87 deletions(-) diff --git a/.travis.yml b/.travis.yml index c6b32510..b38e5565 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ language: python python: - '2.7.6' -# - '2.7.9' +# - '2.7.9' # - '2.7.12' - - '3.3.4' +# - '3.3.4' # - '3.3.5' # - '3.3.6' - '3.4.3' diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index d74993d3..c4061a71 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -10,7 +10,7 @@ """ from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -246,7 +246,7 @@ def is_valid(self, request_data, raise_exceptions=False): if root.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): - raise Exception('Timing issues (please check your clock settings)') + raise Exception('Could not validate timestamp: expired. Check system clock.)') # Check destination if root.get('Destination', None): diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 16184336..db739ffe 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -11,7 +11,7 @@ from copy import deepcopy from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -124,8 +124,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): raise Exception('The Assertion must include a Conditions element') # Validates Assertion timestamps - if not self.validate_timestamps(): - raise Exception('Timing issues (please check your clock settings)') + self.validate_timestamps(raise_exceptions=True) # Checks that an AuthnStatement element exists and is unique if not self.check_one_authnstatement(): @@ -216,11 +215,11 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) # If find a Signature on the Response, validates it checking the original response - if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH): + if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, raise_exceptions=False): raise Exception('Signature validation failed. SAML Response rejected') document_check_assertion = self.decrypted_document if self.encrypted else self.document - if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH): + if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=False): raise Exception('Signature validation failed. SAML Response rejected') return True @@ -502,6 +501,7 @@ def validate_signed_elements(self, signed_elements): return True + @return_false_on_exception def validate_timestamps(self): """ Verifies that the document is valid according to Conditions Element @@ -515,9 +515,9 @@ def validate_timestamps(self): nb_attr = conditions_node.get('NotBefore') nooa_attr = conditions_node.get('NotOnOrAfter') if nb_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nb_attr) > OneLogin_Saml2_Utils.now() + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT: - return False + raise Exception('Could not validate timestamp: not yet valid. Check system clock.') if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT <= OneLogin_Saml2_Utils.now(): - return False + raise Exception('Could not validate timestamp: expired. Check system clock.') return True def __query_assertion(self, xpath_expr): diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index a8d9fe87..9765d4f9 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -16,6 +16,7 @@ from isodate import parse_duration as duration_parser import re from textwrap import wrap +from functools import wraps from uuid import uuid4 import zlib @@ -33,6 +34,24 @@ from urllib import quote_plus # py2 +def return_false_on_exception(func): + """ + Decorator. When applied to a function, it will, by default, suppress any exceptions + raised by that function and return False. It may be overridden by passing a + "raise_exceptions" keyword argument when calling the wrapped function. + """ + @wraps(func) + def exceptfalse(*args, **kwargs): + if not kwargs.pop('raise_exceptions', False): + try: + return func(*args, **kwargs) + except Exception: + return False + else: + return func(*args, **kwargs) + return exceptfalse + + class OneLogin_Saml2_Utils(object): """ @@ -719,6 +738,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant return OneLogin_Saml2_XML.to_string(elem) @staticmethod + @return_false_on_exception def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None): """ Validates a signature (Message or Assertion). @@ -743,36 +763,34 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid :param xpath: The xpath of the signed element :type: string - """ - try: - if xml is None or xml == '': - raise Exception('Empty string supplied as input') - - elem = OneLogin_Saml2_XML.to_etree(xml) - xmlsec.enable_debug_trace(debug) - xmlsec.tree.add_ids(elem, ["ID"]) - if xpath: - signature_nodes = OneLogin_Saml2_XML.query(elem, xpath) - else: - signature_nodes = OneLogin_Saml2_XML.query(elem, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH) + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean + """ + if xml is None or xml == '': + raise Exception('Empty string supplied as input') - if len(signature_nodes) == 0: - signature_nodes = OneLogin_Saml2_XML.query(elem, OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH) + elem = OneLogin_Saml2_XML.to_etree(xml) + xmlsec.enable_debug_trace(debug) + xmlsec.tree.add_ids(elem, ["ID"]) - if len(signature_nodes) == 1: - signature_node = signature_nodes[0] + if xpath: + signature_nodes = OneLogin_Saml2_XML.query(elem, xpath) + else: + signature_nodes = OneLogin_Saml2_XML.query(elem, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH) - return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug) - else: - return False - except xmlsec.Error as e: - if debug: - print(e) + if len(signature_nodes) == 0: + signature_nodes = OneLogin_Saml2_XML.query(elem, OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH) - return False + if len(signature_nodes) == 1: + signature_node = signature_nodes[0] + # Raises expection if invalid + return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) + else: + raise Exception('Expected exactly one signature node; got {}.'.format(len(signature_nodes))) @staticmethod + @return_false_on_exception def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): """ Validates a signature of a EntityDescriptor. @@ -794,35 +812,36 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha :param debug: Activate the xmlsec debug :type: bool + + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean """ - try: - if xml is None or xml == '': - raise Exception('Empty string supplied as input') + if xml is None or xml == '': + raise Exception('Empty string supplied as input') - elem = OneLogin_Saml2_XML.to_etree(xml) - xmlsec.enable_debug_trace(debug) - xmlsec.tree.add_ids(elem, ["ID"]) + elem = OneLogin_Saml2_XML.to_etree(xml) + xmlsec.enable_debug_trace(debug) + xmlsec.tree.add_ids(elem, ["ID"]) - signature_nodes = OneLogin_Saml2_XML.query(elem, '/md:EntitiesDescriptor/ds:Signature') + signature_nodes = OneLogin_Saml2_XML.query(elem, '/md:EntitiesDescriptor/ds:Signature') - if len(signature_nodes) == 0: - signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/ds:Signature') + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/ds:Signature') - if len(signature_nodes) == 0: - signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature') - signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature') + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature') + signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature') - if len(signature_nodes) > 0: - for signature_node in signature_nodes: - if not OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug): - return False - return True - else: - return False - except Exception: - return False + if len(signature_nodes) > 0: + for signature_node in signature_nodes: + # Raises expection if invalid + OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) + return True + else: + raise Exception('Could not validate metadata signature: No signature nodes found.') @staticmethod + @return_false_on_exception def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): """ Validates a signature node. @@ -847,40 +866,42 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger :param debug: Activate the xmlsec debug :type: bool + + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean """ - try: - if (cert is None or cert == '') and fingerprint: - x509_certificate_nodes = OneLogin_Saml2_XML.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate') - if len(x509_certificate_nodes) > 0: - x509_certificate_node = x509_certificate_nodes[0] - x509_cert_value = x509_certificate_node.text - x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg) - if fingerprint == x509_fingerprint_value: - cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) - - if cert is None or cert == '': - return False + if (cert is None or cert == '') and fingerprint: + x509_certificate_nodes = OneLogin_Saml2_XML.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate') + if len(x509_certificate_nodes) > 0: + x509_certificate_node = x509_certificate_nodes[0] + x509_cert_value = x509_certificate_node.text + x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg) + if fingerprint == x509_fingerprint_value: + cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) - # Check if Reference URI is empty - reference_elem = OneLogin_Saml2_XML.query(signature_node, '//ds:Reference') - if len(reference_elem) > 0: - if reference_elem[0].get('URI') == '': - reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID')) + if cert is None or cert == '': + raise Exception('Could not validate node signature: No certificate provided.') - if validatecert: - manager = xmlsec.KeysManager() - manager.load_cert_from_memory(cert, xmlsec.KeyFormat.CERT_PEM, xmlsec.KeyDataType.TRUSTED) - dsig_ctx = xmlsec.SignatureContext(manager) - else: - dsig_ctx = xmlsec.SignatureContext() - dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None) + # Check if Reference URI is empty + reference_elem = OneLogin_Saml2_XML.query(signature_node, '//ds:Reference') + if len(reference_elem) > 0: + if reference_elem[0].get('URI') == '': + reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID')) + + if validatecert: + manager = xmlsec.KeysManager() + manager.load_cert_from_memory(cert, xmlsec.KeyFormat.CERT_PEM, xmlsec.KeyDataType.TRUSTED) + dsig_ctx = xmlsec.SignatureContext(manager) + else: + dsig_ctx = xmlsec.SignatureContext() + dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None) - dsig_ctx.set_enabled_key_data([xmlsec.KeyData.X509]) + dsig_ctx.set_enabled_key_data([xmlsec.KeyData.X509]) + try: dsig_ctx.verify(signature_node) - return True - except xmlsec.Error as e: - if debug: - print(e) + except Exception: + raise Exception('Signature validation failed. SAML Response rejected') + return True @staticmethod def sign_binary(msg, key, algorithm=xmlsec.Transform.RSA_SHA1, debug=False): diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 3741db90..c4e385cc 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -285,7 +285,7 @@ def testIsInvalidNotOnOrAfter(self): settings.set_strict(True) logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) - with self.assertRaisesRegexp(Exception, 'Timing issues \(please check your clock settings\)'): + with self.assertRaisesRegexp(Exception, 'Could not validate timestamp: expired. Check system clock.'): logout_request2.is_valid(request_data, raise_exceptions=True) def testIsValid(self): diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 5639bd59..1bd726c0 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -533,7 +533,7 @@ def testIsInValidExpired(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'Timing issues \(please check your clock settings\)'): + with self.assertRaisesRegexp(Exception, 'Could not validate timestamp: expired. Check system clock.'): response_2.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidNoStatement(self): @@ -1043,7 +1043,7 @@ def testIsInValidCert(self): xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'failed to load key'): + with self.assertRaisesRegexp(Exception, 'Signature validation failed. SAML Response rejected'): response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidCert2(self): From 9ea32f567508a532101a9888b199062f105b77c6 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 30 Dec 2016 00:16:43 +0100 Subject: [PATCH 10/18] Implement a more specific exception class for handling some validation errors. Improve tests --- src/onelogin/saml2/auth.py | 50 ++-- src/onelogin/saml2/errors.py | 74 +++++- src/onelogin/saml2/logout_request.py | 38 ++- src/onelogin/saml2/logout_response.py | 27 +- src/onelogin/saml2/response.py | 231 ++++++++++++++---- src/onelogin/saml2/settings.py | 5 +- src/onelogin/saml2/utils.py | 28 ++- tests/src/OneLogin/saml2_tests/auth_test.py | 22 +- .../saml2_tests/logout_response_test.py | 13 +- .../src/OneLogin/saml2_tests/response_test.py | 20 +- tests/src/OneLogin/saml2_tests/utils_test.py | 8 +- 11 files changed, 384 insertions(+), 132 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 1e470b32..4b855119 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -16,10 +16,9 @@ from onelogin.saml2 import compat from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.response import OneLogin_Saml2_Response -from onelogin.saml2.errors import OneLogin_Saml2_Error from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request @@ -429,7 +428,7 @@ def __build_signature(self, data, saml_type, sign_algorithm=OneLogin_Saml2_Const if not key: raise OneLogin_Saml2_Error( "Trying to sign the %s but can't load the SP private key." % saml_type, - OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND + OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND ) msg = self.__build_sign_query(data[saml_type], @@ -472,7 +471,7 @@ def validate_response_signature(self, request_data): return self.__validate_signature(request_data, 'SAMLResponse') - def __validate_signature(self, data, saml_type): + def __validate_signature(self, data, saml_type, raise_exceptions=False): """ Validate Signature @@ -484,22 +483,30 @@ def __validate_signature(self, data, saml_type): :param saml_type: The target URL the user should be redirected to :type saml_type: string SAMLRequest | SAMLResponse - """ - - signature = data.get('Signature', None) - if signature is None: - if self.__settings.is_strict() and self.__settings.get_security_data().get('wantMessagesSigned', False): - self.__error_reason = 'The %s is not signed. Rejected.' % saml_type - return False - return True - - x509cert = self.get_settings().get_idp_cert() - - if x509cert is None: - self.__errors.append("In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type) - return False + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean + """ try: + signature = data.get('Signature', None) + if signature is None: + if self.__settings.is_strict() and self.__settings.get_security_data().get('wantMessagesSigned', False): + raise OneLogin_Saml2_ValidationError( + 'The %s is not signed. Rejected.' % saml_type, + OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + ) + return True + + x509cert = self.get_settings().get_idp_cert() + + if not x509cert: + error_msg = "In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type + self.__errors.append(error_msg) + raise OneLogin_Saml2_Error( + error_msg, + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) + sign_alg = data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) if isinstance(sign_alg, bytes): sign_alg = sign_alg.decode('utf8') @@ -520,8 +527,13 @@ def __validate_signature(self, data, saml_type): x509cert, sign_alg, self.__settings.is_debug_active()): - raise Exception('Signature validation failed. %s rejected.' % saml_type) + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. %s rejected.' % saml_type, + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True except Exception as e: self.__error_reason = str(e) + if raise_exceptions: + raise e return False diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index c28a23bd..0f84a09b 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -25,7 +25,7 @@ class OneLogin_Saml2_Error(Exception): SETTINGS_INVALID_SYNTAX = 1 SETTINGS_INVALID = 2 METADATA_SP_INVALID = 3 - SP_CERTS_NOT_FOUND = 4 + CERT_NOT_FOUND = 4 REDIRECT_INVALID_URL = 5 PUBLIC_CERT_FILE_NOT_FOUND = 6 PRIVATE_KEY_FILE_NOT_FOUND = 7 @@ -34,6 +34,8 @@ class OneLogin_Saml2_Error(Exception): SAML_LOGOUTREQUEST_INVALID = 10 SAML_LOGOUTRESPONSE_INVALID = 11 SAML_SINGLE_LOGOUT_NOT_SUPPORTED = 12 + PRIVATE_KEY_NOT_FOUND = 13 + UNSUPPORTED_SETTINGS_OBJECT = 14 def __init__(self, message, code=0, errors=None): """ @@ -50,3 +52,73 @@ def __init__(self, message, code=0, errors=None): Exception.__init__(self, message) self.code = code + + +class OneLogin_Saml2_ValidationError(Exception): + """ + This class implements another custom Exception handler, related + to exceptions that happens during validation process. + Defines custom error codes . + """ + + # Validation Errors + UNSUPPORTED_SAML_VERSION = 0 + MISSING_ID = 1 + WRONG_NUMBER_OF_ASSERTIONS = 2 + MISSING_STATUS = 3 + MISSING_STATUS_CODE = 4 + STATUS_CODE_IS_NOT_SUCCESS = 5 + WRONG_SIGNED_ELEMENT = 6 + ID_NOT_FOUND_IN_SIGNED_ELEMENT = 7 + DUPLICATED_ID_IN_SIGNED_ELEMENTS = 8 + INVALID_SIGNED_ELEMENT = 9 + DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS = 10 + UNEXPECTED_SIGNED_ELEMENTS = 11 + WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE = 12 + WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION = 13 + INVALID_XML_FORMAT = 14 + WRONG_INRESPONSETO = 15 + NO_ENCRYPTED_ASSERTION = 16 + NO_ENCRYPTED_NAMEID = 17 + MISSING_CONDITIONS = 18 + ASSERTION_TOO_EARLY = 19 + ASSERTION_EXPIRED = 20 + WRONG_NUMBER_OF_AUTHSTATEMENTS = 21 + NO_ATTRIBUTESTATEMENT = 22 + ENCRYPTED_ATTRIBUTES = 23 + WRONG_DESTINATION = 24 + EMPTY_DESTINATION = 25 + WRONG_AUDIENCE = 26 + ISSUER_NOT_FOUND_IN_RESPONSE = 27 + ISSUER_NOT_FOUND_IN_ASSERTION = 28 + WRONG_ISSUER = 29 + SESSION_EXPIRED = 30 + WRONG_SUBJECTCONFIRMATION = 31 + NO_SIGNED_RESPONSE = 32 + NO_SIGNED_ASSERTION = 33 + NO_SIGNATURE_FOUND = 34 + KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35 + CHILDREN_NODE_NOT_FOIND_IN_KEYINFO = 36 + UNSUPPORTED_RETRIEVAL_METHOD = 37 + NO_NAMEID = 38 + EMPTY_NAMEID = 39 + SP_NAME_QUALIFIER_NAME_MISMATCH = 40 + DUPLICATED_ATTRIBUTE_NAME_FOUND = 41 + INVALID_SIGNATURE = 42 + WRONG_NUMBER_OF_SIGNATURES = 43 + RESPONSE_EXPIRED = 44 + + def __init__(self, message, code=0, errors=None): + """ + Initializes the Exception instance. + Arguments are: + * (str) message. Describes the error. + * (int) code. The code error (defined in the error class). + """ + assert isinstance(code, int) + + if errors is not None: + message = message % errors + + Exception.__init__(self, message) + self.code = code diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index c4061a71..93a57bb2 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -10,7 +10,7 @@ """ from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception +from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -141,7 +141,10 @@ def get_nameid_data(request, key=None): if len(encrypted_entries) == 1: if key is None: - raise Exception('Key is required in order to decrypt the NameID') + raise OneLogin_Saml2_Error( + 'Private Key is required in order to decrypt the NameID, check settings', + OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND + ) encrypted_data_nodes = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_data_nodes) == 1: @@ -153,7 +156,10 @@ def get_nameid_data(request, key=None): name_id = entries[0] if name_id is None: - raise Exception('Not NameID found in the Logout Request') + raise OneLogin_Saml2_ValidationError( + 'Not NameID found in the Logout Request', + OneLogin_Saml2_ValidationError.NO_NAMEID + ) name_id_data = { 'Value': name_id.text @@ -236,7 +242,10 @@ def is_valid(self, request_data, raise_exceptions=False): if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml(root, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): - raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') + raise OneLogin_Saml2_ValidationError( + 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd', + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) security = self.__settings.get_security_data() @@ -246,30 +255,41 @@ def is_valid(self, request_data, raise_exceptions=False): if root.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): - raise Exception('Could not validate timestamp: expired. Check system clock.)') + raise OneLogin_Saml2_ValidationError( + 'Could not validate timestamp: expired. Check system clock.)', + OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED + ) # Check destination if root.get('Destination', None): destination = root.get('Destination') if destination != '': if current_url not in destination: - raise Exception( + raise OneLogin_Saml2_ValidationError( 'The LogoutRequest was received at ' '%(currentURL)s instead of %(destination)s' % { 'currentURL': current_url, 'destination': destination, - } + }, + OneLogin_Saml2_ValidationError.WRONG_DESTINATION ) # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(root) if issuer is not None and issuer != idp_entity_id: - raise Exception('Invalid issuer in the Logout Request') + raise OneLogin_Saml2_ValidationError( + 'Invalid issuer in the Logout Request', + OneLogin_Saml2_ValidationError.WRONG_ISSUER + ) if security['wantMessagesSigned']: if 'Signature' not in get_data: - raise Exception('The Message of the Logout Request is not signed and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The Message of the Logout Request is not signed and the SP require it', + OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + ) + return True except Exception as err: # pylint: disable=R0801 diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index a9cdba8d..18152651 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -9,7 +9,7 @@ """ -from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_ValidationError from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -84,30 +84,45 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): - raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') + raise OneLogin_Saml2_ValidationError( + 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd', + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) security = self.__settings.get_security_data() # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided in_response_to = self.document.get('InResponseTo', None) if request_id is not None and in_response_to and in_response_to != request_id: - raise Exception('The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id)) + raise OneLogin_Saml2_ValidationError( + 'The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id), + OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO + ) # Check issuer issuer = self.get_issuer() if issuer is not None and issuer != idp_entity_id: - raise Exception('Invalid issuer in the Logout Request') + raise OneLogin_Saml2_ValidationError( + 'Invalid issuer in the Logout Request', + OneLogin_Saml2_ValidationError.WRONG_ISSUER + ) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check destination destination = self.document.get('Destination', None) if destination and current_url not in destination: - raise Exception('The LogoutRequest was received at $currentURL instead of $destination') + raise OneLogin_Saml2_ValidationError( + 'The LogoutResponse was received at %s instead of %s' % (current_url, destination), + OneLogin_Saml2_ValidationError.WRONG_DESTINATION + ) if security['wantMessagesSigned']: if 'Signature' not in get_data: - raise Exception('The Message of the Logout Response is not signed and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The Message of the Logout Response is not signed and the SP require it', + OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + ) return True # pylint: disable=R0801 except Exception as err: diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index db739ffe..b8f233aa 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -11,7 +11,7 @@ from copy import deepcopy from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception +from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError, return_false_on_exception from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -67,15 +67,24 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): try: # Checks SAML version if self.document.get('Version', None) != '2.0': - raise Exception('Unsupported SAML version') + raise OneLogin_Saml2_ValidationError( + 'Unsupported SAML version', + OneLogin_Saml2_ValidationError.UNSUPPORTED_SAML_VERSION + ) # Checks that ID exists if self.document.get('ID', None) is None: - raise Exception('Missing ID attribute on SAML Response') + raise OneLogin_Saml2_ValidationError( + 'Missing ID attribute on SAML Response', + OneLogin_Saml2_ValidationError.MISSING_ID + ) # Checks that the response only has one assertion if not self.validate_num_assertions(): - raise Exception('SAML Response must contain 1 assertion') + raise OneLogin_Saml2_ValidationError( + 'SAML Response must contain 1 assertion', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS + ) # Checks that the response has the SUCCESS status self.check_status() @@ -94,13 +103,19 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' res = OneLogin_Saml2_XML.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): - raise Exception(no_valid_xml_msg) + raise OneLogin_Saml2_ValidationError( + no_valid_xml_msg, + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) # If encrypted, check also the decrypted document if self.encrypted: res = OneLogin_Saml2_XML.validate_xml(self.decrypted_document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): - raise Exception(no_valid_xml_msg) + raise OneLogin_Saml2_ValidationError( + no_valid_xml_msg, + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) @@ -109,35 +124,56 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): in_response_to = self.document.get('InResponseTo', None) if in_response_to is not None and request_id is not None: if in_response_to != request_id: - raise Exception('The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id)) + raise OneLogin_Saml2_ValidationError( + 'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id), + OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO + ) if not self.encrypted and security['wantAssertionsEncrypted']: - raise Exception('The assertion of the Response is not encrypted and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The assertion of the Response is not encrypted and the SP require it', + OneLogin_Saml2_ValidationError.NO_ENCRYPTED_ASSERTION + ) if security['wantNameIdEncrypted']: encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_nameid_nodes) != 1: - raise Exception('The NameID of the Response is not encrypted and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The NameID of the Response is not encrypted and the SP require it', + OneLogin_Saml2_ValidationError.NO_ENCRYPTED_NAMEID + ) # Checks that a Conditions element exists if not self.check_one_condition(): - raise Exception('The Assertion must include a Conditions element') + raise OneLogin_Saml2_ValidationError( + 'The Assertion must include a Conditions element', + OneLogin_Saml2_ValidationError.MISSING_CONDITIONS + ) # Validates Assertion timestamps self.validate_timestamps(raise_exceptions=True) # Checks that an AuthnStatement element exists and is unique if not self.check_one_authnstatement(): - raise Exception('The Assertion must include an AuthnStatement element') + raise OneLogin_Saml2_ValidationError( + 'The Assertion must include an AuthnStatement element', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_AUTHSTATEMENTS + ) # Checks that there is at least one AttributeStatement if required attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement') if security.get('wantAttributeStatement', True) and not attribute_statement_nodes: - raise Exception('There is no AttributeStatement on the Response') + raise OneLogin_Saml2_ValidationError( + 'There is no AttributeStatement on the Response', + OneLogin_Saml2_ValidationError.NO_ATTRIBUTESTATEMENT + ) encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute') if encrypted_attributes_nodes: - raise Exception('There is an EncryptedAttribute in the Response and this SP not support them') + raise OneLogin_Saml2_ValidationError( + 'There is an EncryptedAttribute in the Response and this SP not support them', + OneLogin_Saml2_ValidationError.ENCRYPTED_ATTRIBUTES + ) # Checks destination destination = self.document.get('Destination', None) @@ -147,25 +183,39 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): # request_data # current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data) # if not destination.startswith(current_url_routed): - raise Exception('The response was received at %s instead of %s' % (current_url, destination)) + raise OneLogin_Saml2_ValidationError( + 'The response was received at %s instead of %s' % (current_url, destination), + OneLogin_Saml2_ValidationError.WRONG_DESTINATION + ) elif destination == '': - raise Exception('The response has an empty Destination value') - + raise OneLogin_Saml2_ValidationError( + 'The response has an empty Destination value', + OneLogin_Saml2_ValidationError.EMPTY_DESTINATION + ) # Checks audience valid_audiences = self.get_audiences() if valid_audiences and sp_entity_id not in valid_audiences: - raise Exception('%s is not a valid audience for this Response' % sp_entity_id) + raise OneLogin_Saml2_ValidationError( + '%s is not a valid audience for this Response' % sp_entity_id, + OneLogin_Saml2_ValidationError.WRONG_AUDIENCE + ) # Checks the issuers issuers = self.get_issuers() for issuer in issuers: if issuer is None or issuer != idp_entity_id: - raise Exception('Invalid issuer in the Assertion/Response') + raise OneLogin_Saml2_ValidationError( + 'Invalid issuer in the Assertion/Response', + OneLogin_Saml2_ValidationError.WRONG_ISSUER + ) # Checks the session Expiration session_expiration = self.get_session_not_on_or_after() if session_expiration and session_expiration <= OneLogin_Saml2_Utils.now(): - raise Exception('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response') + raise OneLogin_Saml2_ValidationError( + 'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response', + OneLogin_Saml2_ValidationError.SESSION_EXPIRED + ) # Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid any_subject_confirmation = False @@ -199,16 +249,28 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): break if not any_subject_confirmation: - raise Exception('A valid SubjectConfirmation was not found on this Response') + raise OneLogin_Saml2_ValidationError( + 'A valid SubjectConfirmation was not found on this Response', + OneLogin_Saml2_ValidationError.WRONG_SUBJECTCONFIRMATION + ) if security['wantAssertionsSigned'] and not has_signed_assertion: - raise Exception('The Assertion of the Response is not signed and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The Assertion of the Response is not signed and the SP require it', + OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + ) if security['wantMessagesSigned'] and not has_signed_response: - raise Exception('The Message of the Response is not signed and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The Message of the Response is not signed and the SP require it', + OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION + ) if not signed_elements or (not has_signed_response and not has_signed_assertion): - raise Exception('No Signature found. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'No Signature found. SAML Response rejected', + OneLogin_Saml2_ValidationError.NO_SIGNATURE_FOUND + ) else: cert = idp_data.get('x509cert', None) fingerprint = idp_data.get('certFingerprint', None) @@ -216,11 +278,17 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): # If find a Signature on the Response, validates it checking the original response if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, raise_exceptions=False): - raise Exception('Signature validation failed. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. SAML Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) document_check_assertion = self.decrypted_document if self.encrypted else self.document if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=False): - raise Exception('Signature validation failed. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. SAML Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True except Exception as err: @@ -247,7 +315,10 @@ def check_status(self): status_msg = status.get('msg', None) if status_msg: status_exception_msg += ' -> ' + status_msg - raise Exception(status_exception_msg) + raise OneLogin_Saml2_ValidationError( + status_exception_msg, + OneLogin_Saml2_ValidationError.STATUS_CODE_IS_NOT_SUCCESS + ) def check_one_condition(self): """ @@ -292,13 +363,19 @@ def get_issuers(self): if len(message_issuer_nodes) == 1: issuers.add(message_issuer_nodes[0].text) else: - raise Exception('Issuer of the Response not found or multiple.') + raise OneLogin_Saml2_ValidationError( + 'Issuer of the Response not found or multiple.', + OneLogin_Saml2_ValidationError.ISSUER_NOT_FOUND_IN_RESPONSE + ) assertion_issuer_nodes = self.__query_assertion('/saml:Issuer') if len(assertion_issuer_nodes) == 1: issuers.add(assertion_issuer_nodes[0].text) else: - raise Exception('Issuer of the Assertion not found or multiple.') + raise OneLogin_Saml2_ValidationError( + 'Issuer of the Assertion not found or multiple.', + OneLogin_Saml2_ValidationError.ISSUER_NOT_FOUND_IN_ASSERTION + ) return list(set(issuers)) @@ -324,10 +401,16 @@ def get_nameid_data(self): if nameid is None: security = self.__settings.get_security_data() if security.get('wantNameId', True): - raise Exception('Not NameID found in the assertion of the Response') + raise OneLogin_Saml2_ValidationError( + 'Not NameID found in the assertion of the Response', + OneLogin_Saml2_ValidationError.NO_NAMEID + ) else: if self.__settings.is_strict() and not nameid.text: - raise Exception('An empty NameID value found') + raise OneLogin_Saml2_ValidationError( + 'An empty NameID value found', + OneLogin_Saml2_ValidationError.EMPTY_NAMEID + ) nameid_data = {'Value': nameid.text} for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: @@ -337,7 +420,10 @@ def get_nameid_data(self): sp_data = self.__settings.get_sp_data() sp_entity_id = sp_data.get('entityId', '') if sp_entity_id != value: - raise Exception('The SPNameQualifier value mistmatch the SP entityID value.') + raise OneLogin_Saml2_ValidationError( + 'The SPNameQualifier value mistmatch the SP entityID value.', + OneLogin_Saml2_ValidationError.SP_NAME_QUALIFIER_NAME_MISMATCH + ) nameid_data[attr] = value return nameid_data @@ -395,7 +481,10 @@ def get_attributes(self): for attribute_node in attribute_nodes: attr_name = attribute_node.get('Name') if attr_name in attributes.keys(): - raise Exception('Found an Attribute element with duplicated Name') + raise OneLogin_Saml2_ValidationError( + 'Found an Attribute element with duplicated Name', + OneLogin_Saml2_ValidationError.DUPLICATED_ATTRIBUTE_NAME_FOUND + ) values = [] for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP['saml']): @@ -441,14 +530,23 @@ def process_signed_elements(self): for sign_node in sign_nodes: signed_element = sign_node.getparent().tag if signed_element != response_tag and signed_element != assertion_tag: - raise Exception('Invalid Signature Element %s SAML Response rejected' % signed_element) + raise OneLogin_Saml2_ValidationError( + 'Invalid Signature Element %s SAML Response rejected' % signed_element, + OneLogin_Saml2_ValidationError.WRONG_SIGNED_ELEMENT + ) if not sign_node.getparent().get('ID'): - raise Exception('Signed Element must contain an ID. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Signed Element must contain an ID. SAML Response rejected', + OneLogin_Saml2_ValidationError.ID_NOT_FOUND_IN_SIGNED_ELEMENT + ) id_value = sign_node.getparent().get('ID') if id_value in verified_ids: - raise Exception('Duplicated ID. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Duplicated ID. SAML Response rejected', + OneLogin_Saml2_ValidationError.DUPLICATED_ID_IN_SIGNED_ELEMENTS + ) verified_ids.append(id_value) # Check that reference URI matches the parent ID and no duplicate References or IDs @@ -459,22 +557,37 @@ def process_signed_elements(self): sei = ref.get('URI')[1:] if sei != id_value: - raise Exception('Found an invalid Signed Element. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Found an invalid Signed Element. SAML Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNED_ELEMENT + ) if sei in verified_seis: - raise Exception('Duplicated Reference URI. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Duplicated Reference URI. SAML Response rejected', + OneLogin_Saml2_ValidationError.DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS + ) verified_seis.append(sei) signed_elements.append(signed_element) if signed_elements: - if not self.validate_signed_elements(signed_elements): - raise Exception('Found an unexpected Signature Element. SAML Response rejected') + if not self.validate_signed_elements(signed_elements, raise_exceptions=True): + raise OneLogin_Saml2_ValidationError( + 'Found an unexpected Signature Element. SAML Response rejected', + OneLogin_Saml2_ValidationError.UNEXPECTED_SIGNED_ELEMENT + ) return signed_elements + @return_false_on_exception def validate_signed_elements(self, signed_elements): """ Verifies that the document has the expected signed nodes. + + :param signed_elements: The signed elements to be checked + :type signed_elements: list + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean """ if len(signed_elements) > 2: return False @@ -492,12 +605,18 @@ def validate_signed_elements(self, signed_elements): if response_tag in signed_elements: expected_signature_nodes = OneLogin_Saml2_XML.query(self.document, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH) if len(expected_signature_nodes) != 1: - raise Exception('Unexpected number of Response signatures found. SAML Response rejected.') + raise OneLogin_Saml2_ValidationError( + 'Unexpected number of Response signatures found. SAML Response rejected.', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE + ) if assertion_tag in signed_elements: expected_signature_nodes = self.__query(OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH) if len(expected_signature_nodes) != 1: - raise Exception('Unexpected number of Assertion signatures found. SAML Response rejected.') + raise OneLogin_Saml2_ValidationError( + 'Unexpected number of Assertion signatures found. SAML Response rejected.', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION + ) return True @@ -515,9 +634,15 @@ def validate_timestamps(self): nb_attr = conditions_node.get('NotBefore') nooa_attr = conditions_node.get('NotOnOrAfter') if nb_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nb_attr) > OneLogin_Saml2_Utils.now() + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT: - raise Exception('Could not validate timestamp: not yet valid. Check system clock.') + raise OneLogin_Saml2_ValidationError( + 'Could not validate timestamp: not yet valid. Check system clock.', + OneLogin_Saml2_ValidationError.ASSERTION_TOO_EARLY + ) if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT <= OneLogin_Saml2_Utils.now(): - raise Exception('Could not validate timestamp: expired. Check system clock.') + raise OneLogin_Saml2_ValidationError( + 'Could not validate timestamp: expired. Check system clock.', + OneLogin_Saml2_ValidationError.ASSERTION_EXPIRED + ) return True def __query_assertion(self, xpath_expr): @@ -582,7 +707,10 @@ def __decrypt_assertion(self, xml): debug = self.__settings.is_debug_active() if not key: - raise Exception('No private key available, check settings') + raise OneLogin_Saml2_Error( + 'No private key available to decrypt the assertion, check settings', + OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND + ) encrypted_assertion_nodes = OneLogin_Saml2_XML.query(xml, '/samlp:Response/saml:EncryptedAssertion') if encrypted_assertion_nodes: @@ -590,15 +718,24 @@ def __decrypt_assertion(self, xml): if encrypted_data_nodes: keyinfo = OneLogin_Saml2_XML.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData/ds:KeyInfo') if not keyinfo: - raise Exception('No KeyInfo present, invalid Assertion') + raise OneLogin_Saml2_ValidationError( + 'No KeyInfo present, invalid Assertion', + OneLogin_Saml2_ValidationError.KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA + ) keyinfo = keyinfo[0] children = keyinfo.getchildren() if not children: - raise Exception('No child to KeyInfo, invalid Assertion') + raise OneLogin_Saml2_ValidationError( + 'KeyInfo has no children nodes, invalid Assertion', + OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOIND_IN_KEYINFO + ) for child in children: if 'RetrievalMethod' in child.tag: if child.attrib['Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey': - raise Exception('Unsupported Retrieval Method found') + raise OneLogin_Saml2_ValidationError( + 'Unsupported Retrieval Method found', + OneLogin_Saml2_ValidationError.UNSUPPORTED_RETRIEVAL_METHOD + ) uri = child.attrib['URI'] if not uri.startswith('#'): break diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index ea0cf4ce..a96523d3 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -114,7 +114,10 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals ','.join(self.__errors) ) else: - raise Exception('Unsupported settings object') + raise OneLogin_Saml2_Error( + 'Unsupported settings object', + OneLogin_Saml2_Error.UNSUPPORTED_SETTINGS_OBJECT + ) self.format_idp_cert() self.format_sp_cert() diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 9765d4f9..4d042374 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -24,7 +24,7 @@ from onelogin.saml2 import compat from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.errors import OneLogin_Saml2_Error +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -628,11 +628,17 @@ def get_status(dom): status_entry = OneLogin_Saml2_XML.query(dom, '/samlp:Response/samlp:Status') if len(status_entry) != 1: - raise Exception('Missing valid Status on response') + raise OneLogin_Saml2_ValidationError( + 'Missing Status on response', + OneLogin_Saml2_ValidationError.MISSING_STATUS + ) code_entry = OneLogin_Saml2_XML.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode', status_entry[0]) if len(code_entry) != 1: - raise Exception('Missing valid Status Code on response') + raise OneLogin_Saml2_ValidationError( + 'Missing Status Code on response', + OneLogin_Saml2_ValidationError.MISSING_STATUS_CODE + ) code = code_entry[0].values()[0] status['code'] = code @@ -787,7 +793,10 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid # Raises expection if invalid return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) else: - raise Exception('Expected exactly one signature node; got {}.'.format(len(signature_nodes))) + raise OneLogin_Saml2_ValidationError( + 'Expected exactly one signature node; got {}.'.format(len(signature_nodes)), + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES + ) @staticmethod @return_false_on_exception @@ -880,7 +889,10 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) if cert is None or cert == '': - raise Exception('Could not validate node signature: No certificate provided.') + raise OneLogin_Saml2_Error( + 'Could not validate node signature: No certificate provided.', + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) # Check if Reference URI is empty reference_elem = OneLogin_Saml2_XML.query(signature_node, '//ds:Reference') @@ -897,10 +909,8 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None) dsig_ctx.set_enabled_key_data([xmlsec.KeyData.X509]) - try: - dsig_ctx.verify(signature_node) - except Exception: - raise Exception('Signature validation failed. SAML Response rejected') + dsig_ctx.verify(signature_node) + return True @staticmethod diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index b7ff8586..41567617 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -12,7 +12,7 @@ from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.settings import OneLogin_Saml2_Settings -from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request try: @@ -143,10 +143,8 @@ def testProcessNoResponse(self): Case No Response, An exception is throw """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML Response not found'): auth.process_response() - exception = context.exception - self.assertIn("SAML Response not found", str(exception)) self.assertEqual(auth.get_errors(), ['invalid_binding']) def testProcessResponseInvalid(self): @@ -259,10 +257,8 @@ def testProcessNoSLO(self): Case No Message, An exception is throw """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML LogoutRequest/LogoutResponse not found'): auth.process_slo(True) - exception = context.exception - self.assertIn("SAML LogoutRequest/LogoutResponse not found", str(exception)) self.assertEqual(auth.get_errors(), ['invalid_binding']) def testProcessSLOResponseInvalid(self): @@ -773,10 +769,8 @@ def testLogoutNoSLO(self): del settings_info['idp']['singleLogoutService'] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) # The Header of the redirect produces an Exception - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'The IdP does not support Single Log Out'): auth.logout('http://example.com/returnto') - exception = context.exception - self.assertIn("The IdP does not support Single Log Out", str(exception)) def testLogoutNameIDandSessionIndex(self): """ @@ -863,10 +857,8 @@ def testBuildRequestSignature(self): settings['sp']['privateKey'] = '' settings['custom_base_path'] = u'invalid/path/' auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLRequest but can't load the SP private key"): auth2.add_request_signature(parameters) - exception = context.exception - self.assertIn("Trying to sign the SAMLRequest but can't load the SP private key", str(exception)) def testBuildResponseSignature(self): """ @@ -886,10 +878,8 @@ def testBuildResponseSignature(self): settings['sp']['privateKey'] = '' settings['custom_base_path'] = u'invalid/path/' auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLResponse but can't load the SP private key"): auth2.add_response_signature(parameters) - exception = context.exception - self.assertIn("Trying to sign the SAMLResponse but can't load the SP private key", str(exception)) def testIsInValidLogoutResponseSign(self): """ diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 2cc1083b..8ca47436 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -170,11 +170,10 @@ def testIsInValidRequestId(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) - try: - valid = response_2.is_valid(request_data, request_id) - self.assertFalse(valid) - except Exception as e: - self.assertIn('The InResponseTo of the Logout Response:', str(e)) + 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 testIsInValidIssuer(self): """ @@ -223,7 +222,7 @@ def testIsInValidDestination(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) - with self.assertRaisesRegexp(Exception, 'The LogoutRequest was received at'): + with self.assertRaisesRegexp(Exception, 'The LogoutResponse was received at'): response_2.is_valid(request_data, raise_exceptions=True) # Empty destination @@ -258,7 +257,7 @@ def testIsValid(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) - with self.assertRaisesRegexp(Exception, 'The LogoutRequest was received at'): + with self.assertRaisesRegexp(Exception, 'The LogoutResponse was received at'): response_2.is_valid(request_data, raise_exceptions=True) plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 1bd726c0..b3c6e6af 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -388,12 +388,10 @@ def testOnlyRetrieveAssertionWithIDThatMatchesSignatureReference(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'wrapped_response_2.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - try: - self.assertTrue(response.is_valid(self.get_request_data())) - nameid = response.get_nameid() - self.assertNotEqual('root@example.com', nameid) - except Exception: - self.assertEqual('Invalid Signature Element {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor SAML Response rejected', response.get_error()) + with self.assertRaisesRegexp(Exception, 'Invalid Signature Element {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor SAML Response rejected'): + response.is_valid(self.get_request_data(), raise_exceptions=True) + nameid = response.get_nameid() + self.assertEqual('root@example.com', nameid) def testDoesNotAllowSignatureWrappingAttack(self): """ @@ -514,11 +512,11 @@ def testIsInValidReference(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - try: - valid = response.is_valid(self.get_request_data()) - self.assertFalse(valid) - except Exception as e: - self.assertEqual('Reference validation failed', str(e)) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error()) + + with self.assertRaisesRegexp(Exception, 'Signature validation failed. SAML Response rejected'): + response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidExpired(self): """ diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index c1b04ba6..60f79c08 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -391,19 +391,15 @@ def testGetStatus(self): xml_inv = b64decode(xml_inv) dom_inv = etree.fromstring(xml_inv) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(Exception, 'Missing Status on response'): OneLogin_Saml2_Utils.get_status(dom_inv) - exception = context.exception - self.assertIn("Missing valid Status on response", str(exception)) xml_inv2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_status_code.xml.base64')) xml_inv2 = b64decode(xml_inv2) dom_inv2 = etree.fromstring(xml_inv2) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(Exception, 'Missing Status Code on response'): OneLogin_Saml2_Utils.get_status(dom_inv2) - exception = context.exception - self.assertIn("Missing valid Status Code on response", str(exception)) def testParseDuration(self): """ From 73ee97717d75a71f4863deea069f9e83ca83897a Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 30 Dec 2016 00:30:22 +0100 Subject: [PATCH 11/18] Add support for retrieving the last ID of the generated AuthNRequest / LogoutRequest --- README.md | 9 +++++++++ src/onelogin/saml2/auth.py | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/README.md b/README.md index b8b3d1fc..a96162d2 100644 --- a/README.md +++ b/README.md @@ -523,6 +523,10 @@ The login method can recieve 3 more optional parameters: * is_passive When true the AuthNReuqest will set the Ispassive='true' * set_nameid_policy When true the AuthNReuqest will set a nameIdPolicy element. +If a match on the future SAMLResponse ID and the AuthNRequest ID to be sent is required, that AuthNRequest ID must to be extracted and stored for future validation, we can get that ID by + +auth.get_last_request_id() + #### The SP Endpoints #### Related to the SP there are 3 important endpoints: The metadata view, the ACS view and the SLS view. @@ -706,6 +710,10 @@ Also there are 2 optional parameters that can be set: SAML Response with a NameId, then this NameId will be used. * session_index. SessionIndex that identifies the session of the user. +If a match on the LogoutResponse ID and the LogoutRequest ID to be sent is required, that LogoutRequest ID must to be extracted and stored for future validation, we can get that ID by + +auth.get_last_request_id() + ####Example of a view that initiates the SSO request and handles the response (is the acs target)#### We can code a unique file that initiates the SSO process, handle the response, get the attributes, initiate the slo and processes the logout response. @@ -781,6 +789,7 @@ Main class of OneLogin Python Toolkit * ***get_last_error_reason*** Returns the reason of the last error * ***get_sso_url*** Gets the SSO url. * ***get_slo_url*** Gets the SLO url. +* ***get_last_request_id*** The ID of the last Request SAML message generated (AuthNRequest, LogoutRequest). * ***build_request_signature*** Builds the Signature of the SAML Request. * ***build_response_signature*** Builds the Signature of the SAML Response. * ***get_settings*** Returns the settings info. diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 4b855119..0dd776e0 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -58,6 +58,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): self.__authenticated = False self.__errors = [] self.__error_reason = None + self.__last_request_id = None def get_settings(self): """ @@ -260,6 +261,13 @@ def get_attribute(self, name): assert isinstance(name, compat.str_type) return self.__attributes.get(name) + def get_last_request_id(self): + """ + :returns: The ID of the last Request SAML message generated. + :rtype: string + """ + return self.__last_request_id + def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True): """ Initiates the SSO process. @@ -280,6 +288,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_ :rtype: string """ authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy) + self.__last_request_id = authn_request.get_id() saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} @@ -328,6 +337,7 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None): session_index=session_index, nq=nq ) + self.__last_request_id = logout_request.id parameters = {'SAMLRequest': logout_request.get_request()} if return_to is not None: From a510f167812d1ae56f4dd2c9ee42547b4847cf6f Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 30 Dec 2016 01:32:59 +0100 Subject: [PATCH 12/18] Add hooks to retrieve last-sent and last-received requests and responses --- README.md | 8 +- src/onelogin/saml2/auth.py | 32 ++++++++ src/onelogin/saml2/authn_request.py | 8 ++ src/onelogin/saml2/logout_request.py | 9 +++ src/onelogin/saml2/logout_response.py | 9 +++ src/onelogin/saml2/response.py | 11 +++ src/onelogin/saml2/xml_templates.py | 78 +++++++++--------- .../decrypted_valid_encrypted_assertion.xml | 7 ++ ...ty_decrypted_valid_encrypted_assertion.xml | 7 ++ .../pretty_signed_message_response.xml | 48 +++++++++++ tests/src/OneLogin/saml2_tests/auth_test.py | 81 +++++++++++++++++++ .../saml2_tests/authn_request_test.py | 28 +++++++ .../saml2_tests/logout_request_test.py | 20 +++++ .../saml2_tests/logout_response_test.py | 24 ++++++ .../src/OneLogin/saml2_tests/response_test.py | 19 +++++ 15 files changed, 348 insertions(+), 41 deletions(-) create mode 100644 tests/data/responses/decrypted_valid_encrypted_assertion.xml create mode 100644 tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml create mode 100644 tests/data/responses/pretty_signed_message_response.xml diff --git a/README.md b/README.md index a96162d2..a8021196 100644 --- a/README.md +++ b/README.md @@ -794,6 +794,8 @@ Main class of OneLogin Python Toolkit * ***build_response_signature*** Builds the Signature of the SAML Response. * ***get_settings*** Returns the settings info. * ***set_strict*** Set the strict mode active/disable. +* ***get_last_request_xml*** Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest) +* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse was encrypted, by default tries to return the decrypted XML. ####OneLogin_Saml2_Auth - authn_request.py#### @@ -802,7 +804,7 @@ SAML 2 Authentication Request class * `__init__` This class handles an AuthNRequest. It builds an AuthNRequest object. * ***get_request*** Returns unsigned AuthnRequest. * ***get_id*** Returns the AuthNRequest ID. - +* ***get_xml*** Returns the XML that will be sent as part of the request. ####OneLogin_Saml2_Response - response.py#### @@ -821,6 +823,7 @@ SAML 2 Authentication Response class * ***validate_num_assertions*** Verifies that the document only contains a single Assertion (encrypted or not) * ***validate_timestamps*** Verifies that the document is valid according to Conditions Element * ***get_error*** After execute a validation process, if fails this method returns the cause +* ***get_xml_document*** If necessary, decrypt the XML response document, and return it. ####OneLogin_Saml2_LogoutRequest - logout_request.py#### @@ -835,6 +838,7 @@ SAML 2 Logout Request class * ***get_session_indexes*** Gets the SessionIndexes from the Logout Request. * ***is_valid*** Checks if the Logout Request recieved is valid. * ***get_error*** After execute a validation process, if fails this method returns the cause. +* ***get_xml*** Returns the XML that will be sent as part of the request or that was received at the SP ####OneLogin_Saml2_LogoutResponse - logout_response.py#### @@ -847,7 +851,7 @@ SAML 2 Logout Response class * ***build*** Creates a Logout Response object. * ***get_response*** Returns a Logout Response object. * ***get_error*** After execute a validation process, if fails this method returns the cause. - +* ***get_xml*** Returns the XML that will be sent as part of the response or that was received at the SP ####OneLogin_Saml2_Settings - settings.py#### diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 0dd776e0..188c0ea5 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -12,6 +12,7 @@ """ import xmlsec +from lxml import etree from onelogin.saml2 import compat from onelogin.saml2.settings import OneLogin_Saml2_Settings @@ -59,6 +60,8 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): self.__errors = [] self.__error_reason = None self.__last_request_id = None + self.__last_request = None + self.__last_response = None def get_settings(self): """ @@ -92,6 +95,7 @@ def process_response(self, request_id=None): if 'post_data' in self.__request_data and 'SAMLResponse' in self.__request_data['post_data']: # AuthnResponse -- HTTP_POST Binding response = OneLogin_Saml2_Response(self.__settings, self.__request_data['post_data']['SAMLResponse']) + self.__last_response = response.get_xml_document() if response.is_valid(self.__request_data, request_id): self.__attributes = response.get_attributes() @@ -128,6 +132,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ get_data = 'get_data' in self.__request_data and self.__request_data['get_data'] if get_data and 'SAMLResponse' in get_data: logout_response = OneLogin_Saml2_Logout_Response(self.__settings, get_data['SAMLResponse']) + self.__last_response = logout_response.get_xml() if not self.validate_response_signature(get_data): self.__errors.append('invalid_logout_response_signature') self.__errors.append('Signature validation failed. Logout Response rejected') @@ -141,6 +146,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ elif get_data and 'SAMLRequest' in get_data: logout_request = OneLogin_Saml2_Logout_Request(self.__settings, get_data['SAMLRequest']) + self.__last_request = logout_request.get_xml() if not self.validate_request_signature(get_data): self.__errors.append("invalid_logout_request_signature") self.__errors.append('Signature validation failed. Logout Request rejected') @@ -154,6 +160,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ in_response_to = logout_request.id response_builder = OneLogin_Saml2_Logout_Response(self.__settings) response_builder.build(in_response_to) + self.__last_response = response_builder.get_xml() logout_response = response_builder.get_response() parameters = {'SAMLResponse': logout_response} @@ -288,6 +295,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_ :rtype: string """ authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy) + self.__last_request = authn_request.get_xml() self.__last_request_id = authn_request.get_id() saml_request = authn_request.get_request() @@ -337,6 +345,7 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None): session_index=session_index, nq=nq ) + self.__last_request = logout_request.get_xml() self.__last_request_id = logout_request.id parameters = {'SAMLRequest': logout_request.get_request()} @@ -547,3 +556,26 @@ def __validate_signature(self, data, saml_type, raise_exceptions=False): if raise_exceptions: raise e return False + + def get_last_response_xml(self, pretty_print_if_possible=False): + """ + Retrieves the raw XML (decrypted) of the last SAML response, + or the last Logout Response generated or processed + :returns: SAML response XML + :rtype: string|None + """ + response = None + if self.__last_response is not None: + if isinstance(self.__last_response, basestring): + response = self.__last_response + else: + response = etree.tostring(self.__last_response, pretty_print=pretty_print_if_possible) + return response + + def get_last_request_xml(self): + """ + Retrieves the raw XML sent in the last SAML request + :returns: SAML request XML + :rtype: string|None + """ + return self.__last_request or None diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 706ad9de..ee6e1d2b 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -140,3 +140,11 @@ def get_id(self): :rtype: string """ return self.__id + + def get_xml(self): + """ + Returns the XML that will be sent as part of the request + :return: XML request body + :rtype: string + """ + return self.__authn_request diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 93a57bb2..63a7409b 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -111,6 +111,15 @@ def get_request(self, deflate=True): request = OneLogin_Saml2_Utils.b64encode(self.__logout_request) return request + def get_xml(self): + """ + Returns the XML that will be sent as part of the request + or that was received at the SP + :return: XML request body + :rtype: string + """ + return self.__logout_request + @staticmethod def get_id(request): """ diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 18152651..206b3683 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -187,3 +187,12 @@ def get_error(self): After executing a validation process, if it fails this method returns the cause """ return self.__error + + def get_xml(self): + """ + Returns the XML that will be sent as part of the response + or that was received at the SP + :return: XML response body + :rtype: string + """ + return self.__logout_response diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index b8f233aa..f2e7e2ca 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -754,3 +754,14 @@ def get_error(self): After executing a validation process, if it fails this method returns the cause """ return self.__error + + def get_xml_document(self): + """ + If necessary, decrypt the XML response document, and return it. + :return: Decrypted XML response document + :rtype: string + """ + if self.encrypted: + return self.decrypted_document + else: + return self.document diff --git a/src/onelogin/saml2/xml_templates.py b/src/onelogin/saml2/xml_templates.py index c55cf658..6be5c536 100644 --- a/src/onelogin/saml2/xml_templates.py +++ b/src/onelogin/saml2/xml_templates.py @@ -21,26 +21,26 @@ class OneLogin_Saml2_Templates(object): AUTHN_REQUEST = """\ + xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + ID="%(id)s" + Version="2.0"%(provider_name)s%(force_authn_str)s%(is_passive_str)s + IssueInstant="%(issue_instant)s" + Destination="%(destination)s" + ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + AssertionConsumerServiceURL="%(assertion_url)s"%(attr_consuming_service_str)s> %(entity_id)s%(nameid_policy_str)s %(requested_authn_context_str)s """ LOGOUT_REQUEST = """\ + xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + ID="%(id)s" + Version="2.0" + IssueInstant="%(issue_instant)s" + Destination="%(single_logout_url)s"> %(entity_id)s %(name_id)s %(session_index)s @@ -48,13 +48,13 @@ class OneLogin_Saml2_Templates(object): LOGOUT_RESPONSE = """\ + xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + ID="%(id)s" + Version="2.0" + IssueInstant="%(issue_instant)s" + Destination="%(destination)s" + InResponseTo="%(in_response_to)s"> %(entity_id)s @@ -105,18 +105,18 @@ class OneLogin_Saml2_Templates(object): RESPONSE = """\ + xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + ID="%(id)s" + InResponseTo="%(in_response_to)s" + Version="2.0" + IssueInstant="%(issue_instant)s" + Destination="%(destination)s"> %(entity_id)s + xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + Value="%(status)s"> %(entity_id)s %(name_id)s + NameQualifier="%(entity_id)s" + SPNameQualifier="%(requester)s" + Format="%(name_id_policy)s">%(name_id)s + NotOnOrAfter="%(not_after)s" + InResponseTo="%(in_response_to)s" + Recipient="%(destination)s"> @@ -145,9 +145,9 @@ class OneLogin_Saml2_Templates(object): + AuthnInstant="%(issue_instant)s" + SessionIndex="%(session_index)s" + SessionNotOnOrAfter="%(not_after)s"> %(authn_context)s diff --git a/tests/data/responses/decrypted_valid_encrypted_assertion.xml b/tests/data/responses/decrypted_valid_encrypted_assertion.xml new file mode 100644 index 00000000..0237994f --- /dev/null +++ b/tests/data/responses/decrypted_valid_encrypted_assertion.xml @@ -0,0 +1,7 @@ + + http://idp.example.com/ + + + + http://idp.example.com/_68392312d490db6d355555cfbbd8ec95d746516f60http://stuff.com/endpoints/metadata.phpurn:oasis:names:tc:SAML:2.0:ac:classes:Passwordtesttest@example.comtestwaa2useradmin + \ No newline at end of file diff --git a/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml b/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml new file mode 100644 index 00000000..fbc5942f --- /dev/null +++ b/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml @@ -0,0 +1,7 @@ + + http://idp.example.com/ + + + + http://idp.example.com/_68392312d490db6d355555cfbbd8ec95d746516f60http://stuff.com/endpoints/metadata.phpurn:oasis:names:tc:SAML:2.0:ac:classes:Passwordtesttest@example.comtestwaa2useradmin + diff --git a/tests/data/responses/pretty_signed_message_response.xml b/tests/data/responses/pretty_signed_message_response.xml new file mode 100644 index 00000000..7dcb65ee --- /dev/null +++ b/tests/data/responses/pretty_signed_message_response.xml @@ -0,0 +1,48 @@ + + https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php + + + + 1dQFiYU0o2OF7c/RVV8Gpgb4u3I=wRgBXOq/FiLZc2mureTC/j6zY709OikJ5HeUSruHTdYjEg9aZy1RbxlKIYEIfXpnX7NBoKxfAMm+O0fsrqOjgcYxTVkqZjOr71qiXNbtwjeAkdYSpk5brsAcnfcPdv8QReYr3D7t5ZVCgYuvXQ+dNELKeag7e1ASOzVqOdp5Z9Y= +MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo + + + + + https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php + + _b98f98bb1ab512ced653b58baaff543448daed535d + + + + + + + https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + test + + + test@example.com + + + test + + + waa2 + + + user + admin + + + + diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 41567617..fcfe1bde 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -1065,3 +1065,84 @@ def testIsValidLogoutRequestSign(self): auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_2) auth.process_slo() self.assertIn('Signature validation failed. Logout Request rejected', auth.get_errors()) + + def testGetLastSAMLResponse(self): + settings = self.loadSettingsJSON() + message = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64')) + message_wrapper = {'post_data': {'SAMLResponse': message}} + auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings) + auth.process_response() + expected_message = self.file_contents(join(self.data_path, 'responses', 'pretty_signed_message_response.xml')) + self.assertEqual(auth.get_last_response_xml(True), expected_message) + + # with encrypted assertion + message = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) + message_wrapper = {'post_data': {'SAMLResponse': message}} + auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings) + auth.process_response() + decrypted_response = self.file_contents(join(self.data_path, 'responses', 'decrypted_valid_encrypted_assertion.xml')) + self.assertEqual(auth.get_last_response_xml(False), decrypted_response) + pretty_decrypted_response = self.file_contents(join(self.data_path, 'responses', 'pretty_decrypted_valid_encrypted_assertion.xml')) + self.assertEqual(auth.get_last_response_xml(True), pretty_decrypted_response) + + def testGetLastAuthnRequest(self): + settings = self.loadSettingsJSON() + auth = OneLogin_Saml2_Auth({'http_host': 'localhost', 'script_name': 'thing'}, old_settings=settings) + auth.login() + expectedFragment = ( + ' Destination="http://idp.example.com/SSOService.php"\n' + ' ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"\n' + ' AssertionConsumerServiceURL="http://stuff.com/endpoints/endpoints/acs.php">\n' + ' http://stuff.com/endpoints/metadata.php\n' + ' \n' + ' \n' + ' urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport\n' + ' \n' + ) + self.assertIn(expectedFragment, auth.get_last_request_xml()) + + def testGetLastLogoutRequest(self): + settings = self.loadSettingsJSON() + auth = OneLogin_Saml2_Auth({'http_host': 'localhost', 'script_name': 'thing'}, old_settings=settings) + auth.logout() + expectedFragment = ( + ' Destination="http://idp.example.com/SingleLogoutService.php">\n' + ' http://stuff.com/endpoints/metadata.php\n' + ' http://idp.example.com/\n' + ' \n' + ) + self.assertIn(expectedFragment, auth.get_last_request_xml()) + + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(request) + message_wrapper = {'get_data': {'SAMLRequest': message}} + auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings) + auth.process_slo() + self.assertEqual(request, auth.get_last_request_xml()) + + def testGetLastLogoutResponse(self): + settings = self.loadSettingsJSON() + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(request) + message_wrapper = {'get_data': {'SAMLRequest': message}} + auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings) + auth.process_slo() + expectedFragment = ( + ' Destination="http://idp.example.com/SingleLogoutService.php"\n' + ' InResponseTo="ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e">\n' + ' http://stuff.com/endpoints/metadata.php\n' + ' \n' + ' \n' + ' \n' + '' + ) + self.assertIn(expectedFragment, auth.get_last_response_xml()) + + response = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml')) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(response) + message_wrapper = {'get_data': {'SAMLResponse': message}} + auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings) + auth.process_slo() + self.assertEqual(response, auth.get_last_response_xml()) diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 2a46c880..598ed115 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -73,6 +73,34 @@ def testCreateRequest(self): self.assertRegex(inflated, '^\n' + ' http://stuff.com/endpoints/metadata.php\n' + ' http://idp.example.com/\n' + ' \n' + ) + self.assertIn(expectedFragment, logout_request_generated.get_xml()) + + logout_request_processed = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) + self.assertEqual(request, logout_request_processed.get_xml()) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 8ca47436..9689d688 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -284,3 +284,27 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): with self.assertRaises(Exception): response.is_valid(request_data, raise_exceptions=True) + + def testGetXML(self): + """ + Tests that we can get the logout response XML directly without + going through intermediate steps + """ + response = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml')) + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + logout_response_generated = OneLogin_Saml2_Logout_Response(settings) + logout_response_generated.build("InResponseValue") + expectedFragment = ( + 'Destination="http://idp.example.com/SingleLogoutService.php"\n' + ' InResponseTo="InResponseValue">\n' + ' http://stuff.com/endpoints/metadata.php\n' + ' \n' + ' \n' + ' \n' + '' + ) + self.assertIn(expectedFragment, logout_response_generated.get_xml()) + + logout_response_processed = OneLogin_Saml2_Logout_Response(settings, OneLogin_Saml2_Utils.deflate_and_base64_encode(response)) + self.assertEqual(response, logout_response_processed.get_xml()) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index b3c6e6af..49f0f1ca 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -4,6 +4,7 @@ # All rights reserved. from base64 import b64decode +from lxml import etree from datetime import datetime from datetime import timedelta from freezegun import freeze_time @@ -59,6 +60,24 @@ def testConstruct(self): self.assertIsInstance(response_enc, OneLogin_Saml2_Response) + def testGetXMLDocument(self): + """ + Tests that we can retrieve the raw text of an encrypted XML response + without going through intermediate steps + """ + json_settings = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(json_settings) + + xml = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + prety_xml = self.file_contents(join(self.data_path, 'responses', 'pretty_signed_message_response.xml')) + self.assertEqual(etree.tostring(response.get_xml_document(), pretty_print=True), prety_xml) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + decrypted = self.file_contents(join(self.data_path, 'responses', 'decrypted_valid_encrypted_assertion.xml')) + self.assertEqual(etree.tostring(response_2.get_xml_document()), decrypted) + def testReturnNameId(self): """ Tests the get_nameid method of the OneLogin_Saml2_Response From 37788e233a339c8ff213ae28bbc82625dc5ff2ad Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 30 Dec 2016 15:03:53 +0100 Subject: [PATCH 13/18] Suggested changes --- src/onelogin/saml2/errors.py | 2 ++ src/onelogin/saml2/logout_request.py | 2 +- src/onelogin/saml2/response.py | 2 +- src/onelogin/saml2/utils.py | 10 +++++++++- .../OneLogin/saml2_tests/logout_request_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/response_test.py | 14 +++++++------- 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 0f84a09b..cdb825e0 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -25,6 +25,8 @@ class OneLogin_Saml2_Error(Exception): SETTINGS_INVALID_SYNTAX = 1 SETTINGS_INVALID = 2 METADATA_SP_INVALID = 3 + # SP_CERTS_NOT_FOUND is deprecated, use CERT_NOT_FOUND instead + SP_CERTS_NOT_FOUND = 4 CERT_NOT_FOUND = 4 REDIRECT_INVALID_URL = 5 PUBLIC_CERT_FILE_NOT_FOUND = 6 diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 63a7409b..5c73ca29 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -166,7 +166,7 @@ def get_nameid_data(request, key=None): if name_id is None: raise OneLogin_Saml2_ValidationError( - 'Not NameID found in the Logout Request', + 'NameID not found in the Logout Request', OneLogin_Saml2_ValidationError.NO_NAMEID ) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index f2e7e2ca..9968788b 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -402,7 +402,7 @@ def get_nameid_data(self): security = self.__settings.get_security_data() if security.get('wantNameId', True): raise OneLogin_Saml2_ValidationError( - 'Not NameID found in the assertion of the Response', + 'NameID not found in the assertion of the Response', OneLogin_Saml2_ValidationError.NO_NAMEID ) else: diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 4d042374..42b42b10 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -909,7 +909,15 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None) dsig_ctx.set_enabled_key_data([xmlsec.KeyData.X509]) - dsig_ctx.verify(signature_node) + + try: + dsig_ctx.verify(signature_node) + except Exception as err: + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. SAML Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE, + str(err) + ) return True diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 58c02f82..e2bd0cfa 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -138,11 +138,11 @@ def testGetNameIdData(self): encrypted_id_nodes = dom_2.getElementsByTagName('saml:EncryptedID') encrypted_data = encrypted_id_nodes[0].firstChild.nextSibling encrypted_id_nodes[0].removeChild(encrypted_data) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the Logout Request'): + with self.assertRaisesRegexp(Exception, 'NameID not found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid(dom_2.toxml(), key) inv_request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'no_nameId.xml')) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the Logout Request'): + with self.assertRaisesRegexp(Exception, 'NameID not found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid(inv_request) def testGetNameId(self): diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 49f0f1ca..0af0c286 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -98,14 +98,14 @@ def testReturnNameId(self): xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): response_4.get_nameid() json_settings['security']['wantNameId'] = True settings = OneLogin_Saml2_Settings(json_settings) response_5 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): response_5.get_nameid() json_settings['security']['wantNameId'] = False @@ -119,7 +119,7 @@ def testReturnNameId(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): response_7.get_nameid() json_settings['strict'] = True @@ -172,14 +172,14 @@ def testGetNameIdData(self): xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): response_4.get_nameid_data() json_settings['security']['wantNameId'] = True settings = OneLogin_Saml2_Settings(json_settings) response_5 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): response_5.get_nameid_data() json_settings['security']['wantNameId'] = False @@ -193,7 +193,7 @@ def testGetNameIdData(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): response_7.get_nameid_data() json_settings['security']['wantNameId'] = False @@ -207,7 +207,7 @@ def testGetNameIdData(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): response_7.get_nameid_data() json_settings['strict'] = True From a096f36ae1edd477b4c4817df9dd2eb1319fd134 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 30 Dec 2016 17:06:26 +0100 Subject: [PATCH 14/18] Minor bug --- src/onelogin/saml2/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 42b42b10..74d46be1 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -914,7 +914,7 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger dsig_ctx.verify(signature_node) except Exception as err: raise OneLogin_Saml2_ValidationError( - 'Signature validation failed. SAML Response rejected', + 'Signature validation failed. SAML Response rejected. %s', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE, str(err) ) From acb88e0690467a9f591fe172ad6e112dbad44e13 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 31 Dec 2016 10:02:27 +0100 Subject: [PATCH 15/18] Fix typo --- src/onelogin/saml2/errors.py | 2 +- src/onelogin/saml2/response.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index cdb825e0..f00d51b2 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -100,7 +100,7 @@ class OneLogin_Saml2_ValidationError(Exception): NO_SIGNED_ASSERTION = 33 NO_SIGNATURE_FOUND = 34 KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35 - CHILDREN_NODE_NOT_FOIND_IN_KEYINFO = 36 + CHILDREN_NODE_NOT_FOUND_IN_KEYINFO = 36 UNSUPPORTED_RETRIEVAL_METHOD = 37 NO_NAMEID = 38 EMPTY_NAMEID = 39 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 9968788b..f2aca97f 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -727,7 +727,7 @@ def __decrypt_assertion(self, xml): if not children: raise OneLogin_Saml2_ValidationError( 'KeyInfo has no children nodes, invalid Assertion', - OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOIND_IN_KEYINFO + OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOUND_IN_KEYINFO ) for child in children: if 'RetrievalMethod' in child.tag: From fa6d3ea9314650d66c5e855649e4443524dd853d Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 2 Jan 2017 17:23:10 +0100 Subject: [PATCH 16/18] Minor fix on docs --- README.md | 4 ++-- src/onelogin/saml2/response.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a8021196..f873229e 100644 --- a/README.md +++ b/README.md @@ -795,7 +795,7 @@ Main class of OneLogin Python Toolkit * ***get_settings*** Returns the settings info. * ***set_strict*** Set the strict mode active/disable. * ***get_last_request_xml*** Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest) -* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse was encrypted, by default tries to return the decrypted XML. +* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse had an encrypted assertion, decrypts it. ####OneLogin_Saml2_Auth - authn_request.py#### @@ -823,7 +823,7 @@ SAML 2 Authentication Response class * ***validate_num_assertions*** Verifies that the document only contains a single Assertion (encrypted or not) * ***validate_timestamps*** Verifies that the document is valid according to Conditions Element * ***get_error*** After execute a validation process, if fails this method returns the cause -* ***get_xml_document*** If necessary, decrypt the XML response document, and return it. +* ***get_xml_document*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it). ####OneLogin_Saml2_LogoutRequest - logout_request.py#### diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index f2aca97f..7c110f4b 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -757,9 +757,10 @@ def get_error(self): def get_xml_document(self): """ - If necessary, decrypt the XML response document, and return it. + Returns the SAML Response document (If contains an encrypted assertion, decrypts it) + :return: Decrypted XML response document - :rtype: string + :rtype: DOMDocument """ if self.encrypted: return self.decrypted_document From cbbe084b64134799568618259e7385f9a6513051 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 3 Jan 2017 17:13:58 +0100 Subject: [PATCH 17/18] Fix name --- src/onelogin/saml2/auth.py | 2 +- src/onelogin/saml2/errors.py | 2 +- src/onelogin/saml2/logout_request.py | 2 +- src/onelogin/saml2/logout_response.py | 2 +- src/onelogin/saml2/response.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 188c0ea5..340fb0d8 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -512,7 +512,7 @@ def __validate_signature(self, data, saml_type, raise_exceptions=False): if self.__settings.is_strict() and self.__settings.get_security_data().get('wantMessagesSigned', False): raise OneLogin_Saml2_ValidationError( 'The %s is not signed. Rejected.' % saml_type, - OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE ) return True diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index f00d51b2..d206f9c2 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -96,7 +96,7 @@ class OneLogin_Saml2_ValidationError(Exception): WRONG_ISSUER = 29 SESSION_EXPIRED = 30 WRONG_SUBJECTCONFIRMATION = 31 - NO_SIGNED_RESPONSE = 32 + NO_SIGNED_MESSAGE = 32 NO_SIGNED_ASSERTION = 33 NO_SIGNATURE_FOUND = 34 KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35 diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 5c73ca29..710b732d 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -296,7 +296,7 @@ def is_valid(self, request_data, raise_exceptions=False): if 'Signature' not in get_data: raise OneLogin_Saml2_ValidationError( 'The Message of the Logout Request is not signed and the SP require it', - OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE ) return True diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 206b3683..f5ea249c 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -121,7 +121,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if 'Signature' not in get_data: raise OneLogin_Saml2_ValidationError( 'The Message of the Logout Response is not signed and the SP require it', - OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE ) return True # pylint: disable=R0801 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 7c110f4b..55ddeec7 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -257,7 +257,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if security['wantAssertionsSigned'] and not has_signed_assertion: raise OneLogin_Saml2_ValidationError( 'The Assertion of the Response is not signed and the SP require it', - OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE ) if security['wantMessagesSigned'] and not has_signed_response: From c90a62d3c8579fa08285ac3849665f1261d4a875 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 3 Jan 2017 22:24:11 +0100 Subject: [PATCH 18/18] Typo --- src/onelogin/saml2/response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 55ddeec7..ba2b3545 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -257,13 +257,13 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if security['wantAssertionsSigned'] and not has_signed_assertion: raise OneLogin_Saml2_ValidationError( 'The Assertion of the Response is not signed and the SP require it', - OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE + OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION ) if security['wantMessagesSigned'] and not has_signed_response: raise OneLogin_Saml2_ValidationError( 'The Message of the Response is not signed and the SP require it', - OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION + OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE ) if not signed_elements or (not has_signed_response and not has_signed_assertion):