From 74c992c39171ef6044d4253c25cb8cee23b8e59a Mon Sep 17 00:00:00 2001 From: Patrick Kelley Date: Mon, 22 Dec 2014 11:02:29 -0800 Subject: [PATCH 001/352] syntax typo in demo-flask/saml/settings.json Removing extra } in demo-flask/saml/settings.json --- demo-flask/saml/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-flask/saml/settings.json b/demo-flask/saml/settings.json index 53675aa3..142911f1 100644 --- a/demo-flask/saml/settings.json +++ b/demo-flask/saml/settings.json @@ -27,4 +27,4 @@ }, "x509cert": "" } -}} +} From fef7b81efa68f4dc2c5c85e1456e85ee3d868981 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 9 Jan 2015 21:23:01 +0100 Subject: [PATCH 002/352] Single Log Out Fix, Add nameID & sessionIndex support Related to #38 --- src/onelogin/saml2/auth.py | 7 ++- src/onelogin/saml2/logout_request.py | 24 ++++++-- tests/src/OneLogin/saml2_tests/auth_test.py | 66 ++++++++++++++++++++- 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 33078a6a..03f311bf 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -272,7 +272,7 @@ def login(self, return_to=None): parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState']) return self.redirect_to(self.get_sso_url(), parameters) - def logout(self, return_to=None): + def logout(self, return_to=None, name_id=None, session_index=None): """ Initiates the SLO process. @@ -288,7 +288,10 @@ def logout(self, return_to=None): OneLogin_Saml2_Error.SAML_SINGLE_LOGOUT_NOT_SUPPORTED ) - logout_request = OneLogin_Saml2_Logout_Request(self.__settings) + if name_id is None and self.__nameid is not None: + name_id = self.__nameid + + logout_request = OneLogin_Saml2_Logout_Request(self.__settings, name_id=name_id, session_index=session_index) saml_request = logout_request.get_request() diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 44155a2c..be7078b2 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -29,7 +29,7 @@ class OneLogin_Saml2_Logout_Request(object): """ - def __init__(self, settings, request=None): + def __init__(self, settings, request=None, name_id=None, session_index=None): """ Constructs the Logout Request object. @@ -45,20 +45,30 @@ def __init__(self, settings, request=None): security = self.__settings.get_security_data() uid = OneLogin_Saml2_Utils.generate_unique_id() - name_id_value = OneLogin_Saml2_Utils.generate_unique_id() issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) cert = None if 'nameIdEncrypted' in security and security['nameIdEncrypted']: cert = idp_data['x509cert'] - name_id = OneLogin_Saml2_Utils.generate_name_id( - name_id_value, + if name_id is not None: + nameIdFormat = sp_data['NameIDFormat'] + else: + name_id = idp_data['entityId'] + nameIdFormat = OneLogin_Saml2_Constants.NAMEID_ENTITY + + name_id_obj = OneLogin_Saml2_Utils.generate_name_id( + name_id, sp_data['entityId'], - sp_data['NameIDFormat'], + nameIdFormat, cert ) + if session_index: + session_index_str = '%s' % session_index + else: + session_index_str = '' + logout_request = """ %(entity_id)s %(name_id)s + %(session_index)s """ % \ { 'id': uid, 'issue_instant': issue_instant, 'single_logout_url': idp_data['singleLogoutService']['url'], 'entity_id': sp_data['entityId'], - 'name_id': name_id, + 'name_id': name_id_obj, + 'session_index': session_index_str, } else: decoded = b64decode(request) diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 0ec39179..8c5d2453 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -13,6 +13,7 @@ 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.logout_request import OneLogin_Saml2_Logout_Request class OneLogin_Saml2_Auth_Test(unittest.TestCase): @@ -95,9 +96,25 @@ def testGetSessionIndex(self): auth2.process_response() self.assertEqual('_6273d77b8cde0c333ec79d22a9fa0003b9fe2d75cb', auth2.get_session_index()) + def testGetLastErrorReason(self): + """ + Tests the get_last_error_reason method of the OneLogin_Saml2_Auth class + Case Invalid Response + """ + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + del request_data['get_data'] + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + auth.process_response() + + self.assertEqual(auth.get_last_error_reason(), 'Signature validation failed. SAML Response rejected') + def testProcessNoResponse(self): """ - Tests the processResponse method of the OneLogin_Saml2_Auth class + Tests the process_response method of the OneLogin_Saml2_Auth class Case No Response, An exception is throw """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) @@ -645,6 +662,53 @@ def testLogoutNoSLO(self): except Exception as e: self.assertIn('The IdP does not support Single Log Out', e.message) + def testLogoutNameIDandSessionIndex(self): + """ + Tests the logout method of the OneLogin_Saml2_Auth class + Case nameID and sessionIndex as parameters. + """ + settings_info = self.loadSettingsJSON() + request_data = self.get_request() + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) + + name_id = 'name_id_example' + session_index = 'session_index_example' + target_url = auth.logout(name_id=name_id, session_index=session_index) + parsed_query = parse_qs(urlparse(target_url)[4]) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertIn(slo_url, target_url) + self.assertIn('SAMLRequest', parsed_query) + + logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]) + name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request) + sessions_index_in_request = OneLogin_Saml2_Logout_Request.get_session_indexes(logout_request) + self.assertIn(session_index, sessions_index_in_request) + self.assertEqual(name_id, name_id_from_request) + + def testLogoutNameID(self): + """ + Tests the logout method of the OneLogin_Saml2_Auth class + Case nameID loaded after process SAML Response + """ + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + del request_data['get_data'] + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + auth.process_response() + + name_id_from_response = auth.get_nameid() + + target_url = auth.logout() + parsed_query = parse_qs(urlparse(target_url)[4]) + self.assertIn('SAMLRequest', parsed_query) + logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]) + + name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request) + self.assertEqual(name_id_from_response, name_id_from_request) + def testSetStrict(self): """ Tests the set_strict method of the OneLogin_Saml2_Auth From 561047c1672337276bd57e25115997bbf1b5e3ba Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 9 Jan 2015 22:56:29 +0100 Subject: [PATCH 003/352] Update docs --- README.md | 6 +++ docs/saml2/_modules/saml2/auth.html | 7 +++- docs/saml2/_modules/saml2/logout_request.html | 2 +- docs/saml2/saml2.html | 41 +++++++++++++++++-- src/onelogin/saml2/auth.py | 8 +++- src/onelogin/saml2/logout_request.py | 13 +++++- tests/src/OneLogin/saml2_tests/auth_test.py | 4 +- 7 files changed, 70 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0c2cb1f4..ec67f446 100644 --- a/README.md +++ b/README.md @@ -619,6 +619,12 @@ target_url = 'https://example.com' auth.logout(return_to=target_url) ``` +Also there are 2 optional parameters that can be set: + +* name_id. That will be used to build the LogoutRequest. If not name_id parameter is set and the auth object processed a +SAML Response with a NameId, then this NameId will be used. +* session_index. SessionIndex that identifies the session of the user. + ####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. diff --git a/docs/saml2/_modules/saml2/auth.html b/docs/saml2/_modules/saml2/auth.html index 28e19166..82d6e960 100644 --- a/docs/saml2/_modules/saml2/auth.html +++ b/docs/saml2/_modules/saml2/auth.html @@ -280,13 +280,16 @@

Source code for saml2.auth

             parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'])
         return self.redirect_to(self.get_sso_url(), parameters)
 
-
[docs] def logout(self, return_to=None): +
[docs] def logout(self, return_to=None, name_id=None, session_index=None): """ Initiates the SLO process. :param return_to: Optional argument. The target URL the user should be redirected to after logout. :type return_to: string - + :param name_id: Optional argument. The NameID that will be set in the LogoutRequest. + :type name_id: string + :param session_index: Optional argument. SessionIndex that identifies the session of the user. + :type session_index: string :returns: Redirection url """ slo_url = self.get_slo_url() diff --git a/docs/saml2/_modules/saml2/logout_request.html b/docs/saml2/_modules/saml2/logout_request.html index 2589c4d5..b6cddbc1 100644 --- a/docs/saml2/_modules/saml2/logout_request.html +++ b/docs/saml2/_modules/saml2/logout_request.html @@ -70,7 +70,7 @@

Source code for saml2.logout_request

 
 
[docs]class OneLogin_Saml2_Logout_Request: - def __init__(self, settings): + def __init__(self, settings,request=None,name_id=None, session_index=None): """ Constructs the Logout Request object. diff --git a/docs/saml2/saml2.html b/docs/saml2/saml2.html index 4ef557d9..d5b735e6 100644 --- a/docs/saml2/saml2.html +++ b/docs/saml2/saml2.html @@ -142,6 +142,22 @@

OneLogin saml2 Module +
+
+get_last_error_reason()[source]
+

Returns the reason for the last error

+ +++ + + + + + +
Returns:Error
Return type:string
+
+
get_nameid()[source]
@@ -232,13 +248,18 @@

OneLogin saml2 Module
-logout(return_to=None)[source]
+logout(return_to=None, name_id=None, session_index=None)[source]

Initiates the SLO process.

- + + @@ -633,7 +654,7 @@

OneLogin saml2 Module

logout_request Class

-class onelogin.saml2.logout_request.OneLogin_Saml2_Logout_Request(settings)[source]
+class onelogin.saml2.logout_request.OneLogin_Saml2_Logout_Request(settings, request=None, name_id=None, session_index=None)[source]
static get_id(request)[source]
@@ -708,6 +729,13 @@

OneLogin saml2 Module

+
+
+get_error()[source]
+

After execute a validation process, if fails this method returns the cause +:rtype: str object

+
+ @@ -758,6 +786,13 @@

OneLogin saml2 Module +
+
+get_error()[source]
+

After execute a validation process, if fails this method returns the cause +:rtype: str object

+
+ diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 03f311bf..2076f7c1 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -224,7 +224,7 @@ def get_errors(self): def get_last_error_reason(self): """ - Return the reason for the last error + Returns the reason for the last error :returns: Reason of the last error :rtype: None | string @@ -279,6 +279,12 @@ def logout(self, return_to=None, name_id=None, session_index=None): :param return_to: Optional argument. The target URL the user should be redirected to after logout. :type return_to: string + :param name_id: The NameID that will be set in the LogoutRequest. + :type name_id: string + + :param session_index: SessionIndex that identifies the session of the user. + :type session_index: string + :returns: Redirection url """ slo_url = self.get_slo_url() diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index be7078b2..85b66713 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -33,8 +33,17 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): """ Constructs the Logout Request object. - Arguments are: - * (OneLogin_Saml2_Settings) settings. Setting data + :param settings: Setting data + :type request_data: OneLogin_Saml2_Settings + + :param request: Optional. A LogoutRequest to be loaded instead build one. + :type request: string + + :param name_id: The NameID that will be set in the LogoutRequest. + :type name_id: string + + :param session_index: SessionIndex that identifies the session of the user. + :type session_index: string """ self.__settings = settings self.__error = None diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 8c5d2453..9c93ff56 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -680,7 +680,7 @@ def testLogoutNameIDandSessionIndex(self): self.assertIn('SAMLRequest', parsed_query) logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]) - name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request) + name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request) sessions_index_in_request = OneLogin_Saml2_Logout_Request.get_session_indexes(logout_request) self.assertIn(session_index, sessions_index_in_request) self.assertEqual(name_id, name_id_from_request) @@ -706,7 +706,7 @@ def testLogoutNameID(self): self.assertIn('SAMLRequest', parsed_query) logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]) - name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request) + name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request) self.assertEqual(name_id_from_response, name_id_from_request) def testSetStrict(self): From dbf03dc661dad02581ce22e9e19c8d31ad9451f3 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 12 Jan 2015 18:33:06 +0100 Subject: [PATCH 004/352] Reject SAML Response if not signed and strict = false --- src/onelogin/saml2/response.py | 2 + tests/src/OneLogin/saml2_tests/auth_test.py | 21 ++--- .../src/OneLogin/saml2_tests/response_test.py | 88 +++++++++++++------ 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 7cdbfbdf..ba7e156c 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -207,6 +207,8 @@ def is_valid(self, request_data, request_id=None): document_to_validate = self.document if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint): raise Exception('Signature validation failed. SAML Response rejected') + else: + raise Exception('No Signature found. SAML Response rejected') return True except Exception as err: diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 9c93ff56..8ee2cae0 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -166,15 +166,16 @@ def testProcessResponseInvalidRequestId(self): auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) request_id = 'invalid' auth.process_response(request_id) - self.assertEqual(len(auth.get_errors()), 0) + self.assertEqual('No Signature found. SAML Response rejected', auth.get_last_error_reason()) auth.set_strict(True) auth.process_response(request_id) self.assertEqual(auth.get_errors(), ['invalid_response']) + self.assertEqual('The InResponseTo of the Response: _57bcbf70-7b1f-012e-c821-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid', auth.get_last_error_reason()) valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38' auth.process_response(valid_request_id) - self.assertEqual(len(auth.get_errors()), 0) + self.assertEqual('No Signature found. SAML Response rejected', auth.get_last_error_reason()) def testProcessResponseValid(self): """ @@ -184,28 +185,22 @@ def testProcessResponseValid(self): the error array is empty """ request_data = self.get_request() - message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) - plain_message = b64decode(message) - current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) - plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) del request_data['get_data'] request_data['post_data'] = { - 'SAMLResponse': b64encode(plain_message) + 'SAMLResponse': message } auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) auth.process_response() - self.assertTrue(auth.is_authenticated()) self.assertEqual(len(auth.get_errors()), 0) - self.assertEqual('someone@example.com', auth.get_nameid()) + self.assertEqual('492882615acf31c8096b627245d76ae53036c090', auth.get_nameid()) attributes = auth.get_attributes() self.assertNotEqual(len(attributes), 0) self.assertEqual(auth.get_attribute('mail'), attributes['mail']) - - auth.set_strict(True) - auth.process_response() - self.assertEqual(len(auth.get_errors()), 0) + session_index = auth.get_session_index() + self.assertEqual('_6273d77b8cde0c333ec79d22a9fa0003b9fe2d75cb', session_index) def testRedirectTo(self): """ diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 15c5fec5..716aec18 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -314,11 +314,13 @@ def testIsInvalidXML(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) response = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response.is_valid(request_data)) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, message) self.assertFalse(response_2.is_valid(request_data)) + self.assertEqual('Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd', response_2.get_error()) def testValidateNumAssertions(self): """ @@ -408,7 +410,8 @@ def testIsInValidExpired(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'expired_response.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - self.assertTrue(response.is_valid(self.get_request_data())) + response.is_valid(self.get_request_data()) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, xml) @@ -426,7 +429,8 @@ def testIsInValidNoStatement(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_signature.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - self.assertTrue(response.is_valid(self.get_request_data())) + response.is_valid(self.get_request_data()) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, xml) @@ -473,7 +477,8 @@ def testIsInValidEncAttrs(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_attrs.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - self.assertTrue(response.is_valid(self.get_request_data())) + response.is_valid(self.get_request_data()) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, xml) @@ -491,7 +496,8 @@ def testIsInValidDestination(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) response = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response.is_valid(self.get_request_data())) + response.is_valid(self.get_request_data()) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, message) @@ -524,7 +530,8 @@ def testIsInValidAudience(self): message = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_audience.xml.base64')) response = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response.is_valid(request_data)) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, message) @@ -554,10 +561,12 @@ def testIsInValidIssuer(self): message_2 = b64encode(plain_message_2) response = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response.is_valid(request_data)) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) response_2 = OneLogin_Saml2_Response(settings, message_2) - self.assertTrue(response_2.is_valid(request_data)) + response_2.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_2.get_error()) settings.set_strict(True) response_3 = OneLogin_Saml2_Response(settings, message) @@ -591,7 +600,8 @@ def testIsInValidSessionIndex(self): message = b64encode(plain_message) response = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response.is_valid(request_data)) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, message) @@ -619,7 +629,8 @@ def testDatetimeWithMiliseconds(self): plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message = b64encode(plain_message) response = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response.is_valid(request_data)) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) def testIsInValidSubjectConfirmation(self): """ @@ -663,22 +674,28 @@ def testIsInValidSubjectConfirmation(self): message_6 = b64encode(plain_message_6) response = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response.is_valid(request_data)) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) response_2 = OneLogin_Saml2_Response(settings, message_2) - self.assertTrue(response_2.is_valid(request_data)) + response_2.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_2.get_error()) response_3 = OneLogin_Saml2_Response(settings, message_3) - self.assertTrue(response_3.is_valid(request_data)) + response_3.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_3.get_error()) response_4 = OneLogin_Saml2_Response(settings, message_4) - self.assertTrue(response_4.is_valid(request_data)) + response_4.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_4.get_error()) response_5 = OneLogin_Saml2_Response(settings, message_5) - self.assertTrue(response_5.is_valid(request_data)) + response_5.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_5.get_error()) response_6 = OneLogin_Saml2_Response(settings, message_6) - self.assertTrue(response_6.is_valid(request_data)) + response_6.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_6.get_error()) settings.set_strict(True) response = OneLogin_Saml2_Response(settings, message) @@ -735,7 +752,8 @@ def testIsInValidRequestId(self): response = OneLogin_Saml2_Response(settings, message) request_id = 'invalid' - self.assertTrue(response.is_valid(request_data, request_id)) + response.is_valid(request_data, request_id) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) settings.set_strict(True) response = OneLogin_Saml2_Response(settings, message) @@ -745,7 +763,8 @@ def testIsInValidRequestId(self): self.assertEqual('The InResponseTo of the Response', e.message) valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38' - self.assertTrue(response.is_valid(request_data, valid_request_id)) + response.is_valid(request_data, valid_request_id) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) def testIsInValidSignIssues(self): """ @@ -766,18 +785,21 @@ def testIsInValidSignIssues(self): settings_info['security']['wantAssertionsSigned'] = False settings = OneLogin_Saml2_Settings(settings_info) response = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response.is_valid(request_data)) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) settings_info['security']['wantAssertionsSigned'] = True settings_2 = OneLogin_Saml2_Settings(settings_info) response_2 = OneLogin_Saml2_Response(settings_2, message) - self.assertTrue(response_2.is_valid(request_data)) + response_2.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_2.get_error()) settings_info['strict'] = True settings_info['security']['wantAssertionsSigned'] = False settings_3 = OneLogin_Saml2_Settings(settings_info) response_3 = OneLogin_Saml2_Response(settings_3, message) - self.assertTrue(response_3.is_valid(request_data)) + response_3.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_3.get_error()) settings_info['security']['wantAssertionsSigned'] = True settings_4 = OneLogin_Saml2_Settings(settings_info) @@ -793,18 +815,21 @@ def testIsInValidSignIssues(self): settings_info['security']['wantMessagesSigned'] = False settings_5 = OneLogin_Saml2_Settings(settings_info) response_5 = OneLogin_Saml2_Response(settings_5, message) - self.assertTrue(response_5.is_valid(request_data)) + response_5.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_5.get_error()) settings_info['security']['wantMessagesSigned'] = True settings_6 = OneLogin_Saml2_Settings(settings_info) response_6 = OneLogin_Saml2_Response(settings_6, message) - self.assertTrue(response_6.is_valid(request_data)) + response_6.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_6.get_error()) settings_info['strict'] = True settings_info['security']['wantMessagesSigned'] = False settings_7 = OneLogin_Saml2_Settings(settings_info) response_7 = OneLogin_Saml2_Response(settings_7, message) - self.assertTrue(response_7.is_valid(request_data)) + response_7.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_7.get_error()) settings_info['security']['wantMessagesSigned'] = True settings_8 = OneLogin_Saml2_Settings(settings_info) @@ -833,13 +858,15 @@ def testIsInValidEncIssues(self): settings_info['security']['wantAssertionsEncrypted'] = True settings = OneLogin_Saml2_Settings(settings_info) response = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response.is_valid(request_data)) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) settings_info['strict'] = True settings_info['security']['wantAssertionsEncrypted'] = False settings = OneLogin_Saml2_Settings(settings_info) response_2 = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response_2.is_valid(request_data)) + response_2.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_2.get_error()) settings_info['security']['wantAssertionsEncrypted'] = True settings = OneLogin_Saml2_Settings(settings_info) @@ -853,7 +880,8 @@ def testIsInValidEncIssues(self): settings_info['strict'] = False settings = OneLogin_Saml2_Settings(settings_info) response_4 = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response_4.is_valid(request_data)) + response_4.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_4.get_error()) settings_info['strict'] = True settings = OneLogin_Saml2_Settings(settings_info) @@ -916,7 +944,8 @@ def testIsValid(self): xml = self.file_contents(join(self.data_path, 'responses', 'valid_unsigned_response.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - self.assertTrue(response.is_valid(self.get_request_data())) + response.is_valid(self.get_request_data()) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) def testIsValid2(self): """ @@ -992,7 +1021,8 @@ def testIsValidEnc(self): plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message = b64encode(plain_message) response_7 = OneLogin_Saml2_Response(settings, message) - self.assertTrue(response_7.is_valid(request_data)) + response_7.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_7.get_error()) def testIsValidSign(self): """ From 2b0c59fc1181f74e712b62410cc3591c0aaf9da9 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 14 Jan 2015 00:19:44 +0100 Subject: [PATCH 005/352] New release 2.1.0. Add ForceAuh and IsPassive support --- README.md | 11 +++ docs/saml2/saml2.html | 11 ++- setup.py | 2 +- src/onelogin/saml2/auth.py | 10 ++- src/onelogin/saml2/authn_request.py | 24 ++++++- tests/src/OneLogin/saml2_tests/auth_test.py | 68 +++++++++++++++++++ .../saml2_tests/authn_request_test.py | 56 +++++++++++++++ 7 files changed, 173 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ec67f446..79ca0a28 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,13 @@ You can install it executing: If you want to know how a project can handle python packages review this [guide](https://packaging.python.org/en/latest/tutorial.html) and review this [sampleproject](https://github.com/pypa/sampleproject) +Security warning +---------------- + +In production, the **strict** parameter MUST be set as **"true"**. Otherwise +your environment is not secure and will be exposed to attacks. + + Getting started --------------- @@ -447,6 +454,10 @@ We can set a 'return_to' url parameter to the login function and that will be co target_url = 'https://example.com' auth.login(return_to=target_url) ``` +The login method can recieve 2 more optional parameters: + +* force_authn When true the AuthNReuqest will set the ForceAuthn='true' +* is_passive When true the AuthNReuqest will set the Ispassive='true' #### The SP Endpoints #### diff --git a/docs/saml2/saml2.html b/docs/saml2/saml2.html index d5b735e6..6dd1e00a 100644 --- a/docs/saml2/saml2.html +++ b/docs/saml2/saml2.html @@ -232,13 +232,18 @@

OneLogin saml2 Module
-login(return_to=None)[source]
+login(return_to=None, force_authn=False, is_passive=False)[source]

Initiates the SSO process.

Parameters:return_to (string) – Optional argument. The target URL the user should be redirected to after logout.
Parameters:
    +
  • return_to (string) – Optional argument. The target URL the user should be redirected to after logout.
  • +
  • name_id (string) – Optional argument. The NameID that will be set in the LogoutRequest.
  • +
  • session_index (string) – Optional argument. SessionIndex that identifies the session of the user.
  • +
Returns:Redirection url
- + @@ -346,7 +351,7 @@

OneLogin saml2 Module

authn_request Class

-class onelogin.saml2.authn_request.OneLogin_Saml2_Authn_Request(settings)[source]
+class onelogin.saml2.authn_request.OneLogin_Saml2_Authn_Request(settings, force_authn=False, is_passive=False)[source]
get_request()[source]
diff --git a/setup.py b/setup.py index 97887a21..3dd6381f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.0.2', + version='2.1.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 2076f7c1..4be7e87a 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -247,16 +247,22 @@ def get_attribute(self, name): value = self.__attributes[name] return value - def login(self, return_to=None): + def login(self, return_to=None, force_authn=False, is_passive=False): """ Initiates the SSO process. :param return_to: Optional argument. The target URL the user should be redirected to after login. :type return_to: string + :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'. + :type force_authn: string + + :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. + :type is_passive: string + :returns: Redirection url """ - authn_request = OneLogin_Saml2_Authn_Request(self.__settings) + authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive) saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 80d7313f..8e8495e7 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -24,12 +24,18 @@ class OneLogin_Saml2_Authn_Request(object): """ - def __init__(self, settings): + def __init__(self, settings, force_authn=False, is_passive=False): """ Constructs the AuthnRequest object. - Arguments are: - * (OneLogin_Saml2_Settings) settings. Setting data + :param settings: OSetting data + :type return_to: OneLogin_Saml2_Settings + + :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'. + :type force_authn: bool + + :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. + :type is_passive: bool """ self.__settings = settings @@ -58,6 +64,14 @@ def __init__(self, settings): if 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] is not None: provider_name_str = 'ProviderName="%s"' % organization_data[lang]['displayname'] + force_authn_str = '' + if force_authn is True: + force_authn_str = 'ForceAuthn="true"' + + is_passive_str = '' + if is_passive is True: + is_passive_str = 'IsPassive="true"' + requested_authn_context_str = '' if 'requestedAuthnContext' in security.keys() and security['requestedAuthnContext'] is not False: if security['requestedAuthnContext'] is True: @@ -76,6 +90,8 @@ def __init__(self, settings): 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" @@ -89,6 +105,8 @@ def __init__(self, settings): { 'id': uid, 'provider_name': provider_name_str, + 'force_authn_str': force_authn_str, + 'is_passive_str': is_passive_str, 'issue_instant': issue_instant, 'destination': destination, 'assertion_url': sp_data['assertionConsumerService']['url'], diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 8ee2cae0..cf9eb648 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -582,6 +582,74 @@ def testLoginSigned(self): self.assertIn(return_to, parsed_query['RelayState']) self.assertIn(OneLogin_Saml2_Constants.RSA_SHA1, parsed_query['SigAlg']) + def testLoginForceAuthN(self): + """ + Tests the login method of the OneLogin_Saml2_Auth class + Case Logout with no parameters. A AuthN Request is built with ForceAuthn and redirect executed + """ + settings_info = self.loadSettingsJSON() + return_to = u'http://example.com/returnto' + sso_url = settings_info['idp']['singleSignOnService']['url'] + + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url = auth.login(return_to) + parsed_query = parse_qs(urlparse(target_url)[4]) + sso_url = settings_info['idp']['singleSignOnService']['url'] + self.assertIn(sso_url, target_url) + self.assertIn('SAMLRequest', parsed_query) + request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]) + self.assertNotIn('ForceAuthn="true"', request) + + auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url_2 = auth_2.login(return_to, False, False) + parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) + self.assertIn(sso_url, target_url_2) + self.assertIn('SAMLRequest', parsed_query_2) + request_2 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0]) + self.assertNotIn('ForceAuthn="true"', request_2) + + auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url_3 = auth_3.login(return_to, True, False) + parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) + self.assertIn(sso_url, target_url_3) + self.assertIn('SAMLRequest', parsed_query_3) + request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0]) + self.assertIn('ForceAuthn="true"', request_3) + + def testLoginIsPassive(self): + """ + Tests the login method of the OneLogin_Saml2_Auth class + Case Logout with no parameters. A AuthN Request is built with IsPassive and redirect executed + """ + settings_info = self.loadSettingsJSON() + return_to = u'http://example.com/returnto' + sso_url = settings_info['idp']['singleSignOnService']['url'] + + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url = auth.login(return_to) + parsed_query = parse_qs(urlparse(target_url)[4]) + sso_url = settings_info['idp']['singleSignOnService']['url'] + self.assertIn(sso_url, target_url) + self.assertIn('SAMLRequest', parsed_query) + request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]) + self.assertNotIn('IsPassive="true"', request) + + auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url_2 = auth_2.login(return_to, False, False) + parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) + self.assertIn(sso_url, target_url_2) + self.assertIn('SAMLRequest', parsed_query_2) + request_2 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0]) + self.assertNotIn('IsPassive="true"', request_2) + + auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url_3 = auth_3.login(return_to, False, True) + parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) + self.assertIn(sso_url, target_url_3) + self.assertIn('SAMLRequest', parsed_query_3) + request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0]) + self.assertIn('IsPassive="true"', request_3) + def testLogout(self): """ Tests the logout method of the OneLogin_Saml2_Auth class diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 1b1249b1..2d5a0a2f 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -117,6 +117,62 @@ def testCreateRequestAuthContext(self): self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, inflated) self.assertIn(OneLogin_Saml2_Constants.AC_X509, inflated) + def testCreateRequestForceAuthN(self): + """ + Tests the OneLogin_Saml2_Authn_Request Constructor. + The creation of a deflated SAML Request with ForceAuthn="true" + """ + saml_settings = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(saml_settings) + authn_request = OneLogin_Saml2_Authn_Request(settings) + authn_request_encoded = authn_request.get_request() + decoded = b64decode(authn_request_encoded) + inflated = decompress(decoded, -15) + self.assertRegexpMatches(inflated, '^ Date: Wed, 14 Jan 2015 14:48:05 +0100 Subject: [PATCH 006/352] Update the dm.xmlsec.binding library to 1.3.2 (Improved transform support, Workaround for buildout problem) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3dd6381f..43396b2e 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ test_suite='tests', install_requires=[ 'M2Crypto==0.22.3', - 'dm.xmlsec.binding==1.3.1', + 'dm.xmlsec.binding==1.3.2', 'isodate==0.5.0', 'defusedxml==0.4.1', ], From bda5bcba03b143db1e8806a53751ec92c7084197 Mon Sep 17 00:00:00 2001 From: Steven Das Date: Wed, 14 Jan 2015 22:49:51 -0600 Subject: [PATCH 007/352] Fixed typo in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79ca0a28..de4e7519 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![PyPi Version](https://pypip.in/v/python-saml/badge.png)](https://pypi.python.org/pypi/python-saml) ![PyPi Downloads](https://pypip.in/d/python-saml/badge.png) -Add SAML support to your Python softwares using this library. +Add SAML support to your Python software using this library. Forget those complicated libraries and use that open source library provided and supported by OneLogin Inc. From 09e33c098ce5e6732d76c11c1fcc66129cb5a4eb Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 15 Jan 2015 21:28:28 +0100 Subject: [PATCH 008/352] Fix wrong element order in generated metadata (SLS before NameID). metadata xsd updated --- src/onelogin/saml2/metadata.py | 15 ++++++----- .../schemas/saml-schema-metadata-2.0.xsd | 3 --- src/onelogin/saml2/settings.py | 2 ++ .../metadata/expired_metadata_settings1.xml | 5 ++-- tests/data/metadata/metadata_settings1.xml | 4 +-- .../metadata/no_expiration_mark_metadata.xml | 4 +-- .../metadata/noentity_metadata_settings1.xml | 5 ++-- .../metadata/signed_metadata_settings1.xml | 27 ++++++++++++++----- tests/data/metadata/unparsed_metadata.xml | 4 +-- tests/src/OneLogin/saml2_tests/utils_test.py | 8 ++++++ 10 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index f0447047..2dcb3efc 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -75,8 +75,8 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N sls = '' if 'singleLogoutService' in sp: - sls = """""" % \ + sls = """ \n""" % \ { 'binding': sp['singleLogoutService']['binding'], 'location': sp['singleLogoutService']['url'], @@ -125,11 +125,10 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N cacheDuration="%(cache)s" entityID="%(entity_id)s"> - %(name_id_format)s +%(sls)s %(name_id_format)s -%(sls)s %(organization)s %(contacts)s @@ -204,10 +203,9 @@ def add_x509_key_descriptors(metadata, cert=None): key_descriptor = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'md:KeyDescriptor') - entity_descriptor = sp_sso_descriptor = xml.getElementsByTagName('md:EntityDescriptor')[0] - entity_descriptor.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) + entity_descriptor = xml.getElementsByTagName('md:EntityDescriptor')[0] - sp_sso_descriptor = xml.getElementsByTagName('md:SPSSODescriptor')[0] + sp_sso_descriptor = entity_descriptor.getElementsByTagName('md:SPSSODescriptor')[0] sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) @@ -220,4 +218,7 @@ def add_x509_key_descriptors(metadata, cert=None): signing.appendChild(key_info) encryption.appendChild(key_info.cloneNode(True)) + signing.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) + encryption.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) + return xml.toxml() diff --git a/src/onelogin/saml2/schemas/saml-schema-metadata-2.0.xsd b/src/onelogin/saml2/schemas/saml-schema-metadata-2.0.xsd index 8d6ad0db..86e58f9b 100644 --- a/src/onelogin/saml2/schemas/saml-schema-metadata-2.0.xsd +++ b/src/onelogin/saml2/schemas/saml-schema-metadata-2.0.xsd @@ -247,8 +247,6 @@ - - @@ -327,7 +325,6 @@ - diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 50065bcf..5f1e9208 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -635,6 +635,8 @@ def validate_metadata(self, xml): if expire_time is not None and int(datetime.now().strftime('%s')) > int(expire_time): errors.append('expired_xml') + # TODO: Validate Sign + return errors def format_idp_cert(self): diff --git a/tests/data/metadata/expired_metadata_settings1.xml b/tests/data/metadata/expired_metadata_settings1.xml index 952ac83b..6311dd69 100644 --- a/tests/data/metadata/expired_metadata_settings1.xml +++ b/tests/data/metadata/expired_metadata_settings1.xml @@ -1,8 +1,9 @@ + MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOoMIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo + urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified - - MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOoMIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo + diff --git a/tests/data/metadata/metadata_settings1.xml b/tests/data/metadata/metadata_settings1.xml index 1d1ff9d2..449a8f01 100644 --- a/tests/data/metadata/metadata_settings1.xml +++ b/tests/data/metadata/metadata_settings1.xml @@ -4,11 +4,11 @@ cacheDuration="PT1594475551S" entityID="http://stuff.com/endpoints/metadata.php"> + urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified - diff --git a/tests/data/metadata/no_expiration_mark_metadata.xml b/tests/data/metadata/no_expiration_mark_metadata.xml index f1334397..0d0b8085 100644 --- a/tests/data/metadata/no_expiration_mark_metadata.xml +++ b/tests/data/metadata/no_expiration_mark_metadata.xml @@ -2,11 +2,11 @@ + urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified - diff --git a/tests/data/metadata/noentity_metadata_settings1.xml b/tests/data/metadata/noentity_metadata_settings1.xml index 9d481d04..af1ff3eb 100644 --- a/tests/data/metadata/noentity_metadata_settings1.xml +++ b/tests/data/metadata/noentity_metadata_settings1.xml @@ -1,8 +1,9 @@ + MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOoMIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo + urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified - - MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOoMIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo + diff --git a/tests/data/metadata/signed_metadata_settings1.xml b/tests/data/metadata/signed_metadata_settings1.xml index e15269c4..9a37fe4e 100644 --- a/tests/data/metadata/signed_metadata_settings1.xml +++ b/tests/data/metadata/signed_metadata_settings1.xml @@ -1,10 +1,10 @@ - + - 4umvu47syu+tc8ygQ6EA4FprCSA=vRSWquLe4WjGiXpcycBh9/L13OUEWrD19dwZsodZ+BBjzDRu1qchr0lb6fz8HpuPdm9u+MSdP8oecGv8zrRLABhY+ZR07V18Q68qEHmqqFij4MDGdDLXkaZZogTbsv+2IgEcfjawelVZmeEI3Ee9YCHx0uyg2SSwyJpfVJXWcqk= + +FoWTQxwj75/mQK600oN7ZobfqU=lm/ZJWEoAOeBD+bqimMLJEECySqYSRkcJ5KVU8mKORh044go2YSN5MLyJe9772506FsWf9UxCMV+EhBl7wj4k1E1/SAXQ3GhdWr8qTTBZ4QiVSeB/ReqFQaD/W0vlrYLwu1f+hMoEGdalqsFOetjBSsuiRi3n6qHY2x3ePIXCXY= MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo - + @@ -19,8 +19,21 @@ - - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - - + + urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress + + + + sp_test + SP test + http://sp.example.com + + + technical_name + technical@example.com + + + support_name + support@example.com + diff --git a/tests/data/metadata/unparsed_metadata.xml b/tests/data/metadata/unparsed_metadata.xml index 6f7b3731..6e6c567a 100644 --- a/tests/data/metadata/unparsed_metadata.xml +++ b/tests/data/metadata/unparsed_metadata.xml @@ -4,11 +4,11 @@ cacheDuration="PT1594475551S" entityID="http://stuff.com/endpoints/metadata.php"> + urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified - diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index dfcedfe6..409c280e 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -55,6 +55,14 @@ def testValidateXML(self): res = OneLogin_Saml2_Utils.validate_xml(metadata_ok, 'saml-schema-metadata-2.0.xsd') self.assertTrue(isinstance(res, Document)) + metadata_bad_order = self.file_contents(join(self.data_path, 'metadata', 'metadata_bad_order_settings1.xml')) + res = OneLogin_Saml2_Utils.validate_xml(metadata_bad_order, 'saml-schema-metadata-2.0.xsd') + self.assertFalse(isinstance(res, Document)) + + metadata_signed = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings1.xml')) + res = OneLogin_Saml2_Utils.validate_xml(metadata_signed, 'saml-schema-metadata-2.0.xsd') + self.assertTrue(isinstance(res, Document)) + dom = parseString(metadata_ok) res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-metadata-2.0.xsd') self.assertTrue(isinstance(res, Document)) From 105a0de91037c5f12d1ecaf14188e14cb8b88700 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 15 Jan 2015 21:38:28 +0100 Subject: [PATCH 009/352] Forgot to upload a file --- .../data/metadata/metadata_bad_order_settings1.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/data/metadata/metadata_bad_order_settings1.xml diff --git a/tests/data/metadata/metadata_bad_order_settings1.xml b/tests/data/metadata/metadata_bad_order_settings1.xml new file mode 100644 index 00000000..1d1ff9d2 --- /dev/null +++ b/tests/data/metadata/metadata_bad_order_settings1.xml @@ -0,0 +1,14 @@ + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified + + + + From 4ca42012124dcc361911086bf39b00aeb6f83e61 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 2 Feb 2015 22:16:50 +0100 Subject: [PATCH 010/352] Added SLO with nameID and SessionIndex in the demos --- demo-django/demo/views.py | 11 ++++++++++- demo-flask/index.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index 8ee528c6..0c7938e6 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -40,13 +40,22 @@ def index(request): return_to = OneLogin_Saml2_Utils.get_self_url(req) + reverse('attrs') return HttpResponseRedirect(auth.login(return_to)) elif 'slo' in req['get_data']: - return HttpResponseRedirect(auth.logout()) + name_id = None + session_index = None + if 'samlNameId' in request.session: + name_id = request.session['samlNameId'] + if 'samlSessionIndex' in request.session: + session_index = request.session['samlSessionIndex'] + + return HttpResponseRedirect(auth.logout(name_id=name_id, session_index=session_index)) elif 'acs' in req['get_data']: auth.process_response() errors = auth.get_errors() not_auth_warn = not auth.is_authenticated() if not errors: request.session['samlUserdata'] = auth.get_attributes() + request.session['samlNameId'] = auth.get_nameid() + request.session['samlSessionIndex'] = auth.get_session_index() if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState'])) elif 'sls' in req['get_data']: diff --git a/demo-flask/index.py b/demo-flask/index.py index aa815144..fd1e2ccb 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -46,13 +46,22 @@ def index(): return_to = '%sattrs/' % request.host_url return redirect(auth.login(return_to)) elif 'slo' in request.args: - return redirect(auth.logout()) + name_id = None + session_index = None + if 'samlNameId' in request.session: + name_id = request.session['samlNameId'] + if 'samlSessionIndex' in request.session: + session_index = request.session['samlSessionIndex'] + + return redirect(auth.logout(name_id=name_id, session_index=session_index)) elif 'acs' in request.args: auth.process_response() errors = auth.get_errors() not_auth_warn = not auth.is_authenticated() if len(errors) == 0: session['samlUserdata'] = auth.get_attributes() + request.session['samlNameId'] = auth.get_nameid() + request.session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.form and self_url != request.form['RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) From 30ea4bf60c34c670df69dcc4eeb2827d0baf8beb Mon Sep 17 00:00:00 2001 From: Andrey Chagochkin Date: Mon, 9 Feb 2015 11:58:25 +0500 Subject: [PATCH 011/352] Bugfix --- src/onelogin/saml2/logout_request.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 85b66713..06efd540 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -278,7 +278,14 @@ def is_valid(self, request_data): destination = dom.get('Destination') if destination != '': if current_url not in destination: - raise Exception('The LogoutRequest was received at $currentURL instead of $destination') + raise Exception( + 'The LogoutRequest was received at ' + '%(currentURL)s instead of %(destination)s' % + { + 'currentURL': current_url, + 'destination': destination, + } + ) # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom) From a48bd221a0d1adb4e0a9ee619c18444b9958854d Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 18 Feb 2015 14:57:04 +0100 Subject: [PATCH 012/352] Fix #49 --- demo-flask/index.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/demo-flask/index.py b/demo-flask/index.py index fd1e2ccb..5121b044 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -48,10 +48,10 @@ def index(): elif 'slo' in request.args: name_id = None session_index = None - if 'samlNameId' in request.session: - name_id = request.session['samlNameId'] - if 'samlSessionIndex' in request.session: - session_index = request.session['samlSessionIndex'] + if 'samlNameId' in session: + name_id = session['samlNameId'] + if 'samlSessionIndex' in session: + session_index = session['samlSessionIndex'] return redirect(auth.logout(name_id=name_id, session_index=session_index)) elif 'acs' in request.args: @@ -60,8 +60,8 @@ def index(): not_auth_warn = not auth.is_authenticated() if len(errors) == 0: session['samlUserdata'] = auth.get_attributes() - request.session['samlNameId'] = auth.get_nameid() - request.session['samlSessionIndex'] = auth.get_session_index() + session['samlNameId'] = auth.get_nameid() + session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.form and self_url != request.form['RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) From 9f5cfdb0d6291c10f57ded89ae5ed7b7c9096400 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 26 Feb 2015 19:57:41 +0100 Subject: [PATCH 013/352] Release the 2.1.1 version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 43396b2e..c6405a8f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.1.0', + version='2.1.1', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', From 984365d1551004ac1e1d746de128a67472fdabd6 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 26 Feb 2015 20:54:14 +0100 Subject: [PATCH 014/352] I had a pypi error, so I need to update the version of the python-saml toolkit --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c6405a8f..e6ebaf5e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.1.1', + version='2.1.2', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', From ae9ff12a5e28ffe6b0da957516906d06367103c8 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 5 Mar 2015 03:24:27 +0100 Subject: [PATCH 015/352] Fix #50. Do accesible the ID of the object Logout Request (id attribute) --- README.md | 6 +++--- src/onelogin/saml2/auth.py | 2 +- src/onelogin/saml2/logout_request.py | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index de4e7519..86270ccf 100644 --- a/README.md +++ b/README.md @@ -591,7 +591,7 @@ else: if not keep_local_session: OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) - in_response_to = OneLogin_Saml2_Logout_Request.get_id(request) + in_response_to = request.id response_builder = OneLogin_Saml2_Logout_Response(self.__settings) response_builder.build(in_response_to) logout_response = response_builder.get_response() @@ -748,7 +748,7 @@ SAML 2 Logout Request class * `__init__` Constructs the Logout Request object. * ***get_request*** Returns the Logout Request defated, base64encoded. -* ***get_id*** Returns the ID of the Logout Request. +* ***get_id*** Returns the ID of the Logout Request. (If you have the object you can access to the id attribute) * ***get_nameid_data*** Gets the NameID Data of the the Logout Request (returns a dict). * ***get_nameid*** Gets the NameID of the Logout Request Message (returns a string). * ***get_issuer*** Gets the Issuer of the Logout Request Message. @@ -875,7 +875,7 @@ toolkit on it in development mode executing this: Using this method of deployment the toolkit files will be linked instead of copied, so if you make changes on them you won't need to reinstall the toolkit. -If you want install it in a nomal mode, execute: +If you want install it in a normal mode, execute: ``` python setup.py install ``` diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 4be7e87a..914caada 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -141,7 +141,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ if not keep_local_session: OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) - in_response_to = OneLogin_Saml2_Logout_Request.get_id(OneLogin_Saml2_Utils.decode_base64_and_inflate(self.__request_data['get_data']['SAMLRequest'])) + in_response_to = logout_request.id response_builder = OneLogin_Saml2_Logout_Response(self.__settings) response_builder.build(in_response_to) logout_response = response_builder.get_response() diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 06efd540..92af8d7b 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -47,6 +47,7 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): """ self.__settings = settings self.__error = None + self.id = None if request is None: sp_data = self.__settings.get_sp_data() @@ -54,6 +55,8 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): security = self.__settings.get_security_data() uid = OneLogin_Saml2_Utils.generate_unique_id() + self.id = uid + issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) cert = None @@ -105,6 +108,7 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): logout_request = inflated except Exception: logout_request = decoded + self.id = self.get_id(logout_request) self.__logout_request = logout_request From 698d8ae8defbc2d4f58a349512ac58ed1e36276d Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 5 Mar 2015 03:44:05 +0100 Subject: [PATCH 016/352] Fix #43. Add SAMLServiceProviderBackend reference to the README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 86270ccf..8d642a89 100644 --- a/README.md +++ b/README.md @@ -969,6 +969,8 @@ You'll have the demo running at http://localhost:8000. Note that many of the configuration files expect HTTPS. This is not required by the demo, as replacing these SP URLs with HTTP will work just fine. HTTPS is however highly encouraged, and left as an exercise for the reader for their specific needs. +If you want to integrate a production django application, take a look on this SAMLServiceProviderBackend that uses our toolkit to add SAML support: https://github.com/KristianOellegaard/django-saml-service-provider + ####Content#### The django project contains: From 5226956d1fc7f6990960b32dfa5635b870881145 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 6 Mar 2015 12:31:00 +0100 Subject: [PATCH 017/352] Solve HTTPs issue on demos --- demo-django/demo/views.py | 2 ++ demo-flask/index.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index 0c7938e6..b67c5d99 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -15,7 +15,9 @@ def init_saml_auth(req): def prepare_django_request(request): + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields result = { + 'https': 'on' if request.is_secure() else 'off', 'http_host': request.META['HTTP_HOST'], 'script_name': request.META['PATH_INFO'], 'server_port': request.META['SERVER_PORT'], diff --git a/demo-flask/index.py b/demo-flask/index.py index 5121b044..8aa8c764 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -20,8 +20,10 @@ def init_saml_auth(req): def prepare_flask_request(request): + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields url_data = urlparse(request.url) return { + 'https': 'on' if request.scheme == 'https' else 'off', 'http_host': request.host, 'server_port': url_data.port, 'script_name': request.path, @@ -121,4 +123,5 @@ def metadata(): if __name__ == "__main__": - app.run(host='0.0.0.0', port=8000, debug=True) + ssl_context = ('/home/pitbulk/proyectos/python-saml/demo-flask/saml/certs/sp.crt', '/home/pitbulk/proyectos/python-saml/demo-flask/saml/certs/sp.key') + app.run(host='0.0.0.0', port=444, debug=True, ssl_context=ssl_context) From 59c0182c0760036b37130280cfc71d55f0d594ef Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 6 Mar 2015 12:34:21 +0100 Subject: [PATCH 018/352] Remove custom deploy settings --- demo-flask/index.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/demo-flask/index.py b/demo-flask/index.py index 8aa8c764..a2eb1cb1 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -123,5 +123,4 @@ def metadata(): if __name__ == "__main__": - ssl_context = ('/home/pitbulk/proyectos/python-saml/demo-flask/saml/certs/sp.crt', '/home/pitbulk/proyectos/python-saml/demo-flask/saml/certs/sp.key') - app.run(host='0.0.0.0', port=444, debug=True, ssl_context=ssl_context) + app.run(host='0.0.0.0', port=8000, debug=True) From 7a502199c822fb8876af9561f6339fd6f421bb16 Mon Sep 17 00:00:00 2001 From: ThiefMaster Date: Tue, 10 Mar 2015 23:18:28 +0100 Subject: [PATCH 019/352] Fix PHP-style array element in settings json --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d642a89..25f8f062 100644 --- a/README.md +++ b/README.md @@ -221,8 +221,8 @@ This is the settings.json file: "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", // Usually x509cert and privateKey of the SP are provided by files placed at // the certs folder. But we can also provide them with the following parameters - 'x509cert' => '', - 'privateKey' => '' + "x509cert": "", + "privateKey": "" }, // Identity Provider Data that we want connected with our SP. From fa67f9ec8c5a7197cc7191c706c37b1492afea61 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 14 Mar 2015 01:33:01 +0100 Subject: [PATCH 020/352] Minor typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25f8f062..f390e613 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ In addition to the required settings data (idp, sp), there is extra information // Set to false and no AuthContext will be sent in the AuthNRequest, // Set true or don't present thi parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' // Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'), - 'requestedAuthnContext' => true, + 'requestedAuthnContext': true, }, // Contact information template, it is recommended to suply a From b7c3d90d8ae8a6bf6fe92836cbbe0d824813eb92 Mon Sep 17 00:00:00 2001 From: Changje Jeong Date: Thu, 2 Apr 2015 02:49:23 +0900 Subject: [PATCH 021/352] Implement demo-bottle --- demo-bottle/index.py | 177 ++++++++++++++++++++++++ demo-bottle/requirements.txt | 4 + demo-bottle/saml/advanced_settings.json | 29 ++++ demo-bottle/saml/certs/README | 11 ++ demo-bottle/saml/settings.json | 30 ++++ demo-bottle/templates/attrs.html | 31 +++++ demo-bottle/templates/base.html | 26 ++++ demo-bottle/templates/index.html | 49 +++++++ 8 files changed, 357 insertions(+) create mode 100644 demo-bottle/index.py create mode 100644 demo-bottle/requirements.txt create mode 100644 demo-bottle/saml/advanced_settings.json create mode 100644 demo-bottle/saml/certs/README create mode 100644 demo-bottle/saml/settings.json create mode 100644 demo-bottle/templates/attrs.html create mode 100644 demo-bottle/templates/base.html create mode 100644 demo-bottle/templates/index.html diff --git a/demo-bottle/index.py b/demo-bottle/index.py new file mode 100644 index 00000000..aa23e864 --- /dev/null +++ b/demo-bottle/index.py @@ -0,0 +1,177 @@ +import os + +from bottle import Bottle, run, redirect, request, response, ServerAdapter, jinja2_view +from beaker.middleware import SessionMiddleware + +from urlparse import urlparse + +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.utils import OneLogin_Saml2_Utils + + +app = Bottle(__name__) +app.config['SECRET_KEY'] = 'onelogindemopytoolkit' +app.config['SAML_PATH'] = os.path.join(os.path.dirname(__file__), 'saml') + + +session_opts = { + 'session.type': 'file', + 'session.cookie_expires': 300, + 'session.data_dir': './.data', + 'session.auto': True +} + + +def init_saml_auth(req): + auth = OneLogin_Saml2_Auth(req, custom_base_path=app.config['SAML_PATH']) + return auth + + +def prepare_bottle_request(req): + url_data = urlparse(req.url) + return { + 'http_host': req.get_header('host'), + 'server_port': url_data.port, + 'script_name': req.fullpath, + 'get_data': req.query, + 'post_data': req.forms, + 'https': 'on' if req.urlparts.scheme == 'https' else 'off' + } + + +@app.route('/acs', method='POST') +@jinja2_view('index.html', template_lookup=['templates']) +def index(): + req = prepare_bottle_request(request) + auth = init_saml_auth(req) + paint_logout = False + attributes = False + + session = request.environ['beaker.session'] + + auth.process_response() + errors = auth.get_errors() + not_auth_warn = not auth.is_authenticated() + if len(errors) == 0: + session['samlUserdata'] = auth.get_attributes() + session['samlNameId'] = auth.get_nameid() + session['samlSessionIndex'] = auth.get_session_index() + self_url = OneLogin_Saml2_Utils.get_self_url(req) + if 'RelayState' in request.forms and self_url != request.forms['RelayState']: + return redirect(request.forms['RelayState']) + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return { + 'errors':errors, + 'not_auth_warn':not_auth_warn, + 'attributes':attributes, + 'paint_logout':paint_logout + } + + +@app.route('/', method='GET') +@jinja2_view('index.html', template_lookup=['templates']) +def index(): + req = prepare_bottle_request(request) + auth = init_saml_auth(req) + errors = [] + not_auth_warn = False + success_slo = False + attributes = False + paint_logout = False + + session = request.environ['beaker.session'] + + if 'sso' in request.query: + return_to = '{0}://{1}/'.format(request.urlparts.scheme, request.get_header('host')) + return redirect(auth.login(return_to)) + elif 'sso2' in request.query: + return_to = '{0}://{1}/attrs/'.format(request.urlparts.scheme, request.get_header('host')) + return redirect(auth.login(return_to)) + elif 'slo' in request.query: + name_id = None + session_index = None + if 'samlNameId' in session: + name_id = session['samlNameId'] + if 'samlSessionIndex' in session: + session_index = session['samlSessionIndex'] + + return redirect(auth.logout(name_id=name_id, session_index=session_index)) + elif 'sls' in request.query: + dscb = lambda: session.clear() + url = auth.process_slo(delete_session_cb=dscb) + errors = auth.get_errors() + if len(errors) == 0: + if url is not None: + return redirect(url) + else: + success_slo = True + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return { + 'errors':errors, + 'not_auth_warn':not_auth_warn, + 'success_slo':success_slo, + 'attributes':attributes, + 'paint_logout':paint_logout + } + + +@app.route('/attrs/') +@jinja2_view('attrs.html', template_lookup=['templates']) +def attrs(): + paint_logout = False + attributes = False + session = request.environ['beaker.session'] + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return {'paint_logout':paint_logout, + 'attributes':attributes} + + +@app.route('/metadata/') +def metadata(): + req = prepare_bottle_request(request) + auth = init_saml_auth(req) + settings = auth.get_settings() + metadata = settings.get_sp_metadata() + errors = settings.validate_metadata(metadata) + + if len(errors) == 0: + response.status = 200 + response.set_header('Content-Type', 'text/xml') + return metadata + else: + response.status = 500 + return ','.join(errors) + + +class SSLPasteServer(ServerAdapter): + def run(self, handler): + from paste import httpserver + + server = httpserver.serve(handler, '0.0.0.0', '8000', ssl_pem='local.pem', start_loop=False) + try: + server.serve_forever() + finally: + server.server_close() + + +if __name__ == "__main__": + # To run HTTPS + #run(SessionMiddleware(app, config=session_opts), host='0.0.0.0', port=8000, debug=True, reloader=True, server=SSLPasteServer) + + # To run HTTP + run(SessionMiddleware(app, config=session_opts), host='0.0.0.0', port=8000, debug=True, reloader=True, server='paste') diff --git a/demo-bottle/requirements.txt b/demo-bottle/requirements.txt new file mode 100644 index 00000000..b97d5eeb --- /dev/null +++ b/demo-bottle/requirements.txt @@ -0,0 +1,4 @@ +bottle==0.12.8 +beaker==1.6.4 +paste==1.7.5.1 +jinja2==2.7.3 diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json new file mode 100644 index 00000000..e336fe9d --- /dev/null +++ b/demo-bottle/saml/advanced_settings.json @@ -0,0 +1,29 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": false, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": false, + "wantAssertionsSigned": false, + "wantNameIdEncrypted": false + }, + "contactPerson": { + "technical": { + "givenName": "technical_name", + "emailAddress": "technical@example.com" + }, + "support": { + "givenName": "support_name", + "emailAddress": "support@example.com" + } + }, + "organization": { + "en-US": { + "name": "sp_test", + "displayname": "SP test", + "url": "http://sp.example.com" + } + } +} \ No newline at end of file diff --git a/demo-bottle/saml/certs/README b/demo-bottle/saml/certs/README new file mode 100644 index 00000000..bcb87f11 --- /dev/null +++ b/demo-bottle/saml/certs/README @@ -0,0 +1,11 @@ +Take care of this folder that could contain private key. Be sure that this folder never is published. + +Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: + + * sp.key Private Key + * sp.cert Public cert + +Also you can use other cert to sign the metadata of the SP using the: + + * metadata.key + * metadata.cert diff --git a/demo-bottle/saml/settings.json b/demo-bottle/saml/settings.json new file mode 100644 index 00000000..142911f1 --- /dev/null +++ b/demo-bottle/saml/settings.json @@ -0,0 +1,30 @@ +{ + "strict": true, + "debug": true, + "sp": { + "entityId": "https:///metadata/", + "assertionConsumerService": { + "url": "https:///?acs", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + "singleLogoutService": { + "url": "https:///?sls", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", + "x509cert": "", + "privateKey": "" + }, + "idp": { + "entityId": "https://app.onelogin.com/saml/metadata/", + "singleSignOnService": { + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "singleLogoutService": { + "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "" + } +} diff --git a/demo-bottle/templates/attrs.html b/demo-bottle/templates/attrs.html new file mode 100644 index 00000000..6c60f4c3 --- /dev/null +++ b/demo-bottle/templates/attrs.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% block content %} + +{% if paint_logout %} + {% if attributes %} +

You have the following attributes:

+

Parameters:return_to (string) – Optional argument. The target URL the user should be redirected to after login.
Parameters:
    +
  • return_to (string) – Optional argument. The target URL the user should be redirected to after login.
  • +
  • force_authn (bool) – Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'.
  • +
  • is_passive (bool) – Optional argument. When true the AuthNReuqest will set the Ispassive='true'.
  • +
+
Returns:Redirection url
+ + + + + {% for attr in attributes %} + + + {% endfor %} + +
NameValues
{{ attr.0 }}
    + {% for val in attr.1 %} +
  • {{ val }}
  • + {% endfor %} +
+ {% else %} + + {% endif %} + Logout +{% else %} + Login and access again to this page +{% endif %} + +{% endblock %} diff --git a/demo-bottle/templates/base.html b/demo-bottle/templates/base.html new file mode 100644 index 00000000..a55dbf0b --- /dev/null +++ b/demo-bottle/templates/base.html @@ -0,0 +1,26 @@ + + + + + + + + A Python SAML Toolkit by OneLogin demo + + + + + + + + +
+

A Python SAML Toolkit by OneLogin demo

+ + {% block content %}{% endblock %} +
+ + diff --git a/demo-bottle/templates/index.html b/demo-bottle/templates/index.html new file mode 100644 index 00000000..c7425c6e --- /dev/null +++ b/demo-bottle/templates/index.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block content %} + +{% if errors %} + +{% endif %} + +{% if not_auth_warn %} + +{% endif %} + +{% if success_slo %} + +{% endif %} + +{% if paint_logout %} + {% if attributes %} + + + + + + {% for attr in attributes %} + + + {% endfor %} + +
NameValues
{{ attr.0 }}
    + {% for val in attr.1 %} +
  • {{ val }}
  • + {% endfor %} +
+ {% else %} + + {% endif %} + Logout +{% else %} + Login Login and access to attrs page +{% endif %} + +{% endblock %} From 98c45f31bec3a717a691070d3ac7791680d18844 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 9 Apr 2015 15:15:07 +0200 Subject: [PATCH 022/352] Add fingerprint algorithm support. Previously the toolkit assumed SHA1 algorithm as the algorithm used to generate the fingerprint. Now you can set the 'certFingerprintAlgorithm' parameter and define it --- README.md | 13 +++++--- src/onelogin/saml2/response.py | 3 +- src/onelogin/saml2/settings.py | 2 ++ src/onelogin/saml2/utils.py | 32 ++++++++++++++----- tests/src/OneLogin/saml2_tests/auth_test.py | 8 ++--- .../src/OneLogin/saml2_tests/response_test.py | 23 +++++++++++-- tests/src/OneLogin/saml2_tests/utils_test.py | 10 ++++++ 7 files changed, 71 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f390e613..2fe50fe0 100644 --- a/README.md +++ b/README.md @@ -250,12 +250,17 @@ This is the settings.json file: }, // Public x509 certificate of the IdP "x509cert": "" - /* + /* * Instead of use the whole x509cert you can use a fingerprint - * (openssl x509 -noout -fingerprint -in "idp.crt" to generate it) + * (openssl x509 -noout -fingerprint -in "idp.crt" to generate it, + * or add for example the -sha256 , -sha384 or -sha512 parameter) + * + * If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to + * let the toolkit know which algorithm was used. Possible values: sha1, sha256, sha384 or sha512 + * 'sha1' is the default value. */ - // "certFingerprint": "" - + // 'certFingerprint' => '', + // 'certFingerprintAlgorithm' => 'sha1', } } ``` diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index ba7e156c..c41e3cf0 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -196,6 +196,7 @@ def is_valid(self, request_data, request_id=None): if len(signed_elements) > 0: cert = idp_data.get('x509cert', None) fingerprint = idp_data.get('certFingerprint', None) + fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) # Only validates the first sign found if '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements: @@ -205,7 +206,7 @@ def is_valid(self, request_data, request_id=None): document_to_validate = self.decrypted_document else: document_to_validate = self.document - if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint): + if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg): raise Exception('Signature validation failed. SAML Response rejected') else: raise Exception('No Signature found. SAML Response rejected') diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 5f1e9208..213d078e 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -292,6 +292,8 @@ def __add_default_values(self): self.__idp['x509cert'] = '' if 'certFingerprint' not in self.__idp: self.__idp['certFingerprint'] = '' + if 'certFingerprintAlgorithm' not in self.__idp: + self.__idp['certFingerprintAlgorithm'] = 'sha1' if 'x509cert' not in self.__sp: self.__sp['x509cert'] = '' diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index cc284620..d14a2e7f 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -12,7 +12,7 @@ import base64 from datetime import datetime import calendar -from hashlib import sha1 +from hashlib import sha1, sha256, sha384, sha512 from isodate import parse_duration as duration_parser from lxml import etree from defusedxml.lxml import tostring, fromstring @@ -522,14 +522,17 @@ def delete_local_session(callback=None): callback() @staticmethod - def calculate_x509_fingerprint(x509_cert): + def calculate_x509_fingerprint(x509_cert, alg='sha1'): """ Calculates the fingerprint of a x509cert. :param x509_cert: x509 cert :type: string - :returns: Formated fingerprint + :param alg: The algorithm to build the fingerprint + :type: string + + :returns: fingerprint :rtype: string """ assert isinstance(x509_cert, basestring) @@ -552,9 +555,19 @@ def calculate_x509_fingerprint(x509_cert): else: # Append the current line to the certificate data. data += line - # "data" now contains the certificate as a base64-encoded string. The - # fingerprint of the certificate is the sha1-hash of the certificate. - return sha1(base64.b64decode(data)).hexdigest().lower() + + decoded_data = base64.b64decode(data) + + if alg == 'sha512': + fingerprint = sha512(decoded_data) + elif alg == 'sha384': + fingerprint = sha384(decoded_data) + elif alg == 'sha256': + fingerprint = sha256(decoded_data) + else: + fingerprint = sha1(decoded_data) + + return fingerprint.hexdigest().lower() @staticmethod def format_finger_print(fingerprint): @@ -837,7 +850,7 @@ def add_sign(xml, key, cert, debug=False): return newdoc.saveXML(newdoc.firstChild) @staticmethod - def validate_sign(xml, cert=None, fingerprint=None, validatecert=False, debug=False): + def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): """ Validates a signature (Message or Assertion). @@ -850,6 +863,9 @@ def validate_sign(xml, cert=None, fingerprint=None, validatecert=False, debug=Fa :param fingerprint: The fingerprint of the public cert :type: string + :param fingerprintalg: The algorithm used to build the fingerprint + :type: string + :param validatecert: If true, will verify the signature and if the cert is valid. :type: bool @@ -899,7 +915,7 @@ def validate_sign(xml, cert=None, fingerprint=None, validatecert=False, debug=Fa 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) + 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) diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index cf9eb648..c74371e9 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -384,7 +384,7 @@ def testProcessSLORequestInvalidValid(self): slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url) self.assertIn('SAMLResponse', parsed_query) - self.assertNotIn('RelayState', parsed_query) + #self.assertNotIn('RelayState', parsed_query) auth.set_strict(True) auth.process_slo(True) @@ -398,7 +398,7 @@ def testProcessSLORequestInvalidValid(self): slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url_2) self.assertIn('SAMLResponse', parsed_query_2) - self.assertNotIn('RelayState', parsed_query_2) + #self.assertNotIn('RelayState', parsed_query_2) def testProcessSLORequestNotOnOrAfterFailed(self): """ @@ -447,7 +447,7 @@ def testProcessSLORequestDeletingSession(self): slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url) self.assertIn('SAMLResponse', parsed_query) - self.assertNotIn('RelayState', parsed_query) + #self.assertNotIn('RelayState', parsed_query) # FIXME // Session is not alive # $this->assertFalse(isset($_SESSION['samltest'])); @@ -461,7 +461,7 @@ def testProcessSLORequestDeletingSession(self): slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url_2) self.assertIn('SAMLResponse', parsed_query_2) - self.assertNotIn('RelayState', parsed_query_2) + #self.assertNotIn('RelayState', parsed_query_2) # FIXME // Session is alive # $this->assertTrue(isset($_SESSION['samltest'])); diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 716aec18..b0fff08c 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -967,12 +967,29 @@ def testIsValid2(self): response_2 = OneLogin_Saml2_Response(settings_2, xml_2) self.assertTrue(response_2.is_valid(self.get_request_data())) - settings_info_2['idp']['certFingerprint'] = OneLogin_Saml2_Utils.calculate_x509_fingerprint(settings_info_2['idp']['x509cert']) - settings_info_2['idp']['x509cert'] = '' - settings_3 = OneLogin_Saml2_Settings(settings_info_2) + settings_info_3 = self.loadSettingsJSON('settings2.json') + idp_cert = settings_info_3['idp']['x509cert']; + settings_info_3['idp']['certFingerprint'] = OneLogin_Saml2_Utils.calculate_x509_fingerprint(idp_cert) + settings_info_3['idp']['x509cert'] = '' + settings_3 = OneLogin_Saml2_Settings(settings_info_3) response_3 = OneLogin_Saml2_Response(settings_3, xml_2) self.assertTrue(response_3.is_valid(self.get_request_data())) + settings_info_3['idp']['certFingerprintAlgorithm'] = 'sha1' + settings_4 = OneLogin_Saml2_Settings(settings_info_3) + response_4 = OneLogin_Saml2_Response(settings_4, xml_2) + self.assertTrue(response_4.is_valid(self.get_request_data())) + + settings_info_3['idp']['certFingerprintAlgorithm'] = 'sha256' + settings_5 = OneLogin_Saml2_Settings(settings_info_3) + response_5 = OneLogin_Saml2_Response(settings_5, xml_2) + self.assertFalse(response_5.is_valid(self.get_request_data())) + + settings_info_3['idp']['certFingerprint'] = OneLogin_Saml2_Utils.calculate_x509_fingerprint(idp_cert, 'sha256') + settings_6 = OneLogin_Saml2_Settings(settings_info_3) + response_6 = OneLogin_Saml2_Response(settings_6, xml_2) + self.assertTrue(response_6.is_valid(self.get_request_data())) + def testIsValidEnc(self): """ Tests the is_valid method of the OneLogin_Saml2_Response diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 409c280e..bb95cd02 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -589,6 +589,13 @@ def testCalculateX509Fingerprint(self): self.assertEqual(None, OneLogin_Saml2_Utils.calculate_x509_fingerprint(key)) self.assertEqual('afe71c28ef740bc87425be13a2263d37971da1f9', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert)) + self.assertEqual('afe71c28ef740bc87425be13a2263d37971da1f9', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert, 'sha1')) + + self.assertEqual('c51cfa06c7a49767f6eab18238eae1c56708e29264da3d11f538a12cd2c357ba', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert, 'sha256')) + + self.assertEqual('bc5826e6f9429247254bae5e3c650e6968a36a62d23075eb168134978d88600559c10830c28711b2c29c7947c0c2eb1d', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert, 'sha384')) + + self.assertEqual('3db29251b97559c67988ea0754cb0573fc409b6f75d89282d57cfb75089539b0bbdb2dcd9ec6e032549ecbc466439d5992e18db2cf5494ca2fe1b2e16f348dff', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert, 'sha512')) def testDeleteLocalSession(self): """ @@ -779,6 +786,7 @@ def testValidateSign(self): idp_data2 = settings_2.get_idp_data() cert_2 = idp_data2['x509cert'] fingerprint_2 = OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert_2) + fingerprint_2_256 = OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert_2, 'sha256') try: self.assertFalse(OneLogin_Saml2_Utils.validate_sign('', cert)) @@ -818,6 +826,8 @@ def testValidateSign(self): xml_response_msg_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_message_response2.xml.base64'))) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed_2, cert_2)) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed_2, None, fingerprint_2)) + self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed_2, None, fingerprint_2, 'sha1')) + self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed_2, None, fingerprint_2_256, 'sha256')) xml_response_assert_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response.xml.base64'))) From c44971e331c0acb3d9b0d74b3ff8c1fffb762012 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 9 Apr 2015 15:43:36 +0200 Subject: [PATCH 023/352] pep8 --- tests/src/OneLogin/saml2_tests/auth_test.py | 8 ++++---- tests/src/OneLogin/saml2_tests/response_test.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index c74371e9..ea0b9fd0 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -384,7 +384,7 @@ def testProcessSLORequestInvalidValid(self): slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url) self.assertIn('SAMLResponse', parsed_query) - #self.assertNotIn('RelayState', parsed_query) + # self.assertNotIn('RelayState', parsed_query) auth.set_strict(True) auth.process_slo(True) @@ -398,7 +398,7 @@ def testProcessSLORequestInvalidValid(self): slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url_2) self.assertIn('SAMLResponse', parsed_query_2) - #self.assertNotIn('RelayState', parsed_query_2) + # self.assertNotIn('RelayState', parsed_query_2) def testProcessSLORequestNotOnOrAfterFailed(self): """ @@ -447,7 +447,7 @@ def testProcessSLORequestDeletingSession(self): slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url) self.assertIn('SAMLResponse', parsed_query) - #self.assertNotIn('RelayState', parsed_query) + # self.assertNotIn('RelayState', parsed_query) # FIXME // Session is not alive # $this->assertFalse(isset($_SESSION['samltest'])); @@ -461,7 +461,7 @@ def testProcessSLORequestDeletingSession(self): slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertIn(slo_url, target_url_2) self.assertIn('SAMLResponse', parsed_query_2) - #self.assertNotIn('RelayState', parsed_query_2) + # self.assertNotIn('RelayState', parsed_query_2) # FIXME // Session is alive # $this->assertTrue(isset($_SESSION['samltest'])); diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index b0fff08c..9ed46dd8 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -968,7 +968,7 @@ def testIsValid2(self): self.assertTrue(response_2.is_valid(self.get_request_data())) settings_info_3 = self.loadSettingsJSON('settings2.json') - idp_cert = settings_info_3['idp']['x509cert']; + idp_cert = settings_info_3['idp']['x509cert'] settings_info_3['idp']['certFingerprint'] = OneLogin_Saml2_Utils.calculate_x509_fingerprint(idp_cert) settings_info_3['idp']['x509cert'] = '' settings_3 = OneLogin_Saml2_Settings(settings_info_3) From b1234fd597876247944234a8179bbd03c06edf76 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Sun, 3 May 2015 18:25:17 -0700 Subject: [PATCH 024/352] Fix creation of metadata with no SLS, when using settings.get_sp_metadata() --- src/onelogin/saml2/metadata.py | 2 +- tests/src/OneLogin/saml2_tests/metadata_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 2dcb3efc..3703e0d9 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -74,7 +74,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N organization = {} sls = '' - if 'singleLogoutService' in sp: + if 'singleLogoutService' in sp and 'url' in sp['singleLogoutService']: sls = """ \n""" % \ { diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 37af3589..8ef067a5 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -64,7 +64,7 @@ def testBuilder(self): security['authnRequestsSigned'] = True security['wantAssertionsSigned'] = True - del sp_data['singleLogoutService'] + del sp_data['singleLogoutService']['url'] metadata2 = OneLogin_Saml2_Metadata.builder( sp_data, security['authnRequestsSigned'], From 58605264b15ea707f6fb283e44fba2e05459c1f4 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 5 May 2015 14:06:05 -0700 Subject: [PATCH 025/352] Allow configuration of metadata caching/expiry via settings --- src/onelogin/saml2/metadata.py | 27 +++++++++------- src/onelogin/saml2/settings.py | 10 +++++- .../src/OneLogin/saml2_tests/metadata_test.py | 32 +++++++++++++++++-- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 2dcb3efc..7d6c5ff8 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -41,11 +41,11 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N :param wsign: wantAssertionsSigned attribute :type wsign: string - :param valid_until: Metadata's valid time - :type valid_until: string|DateTime + :param valid_until: Metadata's expiry date + :type valid_until: string|DateTime|Timestamp :param cache_duration: Duration of the cache in seconds - :type cache_duration: string|Timestamp + :type cache_duration: int|string :param contacts: Contacts info :type contacts: dict @@ -56,15 +56,18 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N if valid_until is None: valid_until = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_VALID if not isinstance(valid_until, basestring): - valid_until_time = gmtime(valid_until) - valid_until_time = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time) + if isinstance(valid_until, datetime): + valid_until_time = valid_until + else: + valid_until_time = gmtime(valid_until) + valid_until_str = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time) else: - valid_until_time = valid_until + valid_until_str = valid_until if cache_duration is None: - cache_duration = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_CACHED + cache_duration = OneLogin_Saml2_Metadata.TIME_CACHED if not isinstance(cache_duration, basestring): - cache_duration_str = 'PT%sS' % cache_duration + cache_duration_str = 'PT%sS' % cache_duration # 'P'eriod of 'T'ime x 'S'econds else: cache_duration_str = cache_duration @@ -121,8 +124,8 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N metadata = """ %(sls)s %(name_id_format)s @@ -134,8 +137,8 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N %(contacts)s """ % \ { - 'valid': valid_until_time, - 'cache': cache_duration_str, + 'valid': ('validUntil="%s"' % valid_until_str) if valid_until_str else '', + 'cache': ('cacheDuration="%s"' % cache_duration_str) if cache_duration_str else '', 'entity_id': sp['entityId'], 'authnsign': str_authnsign, 'wsign': str_wsign, diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 213d078e..0e8a7487 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -266,6 +266,12 @@ def __add_default_values(self): if 'nameIdEncrypted' not in self.__security: self.__security['nameIdEncrypted'] = False + # Metadata format + if 'metadataValidUntil' not in self.__security.keys(): + self.__security['metadataValidUntil'] = None # None means use default + if 'metadataCacheDuration' not in self.__security.keys(): + self.__security['metadataCacheDuration'] = None # None means use default + # Sign provided if 'authnRequestsSigned' not in self.__security.keys(): self.__security['authnRequestsSigned'] = False @@ -548,7 +554,9 @@ def get_sp_metadata(self): """ metadata = OneLogin_Saml2_Metadata.builder( self.__sp, self.__security['authnRequestsSigned'], - self.__security['wantAssertionsSigned'], None, None, + self.__security['wantAssertionsSigned'], + self.__security['metadataValidUntil'], + self.__security['metadataCacheDuration'], self.get_contacts(), self.get_organization() ) cert = self.get_sp_cert() diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 37af3589..218fa738 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -84,15 +84,43 @@ def testBuilder(self): sp_data, security['authnRequestsSigned'], security['wantAssertionsSigned'], '2014-10-01T11:04:29Z', - 'PT1412593469S', + 'P1Y', contacts, organization ) self.assertIsNotNone(metadata3) self.assertIn(' Date: Thu, 7 May 2015 19:49:33 -0700 Subject: [PATCH 026/352] Allow metadata signing with SP key specified as config value, not file --- src/onelogin/saml2/settings.py | 58 +++++++++++-------- .../src/OneLogin/saml2_tests/settings_test.py | 24 +++++++- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 213d078e..b6317450 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -557,9 +557,21 @@ def get_sp_metadata(self): # Sign metadata if 'signMetadata' in self.__security and self.__security['signMetadata'] is not False: if self.__security['signMetadata'] is True: - key_file_name = 'sp.key' - cert_file_name = 'sp.crt' + # Use the SP's normal key to sign the metadata: + if not cert: + raise OneLogin_Saml2_Error( + 'Cannot sign metadata: missing SP public key certificate.', + OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND + ) + cert_metadata = cert + key_metadata = self.get_sp_key() + if not key_metadata: + raise OneLogin_Saml2_Error( + 'Cannot sign metadata: missing SP private key.', + OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND + ) else: + # Use a custom key to sign the metadata: if ('keyFileName' not in self.__security['signMetadata'] or 'certFileName' not in self.__security['signMetadata']): raise OneLogin_Saml2_Error( @@ -568,30 +580,28 @@ def get_sp_metadata(self): ) key_file_name = self.__security['signMetadata']['keyFileName'] cert_file_name = self.__security['signMetadata']['certFileName'] - key_metadata_file = self.__paths['cert'] + key_file_name - cert_metadata_file = self.__paths['cert'] + cert_file_name - - if not exists(key_metadata_file): - raise OneLogin_Saml2_Error( - 'Private key file not found: %s', - OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND, - key_metadata_file - ) + key_metadata_file = self.__paths['cert'] + key_file_name + cert_metadata_file = self.__paths['cert'] + cert_file_name - if not exists(cert_metadata_file): - raise OneLogin_Saml2_Error( - 'Public cert file not found: %s', - OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND, - cert_metadata_file - ) - - f_metadata_key = open(key_metadata_file, 'r') - key_metadata = f_metadata_key.read() - f_metadata_key.close() + try: + with open(key_metadata_file, 'r') as f_metadata_key: + key_metadata = f_metadata_key.read() + except IOError: + raise OneLogin_Saml2_Error( + 'Private key file not readable: %s', + OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND, + key_metadata_file + ) - f_metadata_cert = open(cert_metadata_file, 'r') - cert_metadata = f_metadata_cert.read() - f_metadata_cert.close() + try: + with open(cert_metadata_file, 'r') as f_metadata_cert: + cert_metadata = f_metadata_cert.read() + except IOError: + raise OneLogin_Saml2_Error( + 'Public cert file not readable: %s', + OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND, + cert_metadata_file + ) metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key_metadata, cert_metadata) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 6af09960..21bb9a78 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -7,6 +7,7 @@ from os.path import dirname, join, exists, sep import unittest +from onelogin.saml2.errors import OneLogin_Saml2_Error from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils @@ -371,8 +372,24 @@ def testGetSPMetadataSigned(self): if 'security' not in settings_info: settings_info['security'] = {} settings_info['security']['signMetadata'] = True - settings = OneLogin_Saml2_Settings(settings_info) + self.generateAndCheckMetadata(settings_info) + + # Now try again with SP keys set directly in settings and not from files: + del settings_info['custom_base_path'] + # Now the keys should not be found, so metadata generation won't work: + with self.assertRaises(OneLogin_Saml2_Error): + OneLogin_Saml2_Settings(settings_info).get_sp_metadata() + # Set the keys in the settings: + settings_info['sp']['x509cert'] = self.file_contents(join(self.data_path, 'customPath', 'certs', 'sp.crt')) + settings_info['sp']['privateKey'] = self.file_contents(join(self.data_path, 'customPath', 'certs', 'sp.key')) + self.generateAndCheckMetadata(settings_info) + def generateAndCheckMetadata(self, settings): + """ + Helper method: Given some settings, generate metadata and validate it + """ + if not isinstance(settings, OneLogin_Saml2_Settings): + settings = OneLogin_Saml2_Settings(settings) metadata = settings.get_sp_metadata() self.assertIn('', metadata) self.assertIn('', metadata) + return metadata def testGetSPMetadataSignedNoMetadataCert(self): """ @@ -411,7 +429,7 @@ def testGetSPMetadataSignedNoMetadataCert(self): settings.get_sp_metadata() self.assertTrue(False) except Exception as e: - self.assertIn('Private key file not found', e.message) + self.assertIn('Private key file not readable', e.message) settings_info['security']['signMetadata'] = { 'keyFileName': 'sp.key', @@ -422,7 +440,7 @@ def testGetSPMetadataSignedNoMetadataCert(self): settings.get_sp_metadata() self.assertTrue(False) except Exception as e: - self.assertIn('Public cert file not found', e.message) + self.assertIn('Public cert file not readable', e.message) settings_info['security']['signMetadata'] = 'invalid_value' settings = OneLogin_Saml2_Settings(settings_info) From 94a43fec4cf0da4f2afaf63ed12eae683fed5e31 Mon Sep 17 00:00:00 2001 From: Mukul Agrawal Date: Thu, 28 May 2015 19:31:28 +0530 Subject: [PATCH 027/352] Issue 61:Modify the KeyInfo (in EncryptedData) and put the EncryptedKey sub-tree as a child element to it --- src/onelogin/saml2/response.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index c41e3cf0..23518cbc 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -449,6 +449,25 @@ def __decrypt_assertion(self, dom): if encrypted_assertion_nodes: encrypted_data_nodes = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData') if encrypted_data_nodes: + keyinfo = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData/ds:KeyInfo') + if not keyinfo: + raise Exception('No KeyInfo present, invalid Assertion') + keyinfo = keyinfo[0] + children = keyinfo.getchildren() + if not children: + raise Exception('No child to KeyInfo, invalid Assertion') + 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') + uri = child.attrib['URI'] + if not uri.startswith('#'): + break + uri = uri.split('#')[1] + encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id="'+uri +'"]') + if encrypted_key: + keyinfo.append(encrypted_key[0]) + encrypted_data = encrypted_data_nodes[0] OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) return dom From ff1249b2b92df70a5fd58524673b7093d548ab14 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 24 Jun 2015 19:37:21 +0200 Subject: [PATCH 028/352] Metadata validUntil/cacheDuration: Add documentation, test and minor fix. --- README.md | 6 ++++++ src/onelogin/saml2/metadata.py | 2 +- .../src/OneLogin/saml2_tests/metadata_test.py | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2fe50fe0..47deaf81 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,12 @@ In addition to the required settings data (idp, sp), there is extra information // Set true or don't present thi parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' // Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'), 'requestedAuthnContext': true, + + // In some environment you will need to set how long the published metadata of the Service Provider gonna be valid. + // is possible to not set the 2 following parameters (or set to null) and default values will be set (2 days, 1 week) + // Or provide the desire TimeStamp, for example PT518400S (6 days) + 'metadataValidUntil': null, + 'metadataCacheDuration': null, }, // Contact information template, it is recommended to suply a diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 64028c97..ce6b6567 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -57,7 +57,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N valid_until = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_VALID if not isinstance(valid_until, basestring): if isinstance(valid_until, datetime): - valid_until_time = valid_until + valid_until_time = valid_until.timetuple() else: valid_until_time = gmtime(valid_until) valid_until_str = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time) diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 4e775def..229b9307 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -3,8 +3,11 @@ # Copyright (c) 2014, OneLogin, Inc. # All rights reserved. + import json from os.path import dirname, join, exists +from time import gmtime, strftime +from datetime import datetime import unittest from onelogin.saml2.metadata import OneLogin_Saml2_Metadata @@ -121,6 +124,21 @@ def testBuilder(self): self.assertNotIn('cacheDuration', metadata5) self.assertIn('validUntil="2014-10-01T11:04:29Z"', metadata5) + datetime_value = datetime.now() + metadata6 = OneLogin_Saml2_Metadata.builder( + sp_data, security['authnRequestsSigned'], + security['wantAssertionsSigned'], + datetime_value, + 'P1Y', + contacts, + organization + ) + self.assertIsNotNone(metadata5) + self.assertIn(' Date: Wed, 24 Jun 2015 19:39:03 +0200 Subject: [PATCH 029/352] . --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 47deaf81..ab458d22 100644 --- a/README.md +++ b/README.md @@ -320,9 +320,10 @@ In addition to the required settings data (idp, sp), there is extra information // In some environment you will need to set how long the published metadata of the Service Provider gonna be valid. // is possible to not set the 2 following parameters (or set to null) and default values will be set (2 days, 1 week) - // Or provide the desire TimeStamp, for example PT518400S (6 days) + // Provide the desire TimeStamp, for example 2015-06-26T20:00:00Z 'metadataValidUntil': null, - 'metadataCacheDuration': null, + // Provide the desire Duration, for example PT518400S (6 days) + 'metadataCacheDuration': null }, // Contact information template, it is recommended to suply a From 52f54398c046ccfdf267ca1bf3fda396dd023a30 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 24 Jun 2015 21:01:46 +0200 Subject: [PATCH 030/352] Added some more tests --- README.md | 4 ++-- .../src/OneLogin/saml2_tests/settings_test.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ab458d22..5bbd1190 100644 --- a/README.md +++ b/README.md @@ -320,9 +320,9 @@ In addition to the required settings data (idp, sp), there is extra information // In some environment you will need to set how long the published metadata of the Service Provider gonna be valid. // is possible to not set the 2 following parameters (or set to null) and default values will be set (2 days, 1 week) - // Provide the desire TimeStamp, for example 2015-06-26T20:00:00Z + // Provide the desired Timestamp, for example 2015-06-26T20:00:00Z 'metadataValidUntil': null, - // Provide the desire Duration, for example PT518400S (6 days) + // Provide the desired duration, for example PT518400S (6 days) 'metadataCacheDuration': null }, diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 21bb9a78..73f6a0b5 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -371,9 +371,23 @@ def testGetSPMetadataSigned(self): settings_info = self.loadSettingsJSON() if 'security' not in settings_info: settings_info['security'] = {} + + # Use custom cert/key + settings_info['security']['signMetadata'] = { + "keyFileName": "sp.key", + "certFileName": "sp.crt" + } + self.generateAndCheckMetadata(settings_info) + + # Default cert/key settings_info['security']['signMetadata'] = True self.generateAndCheckMetadata(settings_info) + # Now try again with SP keys set directly from files that no exists: + settings_info['custom_base_path'] = '../path/not/exists/' + with self.assertRaises(OneLogin_Saml2_Error): + OneLogin_Saml2_Settings(settings_info).get_sp_metadata() + # Now try again with SP keys set directly in settings and not from files: del settings_info['custom_base_path'] # Now the keys should not be found, so metadata generation won't work: @@ -384,6 +398,11 @@ def testGetSPMetadataSigned(self): settings_info['sp']['privateKey'] = self.file_contents(join(self.data_path, 'customPath', 'certs', 'sp.key')) self.generateAndCheckMetadata(settings_info) + # Now fails due no privateKey + del settings_info['sp']['privateKey'] + with self.assertRaises(OneLogin_Saml2_Error): + OneLogin_Saml2_Settings(settings_info).get_sp_metadata() + def generateAndCheckMetadata(self, settings): """ Helper method: Given some settings, generate metadata and validate it From e5fe4a0f15ab64dac59f012fd3036a59f0434bb8 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 24 Jun 2015 22:41:51 +0200 Subject: [PATCH 031/352] Fix pyflakes reported error --- tests/src/OneLogin/saml2_tests/metadata_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 229b9307..6fbfbf50 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -6,7 +6,7 @@ import json from os.path import dirname, join, exists -from time import gmtime, strftime +from time import strftime from datetime import datetime import unittest From 3a5847be93fd7e3af695d791989825cc6c118808 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 25 Jun 2015 01:20:19 +0200 Subject: [PATCH 032/352] Set NAMEID_UNSPECIFIED as default NameIDFormat to prevent conflicts with IdPsthat don't support NAMEID_PERSISTENT --- src/onelogin/saml2/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 6f75a2a7..73ef16be 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -262,7 +262,7 @@ def __add_default_values(self): # Related to nameID if 'NameIDFormat' not in self.__sp: - self.__sp['NameIDFormat'] = OneLogin_Saml2_Constants.NAMEID_PERSISTENT + self.__sp['NameIDFormat'] = OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED if 'nameIdEncrypted' not in self.__security: self.__security['nameIdEncrypted'] = False From 47278a57fd1336c1e9b27a4bfe503d7e43c5b20e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 25 Jun 2015 02:22:35 +0200 Subject: [PATCH 033/352] Release the 2.1.3 version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e6ebaf5e..a4c8ebcc 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.1.2', + version='2.1.3', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', From db2553229e9c20225f67af78d95af262928f01ce Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 12 Jul 2015 19:56:47 +0200 Subject: [PATCH 034/352] Handle valid but uncommon dsig block with no URI in the reference --- src/onelogin/saml2/utils.py | 8 +++++++- tests/src/OneLogin/saml2_tests/response_test.py | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index d14a2e7f..387abd76 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -806,7 +806,7 @@ def add_sign(xml, key, cert, debug=False): if debug: xmlsec.set_error_callback(print_xmlsec_errors) - # Sign the metadacta with our private key. + # Sign the metadata with our private key. signature = Signature(xmlsec.TransformExclC14N, xmlsec.TransformRsaSha1) issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer') @@ -922,6 +922,12 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid if cert is None or cert == '': return False + # Check if Reference URI is empty + reference_elem = OneLogin_Saml2_Utils.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')) + dsig_ctx = xmlsec.DSigCtx() file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 9ed46dd8..aae4d3c2 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1097,3 +1097,12 @@ def testIsValidSign(self): response_9 = OneLogin_Saml2_Response(settings, xml_9) # Modified message self.assertFalse(response_9.is_valid(self.get_request_data())) + + def testIsValidSignWithEmptyReferenceURI(self): + settings_info = self.loadSettingsJSON() + del settings_info['idp']['x509cert'] + settings_info['idp']['certFingerprint'] = "194d97e4d8c9c8cfa4b721e5ee497fd9660e5213" + settings = OneLogin_Saml2_Settings(settings_info) + xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertTrue(response.is_valid(self.get_request_data())) From 7991213b9b4de85fccf78d75b8d12007ce00f7aa Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 12 Jul 2015 20:43:19 +0200 Subject: [PATCH 035/352] Forgotten file --- tests/data/responses/response_without_reference_uri.xml.base64 | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/data/responses/response_without_reference_uri.xml.base64 diff --git a/tests/data/responses/response_without_reference_uri.xml.base64 b/tests/data/responses/response_without_reference_uri.xml.base64 new file mode 100644 index 00000000..dd5f7b50 --- /dev/null +++ b/tests/data/responses/response_without_reference_uri.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgSUQ9InBmeGQ1OTQzNDdkLTQ5NWYtYjhkMS0wZWUyLTQxY2ZkYTE0ZGQzNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo5MDAxL3YxL3VzZXJzL2F1dGhvcml6ZS9zYW1sIiBDb25zZW50PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y29uc2VudDp1bnNwZWNpZmllZCIgSW5SZXNwb25zZVRvPSJfZWQ5MTVhNDAtNzRmYi0wMTMyLTViMTYtNDhlMGViMTRhMWM3Ij4NCiAgPElzc3VlciB4bWxucz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+aHR0cDovL2V4YW1wbGUuY29tPC9Jc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT5qQ2dlWENQREZsd2pUZ3FnUHAwbVUyVHF3OWc9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPkRmdXByMTh3UityRGFndENQRWZRbFNHSHp3NE5kZlBIWjRIc3pGZTFKUENKWGpmYnlFTTFmZytqemdHYk1NdDZYemdDWGNLSk03RS9DUFNURGt2TWUzRFVKbEh1NERodURPQXovRHN5b0J3V3VWK1JmM1dpTmNGNFhDYzl3QlF6dm4vYXREN3pXNnh3TzdOL2hrQVpKcWZ2SmRkbnBNTUhLR1hxRy9aSFpBdz08L2RzOlNpZ25hdHVyZVZhbHVlPg0KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ3FEQ0NBaEdnQXdJQkFnSUJBREFOQmdrcWhraUc5dzBCQVEwRkFEQnhNUXN3Q1FZRFZRUUdFd0oxY3pFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRWlNQ0FHQTFVRUNnd1pSbXhoZENCWGIzSnNaQ0JMYm05M2JHVmtaMlVzSUVsdVl6RWNNQm9HQTFVRUF3d1RiR1ZoY200dVpteGhkSGR2Y214a0xtTnZiVEVMTUFrR0ExVUVCd3dDUkVNd0hoY05NVFV3TnpBNE1EazFPVEF6V2hjTk1qVXdOekExTURrMU9UQXpXakJ4TVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tWMkZ6YUdsdVozUnZiakVpTUNBR0ExVUVDZ3daUm14aGRDQlhiM0pzWkNCTGJtOTNiR1ZrWjJVc0lFbHVZekVjTUJvR0ExVUVBd3dUYkdWaGNtNHVabXhoZEhkdmNteGtMbU52YlRFTE1Ba0dBMVVFQnd3Q1JFTXdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JBTVBEd3NsNW82eDJRb3VOaTEvRTdJVXFSWWoyWW9jSlJGc3VFR1RldnlVKzJhRkNhQk5WL3R0NnNBYk05V1N1dEx1cWpFL2hmYm5sRWNaMDMrZ24wQ29MbDZZbXdiS0tlUnBrSXplVmhveUoxWVlNUUVBVmhMcmR5OFBvd3U4VUNaMFBiQXorbjlka2lSek01cENDTzc3K2d5Y0ZUQkZLSEFBOXFJcFVaWmtQQWdNQkFBR2pVREJPTUIwR0ExVWREZ1FXQkJRSFU1OGl1R3hGbFp1ckJVSndvbGFsSnIrRlJ6QWZCZ05WSFNNRUdEQVdnQlFIVTU4aXVHeEZsWnVyQlVKd29sYWxKcitGUnpBTUJnTlZIUk1FQlRBREFRSC9NQTBHQ1NxR1NJYjNEUUVCRFFVQUE0R0JBQzZpSGZNbWQraE1TUnpma29zaTNDK3d2cUhDTEVVc2czSEZwa1ptNWp4bVREbEY1cU8rQnQwbjB4bWZvcVdCekJNbE5DOFRzR3JhZmhKM3p1OEdORjBMZW8xMXJmYzFHTUdCdnI1SG9aM1dBQXltbkJFREFBb3N4TjZXWlJtajF4YWdhMTMrNnBXZkdCKysyblB3Y1pXUC84ZGtQY1JvZ2V2VjB4MHA1Njg2PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCg0KICA8QXNzZXJ0aW9uIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iXzcwMGFjMzIwLTc0ZmYtMDEzMi01YjE0LTQ4ZTBlYjE0YTFjNyIgSXNzdWVJbnN0YW50PSIyMDE1LTAxLTAyVDIyOjQ4OjQ4WiIgVmVyc2lvbj0iMi4wIj4NCiAgICA8SXNzdWVyPmh0dHA6Ly9leGFtcGxlLmNvbTwvSXNzdWVyPg0KICAgIDxTdWJqZWN0Pg0KICAgICAgPE5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c2FtbEB1c2VyLmNvbTwvTmFtZUlEPg0KICAgICAgPFN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPFN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iX2VkOTE1YTQwLTc0ZmItMDEzMi01YjE2LTQ4ZTBlYjE0YTFjNyIgTm90T25PckFmdGVyPSIyMDM4LTAxLTAyVDIyOjUxOjQ4WiIgUmVjaXBpZW50PSJodHRwOi8vbG9jYWxob3N0OjkwMDEvdjEvdXNlcnMvYXV0aG9yaXplL3NhbWwiLz4NCiAgICAgIDwvU3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L1N1YmplY3Q+DQogICAgPENvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE1LTAxLTAyVDIyOjQ4OjQzWiIgTm90T25PckFmdGVyPSIyMDM4LTAxLTAyVDIzOjQ4OjQ4WiI+DQogICAgICA8QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPEF1ZGllbmNlPmh0dHA6Ly9sb2NhbGhvc3Q6OTAwMS88L0F1ZGllbmNlPg0KICAgICAgICA8QXVkaWVuY2U+ZmxhdF93b3JsZDwvQXVkaWVuY2U+DQogICAgICA8L0F1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9Db25kaXRpb25zPg0KICAgIDxBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8QXR0cmlidXRlIE5hbWU9Imh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL2VtYWlsYWRkcmVzcyI+DQogICAgICAgIDxBdHRyaWJ1dGVWYWx1ZT5zYW1sQHVzZXIuY29tPC9BdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvQXR0cmlidXRlPg0KICAgIDwvQXR0cmlidXRlU3RhdGVtZW50Pg0KICAgIDxBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBTZXNzaW9uSW5kZXg9Il83MDBhYzMyMC03NGZmLTAxMzItNWIxNC00OGUwZWIxNGExYzciPg0KICAgICAgPEF1dGhuQ29udGV4dD4NCiAgICAgICAgPEF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpmZWRlcmF0aW9uOmF1dGhlbnRpY2F0aW9uOndpbmRvd3M8L0F1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9BdXRobkNvbnRleHQ+DQogICAgPC9BdXRoblN0YXRlbWVudD4NCiAgPC9Bc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== From 22596cb16bf9ae917a93044c6c05216a439d27f9 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 15 Jul 2015 02:32:11 +0200 Subject: [PATCH 036/352] Split the setting check methods. Now 1 method for IdP settings and other for SP settings. Related to #74. --- src/onelogin/saml2/settings.py | 250 +++++++++++++++++++-------------- 1 file changed, 145 insertions(+), 105 deletions(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 73ef16be..037b6eac 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -324,115 +324,155 @@ def check_settings(self, settings): errors = [] if not isinstance(settings, dict) or len(settings) == 0: errors.append('invalid_syntax') - return errors + else: + idp_erros = self.check_idp_settings(settings) + sp_errors = self.check_sp_settings(settings) + errors = idp_erros + sp_errors + + return errors + + def check_idp_settings(self, settings): + """ + Checks the IdP settings info. - if 'idp' not in settings or len(settings['idp']) == 0: - errors.append('idp_not_found') + :param settings: Dict with settings data + :type settings: dict + + :returns: Errors found on the IdP settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or len(settings) == 0: + errors.append('invalid_syntax') else: - idp = settings['idp'] - if 'entityId' not in idp or len(idp['entityId']) == 0: - errors.append('idp_entityId_not_found') - - if 'singleSignOnService' not in idp or \ - 'url' not in idp['singleSignOnService'] or \ - len(idp['singleSignOnService']['url']) == 0: - errors.append('idp_sso_not_found') - elif not validate_url(idp['singleSignOnService']['url']): - errors.append('idp_sso_url_invalid') - - if 'singleLogoutService' in idp and \ - 'url' in idp['singleLogoutService'] and \ - len(idp['singleLogoutService']['url']) > 0 and \ - not validate_url(idp['singleLogoutService']['url']): - errors.append('idp_slo_url_invalid') - - if 'sp' not in settings or len(settings['sp']) == 0: - errors.append('sp_not_found') + if 'idp' not in settings or len(settings['idp']) == 0: + errors.append('idp_not_found') + else: + idp = settings['idp'] + if 'entityId' not in idp or len(idp['entityId']) == 0: + errors.append('idp_entityId_not_found') + + if 'singleSignOnService' not in idp or \ + 'url' not in idp['singleSignOnService'] or \ + len(idp['singleSignOnService']['url']) == 0: + errors.append('idp_sso_not_found') + elif not validate_url(idp['singleSignOnService']['url']): + errors.append('idp_sso_url_invalid') + + if 'singleLogoutService' in idp and \ + 'url' in idp['singleLogoutService'] and \ + len(idp['singleLogoutService']['url']) > 0 and \ + not validate_url(idp['singleLogoutService']['url']): + errors.append('idp_slo_url_invalid') + + if 'security' in settings: + security = settings['security'] + + exists_x509 = ('x509cert' in idp and + len(idp['x509cert']) > 0) + exists_fingerprint = ('certFingerprint' in idp and + len(idp['certFingerprint']) > 0) + + want_assert_sign = 'wantAssertionsSigned' in security.keys() and security['wantAssertionsSigned'] + want_mes_signed = 'wantMessagesSigned' in security.keys() and security['wantMessagesSigned'] + nameid_enc = 'nameIdEncrypted' in security.keys() and security['nameIdEncrypted'] + + if (want_assert_sign or want_mes_signed) and \ + not(exists_x509 or exists_fingerprint): + errors.append('idp_cert_or_fingerprint_not_found_and_required') + if nameid_enc and not exists_x509: + errors.append('idp_cert_not_found_and_required') + + return errors + + def check_sp_settings(self, settings): + """ + Checks the SP settings info. + + :param settings: Dict with settings data + :type settings: dict + + :returns: Errors found on the SP settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or len(settings) == 0: + errors.append('invalid_syntax') else: - # check_sp_certs uses self.__sp so I add it - old_sp = self.__sp - self.__sp = settings['sp'] + if 'sp' not in settings or len(settings['sp']) == 0: + errors.append('sp_not_found') + else: + # check_sp_certs uses self.__sp so I add it + old_sp = self.__sp + self.__sp = settings['sp'] + + sp = settings['sp'] + security = {} + if 'security' in settings: + security = settings['security'] + + if 'entityId' not in sp or len(sp['entityId']) == 0: + errors.append('sp_entityId_not_found') + + if 'assertionConsumerService' not in sp or \ + 'url' not in sp['assertionConsumerService'] or \ + len(sp['assertionConsumerService']['url']) == 0: + errors.append('sp_acs_not_found') + elif not validate_url(sp['assertionConsumerService']['url']): + errors.append('sp_acs_url_invalid') + + if 'singleLogoutService' in sp and \ + 'url' in sp['singleLogoutService'] and \ + len(sp['singleLogoutService']['url']) > 0 and \ + not validate_url(sp['singleLogoutService']['url']): + errors.append('sp_sls_url_invalid') + + if 'signMetadata' in security and isinstance(security['signMetadata'], dict): + if 'keyFileName' not in security['signMetadata'] or \ + 'certFileName' not in security['signMetadata']: + errors.append('sp_signMetadata_invalid') + + authn_sign = 'authnRequestsSigned' in security.keys() and security['authnRequestsSigned'] + logout_req_sign = 'logoutRequestSigned' in security.keys() and security['logoutRequestSigned'] + logout_res_sign = 'logoutResponseSigned' in security.keys() and security['logoutResponseSigned'] + want_assert_enc = 'wantAssertionsEncrypted' in security.keys() and security['wantAssertionsEncrypted'] + want_nameid_enc = 'wantNameIdEncrypted' in security.keys() and security['wantNameIdEncrypted'] + + if not self.check_sp_certs(): + if authn_sign or logout_req_sign or logout_res_sign or \ + want_assert_enc or want_nameid_enc: + errors.append('sp_cert_not_found_and_required') - sp = settings['sp'] - security = {} - if 'security' in settings: - security = settings['security'] - - if 'entityId' not in sp or len(sp['entityId']) == 0: - errors.append('sp_entityId_not_found') - - if 'assertionConsumerService' not in sp or \ - 'url' not in sp['assertionConsumerService'] or \ - len(sp['assertionConsumerService']['url']) == 0: - errors.append('sp_acs_not_found') - elif not validate_url(sp['assertionConsumerService']['url']): - errors.append('sp_acs_url_invalid') - - if 'singleLogoutService' in sp and \ - 'url' in sp['singleLogoutService'] and \ - len(sp['singleLogoutService']['url']) > 0 and \ - not validate_url(sp['singleLogoutService']['url']): - errors.append('sp_sls_url_invalid') - - if 'signMetadata' in security and isinstance(security['signMetadata'], dict): - if 'keyFileName' not in security['signMetadata'] or \ - 'certFileName' not in security['signMetadata']: - errors.append('sp_signMetadata_invalid') - - authn_sign = 'authnRequestsSigned' in security.keys() and security['authnRequestsSigned'] - logout_req_sign = 'logoutRequestSigned' in security.keys() and security['logoutRequestSigned'] - logout_res_sign = 'logoutResponseSigned' in security.keys() and security['logoutResponseSigned'] - want_assert_enc = 'wantAssertionsEncrypted' in security.keys() and security['wantAssertionsEncrypted'] - want_nameid_enc = 'wantNameIdEncrypted' in security.keys() and security['wantNameIdEncrypted'] - - if not self.check_sp_certs(): - if authn_sign or logout_req_sign or logout_res_sign or \ - want_assert_enc or want_nameid_enc: - errors.append('sp_cert_not_found_and_required') - - exists_x509 = ('idp' in settings and - 'x509cert' in settings['idp'] and - len(settings['idp']['x509cert']) > 0) - exists_fingerprint = ('idp' in settings and - 'certFingerprint' in settings['idp'] and - len(settings['idp']['certFingerprint']) > 0) - - want_assert_sign = 'wantAssertionsSigned' in security.keys() and security['wantAssertionsSigned'] - want_mes_signed = 'wantMessagesSigned' in security.keys() and security['wantMessagesSigned'] - nameid_enc = 'nameIdEncrypted' in security.keys() and security['nameIdEncrypted'] - - if (want_assert_sign or want_mes_signed) and \ - not(exists_x509 or exists_fingerprint): - errors.append('idp_cert_or_fingerprint_not_found_and_required') - if nameid_enc and not exists_x509: - errors.append('idp_cert_not_found_and_required') - - if 'contactPerson' in settings: - types = settings['contactPerson'].keys() - valid_types = ['technical', 'support', 'administrative', 'billing', 'other'] - for c_type in types: - if c_type not in valid_types: - errors.append('contact_type_invalid') - break - - for c_type in settings['contactPerson']: - contact = settings['contactPerson'][c_type] - if ('givenName' not in contact or len(contact['givenName']) == 0) or \ - ('emailAddress' not in contact or len(contact['emailAddress']) == 0): - errors.append('contact_not_enought_data') - break - - if 'organization' in settings: - for org in settings['organization']: - organization = settings['organization'][org] - if ('name' not in organization or len(organization['name']) == 0) or \ - ('displayname' not in organization or len(organization['displayname']) == 0) or \ - ('url' not in organization or len(organization['url']) == 0): - errors.append('organization_not_enought_data') - break - # Restores the value that had the self.__sp - if 'old_sp' in locals(): - self.__sp = old_sp + if 'contactPerson' in settings: + types = settings['contactPerson'].keys() + valid_types = ['technical', 'support', 'administrative', 'billing', 'other'] + for c_type in types: + if c_type not in valid_types: + errors.append('contact_type_invalid') + break + + for c_type in settings['contactPerson']: + contact = settings['contactPerson'][c_type] + if ('givenName' not in contact or len(contact['givenName']) == 0) or \ + ('emailAddress' not in contact or len(contact['emailAddress']) == 0): + errors.append('contact_not_enought_data') + break + + if 'organization' in settings: + for org in settings['organization']: + organization = settings['organization'][org] + if ('name' not in organization or len(organization['name']) == 0) or \ + ('displayname' not in organization or len(organization['displayname']) == 0) or \ + ('url' not in organization or len(organization['url']) == 0): + errors.append('organization_not_enought_data') + break + # Restores the value that had the self.__sp + if 'old_sp' in locals(): + self.__sp = old_sp return errors From 4a98ea8a30a35d2ea818fb5a43b378d6e908efbe Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 15 Jul 2015 02:53:35 +0200 Subject: [PATCH 037/352] Let the setting object to avoid the IdP setting check. required if we want to publish SP SAML Metadata when the IdP data is still not provided. Close #74 --- README.md | 8 ++++++++ demo-django/demo/views.py | 8 +++++--- src/onelogin/saml2/settings.py | 8 +++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5bbd1190..04d4b15d 100644 --- a/README.md +++ b/README.md @@ -496,6 +496,12 @@ The get_sp_metadata will return the metadata signed or not based on the security Before the XML metadata is exposed, a check takes place to ensure that the info to be provided is valid. +Instead of use the Auth object, you can directly use +``` +saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp_validation_only=True) +``` +to get the settings object and with the sp_validation_only=True parameter we will avoid the IdP Settings validation. + ***Attribute Consumer Service(ACS)*** This code handles the SAML response that the IdP forwards to the SP through the user's client. @@ -787,6 +793,8 @@ Configuration of the OneLogin Python Toolkit * `__init__` Initializes the settings: Sets the paths of the different folders and Loads settings info from settings file or array/object provided. * ***check_settings*** Checks the settings info. +* ***check_idp_settings*** Checks the IdP settings info. +* ***check_sp_settings*** Checks the SP settings info. * ***get_errors*** Returns an array with the errors, the array is empty when the settings is ok. * ***get_sp_metadata*** Gets the SP metadata. The XML representation. * ***validate_metadata*** Validates an XML SP Metadata. diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index b67c5d99..a2277366 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -6,6 +6,7 @@ from django.template import RequestContext from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils @@ -100,9 +101,10 @@ def attrs(request): def metadata(request): - req = prepare_django_request(request) - auth = init_saml_auth(req) - saml_settings = auth.get_settings() + # req = prepare_django_request(request) + # auth = init_saml_auth(req) + # saml_settings = auth.get_settings() + saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=settings.SAML_FOLDER, sp_validation_only=True) metadata = saml_settings.get_sp_metadata() errors = saml_settings.validate_metadata(metadata) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 037b6eac..c2e4c396 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -58,7 +58,7 @@ class OneLogin_Saml2_Settings(object): """ - def __init__(self, settings=None, custom_base_path=None): + def __init__(self, settings=None, custom_base_path=None, sp_validation_only=False): """ Initializes the settings: - Sets the paths of the different folders @@ -70,6 +70,7 @@ def __init__(self, settings=None, custom_base_path=None): :param custom_base_path: Path where are stored the settings file and the cert folder :type custom_base_path: string """ + self.__sp_validation_only = False self.__paths = {} self.__strict = False self.__debug = False @@ -325,9 +326,10 @@ def check_settings(self, settings): if not isinstance(settings, dict) or len(settings) == 0: errors.append('invalid_syntax') else: - idp_erros = self.check_idp_settings(settings) + if not self.__sp_validation_only: + errors += self.check_idp_settings(settings) sp_errors = self.check_sp_settings(settings) - errors = idp_erros + sp_errors + errors += sp_errors return errors From f64c1570145d5a8517ce6b0f1937b2ccdf66adae Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 15 Jul 2015 02:56:31 +0200 Subject: [PATCH 038/352] Minor pep8 fix --- src/onelogin/saml2/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index c2e4c396..fd661245 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -372,7 +372,7 @@ def check_idp_settings(self, settings): if 'security' in settings: security = settings['security'] - exists_x509 = ('x509cert' in idp and + exists_x509 = ('x509cert' in idp and len(idp['x509cert']) > 0) exists_fingerprint = ('certFingerprint' in idp and len(idp['certFingerprint']) > 0) From c53a11f71f452fcefa244875d9adc9ba6aae767e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 16 Jul 2015 20:56:58 +0200 Subject: [PATCH 039/352] Now the SP is able to select the algorithm to be used on signatures --- README.md | 10 +++++- demo-django/saml/advanced_settings.json | 3 +- demo-flask/saml/advanced_settings.json | 3 +- src/onelogin/saml2/auth.py | 45 ++++++++++++++++++------- src/onelogin/saml2/constants.py | 24 +++++++++++-- src/onelogin/saml2/metadata.py | 7 ++-- src/onelogin/saml2/settings.py | 4 +++ src/onelogin/saml2/utils.py | 20 ++++++++--- 8 files changed, 91 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 04d4b15d..e538bb71 100644 --- a/README.md +++ b/README.md @@ -323,7 +323,15 @@ In addition to the required settings data (idp, sp), there is extra information // Provide the desired Timestamp, for example 2015-06-26T20:00:00Z 'metadataValidUntil': null, // Provide the desired duration, for example PT518400S (6 days) - 'metadataCacheDuration': null + 'metadataCacheDuration': null, + + // Algorithm that the toolkit will use on signing process. Options: + // 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + // 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' + 'signatureAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' }, // Contact information template, it is recommended to suply a diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json index e336fe9d..e4eb9b14 100644 --- a/demo-django/saml/advanced_settings.json +++ b/demo-django/saml/advanced_settings.json @@ -8,6 +8,7 @@ "wantMessagesSigned": false, "wantAssertionsSigned": false, "wantNameIdEncrypted": false + "signatureAlgorithm" => "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, "contactPerson": { "technical": { @@ -26,4 +27,4 @@ "url": "http://sp.example.com" } } -} \ No newline at end of file +} diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json index e336fe9d..5bd0a887 100644 --- a/demo-flask/saml/advanced_settings.json +++ b/demo-flask/saml/advanced_settings.json @@ -7,7 +7,8 @@ "signMetadata": false, "wantMessagesSigned": false, "wantAssertionsSigned": false, - "wantNameIdEncrypted": false + "wantNameIdEncrypted": false, + "signatureAlgorithm" => "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, "contactPerson": { "technical": { diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 914caada..49cbfbce 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -152,8 +152,8 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ security = self.__settings.get_security_data() if 'logoutResponseSigned' in security and security['logoutResponseSigned']: - parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 - parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None)) + parameters['SigAlg'] = security['signatureAlgorithm'] + parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None), security['signatureAlgorithm']) return self.redirect_to(self.get_slo_url(), parameters) else: @@ -274,8 +274,8 @@ def login(self, return_to=None, force_authn=False, is_passive=False): security = self.__settings.get_security_data() if security.get('authnRequestsSigned', False): - parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 - parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState']) + parameters['SigAlg'] = security['signatureAlgorithm'] + parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) return self.redirect_to(self.get_sso_url(), parameters) def logout(self, return_to=None, name_id=None, session_index=None): @@ -315,8 +315,8 @@ def logout(self, return_to=None, name_id=None, session_index=None): security = self.__settings.get_security_data() if security.get('logoutRequestSigned', False): - parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 - parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState']) + parameters['SigAlg'] = security['signatureAlgorithm'] + parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) return self.redirect_to(slo_url, parameters) def get_sso_url(self): @@ -342,7 +342,7 @@ def get_slo_url(self): url = idp_data['singleLogoutService']['url'] return url - def build_request_signature(self, saml_request, relay_state): + def build_request_signature(self, saml_request, relay_state, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ Builds the Signature of the SAML Request. @@ -351,10 +351,13 @@ def build_request_signature(self, saml_request, relay_state): :param relay_state: The target URL the user should be redirected to :type relay_state: string + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string """ - return self.__build_signature(saml_request, relay_state, 'SAMLRequest') + return self.__build_signature(saml_request, relay_state, 'SAMLRequest', sign_algorithm) - def build_response_signature(self, saml_response, relay_state): + def build_response_signature(self, saml_response, relay_state, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ Builds the Signature of the SAML Response. :param saml_request: The SAML Response @@ -362,10 +365,13 @@ def build_response_signature(self, saml_response, relay_state): :param relay_state: The target URL the user should be redirected to :type relay_state: string + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string """ - return self.__build_signature(saml_response, relay_state, 'SAMLResponse') + return self.__build_signature(saml_response, relay_state, 'SAMLResponse', sign_algorithm) - def __build_signature(self, saml_data, relay_state, saml_type): + def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ Builds the Signature :param saml_data: The SAML Data @@ -376,6 +382,9 @@ def __build_signature(self, saml_data, relay_state, saml_type): :param saml_type: The target URL the user should be redirected to :type saml_type: string SAMLRequest | SAMLResponse + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string """ assert saml_type in ['SAMLRequest', 'SAMLResponse'] @@ -395,10 +404,20 @@ def __build_signature(self, saml_data, relay_state, saml_type): saml_data_str = '%s=%s' % (saml_type, quote_plus(saml_data)) relay_state_str = 'RelayState=%s' % quote_plus(relay_state) - alg_str = 'SigAlg=%s' % quote_plus(OneLogin_Saml2_Constants.RSA_SHA1) + alg_str = 'SigAlg=%s' % quote_plus(sign_algorithm) sign_data = [saml_data_str, relay_state_str, alg_str] msg = '&'.join(sign_data) - signature = dsig_ctx.signBinary(str(msg), xmlsec.TransformRsaSha1) + # Sign the metadata with our private key. + sign_algorithm_transform_map = { + OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1, + OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1, + OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256, + OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384, + OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512 + } + sign_algorithm_transform = sign_algorithm_transform_map.get(sign_algorithm, xmlsec.TransformRsaSha1) + + signature = dsig_ctx.signBinary(str(msg), sign_algorithm_transform) return b64encode(signature) diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py index 698d8ee7..f004bbbd 100644 --- a/src/onelogin/saml2/constants.py +++ b/src/onelogin/saml2/constants.py @@ -76,12 +76,30 @@ class OneLogin_Saml2_Constants(object): STATUS_PARTIAL_LOGOUT = 'urn:oasis:names:tc:SAML:2.0:status:PartialLogout' STATUS_PROXY_COUNT_EXCEEDED = 'urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded' - # Crypto - RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' - + # Namespaces NSMAP = { 'samlp': NS_SAMLP, 'saml': NS_SAML, 'ds': NS_DS, 'xenc': NS_XENC } + + # Sign & Crypto + SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1' + SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256' + SHA384 = 'http://www.w3.org/2001/04/xmlencsha384' + SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512' + + DSA_SHA1 = 'http://www.w3.org/2000/09/xmld/sig#dsa-sha1' + RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + RSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' + RSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' + + # Enc + TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' + AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' + AES192_CBC = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' + AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' + RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' + RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index ce6b6567..891500e5 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -153,7 +153,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N return metadata @staticmethod - def sign_metadata(metadata, key, cert): + def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ Signs the metadata with the key/cert provided @@ -166,10 +166,13 @@ def sign_metadata(metadata, key, cert): :param cert: x509 cert :type cert: string + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + :returns: Signed Metadata :rtype: string """ - return OneLogin_Saml2_Utils.add_sign(metadata, key, cert) + return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm) @staticmethod def add_x509_key_descriptors(metadata, cert=None): diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index fd661245..89e2576a 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -295,6 +295,10 @@ def __add_default_values(self): if 'wantNameIdEncrypted' not in self.__security.keys(): self.__security['wantNameIdEncrypted'] = False + # Signature Algorithm + if 'signatureAlgorithm' not in self.__security.keys(): + self.__security['signatureAlgorithm'] = OneLogin_Saml2_Constants.RSA_SHA1 + if 'x509cert' not in self.__idp: self.__idp['x509cert'] = '' if 'certFingerprint' not in self.__idp: diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 387abd76..ece360e6 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -759,7 +759,7 @@ def write_temp_file(content): return f_temp @staticmethod - def add_sign(xml, key, cert, debug=False): + def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ Adds signature key and senders certificate to an element (Message or Assertion). @@ -770,11 +770,14 @@ def add_sign(xml, key, cert, debug=False): :param key: The private key :type: string + :param cert: The public + :type: string + :param debug: Activate the xmlsec debug :type: bool - :param cert: The public - :type: string + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string """ if xml is None or xml == '': raise Exception('Empty string supplied as input') @@ -807,7 +810,16 @@ def add_sign(xml, key, cert, debug=False): xmlsec.set_error_callback(print_xmlsec_errors) # Sign the metadata with our private key. - signature = Signature(xmlsec.TransformExclC14N, xmlsec.TransformRsaSha1) + sign_algorithm_transform_map = { + OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1, + OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1, + OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256, + OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384, + OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512 + } + sign_algorithm_transform = sign_algorithm_transform_map.get(sign_algorithm, xmlsec.TransformRsaSha1) + + signature = Signature(xmlsec.TransformExclC14N, sign_algorithm_transform) issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer') if len(issuer) > 0: From fc407d467a1a779b2c2b9dccce29d11c6bff629c Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 17 Jul 2015 00:37:51 +0200 Subject: [PATCH 040/352] Fix typo --- demo-django/saml/advanced_settings.json | 4 ++-- demo-flask/saml/advanced_settings.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json index e4eb9b14..97f3a374 100644 --- a/demo-django/saml/advanced_settings.json +++ b/demo-django/saml/advanced_settings.json @@ -7,8 +7,8 @@ "signMetadata": false, "wantMessagesSigned": false, "wantAssertionsSigned": false, - "wantNameIdEncrypted": false - "signatureAlgorithm" => "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + "wantNameIdEncrypted": false, + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, "contactPerson": { "technical": { diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json index 5bd0a887..97f3a374 100644 --- a/demo-flask/saml/advanced_settings.json +++ b/demo-flask/saml/advanced_settings.json @@ -8,7 +8,7 @@ "wantMessagesSigned": false, "wantAssertionsSigned": false, "wantNameIdEncrypted": false, - "signatureAlgorithm" => "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, "contactPerson": { "technical": { @@ -27,4 +27,4 @@ "url": "http://sp.example.com" } } -} \ No newline at end of file +} From f1b3f93c56cbb04135ca9cb6d2750d94a863c295 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 17 Jul 2015 01:18:21 +0200 Subject: [PATCH 041/352] Related to #53. Support sign validation of different kinds of algorithm --- src/onelogin/saml2/logout_request.py | 7 ++----- src/onelogin/saml2/logout_response.py | 5 +---- src/onelogin/saml2/utils.py | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 92af8d7b..39281a83 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -306,9 +306,6 @@ def is_valid(self, request_data): else: sign_alg = get_data['SigAlg'] - if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: - raise Exception('Invalid signAlg in the recieved Logout Request') - signed_query = 'SAMLRequest=%s' % quote_plus(get_data['SAMLRequest']) if 'RelayState' in get_data: signed_query = '%s&RelayState=%s' % (signed_query, quote_plus(get_data['RelayState'])) @@ -318,12 +315,12 @@ def is_valid(self, request_data): raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') cert = idp_data['x509cert'] - if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert): + if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): raise Exception('Signature validation failed. Logout Request rejected') return True except Exception as err: - # pylint: disable=R0801 + # pylint: disable=R0801sign_alg self.__error = err.__str__() debug = self.__settings.is_debug_active() if debug: diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 1c4572b3..3c79a989 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -120,9 +120,6 @@ def is_valid(self, request_data, request_id=None): else: sign_alg = get_data['SigAlg'] - if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: - raise Exception('Invalid signAlg in the recieved Logout Response') - signed_query = 'SAMLResponse=%s' % quote_plus(get_data['SAMLResponse']) if 'RelayState' in get_data: signed_query = '%s&RelayState=%s' % (signed_query, quote_plus(get_data['RelayState'])) @@ -132,7 +129,7 @@ def is_valid(self, request_data, request_id=None): raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') cert = idp_data['x509cert'] - if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert): + if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): raise Exception('Signature validation failed. Logout Response rejected') return True diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index ece360e6..b921ee14 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -963,7 +963,7 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid return False @staticmethod - def validate_binary_sign(signed_query, signature, cert=None, algorithm=xmlsec.TransformRsaSha1, debug=False): + def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False): """ Validates signed bynary data (Used to validate GET Signature). @@ -995,7 +995,17 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=xmlsec.Tr dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) file_cert.close() - dsig_ctx.verifyBinary(signed_query, algorithm, signature) + # Sign the metadata with our private key. + sign_algorithm_transform_map = { + OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1, + OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1, + OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256, + OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384, + OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512 + } + sign_algorithm_transform = sign_algorithm_transform_map.get(algorithm, xmlsec.TransformRsaSha1) + + dsig_ctx.verifyBinary(signed_query, sign_algorithm_transform, signature) return True except Exception: return False From 5a11b4a06d404568d9e56cd362c66857b36281f4 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 17 Jul 2015 13:04:17 +0200 Subject: [PATCH 042/352] Fix pep8 of last commit --- 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 23518cbc..f93f756e 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -460,11 +460,11 @@ def __decrypt_assertion(self, dom): if 'RetrievalMethod' in child.tag: if child.attrib['Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey': raise Exception('Unsupported Retrieval Method found') - uri = child.attrib['URI'] + uri = child.attrib['URI'] if not uri.startswith('#'): break uri = uri.split('#')[1] - encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id="'+uri +'"]') + encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id="' + uri + '"]') if encrypted_key: keyinfo.append(encrypted_key[0]) From 4c8db5379fecbf77252b003aeeb1ab66bc7732c5 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 17 Jul 2015 13:31:49 +0200 Subject: [PATCH 043/352] Fix minor errors of demo-bottle and add some documentation --- README.md | 11 ++++++++--- demo-bottle/index.py | 2 +- demo-bottle/saml/advanced_settings.json | 5 +++-- demo-bottle/saml/settings.json | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e538bb71..7a10e277 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Key features: * **Easy to use** - Programmer will be allowed to code high-level and low-level programming, 2 easy to use APIs are available. * **Tested** - Thoroughly tested. - * **Popular** - OneLogin's customers use it. Add easy support to your django/flask web projects. + * **Popular** - OneLogin's customers use it. Add easy support to your django/flask/bottle web projects. Installation @@ -108,7 +108,7 @@ Getting started ### Knowing the toolkit ### -The new OneLogin SAML Toolkit contains different folders (certs, lib, demo-django, demo-flask and tests) and some files. +The new OneLogin SAML Toolkit contains different folders (certs, lib, demo-django, demo-flask, demo-bottle and tests) and some files. Let's start describing them: @@ -140,6 +140,11 @@ If you want to create self-signed certs, you can do it at the https://www.samlto openssl req -new -x509 -days 3652 -nodes -out sp.crt -keyout saml.key ``` +#### demo-bottle #### + +This folder contains a Bottle project that will be used as demo to show how to add SAML support to the Bottle Framework. index.py contains all the logic of the demo project, 'templates' is the Bottle templates of the project and 'saml' is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). + + #### demo-flask #### This folder contains a Flask project that will be used as demo to show how to add SAML support to the Flask Framework. 'index.py' is the main flask file that has all the code, this file uses the templates stored at the 'templates' folder. In the 'saml' folder we found the 'certs' folder to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). @@ -176,7 +181,7 @@ There are two ways to provide the settings information: * Use a json object with the setting data and provide it directly to the constructor of the class (if your toolkit integation requires certs, remember to provide the 'custom_base_path' as part of the settings or as a parameter in the constructor. -In the demo-django and in the demo-flask folders you will find a 'saml' folder, inside there is a 'certs' folder and a settings.json and a advanced_settings.json files. Those files contain the settings for the saml toolkit. Copy them in your project and set the correct values. +In the demo-django, demo-flask and demo-bottle folders you will find a 'saml' folder, inside there is a 'certs' folder and a settings.json and a advanced_settings.json files. Those files contain the settings for the saml toolkit. Copy them in your project and set the correct values. This is the settings.json file: diff --git a/demo-bottle/index.py b/demo-bottle/index.py index aa23e864..bf201066 100644 --- a/demo-bottle/index.py +++ b/demo-bottle/index.py @@ -39,7 +39,7 @@ def prepare_bottle_request(req): } -@app.route('/acs', method='POST') +@app.route('/acs/', method='POST') @jinja2_view('index.html', template_lookup=['templates']) def index(): req = prepare_bottle_request(request) diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json index e336fe9d..4ea002ad 100644 --- a/demo-bottle/saml/advanced_settings.json +++ b/demo-bottle/saml/advanced_settings.json @@ -7,7 +7,8 @@ "signMetadata": false, "wantMessagesSigned": false, "wantAssertionsSigned": false, - "wantNameIdEncrypted": false + "wantNameIdEncrypted": false, + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, "contactPerson": { "technical": { @@ -26,4 +27,4 @@ "url": "http://sp.example.com" } } -} \ No newline at end of file +} diff --git a/demo-bottle/saml/settings.json b/demo-bottle/saml/settings.json index 142911f1..fdb13acd 100644 --- a/demo-bottle/saml/settings.json +++ b/demo-bottle/saml/settings.json @@ -4,7 +4,7 @@ "sp": { "entityId": "https:///metadata/", "assertionConsumerService": { - "url": "https:///?acs", + "url": "https:// Date: Fri, 17 Jul 2015 19:24:04 +0200 Subject: [PATCH 044/352] Release 2.1.4 version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a4c8ebcc..faf8612b 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.1.3', + version='2.1.4', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', From d588d12bbc1834754c124e0c728177d75c4e98e9 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 29 Jul 2015 00:40:02 +0200 Subject: [PATCH 045/352] Fix server_port can be None #77 --- 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 b921ee14..9cf84900 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -261,7 +261,7 @@ def get_self_url_host(request_data): else: protocol = 'http' - if 'server_port' in request_data: + if 'server_port' in request_data and request_data['server_port'] is not None: port_number = str(request_data['server_port']) port = ':' + port_number From 3b124a3d3be74ab8d625cb0a7c80a9419cd57c2a Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 13:35:34 +0200 Subject: [PATCH 046/352] Fix bug on settings constructor related to sp_validation_only --- src/onelogin/saml2/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 89e2576a..abf713f6 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -70,7 +70,7 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals :param custom_base_path: Path where are stored the settings file and the cert folder :type custom_base_path: string """ - self.__sp_validation_only = False + self.__sp_validation_only = sp_validation_only self.__paths = {} self.__strict = False self.__debug = False From 934750641489e6065ff4c8cdab1c79c9326f27b8 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 12 Aug 2015 19:22:33 +0200 Subject: [PATCH 047/352] Make SPNameQualifier optional on the generateNameId method. Avoid the use of SPNameQualifier when generating the NameID on the LogoutRequest builder. --- src/onelogin/saml2/logout_request.py | 4 +++- src/onelogin/saml2/utils.py | 3 ++- tests/src/OneLogin/saml2_tests/utils_test.py | 22 +++++++++++++++++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 39281a83..235e07d2 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -65,13 +65,15 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): if name_id is not None: nameIdFormat = sp_data['NameIDFormat'] + spNameQualifier = None else: name_id = idp_data['entityId'] nameIdFormat = OneLogin_Saml2_Constants.NAMEID_ENTITY + spNameQualifier = sp_data['entityId'] name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, - sp_data['entityId'], + spNameQualifier, nameIdFormat, cert ) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 9cf84900..e7a973fd 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -611,7 +611,8 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): name_id_container.setAttribute("xmlns:saml", OneLogin_Saml2_Constants.NS_SAML) name_id = doc.createElement('saml:NameID') - name_id.setAttribute('SPNameQualifier', sp_nq) + if sp_nq is not None: + name_id.setAttribute('SPNameQualifier', sp_nq) name_id.setAttribute('Format', sp_format) name_id.appendChild(doc.createTextNode(value)) name_id_container.appendChild(name_id) diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index bb95cd02..497b610e 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -557,9 +557,10 @@ def testQuery(self): signature_nodes_5 = OneLogin_Saml2_Utils.query(dom, './/ds:SignatureValue', assertion) self.assertEqual(1, len(signature_nodes_5)) - def testGenerateNameId(self): + def testGenerateNameIdWithSPNameQualifier(self): """ Tests the generateNameId method of the OneLogin_Saml2_Utils + Adding a SPNameQualifier """ name_id_value = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde' entity_id = 'http://stuff.com/endpoints/metadata.php' @@ -577,6 +578,25 @@ def testGenerateNameId(self): expected_name_id_enc = '' self.assertIn(expected_name_id_enc, name_id_enc) + def testGenerateNameIdWithoutSPNameQualifier(self): + """ + Tests the generateNameId method of the OneLogin_Saml2_Utils + """ + name_id_value = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde' + name_id_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified' + + name_id = OneLogin_Saml2_Utils.generate_name_id(name_id_value, None, name_id_format) + expected_name_id = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde' + self.assertEqual(name_id, expected_name_id) + + settings_info = self.loadSettingsJSON() + x509cert = settings_info['idp']['x509cert'] + key = OneLogin_Saml2_Utils.format_cert(x509cert) + + name_id_enc = OneLogin_Saml2_Utils.generate_name_id(name_id_value, None, name_id_format, key) + expected_name_id_enc = '' + self.assertIn(expected_name_id_enc, name_id_enc) + def testCalculateX509Fingerprint(self): """ Tests the calculateX509Fingerprint method of the OneLogin_Saml2_Utils From 6ffa4e485f5e89e438994d93f186d839f851c4ec Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 13 Aug 2015 20:50:35 +0200 Subject: [PATCH 048/352] Remove unnecesary dependence. M2crypto is not used. #79 --- README.md | 1 - setup.py | 1 - 2 files changed, 2 deletions(-) diff --git a/README.md b/README.md index 7a10e277..6d4f1084 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ Installation ### Dependences ### * python 2.7 - * [M2Crypto](https://pypi.python.org/pypi/M2Crypto) A Python crypto and SSL toolkit (depends on openssl, swig) * [dm.xmlsec.binding](https://pypi.python.org/pypi/dm.xmlsec.binding) Cython/lxml based binding for the XML security library (depends on python-dev libxml2-dev libxmlsec1-dev) * [isodate](https://pypi.python.org/pypi/isodate) An ISO 8601 date/time/duration parser and formater * [defusedxml](https://pypi.python.org/pypi/defusedxml) XML bomb protection for Python stdlib modules diff --git a/setup.py b/setup.py index faf8612b..87751602 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,6 @@ }, test_suite='tests', install_requires=[ - 'M2Crypto==0.22.3', 'dm.xmlsec.binding==1.3.2', 'isodate==0.5.0', 'defusedxml==0.4.1', From 590e5aeb6d375ead27a2a7c3d400bb27e22df737 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 23 Aug 2015 11:17:35 +0200 Subject: [PATCH 049/352] Allows the RequestedAuthnContext Comparison attribute to be set via config --- README.md | 2 ++ src/onelogin/saml2/authn_request.py | 10 ++++-- .../saml2_tests/authn_request_test.py | 33 +++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6d4f1084..fdc50a83 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,8 @@ In addition to the required settings data (idp, sp), there is extra information // Set true or don't present thi parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' // Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'), 'requestedAuthnContext': true, + // Allows the authn comparison parameter to be set, defaults to 'exact' if the setting is not present. + 'requestedAuthnContextComparison': 'exact', // In some environment you will need to set how long the published metadata of the Service Provider gonna be valid. // is possible to not set the 2 following parameters (or set to null) and default values will be set (2 days, 1 week) diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 8e8495e7..b36734fe 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -74,12 +74,16 @@ def __init__(self, settings, force_authn=False, is_passive=False): requested_authn_context_str = '' if 'requestedAuthnContext' in security.keys() and security['requestedAuthnContext'] is not False: + authn_comparison = 'exact' + if 'requestedAuthnContextComparison' in security.keys(): + authn_comparison = security['requestedAuthnContextComparison'] + if security['requestedAuthnContext'] is True: - requested_authn_context_str = """ + requested_authn_context_str = """ urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport - """ + """ % authn_comparison else: - requested_authn_context_str = ' ' + requested_authn_context_str = ' ' % authn_comparison for authn_context in security['requestedAuthnContext']: requested_authn_context_str += '%s' % authn_context requested_authn_context_str += ' ' diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 2d5a0a2f..eef0a590 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -117,6 +117,39 @@ def testCreateRequestAuthContext(self): self.assertIn(OneLogin_Saml2_Constants.AC_PASSWORD_PROTECTED, inflated) self.assertIn(OneLogin_Saml2_Constants.AC_X509, inflated) + def testCreateRequestAuthContextComparision(self): + """ + Tests the OneLogin_Saml2_Authn_Request Constructor. + The creation of a deflated SAML Request with defined AuthnContextComparison + """ + saml_settings = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(saml_settings) + authn_request = OneLogin_Saml2_Authn_Request(settings) + authn_request_encoded = authn_request.get_request() + decoded = b64decode(authn_request_encoded) + inflated = decompress(decoded, -15) + self.assertRegexpMatches(inflated, '^ Date: Sun, 23 Aug 2015 13:10:20 +0200 Subject: [PATCH 050/352] Be able to retrieve Session Timeout after processResponse --- src/onelogin/saml2/auth.py | 10 ++++++++++ tests/src/OneLogin/saml2_tests/auth_test.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 49cbfbce..a02592ae 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -54,6 +54,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): self.__attributes = [] self.__nameid = None self.__session_index = None + self.__session_expiration = None self.__authenticated = False self.__errors = [] self.__error_reason = None @@ -95,6 +96,7 @@ def process_response(self, request_id=None): self.__attributes = response.get_attributes() self.__nameid = response.get_nameid() self.__session_index = response.get_session_index() + self.__session_expiration = response.get_session_not_on_or_after() self.__authenticated = True else: @@ -213,6 +215,14 @@ def get_session_index(self): """ return self.__session_index + def get_session_expiration(self): + """ + Returns the SessionNotOnOrAfter from the AuthnStatement. + :returns: The SessionNotOnOrAfter of the assertion + :rtype: DateTime|null + """ + return self.__session_expiration + def get_errors(self): """ Returns a list with code errors if something went wrong diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index ea0b9fd0..4b127cea 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -96,6 +96,26 @@ def testGetSessionIndex(self): auth2.process_response() self.assertEqual('_6273d77b8cde0c333ec79d22a9fa0003b9fe2d75cb', auth2.get_session_index()) + def testGetSessionExpiration(self): + """ + Tests the get_session_expiration method of the OneLogin_Saml2_Auth class + """ + settings_info = self.loadSettingsJSON() + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + self.assertIsNone(auth.get_session_expiration()) + + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + del request_data['get_data'] + request_data['post_data'] = { + 'SAMLResponse': message + } + auth2 = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + self.assertIsNone(auth2.get_session_expiration()) + + auth2.process_response() + self.assertEqual(1392802621, auth2.get_session_expiration()) + def testGetLastErrorReason(self): """ Tests the get_last_error_reason method of the OneLogin_Saml2_Auth class From 72165de7564ae8819f723755b2808e54c9531c3d Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 28 Aug 2015 23:09:05 +0200 Subject: [PATCH 051/352] Clarify the use of the certFingerprint --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fdc50a83..e2d6ccc3 100644 --- a/README.md +++ b/README.md @@ -255,13 +255,17 @@ This is the settings.json file: // Public x509 certificate of the IdP "x509cert": "" /* - * Instead of use the whole x509cert you can use a fingerprint + * Instead of use the whole x509cert you can use a fingerprint in order to + * validate a SAMLResponse. * (openssl x509 -noout -fingerprint -in "idp.crt" to generate it, - * or add for example the -sha256 , -sha384 or -sha512 parameter) + * or add for example the -sha256 , -sha384 or -sha512 parameter) * * If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to * let the toolkit know which algorithm was used. Possible values: sha1, sha256, sha384 or sha512 * 'sha1' is the default value. + * + * Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you + * will need to provide the whole x509cert. */ // 'certFingerprint' => '', // 'certFingerprintAlgorithm' => 'sha1', From 6fcc7d44ca232fd1173d69b160f3bb1a1737a058 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 28 Aug 2015 23:10:51 +0200 Subject: [PATCH 052/352] Update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e2d6ccc3..328be709 100644 --- a/README.md +++ b/README.md @@ -742,6 +742,7 @@ Main class of OneLogin Python Toolkit * ***get_attribute*** Returns the requested SAML attribute. * ***get_nameid*** Returns the nameID. * ***get_session_index*** Gets the SessionIndex from the AuthnStatement. +* ***get_session_expiration*** Gets the SessionNotOnOrAfter from the AuthnStatement. * ***get_errors*** Returns a list with code errors if something went wrong. * ***get_last_error_reason*** Returns the reason of the last error * ***get_sso_url*** Gets the SSO url. From f80ee0625fb0fe3c2b2d8eb282c8871c6f84d652 Mon Sep 17 00:00:00 2001 From: Nick Barrett Date: Tue, 8 Sep 2015 17:57:16 +0100 Subject: [PATCH 053/352] Make idp settings optional --- src/onelogin/saml2/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index abf713f6..20af3580 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -196,8 +196,9 @@ def __load_settings_from_dict(self, settings): if len(errors) == 0: self.__errors = [] self.__sp = settings['sp'] - self.__idp = settings['idp'] + if 'idp' in settings: + self.__idp = settings['idp'] if 'strict' in settings: self.__strict = settings['strict'] if 'debug' in settings: From 149767f3402b649ae54c980959f89b5e0409ffa1 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 3 Nov 2015 13:30:15 +0100 Subject: [PATCH 054/352] Release 2.1.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 87751602..07707884 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.1.4', + version='2.1.5', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', From ddaa132f27d13a57752bcf4598217235a188ece5 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 3 Nov 2015 13:42:18 +0100 Subject: [PATCH 055/352] create changelog --- changelog.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 changelog.md diff --git a/changelog.md b/changelog.md new file mode 100644 index 00000000..46d13fd4 --- /dev/null +++ b/changelog.md @@ -0,0 +1,72 @@ +# java-saml changelog + +### 2.1.5 (Nov 3, 2015) +* [#86](https://github.com/onelogin/python-saml/pull/86) Make idp settings optional (Usefull when validating SP metadata) +* [#79](https://github.com/onelogin/python-saml/pull/79) Remove unnecesary dependence. M2crypto is not used. +* [#77](https://github.com/onelogin/python-saml/pull/77) Fix server_port can be None +* Fix bug on settings constructor related to sp_validation_only +* Make SPNameQualifier optional on the generateNameId method. Avoid the use of SPNameQualifier when generating the NameID on the LogoutRequest builder. +* Allows the RequestedAuthnContext Comparison attribute to be set via settings +* Be able to retrieve Session Timeout after processResponse +* Update documentation. Clarify the use of the certFingerprint + +### 2.1.4 (Jul 17, 2015) +* Now the SP is able to select the algorithm to be used on signatures (DSA_SHA1, RSA_SHA1, RSA_SHA256, RSA_SHA384, RSA_SHA512). +* Support sign validation of different kinds of algorithm +* Add demo example of the Bottle framework. +* [#73](https://github.com/onelogin/python-saml/pull/73) Improve decrypt method +* Handle valid but uncommon dsig block with no URI in the reference +* Split the setting check methods. Now 1 method for IdP settings and other for SP settings +* Let the setting object to avoid the IdP setting check. required if we want to publish SP * SAML Metadata when the IdP data is still not provided. + +### 2.1.3 (Jun 25, 2015) +* Do accesible the ID of the object Logout Request (id attribute) +* Add SAMLServiceProviderBackend reference to the README.md +* Solve HTTPs issue on demos +* Fix PHP-style array element in settings json +* Add fingerprint algorithm support. Previously the toolkit assumed SHA-1 algorithm +* Fix creation of metadata with no SLS, when using settings.get_sp_metadata() +* Allow configuration of metadata caching/expiry via settings +* Allow metadata signing with SP key specified as config value, not file +* Set NAMEID_UNSPECIFIED as default NameIDFormat to prevent conflicts +* Improve validUntil/cacheDuration metadata settings + +### 2.1.2 (Feb 26, 2015) +* Fix wrong element order in generated metadata (SLS before NameID). metadata xsd updated +* Added SLO with nameID and SessionIndex in the demos +* Fix Exception message on Destination validation of the Logout_request + +### 2.1.0 (Jan 14, 2015) +* Update the dm.xmlsec.binding library to 1.3.2 (Improved transform support, Workaround for buildout problem) +* Fix flask demo settings example. +* Add nameID & sessionIndex support on Logout Request +* Reject SAML Response if not signed and strict = false +* Add ForceAuh and IsPassive support on AuthN Request + +### 2.0.2 (Dec 5, 2014) +* Adding AuthnContextClassRef support +* Process nested StatusCode +* Fix settings bug + +### 2.0.1 (Nov 13, 2014) +* SSO and SLO (SP-Initiated and IdP-Initiated). +* Assertion and nameId encryption. +* Assertion signature. +* Message signature: AuthNRequest, LogoutRequest, LogoutResponses. +* Enable an Assertion Consumer Service endpoint. +* Enable a Single Logout Service endpoint. +* Publish the SP metadata (which can be signed). + +### 1.1.0 (Sep 4, 2014) +* Security improved, added more checks at the SAMLResponse validation + +### 1.0.0 (Jun 26, 2014) +* OneLogin's SAML Python Toolkit v1.0.0 + + + + + + + + From 8fa11cc3d881f90ec0859eccc704501ee0c584d4 Mon Sep 17 00:00:00 2001 From: Laurent Raufaste Date: Tue, 3 Nov 2015 14:27:02 -0500 Subject: [PATCH 056/352] The changelog should not reference java-saml --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 46d13fd4..c6b6df60 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,4 @@ -# java-saml changelog +# python-saml changelog ### 2.1.5 (Nov 3, 2015) * [#86](https://github.com/onelogin/python-saml/pull/86) Make idp settings optional (Usefull when validating SP metadata) From 53f6bf4baf42c54cc60a306d5683a9700467f91a Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 27 Nov 2015 20:28:33 +0100 Subject: [PATCH 057/352] Support Responses that don't have AttributeStatements --- setup.py | 1 + src/onelogin/saml2/response.py | 4 +- src/onelogin/saml2/settings.py | 4 + .../signed_assertion_response.xml.base64 | 1 + .../src/OneLogin/saml2_tests/response_test.py | 79 +++++++++++++++++-- 5 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 tests/data/responses/invalids/signed_assertion_response.xml.base64 diff --git a/setup.py b/setup.py index 07707884..af4c8a1f 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ extras_require={ 'test': ( 'coverage==3.7.1', + 'freezegun==0.3.5', 'pylint==1.3.1', 'pep8==1.5.7', 'pyflakes==0.8.1', diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index f93f756e..a8e9d444 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -114,9 +114,9 @@ def is_valid(self, request_data, request_id=None): if len(encrypted_nameid_nodes) == 0: raise Exception('The NameID of the Response is not encrypted and the SP require it') - # Checks that there is at least one AttributeStatement + # Checks that there is at least one AttributeStatement if required attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement') - if not attribute_statement_nodes: + if security.get('wantAttributeStatement', True) and not attribute_statement_nodes: raise Exception('There is no AttributeStatement on the Response') # Validates Asserion timestamps diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 20af3580..e93fee51 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -300,6 +300,10 @@ def __add_default_values(self): if 'signatureAlgorithm' not in self.__security.keys(): self.__security['signatureAlgorithm'] = OneLogin_Saml2_Constants.RSA_SHA1 + # AttributeStatement required by default + if 'wantAttributeStatement' not in self.__security.keys(): + self.__security['wantAttributeStatement'] = True + if 'x509cert' not in self.__idp: self.__idp['x509cert'] = '' if 'certFingerprint' not in self.__idp: diff --git a/tests/data/responses/invalids/signed_assertion_response.xml.base64 b/tests/data/responses/invalids/signed_assertion_response.xml.base64 new file mode 100644 index 00000000..b169ac9c --- /dev/null +++ b/tests/data/responses/invalids/signed_assertion_response.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIERlc3RpbmF0aW9uPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIElEPSJwZnhkYjRkOWVmZS1kMGFkLTAwODYtY2U4OC1jMjg4Njg3Y2FjNjEiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNjEyYmJmOWIxNjQ1Mjk0YWEwYjQ2MzdiMWJjNWYzOWRlOGI3OWNlYiIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTMxVDAwOjM3OjE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhkYjRkOWVmZS1kMGFkLTAwODYtY2U4OC1jMjg4Njg3Y2FjNjEiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmpjMklRWFNoc3dzTG85TkdJSHp2cGtBaXY4ND08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+aUVqR2QrdWFqSVArYU9ucGo4MjYxUzRBaWdMeXJqc0pheTJzdVFKakhhVHlETlh4TFhWQ3AxZG1PR0JhZGhmRUtnWVJsaTFBZDA1QktBejlpd3NBME14OGZ6SmFhSlBUbHM2NS93ODZTSEN4NTdrNXhteDBSUjhuR09MOU1vb2lidnZWeTVRODl2Z2lnVWN5cWJUY0dxaU5uSVNCWGZuYVR2dnpQYS9QbWJ3PTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNWekNDQWNBQ0NRRElWSGFOU0JZTDZUQU5CZ2txaGtpRzl3MEJBUXNGQURCd01Rc3dDUVlEVlFRR0V3SkdVakVPTUF3R0ExVUVDQXdGVUdGeWFYTXhEakFNQmdOVkJBY01CVkJoY21sek1SWXdGQVlEVlFRS0RBMU9iM1poY0c5emRDQlVSVk5VTVNrd0p3WUpLb1pJaHZjTkFRa0JGaHBtYkc5eVpXNTBMbkJwWjI5MWRFQnViM1poY0c5emRDNW1jakFlRncweE5EQXlNVE14TXpVek5EQmFGdzB4TlRBeU1UTXhNelV6TkRCYU1IQXhDekFKQmdOVkJBWVRBa1pTTVE0d0RBWURWUVFJREFWUVlYSnBjekVPTUF3R0ExVUVCd3dGVUdGeWFYTXhGakFVQmdOVkJBb01EVTV2ZG1Gd2IzTjBJRlJGVTFReEtUQW5CZ2txaGtpRzl3MEJDUUVXR21ac2IzSmxiblF1Y0dsbmIzVjBRRzV2ZG1Gd2IzTjBMbVp5TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FDaExGSG4zTG5ONEpRLzdXQ2RZdXB4a1VnY05PUW5QRit5bGwrL0RQcHV4OW5wZlkwNTlQSVVhdEI4WDdrQ241aTh0UndJeS9pa0hKUjZNcjgrTVB2YzZWT1pEeFBOZFp2TW8vOGxoeHJiTjNKZHJ3M3doWm1VL0tQUjlGM0JkRmR1K1NMenJNbDFURFVabFB0WTlYelVGWGNxTjhJWGN5OFRKekNCZU5leTNRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNEdCQUN0SjhmZUd6ZTFOSEI1VncxOGpNVVB2SG83SDNHd21qNlpEQVhRbGFpQVhNdU5CeE5YVldWd2lmbDZWK25XM3c5UWE3RmVvL25aL080VFVPSDFueithZGtsY0NENFFwWmFFSWJtQWJyaVBXSktnYjRMV0docVFydXdZUjdJdFRSMU1OWDlnTGJQMHowenZERVFubnQvVlVXRkVCTFNKcTRaNE5yZThMRm1TMjwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIElEPSJwZng3ZTNmMWYxMS0zZDM4LTdkYTUtNTVlZC05YjRkNmMwYTQ0ZWIiIElzc3VlSW5zdGFudD0iMjAxNC0wMy0zMVQwMDozNzoxNloiIFZlcnNpb249IjIuMCI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPgogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4N2UzZjFmMTEtM2QzOC03ZGE1LTU1ZWQtOWI0ZDZjMGE0NGViIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT42d1dzemxmRllidGRzNnR5K24rT3RESnZLRUE9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmVVRTkxaFA2bTZ3VlVtd0liVkpTZnhWdkppOVFwd3QwZGpIUDRpcW5yMk42Y2ZWVmV3eERVM0dXQTlsOVpWanltV292RkltL1k0dGR3VTM0R2RiaS8yaWhvMmd0OGVWR3c4ajNSdVFoTVVIc1ZmK2hIaDJlSDhuMHhqZEFqdGRoTkhIT3pMMnREV3hYazg2T2VZbmw4Slp1VTdCRUVTZUtlQzlieDBPUW5ZTT08L2RzOlNpZ25hdHVyZVZhbHVlPgo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDVnpDQ0FjQUNDUURJVkhhTlNCWUw2VEFOQmdrcWhraUc5dzBCQVFzRkFEQndNUXN3Q1FZRFZRUUdFd0pHVWpFT01Bd0dBMVVFQ0F3RlVHRnlhWE14RGpBTUJnTlZCQWNNQlZCaGNtbHpNUll3RkFZRFZRUUtEQTFPYjNaaGNHOXpkQ0JVUlZOVU1Ta3dKd1lKS29aSWh2Y05BUWtCRmhwbWJHOXlaVzUwTG5CcFoyOTFkRUJ1YjNaaGNHOXpkQzVtY2pBZUZ3MHhOREF5TVRNeE16VXpOREJhRncweE5UQXlNVE14TXpVek5EQmFNSEF4Q3pBSkJnTlZCQVlUQWtaU01RNHdEQVlEVlFRSURBVlFZWEpwY3pFT01Bd0dBMVVFQnd3RlVHRnlhWE14RmpBVUJnTlZCQW9NRFU1dmRtRndiM04wSUZSRlUxUXhLVEFuQmdrcWhraUc5dzBCQ1FFV0dtWnNiM0psYm5RdWNHbG5iM1YwUUc1dmRtRndiM04wTG1aeU1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ2hMRkhuM0xuTjRKUS83V0NkWXVweGtVZ2NOT1FuUEYreWxsKy9EUHB1eDlucGZZMDU5UElVYXRCOFg3a0NuNWk4dFJ3SXkvaWtISlI2TXI4K01QdmM2Vk9aRHhQTmRadk1vLzhsaHhyYk4zSmRydzN3aFptVS9LUFI5RjNCZEZkdStTTHpyTWwxVERVWmxQdFk5WHpVRlhjcU44SVhjeThUSnpDQmVOZXkzUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJDd1VBQTRHQkFDdEo4ZmVHemUxTkhCNVZ3MThqTVVQdkhvN0gzR3dtajZaREFYUWxhaUFYTXVOQnhOWFZXVndpZmw2VituVzN3OVFhN0Zlby9uWi9PNFRVT0gxbnorYWRrbGNDRDRRcFphRUlibUFicmlQV0pLZ2I0TFdHaHFRcnV3WVI3SXRUUjFNTlg5Z0xiUDB6MHp2REVRbm50L1ZVV0ZFQkxTSnE0WjROcmU4TEZtUzI8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCIgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCI+XzNhZjYyZjFkMDM1MTNiZGQ2MWRkNWJmMDRkM2RlYjdhYTYxNzQ4MGUyMjwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNjEyYmJmOWIxNjQ1Mjk0YWEwYjQ2MzdiMWJjNWYzOWRlOGI3OWNlYiIgTm90T25PckFmdGVyPSIyMDIzLTEwLTAyVDA1OjU3OjE2WiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0zMVQwMDozNjo0NloiIE5vdE9uT3JBZnRlcj0iMjAyMy0xMC0wMlQwNTo1NzoxNloiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAzLTMxVDAwOjM3OjE2WiIgU2Vzc2lvbkluZGV4PSJfODVlN2NmZTE2ZDZlN2U2MDBiZDk4YmJjMmI0MzcxZTFjNjk1ODhhNGRhIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAzLTMxVDA4OjM3OjE2WiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index aae4d3c2..35241e52 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -4,6 +4,9 @@ # All rights reserved. from base64 import b64decode, b64encode +from datetime import datetime +from datetime import timedelta +from freezegun import freeze_time import json from os.path import dirname, join, exists import unittest @@ -434,11 +437,77 @@ def testIsInValidNoStatement(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 no AttributeStatement on the Response', e.message) + self.assertFalse(response_2.is_valid(self.get_request_data())) + self.assertEqual('There is no AttributeStatement on the Response', response_2.get_error()) + + def testIsValidOptionalStatement(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case AttributeStatement is optional + """ + # shortcut + json_settings = self.loadSettingsJSON() + # ensure valid entityid + json_settings['sp']['entityId'] = 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php' + json_settings['idp']['entityId'] = 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php' + json_settings['idp']['x509cert'] = """ +MIICVzCCAcACCQDIVHaNSBYL6TANBgkqhkiG9w0BAQsFADBwMQswCQYDVQQGEwJG +UjEOMAwGA1UECAwFUGFyaXMxDjAMBgNVBAcMBVBhcmlzMRYwFAYDVQQKDA1Ob3Zh +cG9zdCBURVNUMSkwJwYJKoZIhvcNAQkBFhpmbG9yZW50LnBpZ291dEBub3ZhcG9z +dC5mcjAeFw0xNDAyMTMxMzUzNDBaFw0xNTAyMTMxMzUzNDBaMHAxCzAJBgNVBAYT +AkZSMQ4wDAYDVQQIDAVQYXJpczEOMAwGA1UEBwwFUGFyaXMxFjAUBgNVBAoMDU5v +dmFwb3N0IFRFU1QxKTAnBgkqhkiG9w0BCQEWGmZsb3JlbnQucGlnb3V0QG5vdmFw +b3N0LmZyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQChLFHn3LnN4JQ/7WCd +YupxkUgcNOQnPF+yll+/DPpux9npfY059PIUatB8X7kCn5i8tRwIy/ikHJR6Mr8+ +MPvc6VOZDxPNdZvMo/8lhxrbN3Jdrw3whZmU/KPR9F3BdFdu+SLzrMl1TDUZlPtY +9XzUFXcqN8IXcy8TJzCBeNey3QIDAQABMA0GCSqGSIb3DQEBCwUAA4GBACtJ8feG +ze1NHB5Vw18jMUPvHo7H3Gwmj6ZDAXQlaiAXMuNBxNXVWVwifl6V+nW3w9Qa7Feo +/nZ/O4TUOH1nz+adklcCD4QpZaEIbmAbriPWJKgb4LWGhqQruwYR7ItTR1MNX9gL +bP0z0zvDEQnnt/VUWFEBLSJq4Z4Nre8LFmS2 +""".strip() + + settings = OneLogin_Saml2_Settings(json_settings) + settings.set_strict(True) + + # want AttributeStatement True by default + self.assertTrue(settings.get_security_data()['wantAttributeStatement']) + + xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'signed_assertion_response.xml.base64')) + + not_on_or_after = datetime.strptime('2014-03-31T08:37:16Z', '%Y-%m-%dT%H:%M:%SZ') + not_on_or_after -= timedelta(seconds=150) + + response = OneLogin_Saml2_Response(settings, xml) + with freeze_time(not_on_or_after): + self.assertFalse(response.is_valid({ + 'https': 'on', + 'http_host': 'pitbulk.no-ip.org', + 'script_name': 'newonelogin/demo1/index.php?acs' + })) + self.assertEqual('There is no AttributeStatement on the Response', response.get_error()) + + security = settings.get_security_data() + self.assertTrue(security['wantAttributeStatement']) + + # change wantAttributeStatement to optional + json_settings['security']['wantAttributeStatement'] = False + settings = OneLogin_Saml2_Settings(json_settings) + settings.set_strict(True) + + # check settings + self.assertFalse(settings.get_security_data()['wantAttributeStatement']) + + response = OneLogin_Saml2_Response(settings, xml) + response.is_valid(self.get_request_data()) + + # check response + with freeze_time(not_on_or_after): + self.assertTrue(response.is_valid({ + 'https': 'on', + 'http_host': 'pitbulk.no-ip.org', + 'script_name': 'newonelogin/demo1/index.php?acs' + })) + self.assertIsNone(response.get_error()) def testIsInValidNoKey(self): """ From 0470546c09663261850decc0b06298696734df04 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 16 Dec 2015 01:01:53 +0100 Subject: [PATCH 058/352] Fix Organization element on SP metadata. There was a bug when adding more than 1 organization. --- src/onelogin/saml2/metadata.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 891500e5..05cd1e50 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -90,21 +90,17 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N str_organization = '' if len(organization) > 0: - organization_info = [] + organization_names = [] + organization_displaynames = [] + organization_urls = [] for (lang, info) in organization.items(): - org = """ - %(name)s - %(display_name)s - %(url)s - """ % \ - { - 'lang': lang, - 'name': info['name'], - 'display_name': info['displayname'], - 'url': info['url'], - } - organization_info.append(org) - str_organization = '\n'.join(organization_info) + organization_names.append(""" %s""" % (lang, info['name'])) + organization_displaynames.append(""" %s""" % (lang, info['displayname'])) + organization_urls.append(""" %s""" % (lang, info['url'])) + org_data = '\n'.join(organization_names) + '\n' + '\n'.join(organization_displaynames) + '\n' + '\n'.join(organization_urls) + str_organization = """ +%(org)s + """ % {'org': org_data} str_contacts = '' if len(contacts) > 0: @@ -149,7 +145,6 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N 'organization': str_organization, 'contacts': str_contacts, } - return metadata @staticmethod From f4857f04157480249b78902c554430422da9f0cd Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 29 Jan 2016 16:51:05 +0100 Subject: [PATCH 059/352] Update docs adding reference to test depencence installation --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 328be709..330e2aaf 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,13 @@ Read more at https://pythonhosted.org/an_example_pypi_project/setuptools.html Contains the unit test of the toolkit. -In order to execute the test you only need to load the virtualenv with the toolkit installed on it and execute: +In order to execute the test you need to load the virtualenv with the toolkit installed on it and execute: +``` +pip install -e ".[test]" +``` +that will install dependences that the test requires. + +and later execute: ``` python setup.py test ``` From efe33a736833797663c82f341906afb9a8245a0e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 2 Feb 2016 12:38:04 +0100 Subject: [PATCH 060/352] CI Integration. TeamCity --- setup.py | 3 ++- tests/src/OneLogin/saml2_tests/auth_test.py | 10 ++++++++++ tests/src/OneLogin/saml2_tests/authn_request_test.py | 10 ++++++++++ tests/src/OneLogin/saml2_tests/error_test.py | 11 ++++++++++- tests/src/OneLogin/saml2_tests/logout_request_test.py | 10 ++++++++++ .../src/OneLogin/saml2_tests/logout_response_test.py | 10 ++++++++++ tests/src/OneLogin/saml2_tests/metadata_test.py | 10 ++++++++++ tests/src/OneLogin/saml2_tests/response_test.py | 10 ++++++++++ tests/src/OneLogin/saml2_tests/settings_test.py | 10 ++++++++++ .../src/OneLogin/saml2_tests/signed_response_test.py | 10 ++++++++++ tests/src/OneLogin/saml2_tests/utils_test.py | 10 ++++++++++ 11 files changed, 102 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index af4c8a1f..05bbe08c 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,8 @@ ], extras_require={ 'test': ( - 'coverage==3.7.1', + 'teamcity-messages==1.17', + 'coverage==4.0.3', 'freezegun==0.3.5', 'pylint==1.3.1', 'pep8==1.5.7', diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 4b127cea..ee16c726 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -7,6 +7,8 @@ import json from os.path import dirname, join, exists import unittest +from teamcity import is_running_under_teamcity +from teamcity.unittestpy import TeamcityTestRunner from urlparse import urlparse, parse_qs from onelogin.saml2.auth import OneLogin_Saml2_Auth @@ -858,3 +860,11 @@ def testBuildResponseSignature(self): auth2.build_response_signature(message, relay_state) except Exception as e: self.assertIn("Trying to sign the SAMLResponse but can't load the SP private key", e.message) + + +if __name__ == '__main__': + if is_running_under_teamcity(): + runner = TeamcityTestRunner() + else: + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index eef0a590..1c8b1eb5 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -7,6 +7,8 @@ import json from os.path import dirname, join, exists import unittest +from teamcity import is_running_under_teamcity +from teamcity.unittestpy import TeamcityTestRunner from urlparse import urlparse, parse_qs from zlib import decompress @@ -257,3 +259,11 @@ def testCreateEncSAMLRequest(self): self.assertRegexpMatches(inflated, 'http://stuff.com/endpoints/metadata.php') self.assertRegexpMatches(inflated, 'Format="urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"') self.assertRegexpMatches(inflated, 'ProviderName="SP prueba"') + + +if __name__ == '__main__': + if is_running_under_teamcity(): + runner = TeamcityTestRunner() + else: + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/error_test.py b/tests/src/OneLogin/saml2_tests/error_test.py index 76e24308..959c2390 100644 --- a/tests/src/OneLogin/saml2_tests/error_test.py +++ b/tests/src/OneLogin/saml2_tests/error_test.py @@ -4,6 +4,8 @@ # All rights reserved. import unittest +from teamcity import is_running_under_teamcity +from teamcity.unittestpy import TeamcityTestRunner from onelogin.saml2.errors import OneLogin_Saml2_Error @@ -11,7 +13,14 @@ class OneLogin_Saml2_Error_Test(unittest.TestCase): """ Tests the OneLogin_Saml2_Error Constructor. """ - def runTest(self): exception = OneLogin_Saml2_Error('test') self.assertEqual(exception.message, 'test') + + +if __name__ == '__main__': + if is_running_under_teamcity(): + runner = TeamcityTestRunner() + else: + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index da201817..9da12166 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -7,6 +7,8 @@ import json from os.path import dirname, join, exists import unittest +from teamcity import is_running_under_teamcity +from teamcity.unittestpy import TeamcityTestRunner from urlparse import urlparse, parse_qs from xml.dom.minidom import parseString @@ -438,3 +440,11 @@ def testIsValidSign(self): self.assertFalse(valid) except Exception as e: self.assertIn('In order to validate the sign on the Logout Request, the x509cert of the IdP is required', e.message) + + +if __name__ == '__main__': + if is_running_under_teamcity(): + runner = TeamcityTestRunner() + else: + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 5b618b8d..0cef7f37 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -6,6 +6,8 @@ import json from os.path import dirname, join, exists import unittest +from teamcity import is_running_under_teamcity +from teamcity.unittestpy import TeamcityTestRunner from urlparse import urlparse, parse_qs from xml.dom.minidom import parseString @@ -381,3 +383,11 @@ def testIsValid(self): response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) self.assertTrue(response_3.is_valid(request_data)) + + +if __name__ == '__main__': + if is_running_under_teamcity(): + runner = TeamcityTestRunner() + else: + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 6fbfbf50..26a03f11 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -9,6 +9,8 @@ from time import strftime from datetime import datetime import unittest +from teamcity import is_running_under_teamcity +from teamcity.unittestpy import TeamcityTestRunner from onelogin.saml2.metadata import OneLogin_Saml2_Metadata from onelogin.saml2.settings import OneLogin_Saml2_Settings @@ -222,3 +224,11 @@ def testAddX509KeyDescriptors(self): self.assertFalse(True) except Exception as e: self.assertIn('Error parsing metadata', e.message) + + +if __name__ == '__main__': + if is_running_under_teamcity(): + runner = TeamcityTestRunner() + else: + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 35241e52..8f0066fe 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -10,6 +10,8 @@ import json from os.path import dirname, join, exists import unittest +from teamcity import is_running_under_teamcity +from teamcity.unittestpy import TeamcityTestRunner from xml.dom.minidom import parseString from onelogin.saml2.response import OneLogin_Saml2_Response @@ -1175,3 +1177,11 @@ def testIsValidSignWithEmptyReferenceURI(self): xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) self.assertTrue(response.is_valid(self.get_request_data())) + + +if __name__ == '__main__': + if is_running_under_teamcity(): + runner = TeamcityTestRunner() + else: + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 73f6a0b5..62fbe3c2 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -6,6 +6,8 @@ import json from os.path import dirname, join, exists, sep import unittest +from teamcity import is_running_under_teamcity +from teamcity.unittestpy import TeamcityTestRunner from onelogin.saml2.errors import OneLogin_Saml2_Error from onelogin.saml2.settings import OneLogin_Saml2_Settings @@ -669,3 +671,11 @@ def testIsDebugActive(self): settings_info['debug'] = True settings_3 = OneLogin_Saml2_Settings(settings_info) self.assertTrue(settings_3.is_debug_active()) + + +if __name__ == '__main__': + if is_running_under_teamcity(): + runner = TeamcityTestRunner() + else: + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py index 0b7860ae..c005380f 100644 --- a/tests/src/OneLogin/saml2_tests/signed_response_test.py +++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py @@ -7,6 +7,8 @@ import json from os.path import dirname, join, exists import unittest +from teamcity import is_running_under_teamcity +from teamcity.unittestpy import TeamcityTestRunner from onelogin.saml2.response import OneLogin_Saml2_Response from onelogin.saml2.settings import OneLogin_Saml2_Settings @@ -52,3 +54,11 @@ def testResponseAndAssertionSigned(self): response = OneLogin_Saml2_Response(settings, b64encode(message)) self.assertEquals('someone@example.com', response.get_nameid()) + + +if __name__ == '__main__': + if is_running_under_teamcity(): + runner = TeamcityTestRunner() + else: + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 497b610e..def7cf89 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -8,6 +8,8 @@ from lxml import etree from os.path import dirname, join, exists import unittest +from teamcity import is_running_under_teamcity +from teamcity.unittestpy import TeamcityTestRunner from xml.dom.minidom import Document, parseString from onelogin.saml2.constants import OneLogin_Saml2_Constants @@ -899,3 +901,11 @@ def testValidateSign(self): no_key = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_key, cert)) + + +if __name__ == '__main__': + if is_running_under_teamcity(): + runner = TeamcityTestRunner() + else: + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) From 24b20226e7353891ca9c1b9e1c83cd06339b69cf Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 2 Feb 2016 12:53:45 +0100 Subject: [PATCH 061/352] Update badges --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 330e2aaf..96d827ee 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ [![Build Status](https://api.travis-ci.org/onelogin/python-saml.png?branch=master)](http://travis-ci.org/onelogin/python-saml) [![Coverage Status](https://coveralls.io/repos/onelogin/python-saml/badge.png)](https://coveralls.io/r/onelogin/python-saml) -[![PyPi Version](https://pypip.in/v/python-saml/badge.png)](https://pypi.python.org/pypi/python-saml) -![PyPi Downloads](https://pypip.in/d/python-saml/badge.png) +[![PyPi Version](https://img.shields.io/pypi/v/python-saml.svg)](https://pypi.python.org/pypi/python-saml) +![PyPi Downloads](https://img.shields.io/pypi/dm/python-saml.svg) +[Python versions](https://img.shields.io/pypi/pyversions/coverage.svg) Add SAML support to your Python software using this library. Forget those complicated libraries and use that open source library provided From 2308a90ec50cd376a5d3959b75bf500a851ee923 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 2 Feb 2016 13:10:41 +0100 Subject: [PATCH 062/352] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 96d827ee..2d444398 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Coverage Status](https://coveralls.io/repos/onelogin/python-saml/badge.png)](https://coveralls.io/r/onelogin/python-saml) [![PyPi Version](https://img.shields.io/pypi/v/python-saml.svg)](https://pypi.python.org/pypi/python-saml) ![PyPi Downloads](https://img.shields.io/pypi/dm/python-saml.svg) -[Python versions](https://img.shields.io/pypi/pyversions/coverage.svg) +![Python versions](https://img.shields.io/pypi/pyversions/python-saml.svg) Add SAML support to your Python software using this library. Forget those complicated libraries and use that open source library provided From 9327bcc381935a1e6296209e82d520896e17250c Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 2 Feb 2016 16:28:06 +0100 Subject: [PATCH 063/352] Teamcity stuff --- tests/src/OneLogin/saml2_tests/auth_test.py | 2 +- tests/src/OneLogin/saml2_tests/authn_request_test.py | 2 +- tests/src/OneLogin/saml2_tests/error_test.py | 2 +- tests/src/OneLogin/saml2_tests/logout_request_test.py | 2 +- tests/src/OneLogin/saml2_tests/logout_response_test.py | 2 +- tests/src/OneLogin/saml2_tests/metadata_test.py | 2 +- tests/src/OneLogin/saml2_tests/response_test.py | 2 +- tests/src/OneLogin/saml2_tests/settings_test.py | 2 +- tests/src/OneLogin/saml2_tests/signed_response_test.py | 2 +- tests/src/OneLogin/saml2_tests/utils_test.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index ee16c726..aee0ad8d 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -867,4 +867,4 @@ def testBuildResponseSignature(self): runner = TeamcityTestRunner() else: runner = unittest.TextTestRunner() - unittest.main(testRunner=runner) + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 1c8b1eb5..deccbba0 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -266,4 +266,4 @@ def testCreateEncSAMLRequest(self): runner = TeamcityTestRunner() else: runner = unittest.TextTestRunner() - unittest.main(testRunner=runner) + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/error_test.py b/tests/src/OneLogin/saml2_tests/error_test.py index 959c2390..7699300a 100644 --- a/tests/src/OneLogin/saml2_tests/error_test.py +++ b/tests/src/OneLogin/saml2_tests/error_test.py @@ -23,4 +23,4 @@ def runTest(self): runner = TeamcityTestRunner() else: runner = unittest.TextTestRunner() - unittest.main(testRunner=runner) + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 9da12166..9464c044 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -447,4 +447,4 @@ def testIsValidSign(self): runner = TeamcityTestRunner() else: runner = unittest.TextTestRunner() - unittest.main(testRunner=runner) + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 0cef7f37..dd91f317 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -390,4 +390,4 @@ def testIsValid(self): runner = TeamcityTestRunner() else: runner = unittest.TextTestRunner() - unittest.main(testRunner=runner) + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 26a03f11..b4891537 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -231,4 +231,4 @@ def testAddX509KeyDescriptors(self): runner = TeamcityTestRunner() else: runner = unittest.TextTestRunner() - unittest.main(testRunner=runner) + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 8f0066fe..5261c005 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1184,4 +1184,4 @@ def testIsValidSignWithEmptyReferenceURI(self): runner = TeamcityTestRunner() else: runner = unittest.TextTestRunner() - unittest.main(testRunner=runner) + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 62fbe3c2..a4810216 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -678,4 +678,4 @@ def testIsDebugActive(self): runner = TeamcityTestRunner() else: runner = unittest.TextTestRunner() - unittest.main(testRunner=runner) + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py index c005380f..63cd2650 100644 --- a/tests/src/OneLogin/saml2_tests/signed_response_test.py +++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py @@ -61,4 +61,4 @@ def testResponseAndAssertionSigned(self): runner = TeamcityTestRunner() else: runner = unittest.TextTestRunner() - unittest.main(testRunner=runner) + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index def7cf89..e9c1ea1e 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -908,4 +908,4 @@ def testValidateSign(self): runner = TeamcityTestRunner() else: runner = unittest.TextTestRunner() - unittest.main(testRunner=runner) + unittest.main(testRunner=runner) From 9f4561921d20c6c427a99ed14b79cb9da85fc5b1 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 2 Feb 2016 17:24:52 +0100 Subject: [PATCH 064/352] Improve how we obtain the settings path --- src/onelogin/saml2/settings.py | 6 +++--- tests/src/OneLogin/saml2_tests/auth_test.py | 5 +++-- tests/src/OneLogin/saml2_tests/authn_request_test.py | 2 +- tests/src/OneLogin/saml2_tests/logout_request_test.py | 5 +++-- tests/src/OneLogin/saml2_tests/logout_response_test.py | 5 +++-- tests/src/OneLogin/saml2_tests/metadata_test.py | 4 +++- tests/src/OneLogin/saml2_tests/response_test.py | 6 +++--- tests/src/OneLogin/saml2_tests/settings_test.py | 8 ++++---- tests/src/OneLogin/saml2_tests/signed_response_test.py | 5 +++-- 9 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index e93fee51..2314a732 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -12,7 +12,7 @@ from datetime import datetime import json import re -from os.path import dirname, exists, join, sep +from os.path import dirname, exists, join, sep, abspath from xml.dom.minidom import Document from onelogin.saml2.constants import OneLogin_Saml2_Constants @@ -115,7 +115,7 @@ def __load_paths(self, base_path=None): Sets the paths of the different folders """ if base_path is None: - base_path = dirname(dirname(dirname(__file__))) + base_path = dirname(dirname(dirname(abspath(__file__)))) if not base_path.endswith(sep): base_path += sep self.__paths = { @@ -134,7 +134,7 @@ def __update_paths(self, settings): if 'custom_base_path' in settings: base_path = settings['custom_base_path'] - base_path = join(dirname(__file__), base_path) + base_path = join(dirname(abspath(__file__)), base_path) self.__load_paths(base_path) def get_base_path(self): diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index aee0ad8d..e75dfef7 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -19,10 +19,11 @@ class OneLogin_Saml2_Auth_Test(unittest.TestCase): - data_path = join(dirname(__file__), '..', '..', '..', 'data') + data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') def loadSettingsJSON(self): - filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json') + filename = join(self.settings_path, 'settings1.json') if exists(filename): stream = open(filename, 'r') settings = json.load(stream) diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index deccbba0..46c526e4 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -20,7 +20,7 @@ class OneLogin_Saml2_Authn_Request_Test(unittest.TestCase): def loadSettingsJSON(self): - filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json') + filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', 'settings1.json') if exists(filename): stream = open(filename, 'r') settings = json.load(stream) diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 9464c044..3262aaeb 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -18,10 +18,11 @@ class OneLogin_Saml2_Logout_Request_Test(unittest.TestCase): - data_path = join(dirname(__file__), '..', '..', '..', 'data') + data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') def loadSettingsJSON(self): - filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json') + filename = join(self.settings_path, 'settings1.json') if exists(filename): stream = open(filename, 'r') settings = json.load(stream) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index dd91f317..d69c739c 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -18,10 +18,11 @@ class OneLogin_Saml2_Logout_Response_Test(unittest.TestCase): - data_path = join(dirname(__file__), '..', '..', '..', 'data') + data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') def loadSettingsJSON(self): - filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json') + filename = join(self.settings_path, 'settings1.json') if exists(filename): stream = open(filename, 'r') settings = json.load(stream) diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index b4891537..15cc44e9 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -17,8 +17,10 @@ class OneLogin_Saml2_Metadata_Test(unittest.TestCase): + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') + def loadSettingsJSON(self): - filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json') + filename = join(self.settings_path, 'settings1.json') if exists(filename): stream = open(filename, 'r') settings = json.load(stream) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 5261c005..f0025e6b 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -20,13 +20,13 @@ class OneLogin_Saml2_Response_Test(unittest.TestCase): - data_path = join(dirname(__file__), '..', '..', '..', 'data') + data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') def loadSettingsJSON(self, filename=None): if filename: - filename = join(dirname(__file__), '..', '..', '..', 'settings', filename) + filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', filename) else: - filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json') + filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', 'settings1.json') if exists(filename): stream = open(filename, 'r') settings = json.load(stream) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index a4810216..c13595ec 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -15,8 +15,8 @@ class OneLogin_Saml2_Settings_Test(unittest.TestCase): - data_path = join(dirname(__file__), '..', '..', '..', 'data') - settings_path = join(dirname(__file__), '..', '..', '..', 'settings') + data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') def loadSettingsJSON(self): filename = join(self.settings_path, 'settings1.json') @@ -121,7 +121,7 @@ def testLoadSettingsFromFile(self): Tests the OneLogin_Saml2_Settings Constructor. Case load setting from file """ - custom_base_path = join(dirname(__file__), '..', '..', '..', 'settings') + custom_base_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') settings = OneLogin_Saml2_Settings(custom_base_path=custom_base_path) self.assertEqual(len(settings.get_errors()), 0) @@ -131,7 +131,7 @@ def testLoadSettingsFromFile(self): except Exception as e: self.assertIn('Settings file not found', e.message) - custom_base_path = join(dirname(__file__), '..', '..', '..', 'data', 'customPath') + custom_base_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data', 'customPath') settings_3 = OneLogin_Saml2_Settings(custom_base_path=custom_base_path) self.assertEqual(len(settings_3.get_errors()), 0) diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py index 63cd2650..cdbba1fa 100644 --- a/tests/src/OneLogin/saml2_tests/signed_response_test.py +++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py @@ -15,10 +15,11 @@ class OneLogin_Saml2_SignedResponse_Test(unittest.TestCase): - data_path = join(dirname(__file__), '..', '..', '..', 'data') + data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') def loadSettingsJSON(self): - filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json') + filename = join(self.settings_path, 'settings1.json') if exists(filename): stream = open(filename, 'r') settings = json.load(stream) From 185ebbd177cc603f95abfef9a39e57baf637cb82 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 3 Feb 2016 22:33:56 +0100 Subject: [PATCH 065/352] Fix #106. Make Request ids accesible --- README.md | 13 +++++++++++++ demo-django/demo/views.py | 22 ++++++++++++++++++++-- src/onelogin/saml2/auth.py | 14 ++++++++++++++ src/onelogin/saml2/constants.py | 1 + 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2d444398..53549084 100644 --- a/README.md +++ b/README.md @@ -496,6 +496,12 @@ The login method can recieve 2 more optional parameters: * force_authn When true the AuthNReuqest will set the ForceAuthn='true' * is_passive When true the AuthNReuqest will set the Ispassive='true' +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 + +```python +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. @@ -679,6 +685,12 @@ 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 + +```python +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. @@ -754,6 +766,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. * ***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/demo-django/demo/views.py b/demo-django/demo/views.py index a2277366..3b67b2fe 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -39,6 +39,10 @@ def index(request): if 'sso' in req['get_data']: return HttpResponseRedirect(auth.login()) + # If AuthNRequest ID need to be stored in order to later validate it, do instead + # sso_built_url = auth.login() + # request.session['AuthNRequestID'] = auth.get_last_request_id() + # return HttpResponseRedirect(sso_built_url) elif 'sso2' in req['get_data']: return_to = OneLogin_Saml2_Utils.get_self_url(req) + reverse('attrs') return HttpResponseRedirect(auth.login(return_to)) @@ -51,19 +55,33 @@ def index(request): session_index = request.session['samlSessionIndex'] return HttpResponseRedirect(auth.logout(name_id=name_id, session_index=session_index)) + + # If LogoutRequest ID need to be stored in order to later validate it, do instead + # slo_built_url = auth.logout(name_id=name_id, session_index=session_index) + # request.session['LogoutRequestID'] = auth.get_last_request_id() + #return HttpResponseRedirect(slo_built_url) elif 'acs' in req['get_data']: - auth.process_response() + request_id = None + if 'AuthNRequestID' in request.session: + request_id = request.session['AuthNRequestID'] + + auth.process_response(request_id=request_id) errors = auth.get_errors() not_auth_warn = not auth.is_authenticated() if not errors: + if 'AuthNRequestID' in request.session: + del request.session['AuthNRequestID'] request.session['samlUserdata'] = auth.get_attributes() request.session['samlNameId'] = auth.get_nameid() request.session['samlSessionIndex'] = auth.get_session_index() if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState'])) elif 'sls' in req['get_data']: + request_id = None + if 'LogoutRequestID' in request.session: + request_id = request.session['LogoutRequestID'] dscb = lambda: request.session.flush() - url = auth.process_slo(delete_session_cb=dscb) + url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) errors = auth.get_errors() if len(errors) == 0: if url is not None: diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index a02592ae..17a19d51 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): """ @@ -151,6 +152,8 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ parameters = {'SAMLResponse': logout_response} if 'RelayState' in self.__request_data['get_data']: parameters['RelayState'] = self.__request_data['get_data']['RelayState'] + else: + parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) security = self.__settings.get_security_data() if 'logoutResponseSigned' in security and security['logoutResponseSigned']: @@ -257,6 +260,13 @@ def get_attribute(self, name): value = self.__attributes[name] return value + 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): """ Initiates the SSO process. @@ -274,6 +284,8 @@ def login(self, return_to=None, force_authn=False, is_passive=False): """ authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive) + self.__last_request_id = authn_request.get_id() + saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} @@ -315,6 +327,8 @@ def logout(self, return_to=None, name_id=None, session_index=None): logout_request = OneLogin_Saml2_Logout_Request(self.__settings, name_id=name_id, session_index=session_index) + self.__last_request_id = logout_request.id + saml_request = logout_request.get_request() parameters = {'SAMLRequest': logout_request.get_request()} diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py index f004bbbd..3f7e08a4 100644 --- a/src/onelogin/saml2/constants.py +++ b/src/onelogin/saml2/constants.py @@ -80,6 +80,7 @@ class OneLogin_Saml2_Constants(object): NSMAP = { 'samlp': NS_SAMLP, 'saml': NS_SAML, + 'md': NS_MD, 'ds': NS_DS, 'xenc': NS_XENC } From bd1ebed83235c01dbc70188a7a8034af2e88dcdb Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 3 Feb 2016 22:38:48 +0100 Subject: [PATCH 066/352] Fix #103. ALOWED Misspell --- src/onelogin/saml2/constants.py | 2 +- src/onelogin/saml2/response.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py index 3f7e08a4..632ea4c8 100644 --- a/src/onelogin/saml2/constants.py +++ b/src/onelogin/saml2/constants.py @@ -19,7 +19,7 @@ class OneLogin_Saml2_Constants(object): """ # Value added to the current time in time condition validations - ALOWED_CLOCK_DRIFT = 300 + ALLOWED_CLOCK_DRIFT = 300 # NameID Formats NAMEID_EMAIL_ADDRESS = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index a8e9d444..0ad1ecea 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -374,9 +374,9 @@ def validate_timestamps(self): for conditions_node in conditions_nodes: 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.ALOWED_CLOCK_DRIFT: + 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 - if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALOWED_CLOCK_DRIFT <= OneLogin_Saml2_Utils.now(): + 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 return True From 7c96feec5ebe19785e25295576846ad5585161f7 Mon Sep 17 00:00:00 2001 From: Nick Barrett Date: Thu, 11 Feb 2016 13:46:24 +0000 Subject: [PATCH 067/352] Add support for nested `NameID` children inside `AttributeValue`s. --- src/onelogin/saml2/response.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 0ad1ecea..bfaeee67 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -347,7 +347,22 @@ def get_attributes(self): attr_name = attribute_node.get('Name') values = [] for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP['saml']): - values.append(attr.text) + # Remove any whitespace (which may be present where attributes are + # nested inside NameID children). + text = attr.text.strip() + if text: + values.append(text) + + # Parse any nested NameID children + for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP['saml']): + values.append({ + 'NameID': { + 'Format': nameid.get('Format'), + 'NameQualifier': nameid.get('NameQualifier'), + 'value': nameid.text + } + }) + attributes[attr_name] = values return attributes From b727312ef3ce22d6a84cfe9e9fd78b3af02a0470 Mon Sep 17 00:00:00 2001 From: Nick Barrett Date: Thu, 11 Feb 2016 13:46:34 +0000 Subject: [PATCH 068/352] Add test for `NameID` nested inside `AttributeValue`. --- ...ponse_with_nested_nameid_values.xml.base64 | 71 +++++++++++++++++++ .../src/OneLogin/saml2_tests/response_test.py | 20 ++++++ 2 files changed, 91 insertions(+) create mode 100644 tests/data/responses/response_with_nested_nameid_values.xml.base64 diff --git a/tests/data/responses/response_with_nested_nameid_values.xml.base64 b/tests/data/responses/response_with_nested_nameid_values.xml.base64 new file mode 100644 index 00000000..3092c3cf --- /dev/null +++ b/tests/data/responses/response_with_nested_nameid_values.xml.base64 @@ -0,0 +1,71 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDph +c3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9j +b2wiIElEPSJHT1NBTUxSMTI5MDExNzQ1NzE3OTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50 +PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgRGVzdGluYXRpb249IntyZWNpcGllbnR9Ij4KICA8c2Ft +bHA6U3RhdHVzPgogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0 +YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPgogIDxzYW1sOkFzc2Vy +dGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhz +aT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIu +MCIgSUQ9InBmeGE0NjU3NGRmLWIzYjAtYTA2YS0yM2M4LTYzNjQxMzE5ODc3MiIgSXNzdWVJbnN0 +YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiI+CiAgICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9hcHAu +b25lbG9naW4uY29tL3NhbWwvbWV0YWRhdGEvMTM1OTA8L3NhbWw6SXNzdWVyPgogICAgPGRzOlNp +Z25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAg +ICAgIDxkczpTaWduZWRJbmZvPgogICAgICAgIDxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFs +Z29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAg +ICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAv +MDkveG1sZHNpZyNyc2Etc2hhMSIvPgogICAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4YTQ2 +NTc0ZGYtYjNiMC1hMDZhLTIzYzgtNjM2NDEzMTk4NzcyIj4KICAgICAgICAgIDxkczpUcmFuc2Zv +cm1zPgogICAgICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5v +cmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KICAgICAgICAgICAgPGRz +OlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1j +MTRuIyIvPgogICAgICAgICAgPC9kczpUcmFuc2Zvcm1zPgogICAgICAgICAgPGRzOkRpZ2VzdE1l +dGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+ +CiAgICAgICAgICA8ZHM6RGlnZXN0VmFsdWU+cEpRN01TL2VrNEtSUldHbXYvSDQzUmVIWU1zPTwv +ZHM6RGlnZXN0VmFsdWU+CiAgICAgICAgPC9kczpSZWZlcmVuY2U+CiAgICAgIDwvZHM6U2lnbmVk +SW5mbz4KICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlPnlpdmVLY1BkRHB1RE5qNnNoclEzQUJ3ci9j +QTNDcnlEMnBoRy94TFpzektXeFU1L21sYUt0OGV3YlpPZEtLdnRPczJwSEJ5NUR1YTNrOTRBRit6 +eEd5ZWw1Z09vd21veVhKcitBT3Ira1BPMHZsaTFWOG8zaFBQVVp3UmdTWDZROXBTMUNxUWdoS2lF +YXNSeXlscXFKVWFQWXptT3pPRTgvWGxNa3dpV21PMD08L2RzOlNpZ25hdHVyZVZhbHVlPgogICAg +ICA8ZHM6S2V5SW5mbz4KICAgICAgICA8ZHM6WDUwOURhdGE+CiAgICAgICAgICA8ZHM6WDUwOUNl +cnRpZmljYXRlPk1JSUJyVENDQWFHZ0F3SUJBZ0lCQVRBREJnRUFNR2N4Q3pBSkJnTlZCQVlUQWxW +VE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUlV3RXdZRFZRUUhEQXhUWVc1MFlTQk5iMjVw +WTJFeEVUQVBCZ05WQkFvTUNFOXVaVXh2WjJsdU1Sa3dGd1lEVlFRRERCQmhjSEF1YjI1bGJHOW5h +VzR1WTI5dE1CNFhEVEV3TURNd09UQTVOVGcwTlZvWERURTFNRE13T1RBNU5UZzBOVm93WnpFTE1B +a0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RlRBVEJnTlZCQWNNREZO +aGJuUmhJRTF2Ym1sallURVJNQThHQTFVRUNnd0lUMjVsVEc5bmFXNHhHVEFYQmdOVkJBTU1FR0Z3 +Y0M1dmJtVnNiMmRwYmk1amIyMHdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JB +T2pTdTFmalB5OGQ1dzRReUwxK3pkNGhJdzFNa2tmZjRXWS9UTEc4T1prVTVZVFNXbW1IUEQ1a3ZZ +SDV1b1hTLzZxUTgxcVhwUjJ3VjhDVG93WkpVTGcwOWRkUmRSbjhRc3FqMUZ5T0M1c2xFM3kyYloy +b0Z1YTcyb2YvNDlmcHVqbkZUNktuUTYxQ0JNcWxEb1RRcU9UNjJ2R0o4blA2TVpXdkE2c3hxdWQ1 +QWdNQkFBRXdBd1lCQUFNQkFBPT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT4KICAgICAgICA8L2RzOlg1 +MDlEYXRhPgogICAgICA8L2RzOktleUluZm8+CiAgICA8L2RzOlNpZ25hdHVyZT4KICAgIDxzYW1s +OlN1YmplY3Q+CiAgICAgIDxzYW1sOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpT +QU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c3VwcG9ydEBvbmVsb2dpbi5jb208 +L3NhbWw6TmFtZUlEPgogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJu +Om9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+CiAgICAgICAgPHNhbWw6U3ViamVj +dENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDEwLTExLTE4VDIyOjAyOjM3WiIgUmVj +aXBpZW50PSJ7cmVjaXBpZW50fSIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPgogICAgPC9z +YW1sOlN1YmplY3Q+CiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0xMS0xOFQy +MTo1MjozN1oiIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiPgogICAgICA8c2Ft +bDpBdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgICAgIDxzYW1sOkF1ZGllbmNlPnthdWRpZW5jZX08 +L3NhbWw6QXVkaWVuY2U+CiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgPC9z +YW1sOkNvbmRpdGlvbnM+CiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIw +MTAtMTEtMThUMjE6NTc6MzdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDEwLTExLTE5VDIxOjU3 +OjM3WiIgU2Vzc2lvbkluZGV4PSJfNTMxYzMyZDI4M2JkZmY3ZTA0ZTQ4N2JjZGJjNGRkOGQiPgog +ICAgICA8c2FtbDpBdXRobkNvbnRleHQ+CiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NS +ZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6 +QXV0aG5Db250ZXh0Q2xhc3NSZWY+CiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+CiAgICA8L3Nh +bWw6QXV0aG5TdGF0ZW1lbnQ+CiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+CiAgICAgIDxz +YW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHht +bG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRw +Oi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmlu +ZyI+ZGVtbzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4KICAg +ICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9ImFub3RoZXJfdmFsdWUiPgogICAgICAgIDxzYW1sOkF0 +dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIg +eG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNp +OnR5cGU9InhzOnN0cmluZyI+CiAgICAgICAgICAgIDxzYW1sOk5hbWVJRCBGb3JtYXQ9InVybjpv +YXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnBlcnNpc3RlbnQiIE5hbWVRdWFs +aWZpZXI9Imh0dHBzOi8vaWRwSUQiIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9zcElEIj52YWx1 +ZTwvc2FtbDpOYW1lSUQ+CiAgICAgICAgPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3Nh +bWw6QXR0cmlidXRlPgogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4KICA8L3NhbWw6QXNz +ZXJ0aW9uPgo8L3NhbWxwOlJlc3BvbnNlPgo= diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index f0025e6b..04a00a14 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -261,6 +261,26 @@ def testGetAttributes(self): response_3 = OneLogin_Saml2_Response(settings, xml_3) self.assertEqual({}, response_3.get_attributes()) + def testGetNestedNameIDAttributes(self): + """ + Tests the getAttributes method of the OneLogin_Saml2_Response with nested + nameID data + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'response_with_nested_nameid_values.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + expected_attributes = { + 'uid': ['demo'], + 'another_value': [{ + 'NameID': { + 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 'NameQualifier': 'https://idpID', + 'value': 'value' + } + }] + } + self.assertEqual(expected_attributes, response.get_attributes()) + def testOnlyRetrieveAssertionWithIDThatMatchesSignatureReference(self): """ Tests the get_nameid method of the OneLogin_Saml2_Response From a00abf526c7fe3c68a9804ef43d6208ebc74d939 Mon Sep 17 00:00:00 2001 From: Nick Barrett Date: Thu, 11 Feb 2016 14:55:45 +0000 Subject: [PATCH 069/352] Fix case where text is None. --- src/onelogin/saml2/response.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index bfaeee67..41ca5791 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -349,9 +349,10 @@ def get_attributes(self): for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP['saml']): # Remove any whitespace (which may be present where attributes are # nested inside NameID children). - text = attr.text.strip() - if text: - values.append(text) + if attr.text: + text = attr.text.strip() + if text: + values.append(text) # Parse any nested NameID children for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP['saml']): From df8836fe47f998546a9f4528c4377cc31ca568a6 Mon Sep 17 00:00:00 2001 From: Jimmy John Date: Fri, 12 Feb 2016 17:05:27 -0800 Subject: [PATCH 070/352] AttributeConsumingService support. First pass at adding AttributeConsumingService support in the AuthnRequest. --- .../settings_with_req_attributes_example.json | 46 +++++++++++ setup.py | 2 +- src/onelogin/saml2/authn_request.py | 10 ++- src/onelogin/saml2/constants.py | 3 + src/onelogin/saml2/metadata.py | 76 ++++++++++++++++++- src/onelogin/saml2/settings.py | 49 ++++++++++++ 6 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 demo-django/saml/settings_with_req_attributes_example.json diff --git a/demo-django/saml/settings_with_req_attributes_example.json b/demo-django/saml/settings_with_req_attributes_example.json new file mode 100644 index 00000000..e27998eb --- /dev/null +++ b/demo-django/saml/settings_with_req_attributes_example.json @@ -0,0 +1,46 @@ +{ + "strict": true, + "debug": true, + "sp": { + "entityId": "https:///metadata/", + "assertionConsumerService": { + "url": "https:///?acs", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + "attributeConsumingService": [ + { + "isDefault": false, + "serviceName": "Django Demo", + "serviceDescription": "Django Name", + "requestedAttributes": [ { + "name": "", + "nameFormat": "", + "friendlyName": "", + "isRequired": false, + "attributeValue": [ + ] + } + ] + } + ], + "singleLogoutService": { + "url": "https:///?sls", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", + "x509cert": "", + "privateKey": "" + }, + "idp": { + "entityId": "https://app.onelogin.com/saml/metadata/", + "singleSignOnService": { + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "singleLogoutService": { + "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "" + } +} \ No newline at end of file diff --git a/setup.py b/setup.py index 05bbe08c..a8952a2d 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.1.5', + version='2.2.5', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index b36734fe..16f46c2f 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -88,6 +88,12 @@ def __init__(self, settings, force_authn=False, is_passive=False): requested_authn_context_str += '%s' % authn_context requested_authn_context_str += ' ' + attr_consuming_service_str = '' + if 'attributeConsumingService' in sp_data: + # TODO: Do we have to account for the case when we have multiple attributeconsumers? + # like will the index be > 1? + attr_consuming_service_str = 'AttributeConsumingServiceIndex="1"' + request = """ + AssertionConsumerServiceURL="%(assertion_url)s" + %(attr_consuming_service_str)s> %(entity_id)s - @@ -145,7 +208,14 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N 'organization': str_organization, 'contacts': str_contacts, } - return metadata + + # i'm not sure why the above xml was build by hand. Building via lxml is way easier, + # especially for conditional attributes etc.. + # So as a work around, i'm creating a xml dom, insert the attibute_consumer_service + # nodes into it and then return the serialized xml + root = etree.fromstring(metadata) + OneLogin_Saml2_Metadata.add_attribute_consuming_service(root, attr_consuming_service) + return etree.tostring(root, pretty_print=True) @staticmethod def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 2314a732..9db9874b 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -257,6 +257,10 @@ def __add_default_values(self): if 'binding' not in self.__sp['assertionConsumerService'].keys(): self.__sp['assertionConsumerService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_POST + # attributeConsumingService is optional + if 'attributeConsumingService' not in self.__sp: + self.__sp['attributeConsumingService'] = [] + if 'singleLogoutService' not in self.__sp.keys(): self.__sp['singleLogoutService'] = {} if 'binding' not in self.__sp['singleLogoutService']: @@ -436,6 +440,51 @@ def check_sp_settings(self, settings): elif not validate_url(sp['assertionConsumerService']['url']): errors.append('sp_acs_url_invalid') + if len(sp['attributeConsumingService']): + # so we have a attributeConsumingService element... + + # serviceName and requestedAttrib are required + attribute_consuming_service = sp['attributeConsumingService'] + for attrib in attribute_consuming_service: + if 'serviceName' not in attrib: + errors.append('sp_attributeConsumingService_serviceName_not_found') + if 'requestedAttributes' not in attrib: + errors.append('sp_attributeConsumingService_requestedAttributes_not_found') + + # verify that tags are of the correct types + try: + if type(attrib['isDefault']) != bool: + errors.append('sp_attributeConsumingService_isDefault_type_invalid') + except KeyError: + # isDefault attribute is optional + pass + + if 'serviceName' in attrib and not isinstance(attrib['serviceName'], basestring): + errors.append('sp_attributeConsumingService_serviceName_type_invalid') + + try: + if not isinstance(attrib['serviceDescription'], basestring): + errors.append('sp_attributeConsumingService_serviceDescription_type_invalid') + except KeyError: + # serviceDescription attribute is optional + pass + + if 'requestedAttributes' in attrib: + if type(attrib['requestedAttributes']) != list: + errors.append('sp_attributeConsumingService_requestedAttributes_type_invalid') + + for req_attrib in attrib['requestedAttributes']: + if 'name' not in req_attrib: + errors.append('sp_attributeConsumingService_requestedAttributes_name_not_found') + if 'name' in req_attrib and not req_attrib['name'].strip(): + # name cannot be empty + errors.append('sp_attributeConsumingService_requestedAttributes_name_invalid') + if 'attributeValue' in req_attrib and type(req_attrib['attributeValue']) != list: + errors.append('sp_attributeConsumingService_requestedAttributes_attributeValue_type_invalid') + if 'isRequired' in req_attrib and type(req_attrib['isRequired']) != bool: + errors.append('sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid') + + if 'singleLogoutService' in sp and \ 'url' in sp['singleLogoutService'] and \ len(sp['singleLogoutService']['url']) > 0 and \ From f73b58169f6af53c9cf4b7f33955b980e269002e Mon Sep 17 00:00:00 2001 From: Jimmy John Date: Fri, 12 Feb 2016 17:42:19 -0800 Subject: [PATCH 071/352] Fix unit test breakage --- src/onelogin/saml2/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 9db9874b..44137198 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -440,7 +440,7 @@ def check_sp_settings(self, settings): elif not validate_url(sp['assertionConsumerService']['url']): errors.append('sp_acs_url_invalid') - if len(sp['attributeConsumingService']): + if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']): # so we have a attributeConsumingService element... # serviceName and requestedAttrib are required From ea93835844441da602ddbc6433535cbe66d0de11 Mon Sep 17 00:00:00 2001 From: Jimmy John Date: Fri, 12 Feb 2016 17:53:52 -0800 Subject: [PATCH 072/352] Fix unit test breakage --- src/onelogin/saml2/authn_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 16f46c2f..1ffdae4a 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -89,7 +89,7 @@ def __init__(self, settings, force_authn=False, is_passive=False): requested_authn_context_str += ' ' attr_consuming_service_str = '' - if 'attributeConsumingService' in sp_data: + if 'attributeConsumingService' in sp_data and sp_data['attributeConsumingService']: # TODO: Do we have to account for the case when we have multiple attributeconsumers? # like will the index be > 1? attr_consuming_service_str = 'AttributeConsumingServiceIndex="1"' From 7854c08d453c2c314e3a1ae8ae030c45d0a401a7 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 16 Feb 2016 01:38:13 +0100 Subject: [PATCH 073/352] Fix security vulnerability. Prevent signature wrapping --- src/onelogin/saml2/response.py | 13 +- src/onelogin/saml2/utils.py | 170 +++++++++++++++--- .../signature_wrapping_attack.xml.base64 | 1 + tests/src/OneLogin/saml2_tests/utils_test.py | 24 ++- 4 files changed, 165 insertions(+), 43 deletions(-) create mode 100644 tests/data/responses/invalids/signature_wrapping_attack.xml.base64 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 41ca5791..dfc9cef8 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -119,7 +119,7 @@ def is_valid(self, request_data, request_id=None): if security.get('wantAttributeStatement', True) and not attribute_statement_nodes: raise Exception('There is no AttributeStatement on the Response') - # Validates Asserion timestamps + # Validates Assertion timestamps if not self.validate_timestamps(): raise Exception('Timing issues (please check your clock settings)') @@ -194,13 +194,16 @@ def is_valid(self, request_data, request_id=None): raise Exception('The Message of the Response is not signed and the SP require it') if len(signed_elements) > 0: + if len(signed_elements) > 2: + raise Exception('Too many Signatures found. SAML Response rejected') cert = idp_data.get('x509cert', None) fingerprint = idp_data.get('certFingerprint', None) fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) - # Only validates the first sign found + # If find a Signature on the Response, validates it checking the original response if '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements: document_to_validate = self.document + # Otherwise validates the assertion (decrypted assertion if was encrypted) else: if self.encrypted: document_to_validate = self.decrypted_document @@ -374,8 +377,8 @@ def validate_num_assertions(self): :returns: True if only 1 assertion encrypted or not :rtype: bool """ - encrypted_assertion_nodes = self.__query('/samlp:Response/saml:EncryptedAssertion') - assertion_nodes = self.__query('/samlp:Response/saml:Assertion') + encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:EncryptedAssertion') + assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:Assertion') return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1 def validate_timestamps(self): @@ -461,7 +464,7 @@ def __decrypt_assertion(self, dom): if not key: raise Exception('No private key available, check settings') - encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(dom, '//saml:EncryptedAssertion') + encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/saml:EncryptedAssertion') if encrypted_assertion_nodes: encrypted_data_nodes = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData') if encrypted_data_nodes: diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index e7a973fd..e07c821c 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -918,51 +918,163 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid xmlsec.addIDs(elem, ["ID"]) - signature_nodes = OneLogin_Saml2_Utils.query(elem, '//ds:Signature') + signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature') - if len(signature_nodes) > 0: + if not len(signature_nodes) > 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:EncryptedAssertion/saml:Assertion/ds:Signature') + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature') + + if len(signature_nodes) == 1: signature_node = signature_nodes[0] - if (cert is None or cert == '') and fingerprint: - x509_certificate_nodes = OneLogin_Saml2_Utils.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) + return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug) + else: + return False + except Exception: + return False - if cert is None or cert == '': - return False + @staticmethod + def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): + """ + Validates a signature of a EntityDescriptor. - # Check if Reference URI is empty - reference_elem = OneLogin_Saml2_Utils.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')) + :param xml: The element we should validate + :type: string | Document - dsig_ctx = xmlsec.DSigCtx() + :param cert: The pubic cert + :type: string - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + :param fingerprint: The fingerprint of the public cert + :type: string - if validatecert: - mngr = xmlsec.KeysMngr() - mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted) - dsig_ctx = xmlsec.DSigCtx(mngr) - else: - dsig_ctx = xmlsec.DSigCtx() - dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + :param fingerprintalg: The algorithm used to build the fingerprint + :type: string + + :param validatecert: If true, will verify the signature and if the cert is valid. + :type: bool + + :param debug: Activate the xmlsec debug + :type: bool + """ + try: + if xml is None or xml == '': + raise Exception('Empty string supplied as input') + elif isinstance(xml, etree._Element): + elem = xml + elif isinstance(xml, Document): + xml = xml.toxml() + elem = fromstring(str(xml)) + elif isinstance(xml, Element): + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_MD), + 'xmlns:md', + unicode(OneLogin_Saml2_Constants.NS_MD) + ) + xml = xml.toxml() + elem = fromstring(str(xml)) + elif isinstance(xml, basestring): + elem = fromstring(str(xml)) + else: + raise Exception('Error parsing xml string') + + xmlsec.initialize() + + if debug: + xmlsec.set_error_callback(print_xmlsec_errors) + + xmlsec.addIDs(elem, ["ID"]) + + signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature') - file_cert.close() + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature') - dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) - dsig_ctx.verify(signature_node) + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature') + signature_nodes += OneLogin_Saml2_Utils.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 + @staticmethod + def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): + """ + Validates a signature node. + + :param signature_node: The signature node + :type: Node + + :param xml: The element we should validate + :type: Document + + :param cert: The pubic cert + :type: string + + :param fingerprint: The fingerprint of the public cert + :type: string + + :param fingerprintalg: The algorithm used to build the fingerprint + :type: string + + :param validatecert: If true, will verify the signature and if the cert is valid. + :type: bool + + :param debug: Activate the xmlsec debug + :type: bool + """ + try: + xmlsec.initialize() + + if debug: + xmlsec.set_error_callback(print_xmlsec_errors) + + xmlsec.addIDs(elem, ["ID"]) + + if (cert is None or cert == '') and fingerprint: + x509_certificate_nodes = OneLogin_Saml2_Utils.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 + + # Check if Reference URI is empty + reference_elem = OneLogin_Saml2_Utils.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')) + + dsig_ctx = xmlsec.DSigCtx() + + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + + if validatecert: + mngr = xmlsec.KeysMngr() + mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted) + dsig_ctx = xmlsec.DSigCtx(mngr) + else: + dsig_ctx = xmlsec.DSigCtx() + dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + + file_cert.close() + + dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) + dsig_ctx.verify(signature_node) + return True + except Exception: + return False + @staticmethod def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False): """ diff --git a/tests/data/responses/invalids/signature_wrapping_attack.xml.base64 b/tests/data/responses/invalids/signature_wrapping_attack.xml.base64 new file mode 100644 index 00000000..dc2c9ca9 --- /dev/null +++ b/tests/data/responses/invalids/signature_wrapping_attack.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZnhjM2QyYjU0Mi0wZjdlLTg3NjctOGU4Ny01YjBkYzY5MTMzNzUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZDllMzE5YzFiOGE2N2RhNDgyMjc5NjRjMjhkMjgwZTc4NjBmODA0Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0RldGFpbD48c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzZDJiNTQyLTBmN2UtODc2Ny04ZTg3LTViMGRjNjkxMzM3NSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDE6MDlaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVkOWUzMTljMWI4YTY3ZGE0ODIyNzk2NGMyOGQyODBlNzg2MGY4MDQiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGMzZDJiNTQyLTBmN2UtODc2Ny04ZTg3LTViMGRjNjkxMzM3NSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+MWRRRmlZVTBvMk9GN2MvUlZWOEdwZ2I0dTNJPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT53UmdCWE9xL0ZpTFpjMm11cmVUQy9qNnpZNzA5T2lrSjVIZVVTcnVIVGRZakVnOWFaeTFSYnhsS0lZRUlmWHBuWDdOQm9LeGZBTW0rTzBmc3JxT2pnY1l4VFZrcVpqT3I3MXFpWE5idHdqZUFrZFlTcGs1YnJzQWNuZmNQZHY4UVJlWXIzRDd0NVpWQ2dZdXZYUStkTkVMS2VhZzdlMUFTT3pWcU9kcDVaOVk9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9Il9jY2NkNjAyNDExNjY0MWZlNDhlMGFlMmM1MTIyMGQwMjc1NWY5NmM5OGQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCI+X2I5OGY5OGJiMWFiNTEyY2VkNjUzYjU4YmFhZmY1NDM0NDhkYWVkNTM1ZDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0yMlQxOTowMTowOVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVkOWUzMTljMWI4YTY3ZGE0ODIyNzk2NGMyOGQyODBlNzg2MGY4MDQiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0yMVQxMzo0MDozOVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0yMlQxOTowMTowOVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMy0yMVQyMTo0MTowOVoiIFNlc3Npb25JbmRleD0iXzlmZTBjOGRjZDMzMDJlNzM2NGZjYWIyMmE1Mjc0OGViZjIyMjRkZjBhYSI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3RAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ic24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPndhYTI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT48L3NhbWxwOlN0YXR1c0RldGFpbD48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9Il9jY2NkNjAyNDExNjY0MWZlNDhlMGFlMmM1MTIyMGQwMjc1NWY5NmM5OGQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCI+X2I5OGY5OGJiMWFiNTEyY2VkNjUzYjU4YmFhZmY1NDM0NDhkYWVkNTM1ZDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0yMlQxOTowMTowOVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVkOWUzMTljMWI4YTY3ZGE0ODIyNzk2NGMyOGQyODBlNzg2MGY4MDQiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0yMVQxMzo0MDozOVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0yMlQxOTowMTowOVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyMy0wMy0yMVQyMTo0MTowOVoiIFNlc3Npb25JbmRleD0iXzlmZTBjOGRjZDMzMDJlNzM2NGZjYWIyMmE1Mjc0OGViZjIyMjRkZjBhYSI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5oYWNrZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+aGFja2VyQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5oYWNrZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ic24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPndhYTI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index e9c1ea1e..33d9d27b 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -18,13 +18,13 @@ class OneLogin_Saml2_Utils_Test(unittest.TestCase): - data_path = join(dirname(__file__), '..', '..', '..', 'data') + data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') def loadSettingsJSON(self, filename=None): if filename: - filename = join(dirname(__file__), '..', '..', '..', 'settings', filename) + filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', filename) else: - filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json') + filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', 'settings1.json') if exists(filename): stream = open(filename, 'r') settings = json.load(stream) @@ -822,13 +822,13 @@ def testValidateSign(self): # expired cert xml_metadata_signed = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings1.xml')) - self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed, cert)) + self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert)) # expired cert, verified it - self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed, cert, validatecert=True)) + self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True)) xml_metadata_signed_2 = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings2.xml')) - self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed_2, cert_2)) - self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed_2, None, fingerprint_2)) + self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, cert_2)) + self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, fingerprint_2)) xml_response_msg_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64'))) @@ -884,7 +884,7 @@ def testValidateSign(self): invalid_fingerprint = 'afe71c34ef740bc87434be13a2263d31271da1f9' # Wrong fingerprint - self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed_2, None, invalid_fingerprint)) + self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint)) dom_2 = parseString(xml_response_double_signed_2) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2)) @@ -892,16 +892,22 @@ def testValidateSign(self): # Modified message self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2)) + # Try to validate directly the Assertion dom_3 = parseString(xml_response_double_signed_2) assert_elem_3 = dom_3.firstChild.firstChild.nextSibling.nextSibling.nextSibling - self.assertTrue(OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2)) + self.assertFalse(OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2)) + # Wrong scheme no_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_signature.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_signed, cert)) no_key = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_key, cert)) + # Signature Wrapping attack + wrapping_attack1 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'signature_wrapping_attack.xml.base64'))) + self.assertFalse(OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert)) + if __name__ == '__main__': if is_running_under_teamcity(): From c1f3323642e4273797ffbcd0383659b02bf1e146 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 16 Feb 2016 01:39:30 +0100 Subject: [PATCH 074/352] New version 2.1.6 --- README.md | 7 +++++++ changelog.md | 9 +++++++++ setup.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 53549084..e877b85d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ Add SAML support to your Python software using this library. Forget those complicated libraries and use that open source library provided and supported by OneLogin Inc. +This version supports Python2, exists an alternative version compatible with Python 3: [python3-saml](https://github.com/onelogin/python3-saml) + +#### Warning #### + +`Please if you are using python-saml < v2.1.6. Update it! +v2.1.6 includes a security patch that will prevent signature wrapping attacks, older versions are vulnerable.` :exclamation: + Why add SAML support to my software? ------------------------------------ diff --git a/changelog.md b/changelog.md index c6b6df60..3fcbfff7 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # python-saml changelog +### 2.1.6 (Feb 15, 2016) +* Prevent signature wrapping attack!! +* [#111](https://github.com/onelogin/python-saml/pull/111 Add support for nested `NameID` children inside `AttributeValue`s +* ALOWED Misspell +* Improve how we obtain the settings path. +* Update docs adding reference to test depencence installation +* Fix Organization element on SP metadata. +* [#100](https://github.com/onelogin/python-saml/pull/100) Support Responses that don't have AttributeStatements. + ### 2.1.5 (Nov 3, 2015) * [#86](https://github.com/onelogin/python-saml/pull/86) Make idp settings optional (Usefull when validating SP metadata) * [#79](https://github.com/onelogin/python-saml/pull/79) Remove unnecesary dependence. M2crypto is not used. diff --git a/setup.py b/setup.py index 05bbe08c..d14890ad 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.1.5', + version='2.1.6', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', From bfdedb37cb8aacb5902e40e3055881b319b2f01f Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 16 Feb 2016 01:41:26 +0100 Subject: [PATCH 075/352] Update changelog.md --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 3fcbfff7..45411dcb 100644 --- a/changelog.md +++ b/changelog.md @@ -2,7 +2,7 @@ ### 2.1.6 (Feb 15, 2016) * Prevent signature wrapping attack!! -* [#111](https://github.com/onelogin/python-saml/pull/111 Add support for nested `NameID` children inside `AttributeValue`s +* [#111](https://github.com/onelogin/python-saml/pull/111) Add support for nested `NameID` children inside `AttributeValue`s * ALOWED Misspell * Improve how we obtain the settings path. * Update docs adding reference to test depencence installation From ef609235a121a045aaa063f2d0c3331dba408572 Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 12 Feb 2016 15:03:05 +0100 Subject: [PATCH 076/352] Compare Assertion InResponseTo if not None If assertion contains InResponseTo but not the Response tag, we should not compare the assertion InResponseTo value to None. --- src/onelogin/saml2/response.py | 4 ++- ...d_response_without_inresponseto.xml.base64 | 1 + .../src/OneLogin/saml2_tests/response_test.py | 27 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/data/responses/valid_response_without_inresponseto.xml.base64 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index dfc9cef8..628c7934 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -166,7 +166,9 @@ def is_valid(self, request_data, request_id=None): continue else: irt = sc_data.get('InResponseTo', None) - if irt != in_response_to: + # We compare Assertion InResponseTo with Response value + # if we have both. + if in_response_to and irt and irt != in_response_to: continue recipient = sc_data.get('Recipient', None) if recipient and current_url not in recipient: diff --git a/tests/data/responses/valid_response_without_inresponseto.xml.base64 b/tests/data/responses/valid_response_without_inresponseto.xml.base64 new file mode 100644 index 00000000..388e2709 --- /dev/null +++ b/tests/data/responses/valid_response_without_inresponseto.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0icGZ4MDVmM2NlMTAtMTYxNS1mM2VhLWE5ODgtNjBlMzgwYjMyOTlmIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIERlc3RpbmF0aW9uPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiPgogIDxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+CiAgPGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgICA8ZHM6U2lnbmVkSW5mbz4KICAgICAgPGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPgogICAgICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDA1ZjNjZTEwLTE2MTUtZjNlYS1hOTg4LTYwZTM4MGIzMjk5ZiI+CiAgICAgICAgPGRzOlRyYW5zZm9ybXM+CiAgICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgICAgICA8L2RzOlRyYW5zZm9ybXM+CiAgICAgICAgPGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+CiAgICAgICAgPGRzOkRpZ2VzdFZhbHVlPkRjUWNDL1BoS05qRTlLa29YRXZZRlhXMHZGdz08L2RzOkRpZ2VzdFZhbHVlPgogICAgICA8L2RzOlJlZmVyZW5jZT4KICAgIDwvZHM6U2lnbmVkSW5mbz4KICAgIDxkczpTaWduYXR1cmVWYWx1ZT5xVjcvc2YvVEt1S0x5allaMGNDSlhCWnZSYmF1RXNoMXQvaEtJeStpVHJRSjYxWG0rMXZDcEtvVXdleGNuL1ZpCitsemZlaHZjL2tDMjE5TjZVTUUxZnRLTDY2OSsxYkpFb1NLejQrN2VhWi9XTFdYL0hRYndMVmh6dlh3bWdMQVAKUEhLNmZJZHpocGRkLzRydjlXVnpjaGoveGcxWVNkaXFrcnU3YUhhS2FEOD08L2RzOlNpZ25hdHVyZVZhbHVlPgogIDwvZHM6U2lnbmF0dXJlPgogIDxzYW1scDpTdGF0dXM+CiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+CiAgPC9zYW1scDpTdGF0dXM+CiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj4KICAgIDxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+CiAgICA8c2FtbDpTdWJqZWN0PgogICAgICA8c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPjQ5Mjg4MjYxNWFjZjMxYzgwOTZiNjI3MjQ1ZDc2YWU1MzAzNmMwOTA8L3NhbWw6TmFtZUlEPgogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+CiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPgogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4KICAgIDwvc2FtbDpTdWJqZWN0PgogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDItMTlUMDE6MzY6MzFaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIj4KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICA8L3NhbWw6Q29uZGl0aW9ucz4KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPgogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+CiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+CiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+CiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+CiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW48L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ic24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPk1hcnRpbjI8L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+CiAgPC9zYW1sOkFzc2VydGlvbj4KPC9zYW1scDpSZXNwb25zZT4K \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 04a00a14..daf1c2d2 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1198,6 +1198,33 @@ def testIsValidSignWithEmptyReferenceURI(self): response = OneLogin_Saml2_Response(settings, xml) self.assertTrue(response.is_valid(self.get_request_data())) + def testIsValidWithoutInResponseTo(self): + """ + If assertion contains InResponseTo but not the Response tag, we should + not compare the assertion InResponseTo value to None. + """ + + # prepare strict settings + settings_info = self.loadSettingsJSON() + settings_info['strict'] = True + settings_info['idp']['entityId'] = 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php' + settings_info['sp']['entityId'] = 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php' + + settings = OneLogin_Saml2_Settings(settings_info) + + xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_without_inresponseto.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + + not_on_or_after = datetime.strptime('2014-02-19T09:37:01Z', '%Y-%m-%dT%H:%M:%SZ') + not_on_or_after -= timedelta(seconds=150) + + with freeze_time(not_on_or_after): + self.assertTrue(response.is_valid({ + 'https': 'on', + 'http_host': 'pitbulk.no-ip.org', + 'script_name': 'newonelogin/demo1/index.php?acs' + })) + if __name__ == '__main__': if is_running_under_teamcity(): From 39c366dde0d40b65da0a40086f8608cf94c05f32 Mon Sep 17 00:00:00 2001 From: Jimmy John Date: Tue, 16 Feb 2016 10:26:33 -0800 Subject: [PATCH 077/352] fix unit test breakage --- tests/src/OneLogin/saml2_tests/authn_request_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 46c526e4..194bfaf7 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -255,7 +255,7 @@ def testCreateEncSAMLRequest(self): inflated = decompress(decoded, -15) self.assertRegexpMatches(inflated, '^') + self.assertRegexpMatches(inflated, 'AssertionConsumerServiceURL="http://stuff.com/endpoints/endpoints/acs.php"(\s)*>') self.assertRegexpMatches(inflated, 'http://stuff.com/endpoints/metadata.php') self.assertRegexpMatches(inflated, 'Format="urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"') self.assertRegexpMatches(inflated, 'ProviderName="SP prueba"') From 5d907e6b30dbc63ce3f81f2dbf393d703dce5841 Mon Sep 17 00:00:00 2001 From: Jimmy John Date: Tue, 16 Feb 2016 11:11:18 -0800 Subject: [PATCH 078/352] fix pep8 warnings --- src/onelogin/saml2/settings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 44137198..0f2cdc57 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -484,7 +484,6 @@ def check_sp_settings(self, settings): if 'isRequired' in req_attrib and type(req_attrib['isRequired']) != bool: errors.append('sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid') - if 'singleLogoutService' in sp and \ 'url' in sp['singleLogoutService'] and \ len(sp['singleLogoutService']['url']) > 0 and \ @@ -813,4 +812,4 @@ def is_debug_active(self): :returns: Debug parameter :rtype: boolean """ - return self.__debug + return self.__debug \ No newline at end of file From 07ec2001b7248f3817a328cc3c7c60ddce1ac83d Mon Sep 17 00:00:00 2001 From: Jimmy John Date: Tue, 16 Feb 2016 11:49:35 -0800 Subject: [PATCH 079/352] pep8 fix Everything passes on my ubuntu vm, but for some strange reason it fails on circle-ci, oh wel... --- src/onelogin/saml2/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 0f2cdc57..e8b028c8 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -812,4 +812,4 @@ def is_debug_active(self): :returns: Debug parameter :rtype: boolean """ - return self.__debug \ No newline at end of file + return self.__debug From fe3ad906aad7738cb76f0b3f66a6dd38f703c2d7 Mon Sep 17 00:00:00 2001 From: Jimmy John Date: Thu, 18 Feb 2016 21:45:11 -0800 Subject: [PATCH 080/352] review comments --- setup.py | 2 +- src/onelogin/saml2/metadata.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 686fd990..d14890ad 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.2.0', + version='2.1.6', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index b237ef30..6544a8e5 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -182,7 +182,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N str_contacts = '\n'.join(contacts_info) metadata = """ - @@ -207,6 +207,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N 'sls': sls, 'organization': str_organization, 'contacts': str_contacts, + 'saml_namespace': 'xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"' if attr_consuming_service else '' } # i'm not sure why the above xml was build by hand. Building via lxml is way easier, From b8c7c3eb3083b95d5f6ae62ce78f8e3317ac3491 Mon Sep 17 00:00:00 2001 From: Jimmy John Date: Fri, 19 Feb 2016 09:37:40 -0800 Subject: [PATCH 081/352] unit tests for attributeConsumingService --- tests/settings/settings4.json | 85 +++++++++++++++++++ .../saml2_tests/authn_request_test.py | 24 +++++- .../src/OneLogin/saml2_tests/metadata_test.py | 34 +++++++- .../src/OneLogin/saml2_tests/settings_test.py | 41 +++++++++ 4 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 tests/settings/settings4.json diff --git a/tests/settings/settings4.json b/tests/settings/settings4.json new file mode 100644 index 00000000..64fc2f6d --- /dev/null +++ b/tests/settings/settings4.json @@ -0,0 +1,85 @@ +{ + "strict": false, + "debug": false, + "custom_base_path": "../../../tests/data/customPath/", + "sp": { + "entityId": "http://pytoolkit.com:8000/metadata/", + "assertionConsumerService": { + "url": "http://pytoolkit.com:8000/?acs" + }, + "attributeConsumingService": [ + { + "isDefault": false, + "serviceName": "Test Service", + "serviceDescription": "Test Service", + "requestedAttributes": [ { + "name": "urn:oid:2.5.4.42", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "givenName", + "isRequired": false + }, + { + "name": "urn:oid:2.5.4.4", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "sn", + "isRequired": false + }, + { + "name": "urn:oid:2.16.840.1.113730.3.1.241", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "displayName", + "isRequired": false + }, + { + "name": "urn:oid:0.9.2342.19200300.100.1.3", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "mail", + "isRequired": false + }, + { + "name": "urn:oid:0.9.2342.19200300.100.1.1", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "uid", + "isRequired": false + } + ] + } + ], + "singleLogoutService": { + "url": "http://pytoolkit.com:8000/?sls" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified" + }, + "idp": { + "entityId": "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php", + "singleSignOnService": { + "url": "http://pitbulk.no-ip.org/SSOService.php" + }, + "singleLogoutService": { + "url": "http://pitbulk.no-ip.org/SingleLogoutService.php" + }, + "x509cert": "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m" + }, + "security": { + "authnRequestsSigned": false, + "wantAssertionsSigned": false, + "signMetadata": false + }, + "contactPerson": { + "technical": { + "givenName": "technical_name", + "emailAddress": "technical@example.com" + }, + "support": { + "givenName": "support_name", + "emailAddress": "support@example.com" + } + }, + "organization": { + "en-US": { + "name": "sp_test", + "displayname": "SP test", + "url": "http://sp.example.com" + } + } +} diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 194bfaf7..f1c21a37 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -19,8 +19,8 @@ class OneLogin_Saml2_Authn_Request_Test(unittest.TestCase): - def loadSettingsJSON(self): - filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', 'settings1.json') + def loadSettingsJSON(self, filename='settings1.json'): + filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', filename) if exists(filename): stream = open(filename, 'r') settings = json.load(stream) @@ -260,6 +260,26 @@ def testCreateEncSAMLRequest(self): self.assertRegexpMatches(inflated, 'Format="urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"') self.assertRegexpMatches(inflated, 'ProviderName="SP prueba"') + def testAttributeConsumingService(self): + """ + Tests that the attributeConsumingServiceIndex is present as an attribute + """ + + saml_settings = self.loadSettingsJSON('settings4.json') + settings = OneLogin_Saml2_Settings(saml_settings) + settings._OneLogin_Saml2_Settings__organization = { + u'en-US': { + u'url': u'http://sp.example.com', + u'name': u'sp_test' + } + } + + authn_request = OneLogin_Saml2_Authn_Request(settings) + authn_request_encoded = authn_request.get_request() + decoded = b64decode(authn_request_encoded) + inflated = decompress(decoded, -15) + + self.assertRegexpMatches(inflated, 'AttributeConsumingServiceIndex="1"') if __name__ == '__main__': if is_running_under_teamcity(): diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 15cc44e9..c0842f8e 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -19,8 +19,8 @@ class OneLogin_Saml2_Metadata_Test(unittest.TestCase): settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') - def loadSettingsJSON(self): - filename = join(self.settings_path, 'settings1.json') + def loadSettingsJSON(self, filename='settings1.json'): + filename = join(self.settings_path, filename) if exists(filename): stream = open(filename, 'r') settings = json.load(stream) @@ -143,6 +143,36 @@ def testBuilder(self): parsed_datetime = strftime(r'%Y-%m-%dT%H:%M:%SZ', datetime_value.timetuple()) self.assertIn('validUntil="%s"' % parsed_datetime, metadata6) + def testBuilderAttributeConsumingService(self): + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings4.json')) + sp_data = settings.get_sp_data() + security = settings.get_security_data() + organization = settings.get_organization() + contacts = settings.get_contacts() + + metadata = OneLogin_Saml2_Metadata.builder( + sp_data, security['authnRequestsSigned'], + security['wantAssertionsSigned'], None, None, contacts, + organization + ) + + self.assertIn('xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"', metadata) + self.assertIn('Test ServiceTest Service\ +', metadata) + + + def testSignMetadata(self): """ Tests the signMetadata method of the OneLogin_Saml2_Metadata diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index c13595ec..f5a94bec 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -298,6 +298,47 @@ def testCheckSettings(self): self.assertIn('sp_entityId_not_found', e.message) self.assertIn('sp_acs_not_found', e.message) + #AttributeConsumingService tests + + #serviceName, requestedAttributes are required + settings_info['sp']['attributeConsumingService'] = [ + { + "isDefault": False, + "serviceDescription": "Test Service" + } + ] + try: + OneLogin_Saml2_Settings(settings_info) + self.assertTrue(False) + except Exception as e: + self.assertIn('sp_attributeConsumingService_serviceName_not_found', e.message) + self.assertIn('sp_attributeConsumingService_requestedAttributes_not_found', e.message) + + # requestedAttributes/name is required + settings_info['sp']['attributeConsumingService'] = [ + { + "isDefault": "False", + "serviceName": {}, + "serviceDescription": ["Test Service"], + "requestedAttributes": [ { + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "givenName", + "isRequired": "False" + } + ] + } + ] + try: + OneLogin_Saml2_Settings(settings_info) + self.assertTrue(False) + except Exception as e: + self.assertIn('sp_attributeConsumingService_requestedAttributes_name_not_found', e.message) + self.assertIn('sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid', e.message) + self.assertIn('sp_attributeConsumingService_serviceDescription_type_invalid', e.message) + self.assertIn('sp_attributeConsumingService_serviceName_type_invalid', e.message) + self.assertIn('sp_attributeConsumingService_isDefault_type_invalid', e.message) + + settings_info['idp']['entityID'] = 'entityId' settings_info['idp']['singleSignOnService'] = {} settings_info['idp']['singleSignOnService']['url'] = 'invalid_value' From 51aa23518e397a6ba34a0edc3b4e097e4636ae8b Mon Sep 17 00:00:00 2001 From: Jimmy John Date: Fri, 19 Feb 2016 09:56:22 -0800 Subject: [PATCH 082/352] fixing pep8 violations --- tests/src/OneLogin/saml2_tests/metadata_test.py | 2 -- tests/src/OneLogin/saml2_tests/settings_test.py | 16 +++++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index c0842f8e..f1417480 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -171,8 +171,6 @@ def testBuilderAttributeConsumingService(self): NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="uid" \ isRequired="false"/>', metadata) - - def testSignMetadata(self): """ Tests the signMetadata method of the OneLogin_Saml2_Metadata diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index f5a94bec..f9a899f6 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -298,9 +298,8 @@ def testCheckSettings(self): self.assertIn('sp_entityId_not_found', e.message) self.assertIn('sp_acs_not_found', e.message) - #AttributeConsumingService tests - - #serviceName, requestedAttributes are required + # AttributeConsumingService tests + # serviceName, requestedAttributes are required settings_info['sp']['attributeConsumingService'] = [ { "isDefault": False, @@ -320,11 +319,11 @@ def testCheckSettings(self): "isDefault": "False", "serviceName": {}, "serviceDescription": ["Test Service"], - "requestedAttributes": [ { - "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", - "friendlyName": "givenName", - "isRequired": "False" - } + "requestedAttributes": [{ + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "givenName", + "isRequired": "False" + } ] } ] @@ -338,7 +337,6 @@ def testCheckSettings(self): self.assertIn('sp_attributeConsumingService_serviceName_type_invalid', e.message) self.assertIn('sp_attributeConsumingService_isDefault_type_invalid', e.message) - settings_info['idp']['entityID'] = 'entityId' settings_info['idp']['singleSignOnService'] = {} settings_info['idp']['singleSignOnService']['url'] = 'invalid_value' From aea56bb5890738db9c4be3899a01f3acbafc9412 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 8 Mar 2016 11:47:39 +0100 Subject: [PATCH 083/352] Return empty list when there are no audience values --- src/onelogin/saml2/response.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index dfc9cef8..5d73ff99 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -246,12 +246,8 @@ def get_audiences(self): :returns: The valid audiences for the SAML Response :rtype: list """ - audiences = [] - audience_nodes = self.__query_assertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience') - for audience_node in audience_nodes: - audiences.append(audience_node.text) - return audiences + return [node.text for node in audience_nodes if node.text is not None] def get_issuers(self): """ From efb2515e0a12fb43271d5d6b8bddcbaa4a61f451 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 1 Apr 2016 16:54:43 +0200 Subject: [PATCH 084/352] Passing NameQualifier through to logout request --- src/onelogin/saml2/auth.py | 12 +++++-- src/onelogin/saml2/logout_request.py | 5 ++- src/onelogin/saml2/utils.py | 7 +++- tests/src/OneLogin/saml2_tests/utils_test.py | 36 ++++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 17a19d51..fc0e5020 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -300,7 +300,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False): parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) return self.redirect_to(self.get_sso_url(), parameters) - def logout(self, return_to=None, name_id=None, session_index=None): + def logout(self, return_to=None, name_id=None, session_index=None, nq=None): """ Initiates the SLO process. @@ -313,6 +313,9 @@ def logout(self, return_to=None, name_id=None, session_index=None): :param session_index: SessionIndex that identifies the session of the user. :type session_index: string + :param nq: IDP Name Qualifier + :type: string + :returns: Redirection url """ slo_url = self.get_slo_url() @@ -325,7 +328,12 @@ def logout(self, return_to=None, name_id=None, session_index=None): if name_id is None and self.__nameid is not None: name_id = self.__nameid - logout_request = OneLogin_Saml2_Logout_Request(self.__settings, name_id=name_id, session_index=session_index) + logout_request = OneLogin_Saml2_Logout_Request( + self.__settings, + name_id=name_id, + session_index=session_index, + nq=nq + ) self.__last_request_id = logout_request.id diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 235e07d2..bf3f669a 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -29,7 +29,7 @@ class OneLogin_Saml2_Logout_Request(object): """ - def __init__(self, settings, request=None, name_id=None, session_index=None): + def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None): """ Constructs the Logout Request object. @@ -44,6 +44,9 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): :param session_index: SessionIndex that identifies the session of the user. :type session_index: string + + :param nq: IDP Name Qualifier + :type: string """ self.__settings = settings self.__error = None diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index e07c821c..d7ade21d 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -584,7 +584,7 @@ def format_finger_print(fingerprint): return formated_fingerprint.lower() @staticmethod - def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): + def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None): """ Generates a nameID. @@ -603,6 +603,9 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): :param debug: Activate the xmlsec debug :type: bool + :param nq: IDP Name Qualifier + :type: string + :returns: DOMElement | XMLSec nameID :rtype: string """ @@ -613,6 +616,8 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): name_id = doc.createElement('saml:NameID') if sp_nq is not None: name_id.setAttribute('SPNameQualifier', sp_nq) + if nq is not None: + name_id.setAttribute('NameQualifier', nq) name_id.setAttribute('Format', sp_format) name_id.appendChild(doc.createTextNode(value)) name_id_container.appendChild(name_id) diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 33d9d27b..d84adf65 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -559,6 +559,42 @@ def testQuery(self): signature_nodes_5 = OneLogin_Saml2_Utils.query(dom, './/ds:SignatureValue', assertion) self.assertEqual(1, len(signature_nodes_5)) + def _generate_name_id_element(self, name_qualifier): + name_id_value = 'value' + entity_id = 'sp-entity-id' + name_id_format = 'name-id-format' + + raw_name_id = OneLogin_Saml2_Utils.generate_name_id( + name_id_value, + entity_id, + name_id_format, + nq=name_qualifier, + ) + parser = etree.XMLParser(recover=True) + return etree.fromstring(raw_name_id, parser) + + def testNameidGenerationIncludesNameQualifierAttribute(self): + """ + Tests the inclusion of NameQualifier in the generateNameId method of the OneLogin_Saml2_Utils + """ + idp_name_qualifier = 'idp-name-qualifier' + idp_name_qualifier_attribute = ('NameQualifier', idp_name_qualifier) + + name_id = self._generate_name_id_element(idp_name_qualifier) + + self.assertIn(idp_name_qualifier_attribute, name_id.attrib.items()) + + def testNameidGenerationDoesNotIncludeNameQualifierAttribute(self): + """ + Tests the (not) inclusion of NameQualifier in the generateNameId method of the OneLogin_Saml2_Utils + """ + idp_name_qualifier = None + not_expected_attribute = 'NameQualifier' + + name_id = self._generate_name_id_element(idp_name_qualifier) + + self.assertNotIn(not_expected_attribute, name_id.attrib.keys()) + def testGenerateNameIdWithSPNameQualifier(self): """ Tests the generateNameId method of the OneLogin_Saml2_Utils From 3b3ec183355486d5fab7ae027b5110aa7fade7f6 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 4 Apr 2016 11:14:23 +0200 Subject: [PATCH 085/352] Make deflate process when retrieving built SAML messages optional --- src/onelogin/saml2/authn_request.py | 17 ++++++++++------- src/onelogin/saml2/logout_request.py | 14 ++++++++++---- src/onelogin/saml2/logout_response.py | 16 +++++++++++----- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index b36734fe..eaa1bbef 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -8,12 +8,10 @@ AuthNRequest class of OneLogin's Python Toolkit. """ - from base64 import b64encode -from zlib import compress -from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.utils import OneLogin_Saml2_Utils class OneLogin_Saml2_Authn_Request(object): @@ -121,14 +119,19 @@ def __init__(self, settings, force_authn=False, is_passive=False): self.__authn_request = request - def get_request(self): + def get_request(self, deflate=True): """ Returns unsigned AuthnRequest. - :return: Unsigned AuthnRequest + :param deflate: It makes the deflate process optional + :type: bool + :return: AuthnRequest maybe deflated and base64 encoded :rtype: str object """ - deflated_request = compress(self.__authn_request)[2:-4] - return b64encode(deflated_request) + if deflate: + request = OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__authn_request) + else: + request = b64encode(self.__authn_request) + return request def get_id(self): """ diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index bf3f669a..6239bbf3 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -10,7 +10,7 @@ """ from zlib import decompress -from base64 import b64decode +from base64 import b64encode, b64decode from lxml import etree from defusedxml.lxml import fromstring from urllib import quote_plus @@ -117,13 +117,19 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= self.__logout_request = logout_request - def get_request(self): + def get_request(self, deflate=True): """ Returns the Logout Request defated, base64encoded - :return: Deflated base64 encoded Logout Request + :param deflate: It makes the deflate process optional + :type: bool + :return: Logout Request maybe deflated and base64 encoded :rtype: str object """ - return OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_request) + if deflate: + request = OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_request) + else: + request = b64encode(self.__logout_request) + return request @staticmethod def get_id(request): diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 3c79a989..fb049bb3 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -9,7 +9,7 @@ """ -from base64 import b64decode +from base64 import b64encode, b64decode from defusedxml.lxml import fromstring from urllib import quote_plus @@ -188,13 +188,19 @@ def build(self, in_response_to): self.__logout_response = logout_response - def get_response(self): + def get_response(self, deflate=True): """ - Returns a Logout Response object. - :return: Logout Response deflated and base64 encoded + Returns the Logout Response defated, base64encoded + :param deflate: It makes the deflate process optional + :type: bool + :return: Logout Response maybe deflated and base64 encoded :rtype: string """ - return OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_response) + if deflate: + response = OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_response) + else: + response = b64encode(self.__logout_response) + return response def get_error(self): """ From 978ba1ec3a9da727998eaeeaae6a7d54f32ea02a Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 4 Apr 2016 19:10:41 +0200 Subject: [PATCH 086/352] Update README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e877b85d..c185510e 100644 --- a/README.md +++ b/README.md @@ -334,6 +334,9 @@ In addition to the required settings data (idp, sp), there is extra information // this SP to be encrypted. "wantNameIdEncrypted": false, + // Indicates a requirement for the AttributeStatement element + "wantAttributeStatement": true, + // Authentication context. // Set to false and no AuthContext will be sent in the AuthNRequest, // Set true or don't present thi parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' From b31d574629be484d70211a4f7cbf04713bfbfb44 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 4 Apr 2016 20:03:24 +0200 Subject: [PATCH 087/352] Define attributeConsumingService on README and remove custom settings on django demo --- README.md | 16 +++++++ .../settings_with_req_attributes_example.json | 46 ------------------- 2 files changed, 16 insertions(+), 46 deletions(-) delete mode 100644 demo-django/saml/settings_with_req_attributes_example.json diff --git a/README.md b/README.md index e877b85d..d4f83f45 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,22 @@ This is the settings.json file: // HTTP-POST binding only. "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, + // If you need to specify requested attributes, set a + // attributeConsumingService. nameFormat, attributeValue and + // friendlyName can be ommited + "attributeConsumingService": { + "ServiceName": "SP test", + "serviceDescription": "Test Service", + "requestedAttributes": [ + { + "name": "", + "isRequired": false, + "nameFormat": "", + "friendlyName": "", + "attributeValue": "" + } + ] + }, // Specifies info about where and how the message MUST be // returned to the requester, in this case our SP. "singleLogoutService": { diff --git a/demo-django/saml/settings_with_req_attributes_example.json b/demo-django/saml/settings_with_req_attributes_example.json deleted file mode 100644 index e27998eb..00000000 --- a/demo-django/saml/settings_with_req_attributes_example.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "strict": true, - "debug": true, - "sp": { - "entityId": "https:///metadata/", - "assertionConsumerService": { - "url": "https:///?acs", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" - }, - "attributeConsumingService": [ - { - "isDefault": false, - "serviceName": "Django Demo", - "serviceDescription": "Django Name", - "requestedAttributes": [ { - "name": "", - "nameFormat": "", - "friendlyName": "", - "isRequired": false, - "attributeValue": [ - ] - } - ] - } - ], - "singleLogoutService": { - "url": "https:///?sls", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", - "x509cert": "", - "privateKey": "" - }, - "idp": { - "entityId": "https://app.onelogin.com/saml/metadata/", - "singleSignOnService": { - "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - }, - "singleLogoutService": { - "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - }, - "x509cert": "" - } -} \ No newline at end of file From 0e2ee595b760c48590a63225c6f2392014407d02 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 5 Apr 2016 02:13:45 +0200 Subject: [PATCH 088/352] Adapt PR. Toolkit will only support 1 AttributeConsumingService. Adding AttributeConsumingService with String style --- README.md | 22 +-- src/onelogin/saml2/authn_request.py | 2 - src/onelogin/saml2/metadata.py | 133 ++++++++---------- src/onelogin/saml2/settings.py | 70 ++++----- tests/settings/settings4.json | 74 +++++----- .../saml2_tests/authn_request_test.py | 17 ++- .../src/OneLogin/saml2_tests/metadata_test.py | 24 ++-- .../src/OneLogin/saml2_tests/settings_test.py | 31 ++-- 8 files changed, 158 insertions(+), 215 deletions(-) diff --git a/README.md b/README.md index d4f83f45..016bec28 100644 --- a/README.md +++ b/README.md @@ -227,17 +227,17 @@ This is the settings.json file: // attributeConsumingService. nameFormat, attributeValue and // friendlyName can be ommited "attributeConsumingService": { - "ServiceName": "SP test", - "serviceDescription": "Test Service", - "requestedAttributes": [ - { - "name": "", - "isRequired": false, - "nameFormat": "", - "friendlyName": "", - "attributeValue": "" - } - ] + "ServiceName": "SP test", + "serviceDescription": "Test Service", + "requestedAttributes": [ + { + "name": "", + "isRequired": false, + "nameFormat": "", + "friendlyName": "", + "attributeValue": "" + } + ] }, // Specifies info about where and how the message MUST be // returned to the requester, in this case our SP. diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 72b7a122..81d53200 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -88,8 +88,6 @@ def __init__(self, settings, force_authn=False, is_passive=False): attr_consuming_service_str = '' if 'attributeConsumingService' in sp_data and sp_data['attributeConsumingService']: - # TODO: Do we have to account for the case when we have multiple attributeconsumers? - # like will the index be > 1? attr_consuming_service_str = 'AttributeConsumingServiceIndex="1"' request = """%s +""" % sp['attributeConsumingService']['serviceDescription'] + + requested_attribute_data = [] + for req_attribs in sp['attributeConsumingService']['requestedAttributes']: + req_attr_nameformat_str = req_attr_friendlyname_str = req_attr_isrequired_str = '' + req_attr_aux_str = ' \>' + + if 'nameFormat' in req_attribs.keys() and req_attribs['nameFormat']: + req_attr_nameformat_str = " NameFormat=\"%s\"" % req_attribs['nameFormat'] + if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']: + req_attr_nameformat_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName'] + if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']: + req_attr_isrequired_str = " isRequired=\"%s\"" % req_attribs['isRequired'] + if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']: + req_attr_aux_str = """ > + + %(service_name)s +%(attr_cs_desc)s%(requested_attribute_str)s + +""" % \ + { + 'service_name': sp['attributeConsumingService']['serviceName'], + 'attr_cs_desc': attr_cs_desc_str, + 'requested_attribute_str': '\n'.join(requested_attribute_data) + } sls = '' if 'singleLogoutService' in sp and 'url' in sp['singleLogoutService']: @@ -163,7 +149,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N org_data = '\n'.join(organization_names) + '\n' + '\n'.join(organization_displaynames) + '\n' + '\n'.join(organization_urls) str_organization = """ %(org)s - """ % {'org': org_data} + \n""" % {'org': org_data} str_contacts = '' if len(contacts) > 0: @@ -179,10 +165,10 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N 'email': info['emailAddress'], } contacts_info.append(contact) - str_contacts = '\n'.join(contacts_info) + str_contacts = '\n'.join(contacts_info) + '\n' metadata = """ - @@ -191,10 +177,8 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N - -%(organization)s -%(contacts)s -""" % \ +%(attribute_consuming_service)s +%(organization)s%(contacts)s""" % \ { 'valid': ('validUntil="%s"' % valid_until_str) if valid_until_str else '', 'cache': ('cacheDuration="%s"' % cache_duration_str) if cache_duration_str else '', @@ -207,16 +191,9 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N 'sls': sls, 'organization': str_organization, 'contacts': str_contacts, - 'saml_namespace': 'xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"' if attr_consuming_service else '' + 'attribute_consuming_service': str_attribute_consuming_service } - - # i'm not sure why the above xml was build by hand. Building via lxml is way easier, - # especially for conditional attributes etc.. - # So as a work around, i'm creating a xml dom, insert the attibute_consumer_service - # nodes into it and then return the serialized xml - root = etree.fromstring(metadata) - OneLogin_Saml2_Metadata.add_attribute_consuming_service(root, attr_consuming_service) - return etree.tostring(root, pretty_print=True) + return metadata @staticmethod def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index e8b028c8..5ac99992 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -257,9 +257,8 @@ def __add_default_values(self): if 'binding' not in self.__sp['assertionConsumerService'].keys(): self.__sp['assertionConsumerService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_POST - # attributeConsumingService is optional - if 'attributeConsumingService' not in self.__sp: - self.__sp['attributeConsumingService'] = [] + if 'attributeConsumingService' not in self.__sp.keys(): + self.__sp['attributeConsumingService'] = {} if 'singleLogoutService' not in self.__sp.keys(): self.__sp['singleLogoutService'] = {} @@ -441,48 +440,29 @@ def check_sp_settings(self, settings): errors.append('sp_acs_url_invalid') if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']): - # so we have a attributeConsumingService element... - - # serviceName and requestedAttrib are required - attribute_consuming_service = sp['attributeConsumingService'] - for attrib in attribute_consuming_service: - if 'serviceName' not in attrib: - errors.append('sp_attributeConsumingService_serviceName_not_found') - if 'requestedAttributes' not in attrib: - errors.append('sp_attributeConsumingService_requestedAttributes_not_found') - - # verify that tags are of the correct types - try: - if type(attrib['isDefault']) != bool: - errors.append('sp_attributeConsumingService_isDefault_type_invalid') - except KeyError: - # isDefault attribute is optional - pass - - if 'serviceName' in attrib and not isinstance(attrib['serviceName'], basestring): - errors.append('sp_attributeConsumingService_serviceName_type_invalid') - - try: - if not isinstance(attrib['serviceDescription'], basestring): - errors.append('sp_attributeConsumingService_serviceDescription_type_invalid') - except KeyError: - # serviceDescription attribute is optional - pass - - if 'requestedAttributes' in attrib: - if type(attrib['requestedAttributes']) != list: - errors.append('sp_attributeConsumingService_requestedAttributes_type_invalid') - - for req_attrib in attrib['requestedAttributes']: - if 'name' not in req_attrib: - errors.append('sp_attributeConsumingService_requestedAttributes_name_not_found') - if 'name' in req_attrib and not req_attrib['name'].strip(): - # name cannot be empty - errors.append('sp_attributeConsumingService_requestedAttributes_name_invalid') - if 'attributeValue' in req_attrib and type(req_attrib['attributeValue']) != list: - errors.append('sp_attributeConsumingService_requestedAttributes_attributeValue_type_invalid') - if 'isRequired' in req_attrib and type(req_attrib['isRequired']) != bool: - errors.append('sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid') + attributeConsumingService = sp['attributeConsumingService'] + if 'serviceName' not in attributeConsumingService: + errors.append('sp_attributeConsumingService_serviceName_not_found') + elif not isinstance(attributeConsumingService['serviceName'], basestring): + errors.append('sp_attributeConsumingService_serviceName_type_invalid') + + if 'requestedAttributes' not in attributeConsumingService: + errors.append('sp_attributeConsumingService_requestedAttributes_not_found') + elif not isinstance(attributeConsumingService['requestedAttributes'], list): + errors.append('sp_attributeConsumingService_serviceName_type_invalid') + else: + for req_attrib in attributeConsumingService['requestedAttributes']: + if 'name' not in req_attrib: + errors.append('sp_attributeConsumingService_requestedAttributes_name_not_found') + if 'name' in req_attrib and not req_attrib['name'].strip(): + errors.append('sp_attributeConsumingService_requestedAttributes_name_invalid') + if 'attributeValue' in req_attrib and type(req_attrib['attributeValue']) != list: + errors.append('sp_attributeConsumingService_requestedAttributes_attributeValue_type_invalid') + if 'isRequired' in req_attrib and type(req_attrib['isRequired']) != bool: + errors.append('sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid') + + if "serviceDescription" in attributeConsumingService and not isinstance(attributeConsumingService['serviceDescription'], basestring): + errors.append('sp_attributeConsumingService_serviceDescription_type_invalid') if 'singleLogoutService' in sp and \ 'url' in sp['singleLogoutService'] and \ diff --git a/tests/settings/settings4.json b/tests/settings/settings4.json index 64fc2f6d..1300c7df 100644 --- a/tests/settings/settings4.json +++ b/tests/settings/settings4.json @@ -7,44 +7,42 @@ "assertionConsumerService": { "url": "http://pytoolkit.com:8000/?acs" }, - "attributeConsumingService": [ - { - "isDefault": false, - "serviceName": "Test Service", - "serviceDescription": "Test Service", - "requestedAttributes": [ { - "name": "urn:oid:2.5.4.42", - "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", - "friendlyName": "givenName", - "isRequired": false - }, - { - "name": "urn:oid:2.5.4.4", - "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", - "friendlyName": "sn", - "isRequired": false - }, - { - "name": "urn:oid:2.16.840.1.113730.3.1.241", - "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", - "friendlyName": "displayName", - "isRequired": false - }, - { - "name": "urn:oid:0.9.2342.19200300.100.1.3", - "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", - "friendlyName": "mail", - "isRequired": false - }, - { - "name": "urn:oid:0.9.2342.19200300.100.1.1", - "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", - "friendlyName": "uid", - "isRequired": false - } - ] - } - ], + "attributeConsumingService": { + "isDefault": false, + "serviceName": "Test Service", + "serviceDescription": "Test Service", + "requestedAttributes": [ { + "name": "urn:oid:2.5.4.42", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "givenName", + "isRequired": false + }, + { + "name": "urn:oid:2.5.4.4", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "sn", + "isRequired": false + }, + { + "name": "urn:oid:2.16.840.1.113730.3.1.241", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "displayName", + "isRequired": false + }, + { + "name": "urn:oid:0.9.2342.19200300.100.1.3", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "mail", + "isRequired": false + }, + { + "name": "urn:oid:0.9.2342.19200300.100.1.1", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "uid", + "isRequired": false + } + ] + }, "singleLogoutService": { "url": "http://pytoolkit.com:8000/?sls" }, diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index f1c21a37..8f62cd21 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -255,7 +255,7 @@ def testCreateEncSAMLRequest(self): inflated = decompress(decoded, -15) self.assertRegexpMatches(inflated, '^') + self.assertRegexpMatches(inflated, 'AssertionConsumerServiceURL="http://stuff.com/endpoints/endpoints/acs.php"') self.assertRegexpMatches(inflated, 'http://stuff.com/endpoints/metadata.php') self.assertRegexpMatches(inflated, 'Format="urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"') self.assertRegexpMatches(inflated, 'ProviderName="SP prueba"') @@ -264,15 +264,18 @@ def testAttributeConsumingService(self): """ Tests that the attributeConsumingServiceIndex is present as an attribute """ + saml_settings = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(saml_settings) + + authn_request = OneLogin_Saml2_Authn_Request(settings) + authn_request_encoded = authn_request.get_request() + decoded = b64decode(authn_request_encoded) + inflated = decompress(decoded, -15) + + self.assertNotIn('AttributeConsumingServiceIndex="1"', inflated) saml_settings = self.loadSettingsJSON('settings4.json') settings = OneLogin_Saml2_Settings(saml_settings) - settings._OneLogin_Saml2_Settings__organization = { - u'en-US': { - u'url': u'http://sp.example.com', - u'name': u'sp_test' - } - } authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index f1417480..885dd121 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -155,21 +155,15 @@ def testBuilderAttributeConsumingService(self): security['wantAssertionsSigned'], None, None, contacts, organization ) - - self.assertIn('xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"', metadata) - self.assertIn('Test ServiceTest Service\ -', metadata) + self.assertIn(""" + Test Service + Test Service + + + + + + """, metadata) def testSignMetadata(self): """ diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index f9a899f6..6fdd4703 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -300,12 +300,9 @@ def testCheckSettings(self): # AttributeConsumingService tests # serviceName, requestedAttributes are required - settings_info['sp']['attributeConsumingService'] = [ - { - "isDefault": False, - "serviceDescription": "Test Service" - } - ] + settings_info['sp']['attributeConsumingService'] = { + "serviceDescription": "Test Service" + } try: OneLogin_Saml2_Settings(settings_info) self.assertTrue(False) @@ -314,19 +311,16 @@ def testCheckSettings(self): self.assertIn('sp_attributeConsumingService_requestedAttributes_not_found', e.message) # requestedAttributes/name is required - settings_info['sp']['attributeConsumingService'] = [ - { - "isDefault": "False", - "serviceName": {}, - "serviceDescription": ["Test Service"], - "requestedAttributes": [{ - "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", - "friendlyName": "givenName", - "isRequired": "False" - } - ] + settings_info['sp']['attributeConsumingService'] = { + "serviceName": {}, + "serviceDescription": ["Test Service"], + "requestedAttributes": [{ + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "givenName", + "isRequired": "False" } - ] + ] + } try: OneLogin_Saml2_Settings(settings_info) self.assertTrue(False) @@ -335,7 +329,6 @@ def testCheckSettings(self): self.assertIn('sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid', e.message) self.assertIn('sp_attributeConsumingService_serviceDescription_type_invalid', e.message) self.assertIn('sp_attributeConsumingService_serviceName_type_invalid', e.message) - self.assertIn('sp_attributeConsumingService_isDefault_type_invalid', e.message) settings_info['idp']['entityID'] = 'entityId' settings_info['idp']['singleSignOnService'] = {} From f7bfbc3eb85e3623ffd3895869418c5144863b23 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 6 Apr 2016 17:54:54 +0200 Subject: [PATCH 089/352] Add debug parameter to decrypt method --- src/onelogin/saml2/response.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 5d73ff99..85acd83b 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -456,6 +456,7 @@ def __decrypt_assertion(self, dom): :rtype: Element """ key = self.__settings.get_sp_key() + debug = self.__settings.is_debug_active() if not key: raise Exception('No private key available, check settings') @@ -484,7 +485,7 @@ def __decrypt_assertion(self, dom): keyinfo.append(encrypted_key[0]) encrypted_data = encrypted_data_nodes[0] - OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) + OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug) return dom def get_error(self): From 9920afb20b3e67a13b0c01799357a01a680245ee Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 6 Apr 2016 18:21:46 +0200 Subject: [PATCH 090/352] Remove comment --- src/onelogin/saml2/response.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index e1b17368..c4c51a5b 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -166,8 +166,6 @@ def is_valid(self, request_data, request_id=None): continue else: irt = sc_data.get('InResponseTo', None) - # We compare Assertion InResponseTo with Response value - # if we have both. if in_response_to and irt and irt != in_response_to: continue recipient = sc_data.get('Recipient', None) From 6b6c2fa5a0579a2873dc61435f3bb833be43c9e3 Mon Sep 17 00:00:00 2001 From: Florent PIGOUT Date: Thu, 11 Feb 2016 08:58:19 +0100 Subject: [PATCH 091/352] Get assertion when response signature reference URI is empty --- src/onelogin/saml2/response.py | 3 +++ ...esponse_with_unsigned_assertion.xml.base64 | 1 + .../src/OneLogin/saml2_tests/response_test.py | 20 +++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 tests/data/responses/valid_response_with_unsigned_assertion.xml.base64 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index c4c51a5b..5fb13574 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -417,8 +417,11 @@ def __query_assertion(self, xpath_expr): # Check if the message is signed signed_message_query = '/samlp:Response' + signature_expr message_reference_nodes = self.__query(signed_message_query) + # we can have reference node but URI can be empty + message_id = None if message_reference_nodes: message_id = message_reference_nodes[0].get('URI') + if message_id: final_query = "/samlp:Response[@ID='%s']/" % message_id[1:] else: final_query = "/samlp:Response" diff --git a/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64 b/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64 new file mode 100644 index 00000000..06b1919b --- /dev/null +++ b/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0icGZ4MDVmM2NlMTAtMTYxNS1mM2VhLWE5ODgtNjBlMzgwYjMyOTlmIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIERlc3RpbmF0aW9uPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyI+CiAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj4KICA8ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICAgIDxkczpTaWduZWRJbmZvPgogICAgICA8ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgogICAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIiPgogICAgICAgIDxkczpUcmFuc2Zvcm1zPgogICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+CiAgICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgICAgPC9kczpUcmFuc2Zvcm1zPgogICAgICAgIDxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPgogICAgICAgIDxkczpEaWdlc3RWYWx1ZT51VFlSOWFBcHBLdWp3YkY3azRUOFczLzBaZDA9PC9kczpEaWdlc3RWYWx1ZT4KICAgICAgPC9kczpSZWZlcmVuY2U+CiAgICA8L2RzOlNpZ25lZEluZm8+CiAgICA8ZHM6U2lnbmF0dXJlVmFsdWU+MnYxSDBSaVNJUEtYdEdUcTh3L3hpZHRrci9SWXZUVU90MXdCRmZlaXRhT0wyQkt1Z0JobEFoQyt2U2FVeFoweQpOUHpIdW1odlMzRHZkV2xmUjNzZlNra2VJeDZtYTd3blB2eko3MHZna09QRnF1UFRRQmdJdTVzMnMwUXJoZGgrCmYrU1dEWkpEbTNQeGpPbkhEQ3FrRE1Sc1VhejhtYzV3M0tWQnozQlhRaEU9PC9kczpTaWduYXR1cmVWYWx1ZT4KICA8L2RzOlNpZ25hdHVyZT4KICA8c2FtbHA6U3RhdHVzPgogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPgogIDwvc2FtbHA6U3RhdHVzPgogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiI+CiAgICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPgogICAgPHNhbWw6U3ViamVjdD4KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD4KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPgogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz4KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+CiAgICA8L3NhbWw6U3ViamVjdD4KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjM2OjMxWiIgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiI+CiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+CiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgPC9zYW1sOkNvbmRpdGlvbnM+CiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDA5OjM3OjAxWiIgU2Vzc2lvbkluZGV4PSJfNjI3M2Q3N2I4Y2RlMGMzMzNlYzc5ZDIyYTlmYTAwMDNiOWZlMmQ3NWNiIj4KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0PgogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPgogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0PgogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50PgogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbkB5YWNvLmVzPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50PgogIDwvc2FtbDpBc3NlcnRpb24+Cjwvc2FtbHA6UmVzcG9uc2U+Cg== \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index daf1c2d2..f7fafcca 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -207,6 +207,26 @@ def testQueryAssertions(self): response_7 = OneLogin_Saml2_Response(settings, xml_7) self.assertEqual(['http://idp.example.com/'], response_7.get_issuers()) + def testQueryAssertionsWithEmptyRefenceURI(self): + """ + Tests the __query_assertion if //Signature/Reference/@URI is empty. + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + # test with signed assertion still work + xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertEqual('492882615acf31c8096b627245d76ae53036c090', response.get_nameid()) + + # test with unsigned assertion still work + xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_unsigned_assertion.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertEqual('492882615acf31c8096b627245d76ae53036c090', response.get_nameid()) + + xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertEqual('saml@user.com', response.get_nameid()) + def testGetIssuers(self): """ Tests the get_issuers method of the OneLogin_Saml2_Response From eafae456df6d5931428801ee6524fac5a1b44f7d Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 12 Feb 2016 12:10:37 +0100 Subject: [PATCH 092/352] Modify signature reference URI only when use inner signature certificate When Response should be verifiy with the IdP certificate and the signature reference URI is empty we can not modify the Response content or it will be considered as invalid, ex.: $ xmlsec1 --sign \ --id-attr:ID urn:oasis:names:tc:SAML:2.0:protocol:Response \ --privkey-pem python-saml/metadata.key \ --output python-saml/signed_assertion.xml \ python-saml/unsigned_assertion.xml $ xmlsec1 --verify \ --id-attr:ID urn:oasis:names:tc:SAML:2.0:protocol:Response \ --pubkey-cert python-saml/metadata.crt \ python-saml/signed_assertion.xml OK $ python -c " from onelogin.saml2.utils import OneLogin_Saml2_Utils with open('python-saml/signed_assertion.xml') as assertion: with open('python-saml/metadata.crt') as cert: print OneLogin_Saml2_Utils.validate_sign(assertion.read(), cert.read(), debug=True) " signatures.c:346(xmlSecOpenSSLEvpSignatureVerify) obj=rsa-sha1 subject=EVP_VerifyFinal msg=signature do not match errno=18 False On another hand we should continue to do that if we validate the xml with an internal certificate. --- src/onelogin/saml2/utils.py | 12 ++++++------ tests/src/OneLogin/saml2_tests/response_test.py | 6 ++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index d7ade21d..844eaf3b 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1051,15 +1051,15 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger if fingerprint == x509_fingerprint_value: cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) + # Check if Reference URI is empty + reference_elem = OneLogin_Saml2_Utils.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 == '': return False - # Check if Reference URI is empty - reference_elem = OneLogin_Saml2_Utils.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')) - dsig_ctx = xmlsec.DSigCtx() file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index f7fafcca..53b00662 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1218,6 +1218,12 @@ def testIsValidSignWithEmptyReferenceURI(self): response = OneLogin_Saml2_Response(settings, xml) self.assertTrue(response.is_valid(self.get_request_data())) + def testIsValidSignWithEmptyReferenceURIAndIdPCert(self): + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_unsigned_assertion.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertTrue(response.is_valid(self.get_request_data())) + def testIsValidWithoutInResponseTo(self): """ If assertion contains InResponseTo but not the Response tag, we should From 5b49b9127cda43676dcf7f5d4014dc582d6d3f44 Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 16 Feb 2016 14:58:21 +0100 Subject: [PATCH 093/352] Remove useless statement --- src/onelogin/saml2/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 844eaf3b..1fafe770 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1060,8 +1060,6 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger if cert is None or cert == '': return False - dsig_ctx = xmlsec.DSigCtx() - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) if validatecert: From 33a7aa50a738649b3ef65ebd3e8ae85a4ee430be Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 7 Apr 2016 18:28:38 +0200 Subject: [PATCH 094/352] Fix #94 Idp Metadata parser --- src/onelogin/saml2/idp_metadata_parser.py | 155 ++++++++++++++++++ .../saml2_tests/idp_metadata_parser_test.py | 102 ++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 src/onelogin/saml2/idp_metadata_parser.py create mode 100644 tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py new file mode 100644 index 00000000..0634c177 --- /dev/null +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_IdPMetadataParser class + +Copyright (c) 2014, OneLogin, Inc. +All rights reserved. + +Metadata class of OneLogin's Python Toolkit. + +""" + +import urllib2 +from defusedxml.lxml import fromstring + +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.utils import OneLogin_Saml2_Utils + + +class OneLogin_Saml2_IdPMetadataParser(object): + """ + A class that contains methods related to obtain and parse metadata from IdP + """ + + @staticmethod + def get_metadata(url): + """ + Get the metadata XML from the provided URL + + :param url: Url where the XML of the Identity Provider Metadata is published. + :type url: string + + :returns: metadata XML + :rtype: string + """ + valid = False + response = urllib2.urlopen(url) + xml = response.read() + + if xml: + try: + dom = fromstring(xml) + idp_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:IDPSSODescriptor') + if idp_descriptor_nodes: + valid = True + except: + pass + + if not valid: + raise Exception('Not valid IdP XML found from URL: %s' % (url)) + + return xml + + @staticmethod + def parse_remote(url): + """ + Get the metadata XML from the provided URL and parse it, returning a dict with extracted data + + :param url: Url where the XML of the Identity Provider Metadata is published. + :type url: string + + :returns: settings dict with extracted data + :rtype: dict + """ + idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url) + return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata) + + @staticmethod + def parse(idp_metadata): + """ + Parse the Identity Provider metadata and returns a dict with extracted data + If there are multiple IDPSSODescriptor it will only parse the first + + :param idp_metadata: XML of the Identity Provider Metadata. + :type idp_metadata: string + + :param url: If true and the URL is HTTPs, the cert of the domain is checked. + :type url: bool + + :returns: settings dict with extracted data + :rtype: string + """ + data = {} + + dom = fromstring(idp_metadata) + entity_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:EntityDescriptor') + + idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = idp_x509_cert = None + + if len(entity_descriptor_nodes) > 0: + for entity_descriptor_node in entity_descriptor_nodes: + idp_descriptor_nodes = OneLogin_Saml2_Utils.query(entity_descriptor_node, './md:IDPSSODescriptor') + if len(idp_descriptor_nodes) > 0: + idp_descriptor_node = idp_descriptor_nodes[0] + + idp_entity_id = entity_descriptor_node.get('entityID', None) + + want_authn_requests_signed = entity_descriptor_node.get('WantAuthnRequestsSigned', None) + + name_id_format_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, './md:NameIDFormat') + if len(name_id_format_nodes) > 0: + idp_name_id_format = name_id_format_nodes[0].text + + sso_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:SingleSignOnService[@Binding='%s']" % OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT) + if len(sso_nodes) > 0: + idp_sso_url = sso_nodes[0].get('Location', None) + + slo_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:SingleLogoutService[@Binding='%s']" % OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT) + if len(slo_nodes) > 0: + idp_slo_url = slo_nodes[0].get('Location', None) + + cert_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate") + if len(cert_nodes) > 0: + idp_x509_cert = cert_nodes[0].text + + data['idp'] = {} + + if idp_entity_id is not None: + data['idp']['entityId'] = idp_entity_id + if idp_sso_url is not None: + data['idp']['singleLogoutService'] = {} + data['idp']['singleLogoutService']['url'] = idp_sso_url + if idp_slo_url is not None: + data['idp']['singleLogoutService'] = {} + data['idp']['singleLogoutService']['url'] = idp_slo_url + if idp_x509_cert is not None: + data['idp']['x509cert'] = idp_x509_cert + + if want_authn_requests_signed is not None: + data['security'] = {} + data['security']['authnRequestsSigned'] = want_authn_requests_signed + + if idp_name_id_format: + data['sp'] = {} + data['sp']['NameIDFormat'] = idp_name_id_format + + break + return data + + @staticmethod + def merge_settings(settings, new_metadata_settings): + """ + Will update the settings with the provided new settings data extracted from the IdP metadata + + :param settings: Current settings dict data + :type settings: string + + :param new_metadata_settings: Settings to be merged (extracted from IdP metadata after parsing) + :type new_metadata_settings: string + + :returns: merged settings + :rtype: dict + """ + result_settings = settings.copy() + result_settings.update(new_metadata_settings) + return result_settings diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py new file mode 100644 index 00000000..85158b49 --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2014, OneLogin, Inc. +# All rights reserved. + + +import json +from os.path import dirname, join, exists +from lxml.etree import XMLSyntaxError +import unittest +from teamcity import is_running_under_teamcity +from teamcity.unittestpy import TeamcityTestRunner + +from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser + + +class OneLogin_Saml2_IdPMetadataParser_Test(unittest.TestCase): + data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') + + def loadSettingsJSON(self, filename='settings1.json'): + filename = join(self.settings_path, filename) + if exists(filename): + stream = open(filename, 'r') + settings = json.load(stream) + stream.close() + return settings + else: + raise Exception('Settings json file does not exist') + + def file_contents(self, filename): + f = open(filename, 'r') + content = f.read() + f.close() + return content + + def testGetMetadata(self): + """ + Tests the get_metadata method of the OneLogin_Saml2_IdPMetadataParser + """ + with self.assertRaises(Exception): + data = OneLogin_Saml2_IdPMetadataParser.get_metadata('http://google.es') + + data = OneLogin_Saml2_IdPMetadataParser.get_metadata('https://www.testshib.org/metadata/testshib-providers.xml') + self.assertTrue(data is not None and data is not {}) + + def testParseRemote(self): + """ + Tests the parse_remote method of the OneLogin_Saml2_IdPMetadataParser + """ + with self.assertRaises(Exception): + data = OneLogin_Saml2_IdPMetadataParser.parse_remote('http://google.es') + + data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://www.testshib.org/metadata/testshib-providers.xml') + self.assertTrue(data is not None and data is not {}) + expected_data = {'sp': {'NameIDFormat': 'urn:mace:shibboleth:1.0:nameIdentifier'}, 'idp': {'singleLogoutService': {'url': 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO'}, 'entityId': 'https://idp.testshib.org/idp/shibboleth'}} + self.assertEqual(expected_data, data) + + def testParse(self): + """ + Tests the parse method of the OneLogin_Saml2_IdPMetadataParser + """ + with self.assertRaises(XMLSyntaxError): + data = OneLogin_Saml2_IdPMetadataParser.parse('') + + xml_sp_metadata = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml')) + data = OneLogin_Saml2_IdPMetadataParser.parse(xml_sp_metadata) + self.assertEqual({}, data) + + xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml')) + data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata) + expected_data = {'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, 'idp': {'singleLogoutService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}} + self.assertEqual(expected_data, data) + + def testMergeSettings(self): + """ + Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser + """ + with self.assertRaises(AttributeError): + settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(None, {}) + + with self.assertRaises(TypeError): + settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings({}, None) + + xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml')) + data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata) + settings = self.loadSettingsJSON() + settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(settings, data) + expected_data = {u'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, u'idp': {'singleLogoutService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'} + self.assertEqual(expected_data, settings_result) + + expected_data2 = {'sp': {u'singleLogoutService': {u'url': u'http://stuff.com/endpoints/endpoints/sls.php'}, u'assertionConsumerService': {u'url': u'http://stuff.com/endpoints/endpoints/acs.php'}, u'entityId': u'http://stuff.com/endpoints/metadata.php', u'NameIDFormat': u'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified'}, 'idp': {u'singleLogoutService': {u'url': u'http://idp.example.com/SingleLogoutService.php'}, u'entityId': u'http://idp.example.com/', u'x509cert': u'MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo', u'singleSignOnService': {u'url': u'http://idp.example.com/SSOService.php'}}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'} + settings_result2 = OneLogin_Saml2_IdPMetadataParser.merge_settings(data, settings) + self.assertEqual(expected_data2, settings_result2) + + +if __name__ == '__main__': + if is_running_under_teamcity(): + runner = TeamcityTestRunner() + else: + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) From 48ca75682888c5b34b8d0f58b8977b361ca1799a Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 8 Apr 2016 12:56:17 +0200 Subject: [PATCH 095/352] Add documentation related to the new IdP metadata parser methods --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 4dec68ea..a2f26de3 100644 --- a/README.md +++ b/README.md @@ -926,6 +926,15 @@ Auxiliary class that contains several methods * ***validate_sign*** Validates a signature (Message or Assertion). * ***validate_binary_sign*** Validates signed bynary data (Used to validate GET Signature). +####OneLogin_Saml2_IdPMetadataParser - idp_metadata_parser.py#### + +A class that contains methods to obtain and parse metadata from IdP + +* ***get_metadata*** Get the metadata XML from the provided URL +* ***parse_remote*** Get the metadata XML from the provided URL and parse it, returning a dict with extracted data +* ***parse*** Parse the Identity Provider metadata and returns a dict with extracted data +* ***merge_settings*** Will update the settings with the provided new settings data extracted from the IdP metadata + For more info, look at the source code; each method is documented and details about what does and how to use it are provided. Make sure to also check the doc folder where HTML documentation about the classes and methods is provided. Demos included in the toolkit From 7fc8370f5efab581c1533322821f3c5c4332023b Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 8 Apr 2016 19:01:03 +0200 Subject: [PATCH 096/352] Related to PR 83. extract the already encoded value directly from --- README.md | 2 ++ demo-bottle/index.py | 1 + demo-django/demo/views.py | 3 ++- demo-flask/index.py | 3 ++- src/onelogin/saml2/logout_request.py | 7 +++---- src/onelogin/saml2/utils.py | 22 ++++++++++++++++++++++ 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a2f26de3..e1a83714 100644 --- a/README.md +++ b/README.md @@ -925,6 +925,8 @@ Auxiliary class that contains several methods * ***add_sign*** Adds signature key and senders certificate to an element (Message or Assertion). * ***validate_sign*** Validates a signature (Message or Assertion). * ***validate_binary_sign*** Validates signed bynary data (Used to validate GET Signature). +* ***def get_encoded_parameter*** Return an url encoded get parameter value +* ***extract_raw_query_parameter*** ####OneLogin_Saml2_IdPMetadataParser - idp_metadata_parser.py#### diff --git a/demo-bottle/index.py b/demo-bottle/index.py index bf201066..2763f66c 100644 --- a/demo-bottle/index.py +++ b/demo-bottle/index.py @@ -35,6 +35,7 @@ def prepare_bottle_request(req): 'script_name': req.fullpath, 'get_data': req.query, 'post_data': req.forms, + 'query_string': req.query_string, 'https': 'on' if req.urlparts.scheme == 'https' else 'off' } diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index 3b67b2fe..614ac067 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -23,7 +23,8 @@ def prepare_django_request(request): 'script_name': request.META['PATH_INFO'], 'server_port': request.META['SERVER_PORT'], 'get_data': request.GET.copy(), - 'post_data': request.POST.copy() + 'post_data': request.POST.copy(), + 'query_string': req.query_string } return result diff --git a/demo-flask/index.py b/demo-flask/index.py index a2eb1cb1..0691e483 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -28,7 +28,8 @@ def prepare_flask_request(request): 'server_port': url_data.port, 'script_name': request.path, 'get_data': request.args.copy(), - 'post_data': request.form.copy() + 'post_data': request.form.copy(), + 'query_string': req.query_string } diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 6239bbf3..8c1a83d2 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -13,7 +13,6 @@ from base64 import b64encode, b64decode from lxml import etree from defusedxml.lxml import fromstring -from urllib import quote_plus from xml.dom.minidom import Document from onelogin.saml2.constants import OneLogin_Saml2_Constants @@ -317,10 +316,10 @@ def is_valid(self, request_data): else: sign_alg = get_data['SigAlg'] - signed_query = 'SAMLRequest=%s' % quote_plus(get_data['SAMLRequest']) + signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest') if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, quote_plus(get_data['RelayState'])) - signed_query = '%s&SigAlg=%s' % (signed_query, quote_plus(sign_alg)) + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState')) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index d7ade21d..d3595cb3 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1127,3 +1127,25 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ return True except Exception: return False + + @staticmethod + def get_encoded_parameter(get_data, name, default=None): + """Return an url encoded get parameter value + Prefer to extract the original encoded value directly from query_string since url + encoding is not canonical. The encoding used by ADFS 3.0 is not compatible with + python's quote_plus (ADFS produces lower case hex numbers and quote_plus produces + upper case hex numbers) + """ + if name not in get_data: + return quote_plus(default) + if 'query_string' in get_data: + return OneLogin_Saml2_Utils.extract_raw_query_parameter(get_data['query_string'], name) + return quote_plus(get_data[name]) + + @staticmethod + def extract_raw_query_parameter(query_string, parameter, default=''): + m = re.search('%s=([^&]+)' % parameter, query_string) + if m: + return m.group(1) + else: + return default From 123e9393e07ab3c5607049e9da8aaaded4b3662e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 8 Apr 2016 19:14:05 +0200 Subject: [PATCH 097/352] . --- demo-django/demo/views.py | 2 +- demo-flask/index.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index 614ac067..13881f65 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -24,7 +24,7 @@ def prepare_django_request(request): 'server_port': request.META['SERVER_PORT'], 'get_data': request.GET.copy(), 'post_data': request.POST.copy(), - 'query_string': req.query_string + 'query_string': request.META['QUERY_STRING'] } return result diff --git a/demo-flask/index.py b/demo-flask/index.py index 0691e483..9c9abf42 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -29,7 +29,7 @@ def prepare_flask_request(request): 'script_name': request.path, 'get_data': request.args.copy(), 'post_data': request.form.copy(), - 'query_string': req.query_string + 'query_string': request.query_string } From d08dd47b3e58adf130bb99c8d991b5ec0ea51664 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 8 Apr 2016 19:30:00 +0200 Subject: [PATCH 098/352] Forgot to modify LogoutResponse code --- src/onelogin/saml2/logout_response.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index fb049bb3..a9cce855 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -12,7 +12,6 @@ from base64 import b64encode, b64decode from defusedxml.lxml import fromstring -from urllib import quote_plus from xml.dom.minidom import Document from defusedxml.minidom import parseString @@ -120,10 +119,10 @@ def is_valid(self, request_data, request_id=None): else: sign_alg = get_data['SigAlg'] - signed_query = 'SAMLResponse=%s' % quote_plus(get_data['SAMLResponse']) + signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse') if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, quote_plus(get_data['RelayState'])) - signed_query = '%s&SigAlg=%s' % (signed_query, quote_plus(sign_alg)) + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState')) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') From ad53d41381afcd7f55a056d4517248c4cab9c579 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 8 Apr 2016 20:57:26 +0200 Subject: [PATCH 099/352] Fix init method's docstring of OneLogin_Saml2_Auth and OneLogin_Saml2_Settings. Only dict is a possible value for settings, no an object --- src/onelogin/saml2/auth.py | 3 ++- src/onelogin/saml2/settings.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index fc0e5020..65e0352d 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -15,6 +15,7 @@ from urllib import quote_plus import dm.xmlsec.binding as xmlsec +import copy from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.response import OneLogin_Saml2_Response @@ -44,7 +45,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): :type request_data: dict :param settings: Optional. SAML Toolkit Settings - :type settings: dict|object + :type settings: dict :param custom_base_path: Optional. Path where are stored the settings file and the cert folder :type custom_base_path: string diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 5ac99992..12b6e1df 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -65,7 +65,7 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals - Loads settings info from settings file or array/object provided :param settings: SAML Toolkit Settings - :type settings: dict|object + :type settings: dict :param custom_base_path: Path where are stored the settings file and the cert folder :type custom_base_path: string From e73ed4d0ec6b3321be16627e53525f126713bb2e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 8 Apr 2016 22:40:05 +0200 Subject: [PATCH 100/352] Notice that x509cert is require for validate HTTP-Redirect signatures --- README.md | 3 ++- src/onelogin/saml2/auth.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e1a83714..5c8aa09e 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,8 @@ This is the settings.json file: "x509cert": "" /* * Instead of use the whole x509cert you can use a fingerprint in order to - * validate a SAMLResponse. + * validate a SAMLResponse, but you will need it to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding. + * * (openssl x509 -noout -fingerprint -in "idp.crt" to generate it, * or add for example the -sha256 , -sha384 or -sha512 parameter) * diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 65e0352d..a41a3f92 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -15,7 +15,6 @@ from urllib import quote_plus import dm.xmlsec.binding as xmlsec -import copy from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.response import OneLogin_Saml2_Response From c12d308e3b81afea22ed201e56d5423aaa2a7285 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 15 Apr 2016 16:27:50 +0200 Subject: [PATCH 101/352] Revert "Get/validate assertion when empty Response signature ref URI" --- src/onelogin/saml2/response.py | 3 --- src/onelogin/saml2/utils.py | 14 +++++----- ...esponse_with_unsigned_assertion.xml.base64 | 1 - .../src/OneLogin/saml2_tests/response_test.py | 26 ------------------- 4 files changed, 8 insertions(+), 36 deletions(-) delete mode 100644 tests/data/responses/valid_response_with_unsigned_assertion.xml.base64 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 5fb13574..c4c51a5b 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -417,11 +417,8 @@ def __query_assertion(self, xpath_expr): # Check if the message is signed signed_message_query = '/samlp:Response' + signature_expr message_reference_nodes = self.__query(signed_message_query) - # we can have reference node but URI can be empty - message_id = None if message_reference_nodes: message_id = message_reference_nodes[0].get('URI') - if message_id: final_query = "/samlp:Response[@ID='%s']/" % message_id[1:] else: final_query = "/samlp:Response" diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index f6fc76dd..d3595cb3 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1051,15 +1051,17 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger if fingerprint == x509_fingerprint_value: cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) - # Check if Reference URI is empty - reference_elem = OneLogin_Saml2_Utils.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 == '': return False + # Check if Reference URI is empty + reference_elem = OneLogin_Saml2_Utils.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')) + + dsig_ctx = xmlsec.DSigCtx() + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) if validatecert: diff --git a/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64 b/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64 deleted file mode 100644 index 06b1919b..00000000 --- a/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64 +++ /dev/null @@ -1 +0,0 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0icGZ4MDVmM2NlMTAtMTYxNS1mM2VhLWE5ODgtNjBlMzgwYjMyOTlmIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIERlc3RpbmF0aW9uPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyI+CiAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj4KICA8ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICAgIDxkczpTaWduZWRJbmZvPgogICAgICA8ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgogICAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIiPgogICAgICAgIDxkczpUcmFuc2Zvcm1zPgogICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+CiAgICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgICAgPC9kczpUcmFuc2Zvcm1zPgogICAgICAgIDxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPgogICAgICAgIDxkczpEaWdlc3RWYWx1ZT51VFlSOWFBcHBLdWp3YkY3azRUOFczLzBaZDA9PC9kczpEaWdlc3RWYWx1ZT4KICAgICAgPC9kczpSZWZlcmVuY2U+CiAgICA8L2RzOlNpZ25lZEluZm8+CiAgICA8ZHM6U2lnbmF0dXJlVmFsdWU+MnYxSDBSaVNJUEtYdEdUcTh3L3hpZHRrci9SWXZUVU90MXdCRmZlaXRhT0wyQkt1Z0JobEFoQyt2U2FVeFoweQpOUHpIdW1odlMzRHZkV2xmUjNzZlNra2VJeDZtYTd3blB2eko3MHZna09QRnF1UFRRQmdJdTVzMnMwUXJoZGgrCmYrU1dEWkpEbTNQeGpPbkhEQ3FrRE1Sc1VhejhtYzV3M0tWQnozQlhRaEU9PC9kczpTaWduYXR1cmVWYWx1ZT4KICA8L2RzOlNpZ25hdHVyZT4KICA8c2FtbHA6U3RhdHVzPgogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPgogIDwvc2FtbHA6U3RhdHVzPgogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiI+CiAgICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPgogICAgPHNhbWw6U3ViamVjdD4KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD4KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPgogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz4KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+CiAgICA8L3NhbWw6U3ViamVjdD4KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjM2OjMxWiIgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiI+CiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+CiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgPC9zYW1sOkNvbmRpdGlvbnM+CiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDA5OjM3OjAxWiIgU2Vzc2lvbkluZGV4PSJfNjI3M2Q3N2I4Y2RlMGMzMzNlYzc5ZDIyYTlmYTAwMDNiOWZlMmQ3NWNiIj4KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0PgogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPgogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0PgogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50PgogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbkB5YWNvLmVzPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50PgogIDwvc2FtbDpBc3NlcnRpb24+Cjwvc2FtbHA6UmVzcG9uc2U+Cg== \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 53b00662..daf1c2d2 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -207,26 +207,6 @@ def testQueryAssertions(self): response_7 = OneLogin_Saml2_Response(settings, xml_7) self.assertEqual(['http://idp.example.com/'], response_7.get_issuers()) - def testQueryAssertionsWithEmptyRefenceURI(self): - """ - Tests the __query_assertion if //Signature/Reference/@URI is empty. - """ - settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - - # test with signed assertion still work - xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) - response = OneLogin_Saml2_Response(settings, xml) - self.assertEqual('492882615acf31c8096b627245d76ae53036c090', response.get_nameid()) - - # test with unsigned assertion still work - xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_unsigned_assertion.xml.base64')) - response = OneLogin_Saml2_Response(settings, xml) - self.assertEqual('492882615acf31c8096b627245d76ae53036c090', response.get_nameid()) - - xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64')) - response = OneLogin_Saml2_Response(settings, xml) - self.assertEqual('saml@user.com', response.get_nameid()) - def testGetIssuers(self): """ Tests the get_issuers method of the OneLogin_Saml2_Response @@ -1218,12 +1198,6 @@ def testIsValidSignWithEmptyReferenceURI(self): response = OneLogin_Saml2_Response(settings, xml) self.assertTrue(response.is_valid(self.get_request_data())) - def testIsValidSignWithEmptyReferenceURIAndIdPCert(self): - settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_unsigned_assertion.xml.base64')) - response = OneLogin_Saml2_Response(settings, xml) - self.assertTrue(response.is_valid(self.get_request_data())) - def testIsValidWithoutInResponseTo(self): """ If assertion contains InResponseTo but not the Response tag, we should From e6d8bb7b5816746117bbf97420ef1e7d1f08135a Mon Sep 17 00:00:00 2001 From: chriskohlbrenner Date: Mon, 18 Apr 2016 15:36:39 -0400 Subject: [PATCH 102/352] [docs] minor spelling improvements to typos in readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5c8aa09e..3118b929 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ SAML requires a x.509 cert to sign and encrypt elements like NameID, Message, As If our environment requires sign or encrypt support, the certs folder may contain the x509 cert and the private key that the SP will use: * sp.crt The public cert of the SP -* sp.key The privake key of the SP +* sp.key The private key of the SP Or also we can provide those data in the setting file at the 'x509cert' and the privateKey' json parameters of the 'sp' element. @@ -225,7 +225,7 @@ This is the settings.json file: }, // If you need to specify requested attributes, set a // attributeConsumingService. nameFormat, attributeValue and - // friendlyName can be ommited + // friendlyName can be omitted "attributeConsumingService": { "ServiceName": "SP test", "serviceDescription": "Test Service", @@ -356,7 +356,7 @@ In addition to the required settings data (idp, sp), there is extra information // Authentication context. // Set to false and no AuthContext will be sent in the AuthNRequest, - // Set true or don't present thi parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + // Set true or don't present this parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' // Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'), 'requestedAuthnContext': true, // Allows the authn comparison parameter to be set, defaults to 'exact' if the setting is not present. @@ -378,7 +378,7 @@ In addition to the required settings data (idp, sp), there is extra information 'signatureAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' }, - // Contact information template, it is recommended to suply a + // Contact information template, it is recommended to supply // technical and support contacts. "contactPerson": { "technical": { @@ -392,7 +392,7 @@ In addition to the required settings data (idp, sp), there is extra information }, // Organization information template, the info in en_US lang is - // recomended, add more if required. + // recommended, add more if required. "organization": { "en-US": { "name": "sp_test", From d61f78d59861087719559cb8f0b6e0646e934f51 Mon Sep 17 00:00:00 2001 From: Chad Bibler Date: Fri, 22 Apr 2016 15:44:26 -0500 Subject: [PATCH 103/352] Fix typo in test --- tests/src/OneLogin/saml2_tests/utils_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index d84adf65..7bc49f95 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -734,7 +734,7 @@ def testDecryptElement(self): except: pass - key_3_file_name = join(self.data_path, 'misc', 'sp2.key') + key_3_file_name = join(self.data_path, 'misc', 'sp3.key') f = open(key_3_file_name, 'r') key3 = f.read() f.close() From b4037992f97b6ee21afc37e752f5a3888ef284d9 Mon Sep 17 00:00:00 2001 From: Chad Bibler Date: Fri, 22 Apr 2016 15:45:27 -0500 Subject: [PATCH 104/352] Assert failure for the expected reason --- tests/src/OneLogin/saml2_tests/utils_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 7bc49f95..f93f7da9 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -731,8 +731,8 @@ def testDecryptElement(self): try: OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key2) self.assertTrue(False) - except: - pass + except Exception as e: + self.assertEqual('failed to decrypt', e[0]) key_3_file_name = join(self.data_path, 'misc', 'sp3.key') f = open(key_3_file_name, 'r') @@ -741,8 +741,8 @@ def testDecryptElement(self): try: OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key3) self.assertTrue(False) - except: - pass + except Exception as e: + self.assertEqual('failed to decrypt', e[0]) xml_nameid_enc_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_EncMethod.xml.base64'))) dom_nameid_enc_2 = parseString(xml_nameid_enc_2) From 12f3d3946f4800ddca0d2824832f75fbbe0b5d03 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 28 Apr 2016 17:19:07 +0200 Subject: [PATCH 105/352] IdP metadata parser: fix singleSignOnService processing --- src/onelogin/saml2/idp_metadata_parser.py | 4 ++-- src/onelogin/saml2/utils.py | 14 +++++++------- .../saml2_tests/idp_metadata_parser_test.py | 6 +++--- tests/src/OneLogin/saml2_tests/response_test.py | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 0634c177..6a396d60 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -117,8 +117,8 @@ def parse(idp_metadata): if idp_entity_id is not None: data['idp']['entityId'] = idp_entity_id if idp_sso_url is not None: - data['idp']['singleLogoutService'] = {} - data['idp']['singleLogoutService']['url'] = idp_sso_url + data['idp']['singleSignOnService'] = {} + data['idp']['singleSignOnService']['url'] = idp_sso_url if idp_slo_url is not None: data['idp']['singleLogoutService'] = {} data['idp']['singleLogoutService']['url'] = idp_slo_url diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index d3595cb3..06a6f5cd 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1051,16 +1051,16 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger if fingerprint == x509_fingerprint_value: cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) - if cert is None or cert == '': - return False # Check if Reference URI is empty - reference_elem = OneLogin_Saml2_Utils.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')) + # reference_elem = OneLogin_Saml2_Utils.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')) - dsig_ctx = xmlsec.DSigCtx() + + if cert is None or cert == '': + return False file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index 85158b49..8e1c5c14 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -53,7 +53,7 @@ def testParseRemote(self): data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://www.testshib.org/metadata/testshib-providers.xml') self.assertTrue(data is not None and data is not {}) - expected_data = {'sp': {'NameIDFormat': 'urn:mace:shibboleth:1.0:nameIdentifier'}, 'idp': {'singleLogoutService': {'url': 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO'}, 'entityId': 'https://idp.testshib.org/idp/shibboleth'}} + expected_data = {'sp': {'NameIDFormat': 'urn:mace:shibboleth:1.0:nameIdentifier'}, 'idp': {'singleSignOnService': {'url': 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO'}, 'entityId': 'https://idp.testshib.org/idp/shibboleth'}} self.assertEqual(expected_data, data) def testParse(self): @@ -69,7 +69,7 @@ def testParse(self): xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml')) data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata) - expected_data = {'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, 'idp': {'singleLogoutService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}} + expected_data = {'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, 'idp': {'singleSignOnService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}} self.assertEqual(expected_data, data) def testMergeSettings(self): @@ -86,7 +86,7 @@ def testMergeSettings(self): data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata) settings = self.loadSettingsJSON() settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(settings, data) - expected_data = {u'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, u'idp': {'singleLogoutService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'} + expected_data = {u'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, u'idp': {'singleSignOnService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'} self.assertEqual(expected_data, settings_result) expected_data2 = {'sp': {u'singleLogoutService': {u'url': u'http://stuff.com/endpoints/endpoints/sls.php'}, u'assertionConsumerService': {u'url': u'http://stuff.com/endpoints/endpoints/acs.php'}, u'entityId': u'http://stuff.com/endpoints/metadata.php', u'NameIDFormat': u'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified'}, 'idp': {u'singleLogoutService': {u'url': u'http://idp.example.com/SingleLogoutService.php'}, u'entityId': u'http://idp.example.com/', u'x509cert': u'MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo', u'singleSignOnService': {u'url': u'http://idp.example.com/SSOService.php'}}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'} diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index daf1c2d2..7041e04f 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1196,7 +1196,7 @@ def testIsValidSignWithEmptyReferenceURI(self): settings = OneLogin_Saml2_Settings(settings_info) xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - self.assertTrue(response.is_valid(self.get_request_data())) + self.assertFalse(response.is_valid(self.get_request_data())) def testIsValidWithoutInResponseTo(self): """ From 414e05e3f3de6f03b3f66d398eda2e37f4342eb0 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 10 May 2016 17:02:11 +0200 Subject: [PATCH 106/352] Fix #119. Allow AuthnRequest with no NameIDPolicy --- README.md | 7 ++-- src/onelogin/saml2/auth.py | 12 ++++-- src/onelogin/saml2/authn_request.py | 26 ++++++++----- src/onelogin/saml2/utils.py | 2 - tests/src/OneLogin/saml2_tests/auth_test.py | 38 ++++++++++++++++++- .../saml2_tests/authn_request_test.py | 28 ++++++++++++++ 6 files changed, 93 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3118b929..6a93e657 100644 --- a/README.md +++ b/README.md @@ -518,10 +518,11 @@ We can set a 'return_to' url parameter to the login function and that will be co target_url = 'https://example.com' auth.login(return_to=target_url) ``` -The login method can recieve 2 more optional parameters: +The login method can recieve 3 more optional parameters: -* force_authn When true the AuthNReuqest will set the ForceAuthn='true' -* is_passive When true the AuthNReuqest will set the Ispassive='true' +* force_authn When true the AuthNReuqest will set the ForceAuthn='true' +* 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 diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index a41a3f92..26e13e28 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -267,7 +267,7 @@ def get_last_request_id(self): """ return self.__last_request_id - def login(self, return_to=None, force_authn=False, is_passive=False): + def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True): """ Initiates the SSO process. @@ -275,14 +275,18 @@ def login(self, return_to=None, force_authn=False, is_passive=False): :type return_to: string :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'. - :type force_authn: string + :type force_authn: bool :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. - :type is_passive: string + :type is_passive: bool + + :param set_nameid_policy: Optional argument. When true the AuthNReuqest will set a nameIdPolicy element. + :type set_nameid_policy: bool :returns: Redirection url + :rtype: string """ - authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive) + authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy) self.__last_request_id = authn_request.get_id() diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 81d53200..452a1975 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -22,7 +22,7 @@ class OneLogin_Saml2_Authn_Request(object): """ - def __init__(self, settings, force_authn=False, is_passive=False): + def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_policy=True): """ Constructs the AuthnRequest object. @@ -34,6 +34,9 @@ def __init__(self, settings, force_authn=False, is_passive=False): :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. :type is_passive: bool + + :param set_nameid_policy: Optional argument. When true the AuthNReuqest will set a nameIdPolicy element. + :type set_nameid_policy: bool """ self.__settings = settings @@ -47,10 +50,6 @@ def __init__(self, settings, force_authn=False, is_passive=False): destination = idp_data['singleSignOnService']['url'] - name_id_policy_format = sp_data['NameIDFormat'] - if 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted']: - name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED - provider_name_str = '' organization_data = settings.get_organization() if isinstance(organization_data, dict) and organization_data: @@ -70,6 +69,17 @@ def __init__(self, settings, force_authn=False, is_passive=False): if is_passive is True: is_passive_str = 'IsPassive="true"' + nameid_policy_str = '' + if set_nameid_policy: + name_id_policy_format = sp_data['NameIDFormat'] + if 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted']: + name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED + + nameid_policy_str = """ + """ % name_id_policy_format + requested_authn_context_str = '' if 'requestedAuthnContext' in security.keys() and security['requestedAuthnContext'] is not False: authn_comparison = 'exact' @@ -104,9 +114,7 @@ def __init__(self, settings, force_authn=False, is_passive=False): AssertionConsumerServiceURL="%(assertion_url)s" %(attr_consuming_service_str)s> %(entity_id)s - +%(nameid_policy_str)s %(requested_authn_context_str)s """ % \ { @@ -118,7 +126,7 @@ def __init__(self, settings, force_authn=False, is_passive=False): 'destination': destination, 'assertion_url': sp_data['assertionConsumerService']['url'], 'entity_id': sp_data['entityId'], - 'name_id_policy': name_id_policy_format, + 'nameid_policy_str': nameid_policy_str, 'requested_authn_context_str': requested_authn_context_str, 'attr_consuming_service_str': attr_consuming_service_str } diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 06a6f5cd..eccbc5bd 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1051,14 +1051,12 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger if fingerprint == x509_fingerprint_value: cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) - # Check if Reference URI is empty # reference_elem = OneLogin_Saml2_Utils.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 == '': return False diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index e75dfef7..1bc0a577 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -608,7 +608,7 @@ def testLoginSigned(self): def testLoginForceAuthN(self): """ Tests the login method of the OneLogin_Saml2_Auth class - Case Logout with no parameters. A AuthN Request is built with ForceAuthn and redirect executed + Case Login with no parameters. A AuthN Request is built with ForceAuthn and redirect executed """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' @@ -642,7 +642,7 @@ def testLoginForceAuthN(self): def testLoginIsPassive(self): """ Tests the login method of the OneLogin_Saml2_Auth class - Case Logout with no parameters. A AuthN Request is built with IsPassive and redirect executed + Case Login with no parameters. A AuthN Request is built with IsPassive and redirect executed """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' @@ -673,6 +673,40 @@ def testLoginIsPassive(self): request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0]) self.assertIn('IsPassive="true"', request_3) + def testLoginSetNameIDPolicy(self): + """ + Tests the login method of the OneLogin_Saml2_Auth class + Case Logout with no parameters. A AuthN Request is built with and without NameIDPolicy + """ + settings_info = self.loadSettingsJSON() + return_to = u'http://example.com/returnto' + sso_url = settings_info['idp']['singleSignOnService']['url'] + + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url = auth.login(return_to) + parsed_query = parse_qs(urlparse(target_url)[4]) + sso_url = settings_info['idp']['singleSignOnService']['url'] + self.assertIn(sso_url, target_url) + self.assertIn('SAMLRequest', parsed_query) + request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]) + self.assertIn(' Date: Tue, 10 May 2016 17:31:51 +0200 Subject: [PATCH 107/352] Improve AuthNRequest format --- src/onelogin/saml2/authn_request.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 452a1975..ea0200e8 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -59,15 +59,15 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol else: lang = langs[0] if 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] is not None: - provider_name_str = 'ProviderName="%s"' % organization_data[lang]['displayname'] + provider_name_str = "\n" + ' ProviderName="%s"' % organization_data[lang]['displayname'] force_authn_str = '' if force_authn is True: - force_authn_str = 'ForceAuthn="true"' + force_authn_str = "\n" + ' ForceAuthn="true"' is_passive_str = '' if is_passive is True: - is_passive_str = 'IsPassive="true"' + is_passive_str = "\n" + ' IsPassive="true"' nameid_policy_str = '' if set_nameid_policy: @@ -87,11 +87,11 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol authn_comparison = security['requestedAuthnContextComparison'] if security['requestedAuthnContext'] is True: - requested_authn_context_str = """ + requested_authn_context_str = "\n" + """ urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport """ % authn_comparison else: - requested_authn_context_str = ' ' % authn_comparison + requested_authn_context_str = "\n" + ' ' % authn_comparison for authn_context in security['requestedAuthnContext']: requested_authn_context_str += '%s' % authn_context requested_authn_context_str += ' ' @@ -104,18 +104,13 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol 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 + 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 + %(entity_id)s%(nameid_policy_str)s%(requested_authn_context_str)s """ % \ { 'id': uid, From 4c14943005f7c787ac59a2b9ad395d3fc565ebd1 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 11 May 2016 21:34:27 +0200 Subject: [PATCH 108/352] #112. Remove requirement on responsesRemove --- README.md | 4 ++ demo-bottle/saml/advanced_settings.json | 1 + demo-django/saml/advanced_settings.json | 1 + demo-flask/saml/advanced_settings.json | 1 + src/onelogin/saml2/auth.py | 4 +- src/onelogin/saml2/response.py | 24 +++++--- src/onelogin/saml2/settings.py | 4 ++ .../src/OneLogin/saml2_tests/response_test.py | 60 ++++++++++++++++++- .../src/OneLogin/saml2_tests/settings_test.py | 41 +++++++++++++ 9 files changed, 129 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6a93e657..07b96dc8 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,10 @@ In addition to the required settings data (idp, sp), there is extra information // this SP to be signed. [Metadata of the SP will offer this info] "wantAssertionsSigned": false, + // Indicates a requirement for the NameID element on the SAMLResponse + // received by this SP to be present. + "wantNameId": true, + // Indicates a requirement for the NameID received by // this SP to be encrypted. "wantNameIdEncrypted": false, diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json index 4ea002ad..78e5662c 100644 --- a/demo-bottle/saml/advanced_settings.json +++ b/demo-bottle/saml/advanced_settings.json @@ -7,6 +7,7 @@ "signMetadata": false, "wantMessagesSigned": false, "wantAssertionsSigned": false, + "wantNameId" : true, "wantNameIdEncrypted": false, "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json index 97f3a374..04bd0fba 100644 --- a/demo-django/saml/advanced_settings.json +++ b/demo-django/saml/advanced_settings.json @@ -7,6 +7,7 @@ "signMetadata": false, "wantMessagesSigned": false, "wantAssertionsSigned": false, + "wantNameId" : true, "wantNameIdEncrypted": false, "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json index 97f3a374..04bd0fba 100644 --- a/demo-flask/saml/advanced_settings.json +++ b/demo-flask/saml/advanced_settings.json @@ -7,6 +7,7 @@ "signMetadata": false, "wantMessagesSigned": false, "wantAssertionsSigned": false, + "wantNameId" : true, "wantNameIdEncrypted": false, "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 26e13e28..ff06da50 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -206,7 +206,7 @@ def get_nameid(self): Returns the nameID. :returns: NameID - :rtype: string + :rtype: string|None """ return self.__nameid @@ -222,7 +222,7 @@ def get_session_expiration(self): """ Returns the SessionNotOnOrAfter from the AuthnStatement. :returns: The SessionNotOnOrAfter of the assertion - :rtype: DateTime|null + :rtype: DateTime|None """ return self.__session_expiration diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index c4c51a5b..77453b86 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -276,6 +276,8 @@ def get_nameid_data(self): :rtype: dict """ nameid = None + nameid_data = {} + encrypted_id_data_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') if encrypted_id_data_nodes: encrypted_data = encrypted_id_data_nodes[0] @@ -286,13 +288,16 @@ def get_nameid_data(self): if nameid_nodes: nameid = nameid_nodes[0] if nameid is None: - raise Exception('Not NameID found in the assertion of the Response') + security = self.__settings.get_security_data() - nameid_data = {'Value': nameid.text} - for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: - value = nameid.get(attr, None) - if value: - nameid_data[attr] = value + if security.get('wantNameId', True): + raise Exception('Not NameID found in the assertion of the Response') + else: + nameid_data = {'Value': nameid.text} + for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: + value = nameid.get(attr, None) + if value: + nameid_data[attr] = value return nameid_data def get_nameid(self): @@ -300,10 +305,13 @@ def get_nameid(self): Gets the NameID provided by the SAML Response from the IdP :returns: NameID (value) - :rtype: string + :rtype: string|None """ + nameid_value = None nameid_data = self.get_nameid_data() - return nameid_data['Value'] + if nameid_data and 'Value' in nameid_data.keys(): + nameid_value = nameid_data['Value'] + return nameid_value def get_session_not_on_or_after(self): """ diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 12b6e1df..60d8cdb5 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -293,6 +293,10 @@ def __add_default_values(self): if 'wantAssertionsSigned' not in self.__security.keys(): self.__security['wantAssertionsSigned'] = False + # NameID element expected + if 'wantNameId' not in self.__security.keys(): + self.__security['wantNameId'] = True + # Encrypt expected if 'wantAssertionsEncrypted' not in self.__security.keys(): self.__security['wantAssertionsEncrypted'] = False diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 7041e04f..34bea8d6 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -64,7 +64,9 @@ def testReturnNameId(self): """ Tests the get_nameid method of the OneLogin_Saml2_Response """ - settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + json_settings = self.loadSettingsJSON() + + settings = OneLogin_Saml2_Settings(json_settings) xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) self.assertEqual('support@onelogin.com', response.get_nameid()) @@ -85,10 +87,39 @@ def testReturnNameId(self): except Exception as e: self.assertIn('Not NameID found in the assertion of the Response', e.message) + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + + response_5 = OneLogin_Saml2_Response(settings, xml_4) + try: + response_5.get_nameid() + self.assertTrue(False) + except Exception as e: + self.assertIn('Not NameID found in the assertion of the Response', e.message) + + json_settings['security']['wantNameId'] = False + settings = OneLogin_Saml2_Settings(json_settings) + + response_6 = OneLogin_Saml2_Response(settings, xml_4) + nameid_6 = response_6.get_nameid() + self.assertIsNone(nameid_6) + + del json_settings['security']['wantNameId'] + settings = OneLogin_Saml2_Settings(json_settings) + + response_7 = OneLogin_Saml2_Response(settings, xml_4) + try: + response_7.get_nameid() + self.assertTrue(False) + except Exception as e: + self.assertIn('Not NameID found in the assertion of the Response', e.message) + def testGetNameIdData(self): """ Tests the get_nameid_data method of the OneLogin_Saml2_Response """ + json_settings = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) @@ -127,6 +158,33 @@ def testGetNameIdData(self): except Exception as e: self.assertIn('Not NameID found in the assertion of the Response', e.message) + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + + response_5 = OneLogin_Saml2_Response(settings, xml_4) + try: + response_5.get_nameid_data() + self.assertTrue(False) + except Exception as e: + self.assertIn('Not NameID found in the assertion of the Response', e.message) + + json_settings['security']['wantNameId'] = False + settings = OneLogin_Saml2_Settings(json_settings) + + response_6 = OneLogin_Saml2_Response(settings, xml_4) + nameid_data_6 = response_6.get_nameid_data() + self.assertEqual({}, nameid_data_6) + + del json_settings['security']['wantNameId'] + settings = OneLogin_Saml2_Settings(json_settings) + + response_7 = OneLogin_Saml2_Response(settings, xml_4) + try: + response_7.get_nameid_data() + self.assertTrue(False) + except Exception as e: + self.assertIn('Not NameID found in the assertion of the Response', e.message) + def testCheckStatus(self): """ Tests the check_status method of the OneLogin_Saml2_Response diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 6fdd4703..398e09a1 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -621,8 +621,49 @@ def testGetSecurityData(self): self.assertIn('signMetadata', security) self.assertIn('wantMessagesSigned', security) self.assertIn('wantAssertionsSigned', security) + self.assertIn('requestedAuthnContext', security) + self.assertIn('wantNameId', security) self.assertIn('wantNameIdEncrypted', security) + def testGetDefaultSecurityValues(self): + """ + Tests default values of Security advanced sesettings + """ + settings_json = self.loadSettingsJSON() + del settings_json['security'] + settings = OneLogin_Saml2_Settings(settings_json) + security = settings.get_security_data() + + self.assertIn('nameIdEncrypted', security) + self.assertFalse(security.get('nameIdEncrypted')) + + self.assertIn('authnRequestsSigned', security) + self.assertFalse(security.get('authnRequestsSigned')) + + self.assertIn('logoutRequestSigned', security) + self.assertFalse(security.get('logoutRequestSigned')) + + self.assertIn('logoutResponseSigned', security) + self.assertFalse(security.get('logoutResponseSigned')) + + self.assertIn('signMetadata', security) + self.assertFalse(security.get('signMetadata')) + + self.assertIn('wantMessagesSigned', security) + self.assertFalse(security.get('wantMessagesSigned')) + + self.assertIn('wantAssertionsSigned', security) + self.assertFalse(security.get('wantAssertionsSigned')) + + self.assertIn('requestedAuthnContext', security) + self.assertTrue(security.get('requestedAuthnContext')) + + self.assertIn('wantNameId', security) + self.assertTrue(security.get('wantNameId')) + + self.assertIn('wantNameIdEncrypted', security) + self.assertFalse(security.get('wantNameIdEncrypted')) + def testGetContacts(self): """ Tests the getContacts method of the OneLogin_Saml2_Settings From 10dc84f974ec10a98ea418a80884d78878568959 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 14 May 2016 12:43:17 +0200 Subject: [PATCH 109/352] IdP metadata parser/merger adjustments --- src/onelogin/saml2/idp_metadata_parser.py | 85 ++++- .../saml2_tests/idp_metadata_parser_test.py | 337 +++++++++++++++++- 2 files changed, 399 insertions(+), 23 deletions(-) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 6a396d60..44c3d4d5 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -10,6 +10,8 @@ """ import urllib2 + +from copy import deepcopy from defusedxml.lxml import fromstring from onelogin.saml2.constants import OneLogin_Saml2_Constants @@ -51,7 +53,7 @@ def get_metadata(url): return xml @staticmethod - def parse_remote(url): + def parse_remote(url, **kwargs): """ Get the metadata XML from the provided URL and parse it, returning a dict with extracted data @@ -62,22 +64,41 @@ def parse_remote(url): :rtype: dict """ idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url) - return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata) + return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata, **kwargs) @staticmethod - def parse(idp_metadata): + def parse( + idp_metadata, + required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, + required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT): """ - Parse the Identity Provider metadata and returns a dict with extracted data - If there are multiple IDPSSODescriptor it will only parse the first + Parse the Identity Provider metadata and return a dict with extracted data. + + If there are multiple tags, parse only the first. + + Parse only those SSO endpoints with the same binding as given by + the `required_sso_binding` parameter. + + Parse only those SLO endpoints with the same binding as given by + the `required_slo_binding` parameter. + + If the metadata specifies multiple SSO endpoints with the required + binding, extract only the first (the same holds true for SLO + endpoints). :param idp_metadata: XML of the Identity Provider Metadata. :type idp_metadata: string - :param url: If true and the URL is HTTPs, the cert of the domain is checked. - :type url: bool + :param required_sso_binding: Parse only POST or REDIRECT SSO endpoints. + :type required_sso_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT + or OneLogin_Saml2_Constants.BINDING_HTTP_POST + + :param required_slo_binding: Parse only POST or REDIRECT SLO endpoints. + :type required_slo_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT + or OneLogin_Saml2_Constants.BINDING_HTTP_POST :returns: settings dict with extracted data - :rtype: string + :rtype: dict """ data = {} @@ -100,11 +121,18 @@ def parse(idp_metadata): if len(name_id_format_nodes) > 0: idp_name_id_format = name_id_format_nodes[0].text - sso_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:SingleSignOnService[@Binding='%s']" % OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT) + sso_nodes = OneLogin_Saml2_Utils.query( + idp_descriptor_node, + "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding + ) + if len(sso_nodes) > 0: idp_sso_url = sso_nodes[0].get('Location', None) - slo_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:SingleLogoutService[@Binding='%s']" % OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT) + slo_nodes = OneLogin_Saml2_Utils.query( + idp_descriptor_node, + "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding + ) if len(slo_nodes) > 0: idp_slo_url = slo_nodes[0].get('Location', None) @@ -116,12 +144,17 @@ def parse(idp_metadata): if idp_entity_id is not None: data['idp']['entityId'] = idp_entity_id + if idp_sso_url is not None: data['idp']['singleSignOnService'] = {} data['idp']['singleSignOnService']['url'] = idp_sso_url + data['idp']['singleSignOnService']['binding'] = required_sso_binding + if idp_slo_url is not None: data['idp']['singleLogoutService'] = {} data['idp']['singleLogoutService']['url'] = idp_slo_url + data['idp']['singleLogoutService']['binding'] = required_slo_binding + if idp_x509_cert is not None: data['idp']['x509cert'] = idp_x509_cert @@ -150,6 +183,34 @@ def merge_settings(settings, new_metadata_settings): :returns: merged settings :rtype: dict """ - result_settings = settings.copy() - result_settings.update(new_metadata_settings) + for d in (settings, new_metadata_settings): + if not isinstance(d, dict): + raise TypeError('Both arguments must be dictionaries.') + + # Guarantee to not modify original data (`settings.copy()` would not + # be sufficient, as it's just a shallow copy). + result_settings = deepcopy(settings) + # Merge `new_metadata_settings` into `result_settings`. + dict_deep_merge(result_settings, new_metadata_settings) return result_settings + + +def dict_deep_merge(a, b, path=None): + """Deep-merge dictionary `b` into dictionary `a`. + Kudos to http://stackoverflow.com/a/7205107/145400 + """ + if path is None: + path = [] + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + dict_deep_merge(a[key], b[key], path + [str(key)]) + elif a[key] == b[key]: + # Key conflict, but equal value. + pass + else: + # Key/value conflict. Prioritize b over a. + a[key] = b[key] + else: + a[key] = b[key] + return a diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index 8e1c5c14..18beb65b 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -4,17 +4,25 @@ # All rights reserved. +from copy import deepcopy import json from os.path import dirname, join, exists from lxml.etree import XMLSyntaxError import unittest +from urllib2 import URLError from teamcity import is_running_under_teamcity from teamcity.unittestpy import TeamcityTestRunner from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser +from onelogin.saml2.constants import OneLogin_Saml2_Constants class OneLogin_Saml2_IdPMetadataParser_Test(unittest.TestCase): + # Instruct unittest to not hide diffs upon test failure, even for complex + # dictionaries. This prevents the message "Diff is 907 characters long. + # Set self.maxDiff to None to see it." from showing up. + maxDiff = None + data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') @@ -41,7 +49,10 @@ def testGetMetadata(self): with self.assertRaises(Exception): data = OneLogin_Saml2_IdPMetadataParser.get_metadata('http://google.es') - data = OneLogin_Saml2_IdPMetadataParser.get_metadata('https://www.testshib.org/metadata/testshib-providers.xml') + try: + data = OneLogin_Saml2_IdPMetadataParser.get_metadata('https://www.testshib.org/metadata/testshib-providers.xml') + except URLError: + data = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml')) self.assertTrue(data is not None and data is not {}) def testParseRemote(self): @@ -51,10 +62,29 @@ def testParseRemote(self): with self.assertRaises(Exception): data = OneLogin_Saml2_IdPMetadataParser.parse_remote('http://google.es') - data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://www.testshib.org/metadata/testshib-providers.xml') + try: + data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://www.testshib.org/metadata/testshib-providers.xml') + except URLError: + xml = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml')) + data = OneLogin_Saml2_IdPMetadataParser.parse(xml) + self.assertTrue(data is not None and data is not {}) - expected_data = {'sp': {'NameIDFormat': 'urn:mace:shibboleth:1.0:nameIdentifier'}, 'idp': {'singleSignOnService': {'url': 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO'}, 'entityId': 'https://idp.testshib.org/idp/shibboleth'}} - self.assertEqual(expected_data, data) + expected_settings_json = """ + { + "sp": { + "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier" + }, + "idp": { + "entityId": "https://idp.testshib.org/idp/shibboleth", + "singleSignOnService": { + "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + } + } + """ + expected_settings = json.loads(expected_settings_json) + self.assertEqual(expected_settings, data) def testParse(self): """ @@ -69,29 +99,314 @@ def testParse(self): xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml')) data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata) - expected_data = {'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, 'idp': {'singleSignOnService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}} - self.assertEqual(expected_data, data) + + # W/o further specification, expect to get the redirect binding SSO + # URL extracted. + expected_settings_json = """ + { + "idp": { + "singleSignOnService": { + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\\n1sE=", + "entityId": "https://app.onelogin.com/saml/metadata/383123" + }, + "sp": { + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + } + } + """ + expected_settings = json.loads(expected_settings_json) + self.assertEqual(expected_settings, data) + + def test_parse_testshib_required_binding_sso_redirect(self): + """ + Test with testshib metadata. + Especially test extracting SSO with REDIRECT binding. + Note that the testshib metadata does not contain an SLO specification + in the first tag. + """ + expected_settings_json = """ + { + "sp": { + "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier" + }, + "idp": { + "entityId": "https://idp.testshib.org/idp/shibboleth", + "singleSignOnService": { + "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + } + } + """ + try: + xmldoc = OneLogin_Saml2_IdPMetadataParser.get_metadata( + 'https://www.testshib.org/metadata/testshib-providers.xml') + except Exception: + xmldoc = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml')) + + # Parse, require SSO REDIRECT binding, implicitly. + settings1 = OneLogin_Saml2_IdPMetadataParser.parse(xmldoc) + # Parse, require SSO REDIRECT binding, explicitly. + settings2 = OneLogin_Saml2_IdPMetadataParser.parse( + xmldoc, + required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT + ) + expected_settings = json.loads(expected_settings_json) + self.assertEqual(expected_settings, settings1) + self.assertEqual(expected_settings, settings2) + + def test_parse_testshib_required_binding_sso_post(self): + """ + Test with testshib metadata. + Especially test extracting SSO with POST binding. + """ + expected_settings_json = """ + { + "sp": { + "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier" + }, + "idp": { + "entityId": "https://idp.testshib.org/idp/shibboleth", + "singleSignOnService": { + "url": "https://idp.testshib.org/idp/profile/SAML2/POST/SSO", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + } + } + } + """ + try: + xmldoc = OneLogin_Saml2_IdPMetadataParser.get_metadata( + 'https://www.testshib.org/metadata/testshib-providers.xml') + except URLError: + xmldoc = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml')) + + # Parse, require POST binding. + settings = OneLogin_Saml2_IdPMetadataParser.parse( + xmldoc, + required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST + ) + expected_settings = json.loads(expected_settings_json) + self.assertEqual(expected_settings, settings) + + def test_parse_required_binding_all(self): + """ + Test all combinations of the `require_slo_binding` and + `require_sso_binding` parameters. + Note: IdP metadata contains a single logout (SLO) + service and does not specify any endpoint for the POST binding. + """ + expected_settings_json = """ + { + "sp": { + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + }, + "idp": { + "entityId": "urn:example:idp", + "x509cert": "MIIDPDCCAiQCCQDydJgOlszqbzANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEQMA4GA1UEChMHSmFua3lDbzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDMxMjE5NDYzM1oXDTI3MTExOTE5NDYzM1owYDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEDAOBgNVBAoTB0phbmt5Q28xEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMGvJpRTTasRUSPqcbqCG+ZnTAurnu0vVpIG9lzExnh11o/BGmzu7lB+yLHcEdwrKBBmpepDBPCYxpVajvuEhZdKFx/Fdy6j5mH3rrW0Bh/zd36CoUNjbbhHyTjeM7FN2yF3u9lcyubuvOzr3B3gX66IwJlU46+wzcQVhSOlMk2tXR+fIKQExFrOuK9tbX3JIBUqItpI+HnAow509CnM134svw8PTFLkR6/CcMqnDfDK1m993PyoC1Y+N4X9XkhSmEQoAlAHPI5LHrvuujM13nvtoVYvKYoj7ScgumkpWNEvX652LfXOnKYlkB8ZybuxmFfIkzedQrbJsyOhfL03cMECAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAeHwzqwnzGEkxjzSD47imXaTqtYyETZow7XwBc0ZaFS50qRFJUgKTAmKS1xQBP/qHpStsROT35DUxJAE6NY1Kbq3ZbCuhGoSlY0L7VzVT5tpu4EY8+Dq/u2EjRmmhoL7UkskvIZ2n1DdERtd+YUMTeqYl9co43csZwDno/IKomeN5qaPc39IZjikJ+nUC6kPFKeu/3j9rgHNlRtocI6S1FdtFz9OZMQlpr0JbUt2T3xS/YoQJn6coDmJL5GTiiKM6cOe+Ur1VwzS1JEDbSS2TWWhzq8ojLdrotYLGd9JOsoQhElmz+tMfCFQUFLExinPAyy7YHlSiVX13QH2XTu/iQQ==", + "singleSignOnService": { + "url": "http://idp.example.com", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "singleLogoutService": { + "url": "http://idp.example.com/logout", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + } + } + """ + xmldoc = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata2.xml')) + + expected_settings = json.loads(expected_settings_json) + + # Parse, require SLO and SSO REDIRECT binding, implicitly. + settings1 = OneLogin_Saml2_IdPMetadataParser.parse(xmldoc) + + # Parse, require SLO and SSO REDIRECT binding, explicitly. + settings2 = OneLogin_Saml2_IdPMetadataParser.parse( + xmldoc, + required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, + required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT + ) + expected_settings1_2 = deepcopy(expected_settings) + self.assertEqual(expected_settings1_2, settings1) + self.assertEqual(expected_settings1_2, settings2) + + settings3 = OneLogin_Saml2_IdPMetadataParser.parse( + xmldoc, + required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST, + required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST + ) + + expected_settings3 = deepcopy(expected_settings) + del expected_settings3['idp']['singleLogoutService'] + del expected_settings3['idp']['singleSignOnService'] + self.assertEqual(expected_settings3, settings3) + + settings4 = OneLogin_Saml2_IdPMetadataParser.parse( + xmldoc, + required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST, + required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT + ) + settings5 = OneLogin_Saml2_IdPMetadataParser.parse( + xmldoc, + required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST + ) + expected_settings4_5 = deepcopy(expected_settings) + del expected_settings4_5['idp']['singleSignOnService'] + self.assertEqual(expected_settings4_5, settings4) + self.assertEqual(expected_settings4_5, settings5) + + settings6 = OneLogin_Saml2_IdPMetadataParser.parse( + xmldoc, + required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, + required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST + ) + settings7 = OneLogin_Saml2_IdPMetadataParser.parse( + xmldoc, + required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST + ) + expected_settings6_7 = deepcopy(expected_settings) + del expected_settings6_7['idp']['singleLogoutService'] + self.assertEqual(expected_settings6_7, settings6) + self.assertEqual(expected_settings6_7, settings7) def testMergeSettings(self): """ Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser """ - with self.assertRaises(AttributeError): + with self.assertRaises(TypeError): settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(None, {}) with self.assertRaises(TypeError): settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings({}, None) xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml')) + + # Parse XML metadata. data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata) + + # Read base settings. settings = self.loadSettingsJSON() + + # Merge settings from XML metadata into base settings, + # let XML metadata have priority if there are conflicting + # attributes. settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(settings, data) - expected_data = {u'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, u'idp': {'singleSignOnService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'} - self.assertEqual(expected_data, settings_result) - expected_data2 = {'sp': {u'singleLogoutService': {u'url': u'http://stuff.com/endpoints/endpoints/sls.php'}, u'assertionConsumerService': {u'url': u'http://stuff.com/endpoints/endpoints/acs.php'}, u'entityId': u'http://stuff.com/endpoints/metadata.php', u'NameIDFormat': u'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified'}, 'idp': {u'singleLogoutService': {u'url': u'http://idp.example.com/SingleLogoutService.php'}, u'entityId': u'http://idp.example.com/', u'x509cert': u'MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo', u'singleSignOnService': {u'url': u'http://idp.example.com/SSOService.php'}}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'} + # Generate readable JSON representation: + # print("%s" % json.dumps(settings_result, indent=2).replace(r'\n', r'\\n')) + + expected_settings_json = """ + { + "custom_base_path": "../../../tests/data/customPath/", + "contactPerson": { + "support": { + "emailAddress": "support@example.com", + "givenName": "support_name" + }, + "technical": { + "emailAddress": "technical@example.com", + "givenName": "technical_name" + } + }, + "idp": { + "singleSignOnService": { + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "entityId": "https://app.onelogin.com/saml/metadata/383123", + "singleLogoutService": { + "url": "http://idp.example.com/SingleLogoutService.php" + }, + "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\\n1sE=" + }, + "sp": { + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "entityId": "http://stuff.com/endpoints/metadata.php", + "assertionConsumerService": { + "url": "http://stuff.com/endpoints/endpoints/acs.php" + }, + "singleLogoutService": { + "url": "http://stuff.com/endpoints/endpoints/sls.php" + } + }, + "security": { + "wantAssertionsSigned": false, + "authnRequestsSigned": false, + "signMetadata": false + }, + "debug": false, + "organization": { + "en-US": { + "displayname": "SP test", + "url": "http://sp.example.com", + "name": "sp_test" + } + }, + "strict": false + } + """ + expected_settings = json.loads(expected_settings_json) + self.assertEqual(expected_settings, settings_result) + + # Commute merge operation. As the order determines which settings + # dictionary has priority, here we expect a different result. settings_result2 = OneLogin_Saml2_IdPMetadataParser.merge_settings(data, settings) - self.assertEqual(expected_data2, settings_result2) + expected_settings2_json = """ + { + "debug": false, + "idp": { + "singleLogoutService": { + "url": "http://idp.example.com/SingleLogoutService.php" + }, + "singleSignOnService": { + "url": "http://idp.example.com/SSOService.php", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "entityId": "http://idp.example.com/", + "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo" + }, + "security": { + "authnRequestsSigned": false, + "wantAssertionsSigned": false, + "signMetadata": false + }, + "contactPerson": { + "technical": { + "emailAddress": "technical@example.com", + "givenName": "technical_name" + }, + "support": { + "emailAddress": "support@example.com", + "givenName": "support_name" + } + }, + "strict": false, + "sp": { + "singleLogoutService": { + "url": "http://stuff.com/endpoints/endpoints/sls.php" + }, + "assertionConsumerService": { + "url": "http://stuff.com/endpoints/endpoints/acs.php" + }, + "entityId": "http://stuff.com/endpoints/metadata.php", + "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified" + }, + "custom_base_path": "../../../tests/data/customPath/", + "organization": { + "en-US": { + "displayname": "SP test", + "url": "http://sp.example.com", + "name": "sp_test" + } + } + } + """ + expected_settings2 = json.loads(expected_settings2_json) + self.assertEqual(expected_settings2, settings_result2) if __name__ == '__main__': From 574b90ed5b6497e65e9cff3c0e69d63f26dd9c7b Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 14 May 2016 13:12:19 +0200 Subject: [PATCH 110/352] Forgot xml files for the tests --- tests/data/metadata/idp_metadata2.xml | 1 + tests/data/metadata/testshib-providers.xml | 311 +++++++++++++++++++++ 2 files changed, 312 insertions(+) create mode 100644 tests/data/metadata/idp_metadata2.xml create mode 100644 tests/data/metadata/testshib-providers.xml diff --git a/tests/data/metadata/idp_metadata2.xml b/tests/data/metadata/idp_metadata2.xml new file mode 100644 index 00000000..0b19f62a --- /dev/null +++ b/tests/data/metadata/idp_metadata2.xml @@ -0,0 +1 @@ + MIIDPDCCAiQCCQDydJgOlszqbzANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEQMA4GA1UEChMHSmFua3lDbzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDMxMjE5NDYzM1oXDTI3MTExOTE5NDYzM1owYDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEDAOBgNVBAoTB0phbmt5Q28xEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMGvJpRTTasRUSPqcbqCG+ZnTAurnu0vVpIG9lzExnh11o/BGmzu7lB+yLHcEdwrKBBmpepDBPCYxpVajvuEhZdKFx/Fdy6j5mH3rrW0Bh/zd36CoUNjbbhHyTjeM7FN2yF3u9lcyubuvOzr3B3gX66IwJlU46+wzcQVhSOlMk2tXR+fIKQExFrOuK9tbX3JIBUqItpI+HnAow509CnM134svw8PTFLkR6/CcMqnDfDK1m993PyoC1Y+N4X9XkhSmEQoAlAHPI5LHrvuujM13nvtoVYvKYoj7ScgumkpWNEvX652LfXOnKYlkB8ZybuxmFfIkzedQrbJsyOhfL03cMECAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAeHwzqwnzGEkxjzSD47imXaTqtYyETZow7XwBc0ZaFS50qRFJUgKTAmKS1xQBP/qHpStsROT35DUxJAE6NY1Kbq3ZbCuhGoSlY0L7VzVT5tpu4EY8+Dq/u2EjRmmhoL7UkskvIZ2n1DdERtd+YUMTeqYl9co43csZwDno/IKomeN5qaPc39IZjikJ+nUC6kPFKeu/3j9rgHNlRtocI6S1FdtFz9OZMQlpr0JbUt2T3xS/YoQJn6coDmJL5GTiiKM6cOe+Ur1VwzS1JEDbSS2TWWhzq8ojLdrotYLGd9JOsoQhElmz+tMfCFQUFLExinPAyy7YHlSiVX13QH2XTu/iQQ== urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient \ No newline at end of file diff --git a/tests/data/metadata/testshib-providers.xml b/tests/data/metadata/testshib-providers.xml new file mode 100644 index 00000000..c00a1b77 --- /dev/null +++ b/tests/data/metadata/testshib-providers.xml @@ -0,0 +1,311 @@ + + + + + + + + + + + + + + + + + + + + testshib.org + + TestShib Test IdP + TestShib IdP. Use this as a source of attributes + for your test SP. + https://www.testshib.org/testshibtwo.jpg + + + + + + + + MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV + MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD + VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4 + MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI + EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl + c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B + AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C + yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe + 3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT + NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614 + kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH + gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G + A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86 + 9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl + bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo + aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN + BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL + I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo + 93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4 + /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj + Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr + 8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA== + + + + + + + + + + + + + + + urn:mace:shibboleth:1.0:nameIdentifier + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + + + + + + + + + + + + + MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV + MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD + VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4 + MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI + EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl + c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B + AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C + yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe + 3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT + NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614 + kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH + gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G + A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86 + 9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl + bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo + aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN + BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL + I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo + 93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4 + /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj + Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr + 8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA== + + + + + + + + + + + + + + + + urn:mace:shibboleth:1.0:nameIdentifier + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + + TestShib Two Identity Provider + TestShib Two + http://www.testshib.org/testshib-two/ + + + Nate + Klingenstein + ndk@internet2.edu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestShib Test SP + TestShib SP. Log into this to test your machine. + Once logged in check that all attributes that you expected have been + released. + https://www.testshib.org/testshibtwo.jpg + + + + + + + + MIIEPjCCAyagAwIBAgIBADANBgkqhkiG9w0BAQUFADB3MQswCQYDVQQGEwJVUzEV + MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMSIwIAYD + VQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQDEw9zcC50ZXN0 + c2hpYi5vcmcwHhcNMDYwODMwMjEyNDM5WhcNMTYwODI3MjEyNDM5WjB3MQswCQYD + VQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1 + cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQD + Ew9zcC50ZXN0c2hpYi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB + AQDJyR6ZP6MXkQ9z6RRziT0AuCabDd3x1m7nLO9ZRPbr0v1LsU+nnC363jO8nGEq + sqkgiZ/bSsO5lvjEt4ehff57ERio2Qk9cYw8XCgmYccVXKH9M+QVO1MQwErNobWb + AjiVkuhWcwLWQwTDBowfKXI87SA7KR7sFUymNx5z1aoRvk3GM++tiPY6u4shy8c7 + vpWbVfisfTfvef/y+galxjPUQYHmegu7vCbjYP3On0V7/Ivzr+r2aPhp8egxt00Q + XpilNai12LBYV3Nv/lMsUzBeB7+CdXRVjZOHGuQ8mGqEbsj8MBXvcxIKbcpeK5Zi + JCVXPfarzuriM1G5y5QkKW+LAgMBAAGjgdQwgdEwHQYDVR0OBBYEFKB6wPDxwYrY + StNjU5P4b4AjBVQVMIGhBgNVHSMEgZkwgZaAFKB6wPDxwYrYStNjU5P4b4AjBVQV + oXukeTB3MQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYD + VQQHEwpQaXR0c2J1cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3Zp + ZGVyMRgwFgYDVQQDEw9zcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN + BgkqhkiG9w0BAQUFAAOCAQEAc06Kgt7ZP6g2TIZgMbFxg6vKwvDL0+2dzF11Onpl + 5sbtkPaNIcj24lQ4vajCrrGKdzHXo9m54BzrdRJ7xDYtw0dbu37l1IZVmiZr12eE + Iay/5YMU+aWP1z70h867ZQ7/7Y4HW345rdiS6EW663oH732wSYNt9kr7/0Uer3KD + 9CuPuOidBacospDaFyfsaJruE99Kd6Eu/w5KLAGG+m0iqENCziDGzVA47TngKz2v + PVA+aokoOyoz3b53qeti77ijatSEoKjxheBWpO+eoJeGq/e49Um3M2ogIX/JAlMa + Inh+vYSYngQB2sx9LGkR9KHaMKNIGCDehk93Xla4pWJx1w== + + + + + + + + + + + + + + + + + + + + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + urn:mace:shibboleth:1.0:nameIdentifier + + + + + + + + + + + + + + + + + + + + TestShib Two Service Provider + TestShib Two + http://www.testshib.org/testshib-two/ + + + Nate + Klingenstein + ndk@internet2.edu + + + + + + + From 431e50475875dcdfe4833911dbff15c7b71c9b34 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 14 May 2016 19:26:27 +0200 Subject: [PATCH 111/352] Remove reference to wrong NameIDFormat: urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified . Should be urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified --- README.md | 2 +- demo-bottle/saml/settings.json | 2 +- demo-django/saml/settings.json | 2 +- demo-flask/saml/settings.json | 2 +- .../logout_requests/invalids/invalid_issuer.xml | 2 +- .../invalids/invalid_issuer.xml.base64 | 2 +- .../logout_requests/invalids/not_after_failed.xml | 2 +- .../invalids/not_after_failed.xml.base64 | 2 +- tests/data/logout_requests/logout_request.xml | 2 +- .../data/logout_requests/logout_request.xml.base64 | 2 +- .../logout_request_deflated.xml.base64 | 2 +- .../logout_request_with_sessionindex.xml | 2 +- tests/data/metadata/expired_metadata_settings1.xml | 2 +- .../data/metadata/metadata_bad_order_settings1.xml | 2 +- tests/data/metadata/metadata_settings1.xml | 2 +- .../data/metadata/no_expiration_mark_metadata.xml | 2 +- .../data/metadata/noentity_metadata_settings1.xml | 2 +- tests/data/metadata/unparsed_metadata.xml | 2 +- .../responses/response_encrypted_nameid.xml.base64 | 2 +- tests/settings/settings1.json | 2 +- tests/settings/settings2.json | 2 +- tests/settings/settings3.json | 2 +- tests/settings/settings4.json | 2 +- tests/src/OneLogin/saml2_tests/auth_test.py | 2 +- .../saml2_tests/idp_metadata_parser_test.py | 2 +- .../OneLogin/saml2_tests/logout_request_test.py | 2 +- tests/src/OneLogin/saml2_tests/metadata_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/response_test.py | 2 +- tests/src/OneLogin/saml2_tests/settings_test.py | 6 +++--- tests/src/OneLogin/saml2_tests/utils_test.py | 14 +++++++------- 30 files changed, 39 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 07b96dc8..ba73879c 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ This is the settings.json file: // Specifies the constraints on the name identifier to be used to // represent the requested subject. // Take a look on src/onelogin/saml2/constants.py to see the NameIdFormat that are supported. - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", // Usually x509cert and privateKey of the SP are provided by files placed at // the certs folder. But we can also provide them with the following parameters "x509cert": "", diff --git a/demo-bottle/saml/settings.json b/demo-bottle/saml/settings.json index fdb13acd..7f861e97 100644 --- a/demo-bottle/saml/settings.json +++ b/demo-bottle/saml/settings.json @@ -11,7 +11,7 @@ "url": "https:///?sls", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "x509cert": "", "privateKey": "" }, diff --git a/demo-django/saml/settings.json b/demo-django/saml/settings.json index cafd090b..391b91c1 100644 --- a/demo-django/saml/settings.json +++ b/demo-django/saml/settings.json @@ -11,7 +11,7 @@ "url": "https:///?sls", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "x509cert": "", "privateKey": "" }, diff --git a/demo-flask/saml/settings.json b/demo-flask/saml/settings.json index 142911f1..ec40b674 100644 --- a/demo-flask/saml/settings.json +++ b/demo-flask/saml/settings.json @@ -11,7 +11,7 @@ "url": "https:///?sls", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "x509cert": "", "privateKey": "" }, diff --git a/tests/data/logout_requests/invalids/invalid_issuer.xml b/tests/data/logout_requests/invalids/invalid_issuer.xml index d5bf6609..e1edabde 100644 --- a/tests/data/logout_requests/invalids/invalid_issuer.xml +++ b/tests/data/logout_requests/invalids/invalid_issuer.xml @@ -9,6 +9,6 @@ > https://example.hello.com/access/saml ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c diff --git a/tests/data/logout_requests/invalids/invalid_issuer.xml.base64 b/tests/data/logout_requests/invalids/invalid_issuer.xml.base64 index 26912f3d..56de49b0 100644 --- a/tests/data/logout_requests/invalids/invalid_issuer.xml.base64 +++ b/tests/data/logout_requests/invalids/invalid_issuer.xml.base64 @@ -1 +1 @@ -jZPJbsIwEEDvfAXKncR2lhILgpBoq0gU2lL10EvlOpMSKbHTjFPx+XXSjQNB+GTZ4zdv7PFscajK8Sc0WGg1d6hLnEUymqGoypqv9btuzSN8tIBmbAMV8n5n7rSN4lpggVyJCpAbyXfLuzVnLuF1o42WunRG41Pjn3MeIxChMVZrgJOu5s52c73e3qabV0bDaSBllgsp/EjQIBQQxyQIWCazOHrLISI0pDDAev69AJt4KB1iC6lCI5SxcYT6E8omlDyRgPsx9+nLwMGVvbxCCdPz98bU3PPQtHnuSl15oLJaF8rg0QxLdOt9PcDbaLNV22aZG2g6EeZPSHiJSNIv92/L+2qapNNB6wMHUdUluHsoS917CSkBrYoNnnnHR44gG/tm6Wq8u+8mD60oi7zonC6inpC80U0lzPmu6FaKbJL3obxVWIPs0mYneMlfe1CwnSApi4HmU8bklMQRmdo+ocSXIQCTV1IAlT+lfhdm/4F34iMkXw== +jZPfb4IwEMffTfwfDO9CW8BJoxgTt8XEyTaXPexl6coxSaBlXFn881dwP3yQxT411+v3+7n2brY4lMXoE2rMtZo71CXOIh4OZijKouIb/a4b8wgfDaAZ2UyFvDuZO02tuBaYI1eiBORG8t3ybsOZS3hVa6OlLpzhYHRu/Qn9ryMQoTYWrE9ovZo7yfZ6k9yut6+MhtNAyjQTUvgTQYNQQBSRIGCpTKPJWwYTQkMKfWLPP49grXsNERtYKzRCGZtIqD+mbEzJEwm4H3GfvvTdXNkXzJUwncPemIp7Hpomy1ypSw9UWulcGTzZYYFuta/6BLfaJCqpl5mBukVh/piEF6HEx3j3x7yrqI5bIrRIcBBlVYC7h6LQHZqQEtDS2OSZd3rlVGVr/269Gu3u281DI4o8y1usi2TPcd7ouhSmvz2oS7tIno6zLpU3CiuQrW96TjD+7RMKtiUkZRHQbMqYnJJoQqa2YSjxZQjA5JUUQOV3tcfS2pnwzgxF/AU= \ No newline at end of file diff --git a/tests/data/logout_requests/invalids/not_after_failed.xml b/tests/data/logout_requests/invalids/not_after_failed.xml index 8f4e2beb..1f825036 100644 --- a/tests/data/logout_requests/invalids/not_after_failed.xml +++ b/tests/data/logout_requests/invalids/not_after_failed.xml @@ -9,6 +9,6 @@ > http://idp.example.com/ ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c diff --git a/tests/data/logout_requests/invalids/not_after_failed.xml.base64 b/tests/data/logout_requests/invalids/not_after_failed.xml.base64 index 753d4b81..8ba510d0 100644 --- a/tests/data/logout_requests/invalids/not_after_failed.xml.base64 +++ b/tests/data/logout_requests/invalids/not_after_failed.xml.base64 @@ -1 +1 @@ -jVJNT8MwDL3vV1S9r036xRptnSYNUKWxAUMcuKCQuqxSm4Q6Rfv5ZGXADutETpbz/Pxsv+l839TOJ7RYKTlzqUfceTaaIm9qzVbqXXXmET46QONYoETW/8zcrpVMcayQSd4AMiPYdnG3YoFHmG6VUULV7sg59/54LtNwRGiNlTXAky9n7mZ9vdrc5uvXgMaTSIii5IKHCadRzCFNSRQFhSjS5K2EhNCYwgDX888CbOOhdogd5BINl8biCA3HNBhT8kQiFqYspC8DhUu7vEpy0/PvjNHM99F0ZekJ1fggC60qafAkwho9vdMDfGtlNnLTLkoD7VEIif8jJOvT/W1ZP02bHeVUhfZgzxtdQy9q6p+CTsrW9kr50tneH4KHjtdVWR1UDPCcEXKj2oaby5c/ZKpiXPZQ1knUIA6NijN82a8FKNhrCxqkQMtJEIgJSRMysV6gJBQxQCCuBAcqjsN9j2K97p8xe/YF +jVJNT8MwDL1P2n+Yel+b9Is12jpNGqBKYwOGOHBBIXVZpTYJdYr282m7ATu0iJwsx37v2X7z5bEsJp9QYa7kwqI2sZbxeDRHXhaabdS7qs0jfNSAZtJUSmTdz8KqK8kUxxyZ5CUgM4LtV3cb5tqE6UoZJVRhjUeTvvcL9DcOR4TKNMKGgJL1wtptrze722T76tJg5guRZlxwL+TUDzhEEfF9NxVpFL5lEBIaUBgCe/5eQkM9SIhYQyLRcGmaQkK9KXWnlDwRn3kR8+jLUOe62WAuuekYDsZo5jho6iyzhSodkKlWuTR4EWGBtj7oIcCtMju5q1aZgeoshQT/khKf8t2NWTdRFZ8V5am24chLXUCna+5cFl32bZtrJevJ/r4NHmpe5FneChkA6tNyo6qSm2ELUJt2mTydZl0pqyVqEC1T2gcY/3iBQnN2Qd0IaDZzXTEjUUhmjSko8UQA4IorwYGK83ynYVrfOz3Gj78A \ No newline at end of file diff --git a/tests/data/logout_requests/logout_request.xml b/tests/data/logout_requests/logout_request.xml index b3b99bee..10904a28 100644 --- a/tests/data/logout_requests/logout_request.xml +++ b/tests/data/logout_requests/logout_request.xml @@ -8,6 +8,6 @@ > http://idp.example.com/ ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c diff --git a/tests/data/logout_requests/logout_request.xml.base64 b/tests/data/logout_requests/logout_request.xml.base64 index adca75e1..870a6383 100644 --- a/tests/data/logout_requests/logout_request.xml.base64 +++ b/tests/data/logout_requests/logout_request.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6TG9nb3V0UmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIg0KICAgICAgICAgICAgICAgICAgICAgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiINCiAgICAgICAgICAgICAgICAgICAgIElEPSJPTkVMT0dJTl8yMTU4NGNjZGZhY2EzNmExNDVhZTk5MDQ0MmRjZDk2YmZlNjAxNTFlIg0KICAgICAgICAgICAgICAgICAgICAgVmVyc2lvbj0iMi4wIg0KICAgICAgICAgICAgICAgICAgICAgSXNzdWVJbnN0YW50PSIyMDEzLTEyLTEwVDA0OjM5OjMxWiINCiAgICAgICAgICAgICAgICAgICAgIERlc3RpbmF0aW9uPSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvc2xzLnBocCINCiAgICAgICAgICAgICAgICAgICAgID4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICAgIDxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vIg0KICAgICAgICAgICAgICAgICBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIg0KICAgICAgICAgICAgICAgICA+T05FTE9HSU5fMWU0NDJjMTI5ZTFmODIyYzgwOTYwODZhMTEwM2M1ZWUyYzdjYWUxYzwvc2FtbDpOYW1lSUQ+DQo8L3NhbWxwOkxvZ291dFJlcXVlc3Q+ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6TG9nb3V0UmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIg0KICAgICAgICAgICAgICAgICAgICAgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiINCiAgICAgICAgICAgICAgICAgICAgIElEPSJPTkVMT0dJTl8yMTU4NGNjZGZhY2EzNmExNDVhZTk5MDQ0MmRjZDk2YmZlNjAxNTFlIg0KICAgICAgICAgICAgICAgICAgICAgVmVyc2lvbj0iMi4wIg0KICAgICAgICAgICAgICAgICAgICAgSXNzdWVJbnN0YW50PSIyMDEzLTEyLTEwVDA0OjM5OjMxWiINCiAgICAgICAgICAgICAgICAgICAgIERlc3RpbmF0aW9uPSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvc2xzLnBocCINCiAgICAgICAgICAgICAgICAgICAgID4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICAgIDxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vIg0KICAgICAgICAgICAgICAgICBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIg0KICAgICAgICAgICAgICAgICA+T05FTE9HSU5fMWU0NDJjMTI5ZTFmODIyYzgwOTYwODZhMTEwM2M1ZWUyYzdjYWUxYzwvc2FtbDpOYW1lSUQ+DQo8L3NhbWxwOkxvZ291dFJlcXVlc3Q+ \ No newline at end of file diff --git a/tests/data/logout_requests/logout_request_deflated.xml.base64 b/tests/data/logout_requests/logout_request_deflated.xml.base64 index 4c0a089e..894de1ec 100644 --- a/tests/data/logout_requests/logout_request_deflated.xml.base64 +++ b/tests/data/logout_requests/logout_request_deflated.xml.base64 @@ -1 +1 @@ -fZLfT8IwEMff+SvI3tnabUzWwIgJapYgqBgffDG1u0mTra27zvDnWyYiMYw+Xa53n/vxvel8V1fDL2hQajXzqE+8eTaYIq8rw5b6Q7f2CT5bQDt0gQpZ9zPz2kYxzVEiU7wGZFawzfX9koU+YabRVgtdeYPhuffHuYzhiNBY11YPJ1/MvPXqZrm+y1dvIR1PYiGKkgseJZzGYw5pSuI4LESRJu8lJISOKfSwXn4X4Ar3lUNsIVdoubIujtBoRMMRJc8kZlHKIvrak7hwy5OK246/tdawIEDblqUvdB2AKoyWyuKJhRX6Zmt6eFnn7iRiXVNNdqDKwviw47WpoGNPg9Ogk7SVW3a+GG4e9sZjyytZSmiO3f3nnGnkVjc1t5cF3HtkMSq7UNYqNCD2hYozvOyoJAUnmqBhCrSchKGYkDQhEycpJZEYA4TiSnCg4jDczyjuZIMzN5t9Aw== +fZJNT4NAEIbvTfofCPfCLh8VNi2NSdWQ1Fat8eDFrMtgSWB3ZRbTny+ltTYG3NNkduaZj3dmi31VWl9QY6Hk3KYOsRfJeDRDXpWardSHaswTfDaAxmojJbLuZ243tWSKY4FM8gqQGcG21/cr5jmE6VoZJVRpj0dW3/sF/c/hiFCbtrEhULqc25v1zWpzl67fPBpGgRBZzgX3p5wGIYc4JkHgZSKLp+85TAkNKQzBXn6W0JYeLIjYQCrRcGnaQEL9CfUmlDyTgPkx8+nrUOay3WAhuekq7IzRzHXRNHnuCFW5IDOtCmnwwsISHb3TQ8Dk6O+UYl1fdXLiFpl2YM8rXUJHn7mXQZd563bn6dLaPhyMx4aXRV5AfW7wL6ivl1tVV9wMC0kd2nmKbJJ3oayRqEEcKmV9wOSsKIVWPEG9GGgeeZ6ISDwlUSstJb4IATxxJThQcZrvOMzhet2e802+AQ== \ No newline at end of file diff --git a/tests/data/logout_requests/logout_request_with_sessionindex.xml b/tests/data/logout_requests/logout_request_with_sessionindex.xml index dd6de13a..5dbc3c32 100644 --- a/tests/data/logout_requests/logout_request_with_sessionindex.xml +++ b/tests/data/logout_requests/logout_request_with_sessionindex.xml @@ -8,7 +8,7 @@ > http://idp.example.com/ ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c _ac72a76526cb6ca19f8438e73879a0e6c8ae5131 diff --git a/tests/data/metadata/expired_metadata_settings1.xml b/tests/data/metadata/expired_metadata_settings1.xml index 6311dd69..9af134d8 100644 --- a/tests/data/metadata/expired_metadata_settings1.xml +++ b/tests/data/metadata/expired_metadata_settings1.xml @@ -3,7 +3,7 @@ MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOoMIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo - urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified diff --git a/tests/data/metadata/metadata_bad_order_settings1.xml b/tests/data/metadata/metadata_bad_order_settings1.xml index 1d1ff9d2..9d100de4 100644 --- a/tests/data/metadata/metadata_bad_order_settings1.xml +++ b/tests/data/metadata/metadata_bad_order_settings1.xml @@ -4,7 +4,7 @@ cacheDuration="PT1594475551S" entityID="http://stuff.com/endpoints/metadata.php"> - urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified diff --git a/tests/data/metadata/metadata_settings1.xml b/tests/data/metadata/metadata_settings1.xml index 449a8f01..c9529f00 100644 --- a/tests/data/metadata/metadata_settings1.xml +++ b/tests/data/metadata/metadata_settings1.xml @@ -6,7 +6,7 @@ - urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified diff --git a/tests/data/metadata/no_expiration_mark_metadata.xml b/tests/data/metadata/no_expiration_mark_metadata.xml index 0d0b8085..92c42ba8 100644 --- a/tests/data/metadata/no_expiration_mark_metadata.xml +++ b/tests/data/metadata/no_expiration_mark_metadata.xml @@ -4,7 +4,7 @@ - urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified diff --git a/tests/data/metadata/noentity_metadata_settings1.xml b/tests/data/metadata/noentity_metadata_settings1.xml index af1ff3eb..b773de92 100644 --- a/tests/data/metadata/noentity_metadata_settings1.xml +++ b/tests/data/metadata/noentity_metadata_settings1.xml @@ -3,7 +3,7 @@ MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOoMIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo - urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified diff --git a/tests/data/metadata/unparsed_metadata.xml b/tests/data/metadata/unparsed_metadata.xml index 6e6c567a..691824da 100644 --- a/tests/data/metadata/unparsed_metadata.xml +++ b/tests/data/metadata/unparsed_metadata.xml @@ -6,7 +6,7 @@ - urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified diff --git a/tests/data/responses/response_encrypted_nameid.xml.base64 b/tests/data/responses/response_encrypted_nameid.xml.base64 index d95040d0..6b1c9856 100644 --- a/tests/data/responses/response_encrypted_nameid.xml.base64 +++ b/tests/data/responses/response_encrypted_nameid.xml.base64 @@ -1 +1 @@ -PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZng4NDMwYjFkZS1mNTU0LTQ5M2YtNjMyZS00YTIxZTBhMGRkZDQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTA5VDEyOjIzOjM3WiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9iZjM3MmI5ZDY3ZDBjODlkMGNmMWFmM2ZmNjI1ZWE3YzA1MWM5ODg1Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng4NDMwYjFkZS1mNTU0LTQ5M2YtNjMyZS00YTIxZTBhMGRkZDQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkhENDgxVEsxVmtPMGVaNGUxL2Vma1VJTVVCaz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+bWZrcnh5SlU4MG1hMWwwQ2xUU2tObTgzajVZQ0Y3YUt2bDJqRmVsVU04M3RuaitwNDB1RXRITjkrbHBJQVgrZEZkNnNCR3JEdUk0Q21lOTZuQ2lDK05mb2Y5MnpYaUcrWExCbFR2K0Z0cWxaZUgxc0ZNTFhuOWwxTHNLT0dPbjM5alA4bklUSGtYL0VkYjRucENNZlpQeWZ2b3M1dkMwbUtnWlFKZ1JtcG5vPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGQwOGM5NTY3LWU3NTEtNjY4Mi0wYTBhLWE0NTBkNmYxNTFlMCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhkMDhjOTU2Ny1lNzUxLTY2ODItMGEwYS1hNDUwZDZmMTUxZTAiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPjZqVkYxVkQwYnBVbUFWbURHOUt4bGoyYU5RMD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+VTdhN0VVZ3F4MWJEL1lpb25OV09VZUwwdElHR1FJTlp3VXo3UUE4M2FibGVWcTl0a0ptSy9abzBqTXQyeW5YeDF6UDgrQzVuM0NFNjhOdnhqd3lLZEVBQTFSMDBuWmd4NExHQmdlaXZZUGtrNEFvSnpDREFsTXUzVUt6VnRJT3R1NXVWMU0yZUZzQ0ljZFRxcFEwa3ZvalVBRVlZWE5tK3pjbGRydVlYZDV3PTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpFbmNyeXB0ZWRJRD48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgeG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI2FlczEyOC1jYmMiLz48ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleT48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLTFfNSIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+RjJQVjV4TjVFM3EzODFJV2Qya25CdjAvUEpPN1ExR01oMGhkUm1USUxSYnJGZ0toNlFtN2VoTFlBS1B5QW52b0lFRDZSK0g4VkhweEtsZ2lybm9xNWYvSUxCajhOV1FpOEFKV21qZkMyWTBxQ3duOHJxVFRWQzVlZFhFZUMzU094ZHBTbzNzWGlOakhKUms3VndRUEV5RXNNeWcvSkN1cytGeUJYbXRickMwPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPgogICA8eGVuYzpDaXBoZXJEYXRhPgogICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5sbjMrVTFWTTJjQ0I1TjF3SHJnM2llTDM0VEg3RXQvRGVYbytBTlJHRE52OXhtRk9zdWFKbmcwdS9XbHl6RjRNdHFsaWViRGVDQjVTWVRPRURxNFRsVVJ6NkFYSHBINVEzWWthK25rbWV5YWMrVnNpcjRwenlwblJoL29MeWRRV3dNcUlmTUIwM3hrc3l4SVM1Qk5kSzRjYnpaa3Z0bGtYL0ZrRnZUQnJLUzBsU1A4M2VlV2l4STRNell2QTVtK1R1SXpjRXQrUUdvc0FQZ0pCdERITEdYaUlhenRSWGFCSzV0eUxLcDdCMnVHVytWY2JaUTVmZVdlUTVtQ0p4WFBYYjJ0OXNMQ1ExRUVBQ2dOQitJMGFiejFhS1l4TitFandQd2pDVVFaZnFnYjlVY0xQRXBDanFzckdEMFpPSWxQYjJRcmJjNHFWc2NMaUlWRDZvaWxGWFM5QzhzbTA3dmJJTzA3SUhoU1ovRm9jK3AzbCsvQVRVb0I4eTc1SEp2TzE8L3hlbmM6Q2lwaGVyVmFsdWU+CiAgIDwveGVuYzpDaXBoZXJEYXRhPgo8L3hlbmM6RW5jcnlwdGVkRGF0YT48L3NhbWw6RW5jcnlwdGVkSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0xMFQxNzo0MzozN1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0wOVQxMjoyMzowN1oiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0xMFQxNzo0MzozN1oiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAzLTA5VDEyOjIzOjM3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMy0wOVQyMDoyMzozN1oiIFNlc3Npb25JbmRleD0iXzk0NGJmY2FjYjBkODMyYjEyZTRiY2Y3NzRlMDJiYmU1ZjY0NTVjNjgwMyI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3RAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ic24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPndhYTI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGNlNWNjMjE0LTI1OGMtNjNkYy1iM2UxLWQ4YmI3ZGQ1ZWM1ZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGNlNWNjMjE0LTI1OGMtNjNkYy1iM2UxLWQ4YmI3ZGQ1ZWM1ZiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+aWptQ20xSDcxUE44TENNWWprbmx2YUVLMDB3PTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5SeDNCODBrSUJnZm1UeU43Sy9HRXFNL0VmQWgwRVllV1NJN2RxdzBzdTRQUDdKclhjcng0N2ZwSjlLYWpRWlBTOU5CcVdjZlhJeVNVdGR5c1l4SUU3VzIvOHd0ZFozakVzbzZLRWlreDBTTlA0Z0RkUzNyNzVLMVNMb3NBZklVSmg2L0lUbll1Q2Y5UFl3RHNXVSt2WU9la2xnWC9xT3lkbm5sKzQ1QmpTd2M9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc1NmNjMjc5LWFmMjEtZDE5Mi1mMGVkLTEyNmRjZTYwMzBkNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng3NTZjYzI3OS1hZjIxLWQxOTItZjBlZC0xMjZkY2U2MDMwZDUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPks0MkdwcnJwREpLNmUvRlhpazBZYjl0cVpIVT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+Z1pYSVkxWEw2UitNYzBzYWE5NzdSRzZEOXYzdUNkYUtXTUF5S0pUZUQ5UHIrL0NrWFNDbjJpbERXbUQyaldEVUVGNEZnZWtBMEt6STZQamRLR3VwRmVuZW96a2pFcHVmU0ozRkhpSXNxRnM5OGdONWZvZEEzRm16RjBLS2dScTRJaVRSd216UG5xT080eE8rQlhNQkoyTkFPVENaYnRyb3RsYnVBVlFUT3lzPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0xMFQxNzo0MzozN1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48c2FtbDpFbmNyeXB0ZWRJRD48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgeG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3RyaXBsZWRlcy1jYmMiLz48ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleT48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLTFfNSIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+Mkx5Ry9wb1RrZUFsVHAvNmpEWUhGQ0JFbU9wUmZGUE5qZ2R5WDRWYTY5RDFkOXMycUIvcHB0UUg0UE8wZjl3cjVsbm9hWUhKME9CWUJJRjdnS1lEOVcvOEpVTUhLN1lNS2svandJcnhmWDNpVlRMQ1VMaTRoSWdRVUhRckR0cXg0eFNsNTJBckcrYzRxcDhNQk5ydTZ5T2lqSzB0NkRnemJGZURlNExxb0ZjPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPg0KICAgPHhlbmM6Q2lwaGVyRGF0YT4NCiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPkJNWHRJZENXM2N6S0h5dk1tTGswMW9MWXNPY0NRcmFQblFzT3ZWN3pKL2Zzbnc3cG1pR2V5TlBsZFJQS2VRSVk1M2hJYTR4WUR5bmpUcHNHUERXZFpDUWo4US83R0R1TlMrSnArdUY2WWZSRzd1bEIyanBhU0xBVkRBRUt3eXVUVTBrZHhnTEw0L1BTZFVTQ1B1SURUMS94UURuZ2F6Q2I5RTNtY1ZsajZSODlxano0R1A5cVBlSC95WmZoV1EwYzRpNUs5NnZWOXFud09ZTUtSWjhHdFBLemNyK2RsbFhSSHlHQ09nOGtyUjd3Y0QxR1BvaHZmdVhvdm5xR3hMKzVPZExrMjVRbDJ4Y1Q1cUxNZnhGaEVxRDNteHVIVmN3WTwveGVuYzpDaXBoZXJWYWx1ZT4NCiAgIDwveGVuYzpDaXBoZXJEYXRhPg0KPC94ZW5jOkVuY3J5cHRlZERhdGE+PC9zYW1sOkVuY3J5cHRlZElEPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAzLTA5VDEyOjIzOjA3WiIgTm90T25PckFmdGVyPSIyMDIzLTA5LTEwVDE3OjQzOjM3WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAzLTA5VDIwOjIzOjM3WiIgU2Vzc2lvbkluZGV4PSJfOTQ0YmZjYWNiMGQ4MzJiMTJlNGJjZjc3NGUwMmJiZTVmNjQ1NWM2ODAzIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdEBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+d2FhMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== diff --git a/tests/settings/settings1.json b/tests/settings/settings1.json index d809b7d6..69d7d25e 100644 --- a/tests/settings/settings1.json +++ b/tests/settings/settings1.json @@ -10,7 +10,7 @@ "singleLogoutService": { "url": "http://stuff.com/endpoints/endpoints/sls.php" }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified" + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" }, "idp": { "entityId": "http://idp.example.com/", diff --git a/tests/settings/settings2.json b/tests/settings/settings2.json index d18b518f..22f92dc1 100644 --- a/tests/settings/settings2.json +++ b/tests/settings/settings2.json @@ -10,7 +10,7 @@ "singleLogoutService": { "url": "http://stuff.com/endpoints/endpoints/sls.php" }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified" + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" }, "idp": { "entityId": "https://idp.example.com/simplesaml/saml2/idp/metadata.php", diff --git a/tests/settings/settings3.json b/tests/settings/settings3.json index c291c615..de72e50d 100644 --- a/tests/settings/settings3.json +++ b/tests/settings/settings3.json @@ -10,7 +10,7 @@ "singleLogoutService": { "url": "http://pytoolkit.com:8000/?sls" }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified" + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" }, "idp": { "entityId": "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php", diff --git a/tests/settings/settings4.json b/tests/settings/settings4.json index 1300c7df..c217c7d8 100644 --- a/tests/settings/settings4.json +++ b/tests/settings/settings4.json @@ -46,7 +46,7 @@ "singleLogoutService": { "url": "http://pytoolkit.com:8000/?sls" }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified" + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" }, "idp": { "entityId": "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php", diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 1bc0a577..2d296e3c 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -864,7 +864,7 @@ def testBuildRequestSignature(self): auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) signature = auth.build_request_signature(message, relay_state) - valid_signature = 'E17GU1STzanOXxBTKjweB1DovP8aMJdj5BEy0fnGoEslKdP6hpPc3enjT/bu7I8D8QzLoir8SxZVWdUDXgIxJIEgfK5snr+jJwfc5U2HujsOa/Xb3c4swoyPcyQhcxLRDhDjPq5cQxJfYoPeElvCuI6HAD1mtdd5PS/xDvbIxuw=' + valid_signature = 'Pb1EXAX5TyipSJ1SndEKZstLQTsT+1D00IZAhEepBM+OkAZQSToivu3njgJu47HZiZAqgXZFgloBuuWE/+GdcSsRYEMkEkiSDWTpUr25zKYLJDSg6GNo6iAHsKSuFt46Z54Xe/keYxYP03Hdy97EwuuSjBzzgRc5tmpV+KC7+a0=' self.assertEqual(signature, valid_signature) settings['sp']['privatekey'] = '' diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index 18beb65b..a9a46908 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -393,7 +393,7 @@ def testMergeSettings(self): "url": "http://stuff.com/endpoints/endpoints/acs.php" }, "entityId": "http://stuff.com/endpoints/metadata.php", - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified" + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" }, "custom_base_path": "../../../tests/data/customPath/", "organization": { diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 3262aaeb..34d8ea95 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -100,7 +100,7 @@ def testGetNameIdData(self): """ expected_name_id_data = { 'Value': 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c', - 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', + 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', 'SPNameQualifier': 'http://idp.example.com/' } diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 885dd121..a3dd4386 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -63,7 +63,7 @@ def testBuilder(self): self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', metadata) + self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata) self.assertIn('sp_test', metadata) self.assertIn('', metadata) @@ -196,7 +196,7 @@ def testSignMetadata(self): self.assertIn('', signed_metadata) - self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', signed_metadata) + self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', signed_metadata) self.assertIn('', signed_metadata) self.assertIn('', signed_metadata) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 34bea8d6..435142a8 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -134,7 +134,7 @@ def testGetNameIdData(self): response_2 = OneLogin_Saml2_Response(settings, xml_2) expected_nameid_data_2 = { 'Value': '2de11defd199f8d5bb63f9b7deb265ba5c675c10', - 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', + 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', 'SPNameQualifier': 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php' } nameid_data_2 = response_2.get_nameid_data() diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 398e09a1..c26ee910 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -395,7 +395,7 @@ def testGetSPMetadata(self): self.assertIn('WantAssertionsSigned="false"', metadata) self.assertIn('', metadata) self.assertIn('', metadata) - self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', metadata) + self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata) def testGetSPMetadataSigned(self): """ @@ -450,7 +450,7 @@ def generateAndCheckMetadata(self, settings): self.assertIn('WantAssertionsSigned="false"', metadata) self.assertIn('', metadata) self.assertIn('', metadata) - self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', metadata) + self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata) self.assertIn('', metadata) self.assertIn('', metadata) self.assertIn(' Date: Sat, 14 May 2016 19:57:27 +0200 Subject: [PATCH 112/352] Release 2.1.7 --- changelog.md | 15 +++++++++++++++ setup.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 45411dcb..695d5e38 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,20 @@ # python-saml changelog +### 2.1.7 (May 14, 2016) +* [#117](https://github.com/onelogin/python-saml/pull/117) AttributeConsumingService support +* [#114](https://github.com/onelogin/python-saml/pull/114) Compare Assertion InResponseTo if not None +* Return empty list when there are no audience values +* Passing NameQualifier through to logout request +* Make deflate process when retrieving built SAML messages optional +* Add debug parameter to decrypt method +* Fix Idp Metadata parser +* Add documentation related to the new IdP metadata parser methods +* Extract the already encoded value directly from get_data +* [#133](https://github.com/onelogin/python-saml/pull/133) Fix typo and add extra assertions in util decrypt test +* Fix Signature with empty URI support +* Allow AuthnRequest with no NameIDPolicy +* Remove requirement of NameID on SAML responses + ### 2.1.6 (Feb 15, 2016) * Prevent signature wrapping attack!! * [#111](https://github.com/onelogin/python-saml/pull/111) Add support for nested `NameID` children inside `AttributeValue`s diff --git a/setup.py b/setup.py index d14890ad..0499d50a 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.1.6', + version='2.1.7', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', From c2b087dcfaf6c771168e678f04a4f4d98ab5b1e4 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 17 May 2016 13:46:06 +0200 Subject: [PATCH 113/352] Fix #135, #136. Metadata XML (RequestedAttribute) --- src/onelogin/saml2/metadata.py | 4 ++-- tests/src/OneLogin/saml2_tests/metadata_test.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 46e8415f..018ab36a 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -87,14 +87,14 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N requested_attribute_data = [] for req_attribs in sp['attributeConsumingService']['requestedAttributes']: req_attr_nameformat_str = req_attr_friendlyname_str = req_attr_isrequired_str = '' - req_attr_aux_str = ' \>' + req_attr_aux_str = ' />' if 'nameFormat' in req_attribs.keys() and req_attribs['nameFormat']: req_attr_nameformat_str = " NameFormat=\"%s\"" % req_attribs['nameFormat'] if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']: req_attr_nameformat_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName'] if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']: - req_attr_isrequired_str = " isRequired=\"%s\"" % req_attribs['isRequired'] + req_attr_isrequired_str = " isRequired=\"%s\"" % 'true' if req_attribs['isRequired'] else 'false' if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']: req_attr_aux_str = """ > Test Service Test Service - - - - - + + + + + """, metadata) def testSignMetadata(self): From eade83963be2f524de068683d71e63068ff5b847 Mon Sep 17 00:00:00 2001 From: Avner Cohen Date: Wed, 18 May 2016 16:42:12 +0300 Subject: [PATCH 114/352] Docs for OSx instlltion of libsecxml1 --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index ba73879c..47af42d2 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,19 @@ Installation * [isodate](https://pypi.python.org/pypi/isodate) An ISO 8601 date/time/duration parser and formater * [defusedxml](https://pypi.python.org/pypi/defusedxml) XML bomb protection for Python stdlib modules + Review the setup.py file to know the version of the library that python-saml is using +### OSX Dependences ### + * python 2.7 + * libxmlsec1 + +```sh + # using brew + brew install libxmlsec1 +``` + + ### Code ### #### Option 1. Download from github #### From 18014888bb464d061b5e324571b3b2aae9a5dd92 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 20 May 2016 12:44:32 +0200 Subject: [PATCH 115/352] Fix SHA384 Constant URI --- src/onelogin/saml2/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py index 19916cdd..2cd2e340 100644 --- a/src/onelogin/saml2/constants.py +++ b/src/onelogin/saml2/constants.py @@ -91,7 +91,7 @@ class OneLogin_Saml2_Constants(object): # Sign & Crypto SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1' SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256' - SHA384 = 'http://www.w3.org/2001/04/xmlencsha384' + SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384' SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512' DSA_SHA1 = 'http://www.w3.org/2000/09/xmld/sig#dsa-sha1' From ed0463e39a02868e01ce0d97b5da2e905c772fef Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 20 May 2016 18:18:34 +0200 Subject: [PATCH 116/352] Fixes Windows specific Unix date formatting bug. Use time() instead of datetime.now().strftime('%s') --- src/onelogin/saml2/metadata.py | 4 ++-- src/onelogin/saml2/settings.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 018ab36a..c8c40b95 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -9,7 +9,7 @@ """ -from time import gmtime, strftime +from time import gmtime, strftime, time from datetime import datetime from defusedxml.minidom import parseString @@ -54,7 +54,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N :type organization: dict """ if valid_until is None: - valid_until = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_VALID + valid_until = int(time()) + OneLogin_Saml2_Metadata.TIME_VALID if not isinstance(valid_until, basestring): if isinstance(valid_until, datetime): valid_until_time = valid_until.timetuple() diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 60d8cdb5..10c6dca9 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -9,9 +9,9 @@ """ -from datetime import datetime import json import re +from time import time from os.path import dirname, exists, join, sep, abspath from xml.dom.minidom import Document @@ -735,7 +735,7 @@ def validate_metadata(self, xml): cache_duration = element.getAttribute('cacheDuration') expire_time = OneLogin_Saml2_Utils.get_expire_time(cache_duration, valid_until) - if expire_time is not None and int(datetime.now().strftime('%s')) > int(expire_time): + if expire_time is not None and int(time()) > int(expire_time): errors.append('expired_xml') # TODO: Validate Sign From 70988bdb08228da8d085e5b5498e138a716b7823 Mon Sep 17 00:00:00 2001 From: Sebastian Brachi Date: Tue, 24 May 2016 15:13:01 -0300 Subject: [PATCH 117/352] Refactor of settings.py to make it a little more readable. No functionality changed --- src/onelogin/saml2/settings.py | 194 ++++++++++++--------------------- 1 file changed, 68 insertions(+), 126 deletions(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 10c6dca9..315d65f0 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -197,20 +197,12 @@ def __load_settings_from_dict(self, settings): self.__errors = [] self.__sp = settings['sp'] - if 'idp' in settings: - self.__idp = settings['idp'] - if 'strict' in settings: - self.__strict = settings['strict'] - if 'debug' in settings: - self.__debug = settings['debug'] - if 'security' in settings: - self.__security = settings['security'] - else: - self.__security = {} - if 'contactPerson' in settings: - self.__contacts = settings['contactPerson'] - if 'organization' in settings: - self.__organization = settings['organization'] + self.__idp = settings.get('idp', {}) + self.__strict = settings.get('strict', False) + self.__debug = settings.get('debug', False) + self.__security = settings.get('security', {}) + self.__contacts = settings.get('contactPerson', {}) + self.__organization = settings.get('organization', {}) self.__add_default_values() return True @@ -252,79 +244,53 @@ def __add_default_values(self): """ Add default values if the settings info is not complete """ - if 'assertionConsumerService' not in self.__sp.keys(): - self.__sp['assertionConsumerService'] = {} - if 'binding' not in self.__sp['assertionConsumerService'].keys(): - self.__sp['assertionConsumerService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_POST + self.__sp.setdefault('assertionConsumerService', {}) + self.__sp['assertionConsumerService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_POST) - if 'attributeConsumingService' not in self.__sp.keys(): - self.__sp['attributeConsumingService'] = {} + self.__sp.setdefault('attributeConsumingService', {}) - if 'singleLogoutService' not in self.__sp.keys(): - self.__sp['singleLogoutService'] = {} - if 'binding' not in self.__sp['singleLogoutService']: - self.__sp['singleLogoutService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT + self.__sp.setdefault('singleLogoutService', {}) + self.__sp['singleLogoutService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT) # Related to nameID - if 'NameIDFormat' not in self.__sp: - self.__sp['NameIDFormat'] = OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED - if 'nameIdEncrypted' not in self.__security: - self.__security['nameIdEncrypted'] = False + self.__sp.setdefault('NameIDFormat', OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED) + self.__security.setdefault('nameIdEncrypted', False) # Metadata format - if 'metadataValidUntil' not in self.__security.keys(): - self.__security['metadataValidUntil'] = None # None means use default - if 'metadataCacheDuration' not in self.__security.keys(): - self.__security['metadataCacheDuration'] = None # None means use default + self.__security.setdefault('metadataValidUntil', None) # None means use default + self.__security.setdefault('metadataCacheDuration', None) # None means use default # Sign provided - if 'authnRequestsSigned' not in self.__security.keys(): - self.__security['authnRequestsSigned'] = False - if 'logoutRequestSigned' not in self.__security.keys(): - self.__security['logoutRequestSigned'] = False - if 'logoutResponseSigned' not in self.__security.keys(): - self.__security['logoutResponseSigned'] = False - if 'signMetadata' not in self.__security.keys(): - self.__security['signMetadata'] = False + self.__security.setdefault('authnRequestsSigned', False) + self.__security.setdefault('logoutRequestSigned', False) + self.__security.setdefault('logoutResponseSigned', False) + self.__security.setdefault('signMetadata', False) # Sign expected - if 'wantMessagesSigned' not in self.__security.keys(): - self.__security['wantMessagesSigned'] = False - if 'wantAssertionsSigned' not in self.__security.keys(): - self.__security['wantAssertionsSigned'] = False + self.__security.setdefault('wantMessagesSigned', False) + self.__security.setdefault('wantAssertionsSigned', False) # NameID element expected - if 'wantNameId' not in self.__security.keys(): - self.__security['wantNameId'] = True + self.__security.setdefault('wantNameId', True) # Encrypt expected - if 'wantAssertionsEncrypted' not in self.__security.keys(): - self.__security['wantAssertionsEncrypted'] = False - if 'wantNameIdEncrypted' not in self.__security.keys(): - self.__security['wantNameIdEncrypted'] = False + self.__security.setdefault('wantAssertionsEncrypted', False) + self.__security.setdefault('wantNameIdEncrypted', False) # Signature Algorithm - if 'signatureAlgorithm' not in self.__security.keys(): - self.__security['signatureAlgorithm'] = OneLogin_Saml2_Constants.RSA_SHA1 + self.__security.setdefault('signatureAlgorithm', OneLogin_Saml2_Constants.RSA_SHA1) # AttributeStatement required by default - if 'wantAttributeStatement' not in self.__security.keys(): - self.__security['wantAttributeStatement'] = True + self.__security.setdefault('wantAttributeStatement', True) - if 'x509cert' not in self.__idp: - self.__idp['x509cert'] = '' - if 'certFingerprint' not in self.__idp: - self.__idp['certFingerprint'] = '' - if 'certFingerprintAlgorithm' not in self.__idp: - self.__idp['certFingerprintAlgorithm'] = 'sha1' + self.__idp.setdefault('x509cert', '') + self.__idp.setdefault('certFingerprint', '') + self.__idp.setdefault('certFingerprintAlgorithm', 'sha1') - if 'x509cert' not in self.__sp: - self.__sp['x509cert'] = '' - if 'privateKey' not in self.__sp: - self.__sp['privateKey'] = '' + self.__sp.setdefault('x509cert', '') + self.__sp.setdefault('privateKey', '') - if 'requestedAuthnContext' not in self.__security.keys(): - self.__security['requestedAuthnContext'] = True + self.__security.setdefault('requestedAuthnContext', True) def check_settings(self, settings): """ @@ -365,37 +331,31 @@ def check_idp_settings(self, settings): if not isinstance(settings, dict) or len(settings) == 0: errors.append('invalid_syntax') else: - if 'idp' not in settings or len(settings['idp']) == 0: + if not settings.get('idp'): errors.append('idp_not_found') else: idp = settings['idp'] - if 'entityId' not in idp or len(idp['entityId']) == 0: + if not idp.get('entityId'): errors.append('idp_entityId_not_found') - if 'singleSignOnService' not in idp or \ - 'url' not in idp['singleSignOnService'] or \ - len(idp['singleSignOnService']['url']) == 0: + if not idp.get('singleSignOnService', {}).get('url'): errors.append('idp_sso_not_found') elif not validate_url(idp['singleSignOnService']['url']): errors.append('idp_sso_url_invalid') - if 'singleLogoutService' in idp and \ - 'url' in idp['singleLogoutService'] and \ - len(idp['singleLogoutService']['url']) > 0 and \ - not validate_url(idp['singleLogoutService']['url']): + slo_url = idp.get('singleLogoutService', {}).get('url') + if slo_url and not validate_url(slo_url): errors.append('idp_slo_url_invalid') if 'security' in settings: security = settings['security'] - exists_x509 = ('x509cert' in idp and - len(idp['x509cert']) > 0) - exists_fingerprint = ('certFingerprint' in idp and - len(idp['certFingerprint']) > 0) + exists_x509 = bool(idp.get('x509cert')) + exists_fingerprint = bool(idp.get('certFingerprint')) - want_assert_sign = 'wantAssertionsSigned' in security.keys() and security['wantAssertionsSigned'] - want_mes_signed = 'wantMessagesSigned' in security.keys() and security['wantMessagesSigned'] - nameid_enc = 'nameIdEncrypted' in security.keys() and security['nameIdEncrypted'] + want_assert_sign = bool(security.get('wantAssertionsSigned')) + want_mes_signed = bool(security.get('wantMessagesSigned')) + nameid_enc = bool(security.get('nameIdEncrypted')) if (want_assert_sign or want_mes_signed) and \ not(exists_x509 or exists_fingerprint): @@ -418,10 +378,10 @@ def check_sp_settings(self, settings): assert isinstance(settings, dict) errors = [] - if not isinstance(settings, dict) or len(settings) == 0: + if not isinstance(settings, dict) or not settings: errors.append('invalid_syntax') else: - if 'sp' not in settings or len(settings['sp']) == 0: + if not settings.get('sp'): errors.append('sp_not_found') else: # check_sp_certs uses self.__sp so I add it @@ -429,21 +389,17 @@ def check_sp_settings(self, settings): self.__sp = settings['sp'] sp = settings['sp'] - security = {} - if 'security' in settings: - security = settings['security'] + security = settings.get('security', {}) - if 'entityId' not in sp or len(sp['entityId']) == 0: + if not sp.get('entityId'): errors.append('sp_entityId_not_found') - if 'assertionConsumerService' not in sp or \ - 'url' not in sp['assertionConsumerService'] or \ - len(sp['assertionConsumerService']['url']) == 0: + if not sp.get('assertionConsumerService', {}).get('url'): errors.append('sp_acs_not_found') elif not validate_url(sp['assertionConsumerService']['url']): errors.append('sp_acs_url_invalid') - if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']): + if sp.get('attributeConsumingService'): attributeConsumingService = sp['attributeConsumingService'] if 'serviceName' not in attributeConsumingService: errors.append('sp_attributeConsumingService_serviceName_not_found') @@ -468,10 +424,8 @@ def check_sp_settings(self, settings): if "serviceDescription" in attributeConsumingService and not isinstance(attributeConsumingService['serviceDescription'], basestring): errors.append('sp_attributeConsumingService_serviceDescription_type_invalid') - if 'singleLogoutService' in sp and \ - 'url' in sp['singleLogoutService'] and \ - len(sp['singleLogoutService']['url']) > 0 and \ - not validate_url(sp['singleLogoutService']['url']): + slo_url = sp.get('singleLogoutService', {}).get('url') + if slo_url and not validate_url(slo_url): errors.append('sp_sls_url_invalid') if 'signMetadata' in security and isinstance(security['signMetadata'], dict): @@ -479,11 +433,11 @@ def check_sp_settings(self, settings): 'certFileName' not in security['signMetadata']: errors.append('sp_signMetadata_invalid') - authn_sign = 'authnRequestsSigned' in security.keys() and security['authnRequestsSigned'] - logout_req_sign = 'logoutRequestSigned' in security.keys() and security['logoutRequestSigned'] - logout_res_sign = 'logoutResponseSigned' in security.keys() and security['logoutResponseSigned'] - want_assert_enc = 'wantAssertionsEncrypted' in security.keys() and security['wantAssertionsEncrypted'] - want_nameid_enc = 'wantNameIdEncrypted' in security.keys() and security['wantNameIdEncrypted'] + authn_sign = bool(security.get('authnRequestsSigned')) + logout_req_sign = bool(security.get('logoutRequestSigned')) + logout_res_sign = bool(security.get('logoutResponseSigned')) + want_assert_enc = bool(security.get('wantAssertionsEncrypted')) + want_nameid_enc = bool(security.get('wantNameIdEncrypted')) if not self.check_sp_certs(): if authn_sign or logout_req_sign or logout_res_sign or \ @@ -537,18 +491,14 @@ def get_sp_key(self): :returns: SP private key :rtype: string """ - key = None + key = self.__sp.get('privateKey') + key_file_name = self.__paths['cert'] + 'sp.key' - if 'privateKey' in self.__sp.keys() and self.__sp['privateKey']: - key = self.__sp['privateKey'] - else: - key_file_name = self.__paths['cert'] + 'sp.key' + if not key and exists(key_file_name): + with open(key_file_name) as f: + key = f.read() - if exists(key_file_name): - f_key = open(key_file_name, 'r') - key = f_key.read() - f_key.close() - return key + return key or None # TODO: Should change rtype docstrings or update tests def get_sp_cert(self): """ @@ -557,18 +507,14 @@ def get_sp_cert(self): :returns: SP public cert :rtype: string """ - cert = None + cert = self.__sp.get('x509cert') + cert_file_name = self.__paths['cert'] + 'sp.crt' - if 'x509cert' in self.__sp.keys() and self.__sp['x509cert']: - cert = self.__sp['x509cert'] - else: - cert_file_name = self.__paths['cert'] + 'sp.crt' - if exists(cert_file_name): - f_cert = open(cert_file_name, 'r') - cert = f_cert.read() - f_cert.close() + if not cert and exists(cert_file_name): + with open(cert_file_name) as f: + cert = f.read() - return cert + return cert or None # TODO: Should change rtype docstrings or update tests def get_idp_cert(self): """ @@ -577,11 +523,7 @@ def get_idp_cert(self): :returns: IdP public cert :rtype: string """ - cert = None - - if 'x509cert' in self.__idp.keys() and self.__idp['x509cert']: - cert = self.__idp['x509cert'] - return cert + return self.__idp.get('x509cert') def get_idp_data(self): """ From 6207906ae50bb88238ff142e656ddd2f027cf3bd Mon Sep 17 00:00:00 2001 From: Sebastian Brachi Date: Tue, 24 May 2016 19:28:18 -0300 Subject: [PATCH 118/352] Removed TODO updating docstrings --- src/onelogin/saml2/settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 315d65f0..4470cc91 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -489,7 +489,7 @@ def get_sp_key(self): Returns the x509 private key of the SP. :returns: SP private key - :rtype: string + :rtype: string or None """ key = self.__sp.get('privateKey') key_file_name = self.__paths['cert'] + 'sp.key' @@ -498,14 +498,14 @@ def get_sp_key(self): with open(key_file_name) as f: key = f.read() - return key or None # TODO: Should change rtype docstrings or update tests + return key or None def get_sp_cert(self): """ Returns the x509 public cert of the SP. :returns: SP public cert - :rtype: string + :rtype: string or None """ cert = self.__sp.get('x509cert') cert_file_name = self.__paths['cert'] + 'sp.crt' @@ -514,7 +514,7 @@ def get_sp_cert(self): with open(cert_file_name) as f: cert = f.read() - return cert or None # TODO: Should change rtype docstrings or update tests + return cert or None def get_idp_cert(self): """ From 25d0bc528d1a3d3bdbf9e32af1b91e14d9823cfa Mon Sep 17 00:00:00 2001 From: Max Leblanc Date: Wed, 25 May 2016 15:14:27 -0400 Subject: [PATCH 119/352] Bugfix for ADFS lowercase signatures --- src/onelogin/saml2/auth.py | 6 +++--- src/onelogin/saml2/logout_request.py | 8 ++++---- src/onelogin/saml2/logout_response.py | 8 ++++---- src/onelogin/saml2/utils.py | 11 ++++++++--- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index ff06da50..5626e8ba 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -111,7 +111,7 @@ def process_response(self, request_id=None): OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND ) - def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None): + def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None, lowercase_urlencoding=False): """ Process the SAML Logout Response / Logout Request sent by the IdP. @@ -127,7 +127,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']: logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) - if not logout_response.is_valid(self.__request_data, request_id): + if not logout_response.is_valid(self.__request_data, request_id, lowercase_urlencoding=lowercase_urlencoding): self.__errors.append('invalid_logout_response') self.__error_reason = logout_response.get_error() elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS: @@ -137,7 +137,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']: logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest']) - if not logout_request.is_valid(self.__request_data): + if not logout_request.is_valid(self.__request_data, lowercase_urlencoding=lowercase_urlencoding): self.__errors.append('invalid_logout_request') self.__error_reason = logout_request.get_error() else: diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 8c1a83d2..96702b74 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -251,7 +251,7 @@ 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, lowercase_urlencoding=False): """ Checks if the Logout Request recieved is valid :param request_data: Request Data @@ -316,10 +316,10 @@ def is_valid(self, request_data): else: sign_alg = get_data['SigAlg'] - signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest') + signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding) if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState')) - signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1)) + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index a9cce855..7a48aac6 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -68,7 +68,7 @@ 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, lowercase_urlencoding=False): """ Determines if the SAML LogoutResponse is valid :param request_id: The ID of the LogoutRequest sent by this SP to the IdP @@ -119,10 +119,10 @@ def is_valid(self, request_data, request_id=None): else: sign_alg = get_data['SigAlg'] - signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse') + signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse', lowercase_urlencoding=lowercase_urlencoding) if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState')) - signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1)) + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index eccbc5bd..24d2a2d2 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1127,7 +1127,7 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ return False @staticmethod - def get_encoded_parameter(get_data, name, default=None): + def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False): """Return an url encoded get parameter value Prefer to extract the original encoded value directly from query_string since url encoding is not canonical. The encoding used by ADFS 3.0 is not compatible with @@ -1135,10 +1135,10 @@ def get_encoded_parameter(get_data, name, default=None): upper case hex numbers) """ if name not in get_data: - return quote_plus(default) + return case_sensitive_urlencode(default, lowercase_urlencoding) if 'query_string' in get_data: return OneLogin_Saml2_Utils.extract_raw_query_parameter(get_data['query_string'], name) - return quote_plus(get_data[name]) + return case_sensitive_urlencode(get_data[name], lowercase_urlencoding) @staticmethod def extract_raw_query_parameter(query_string, parameter, default=''): @@ -1147,3 +1147,8 @@ def extract_raw_query_parameter(query_string, parameter, default=''): return m.group(1) else: return default + + @staticmethod + def case_sensitive_urlencode(to_encode, lowercase=False): + encoded=quote_plus(to_encode) + return re.sub(r"%[A-F0-9]{2}", lambda m: m.group(0).lower(), encoded) if lowercase else encoded From 37330ba200c17b8389ec06975f6dad64e0bc0e84 Mon Sep 17 00:00:00 2001 From: Max Leblanc Date: Wed, 25 May 2016 16:27:19 -0400 Subject: [PATCH 120/352] Bugfix --- src/onelogin/saml2/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 24d2a2d2..fec7ec32 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1135,10 +1135,10 @@ def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=Fa upper case hex numbers) """ if name not in get_data: - return case_sensitive_urlencode(default, lowercase_urlencoding) + return OneLogin_Saml2_Utils.case_sensitive_urlencode(default, lowercase_urlencoding) if 'query_string' in get_data: return OneLogin_Saml2_Utils.extract_raw_query_parameter(get_data['query_string'], name) - return case_sensitive_urlencode(get_data[name], lowercase_urlencoding) + return OneLogin_Saml2_Utils.case_sensitive_urlencode(get_data[name], lowercase_urlencoding) @staticmethod def extract_raw_query_parameter(query_string, parameter, default=''): From 05fd047aed261978dc96ad05a15a2f24d5f36045 Mon Sep 17 00:00:00 2001 From: Max Leblanc Date: Thu, 26 May 2016 11:55:10 -0400 Subject: [PATCH 121/352] Better solution for the problem --- src/onelogin/saml2/auth.py | 6 +++--- src/onelogin/saml2/logout_request.py | 8 ++++---- src/onelogin/saml2/logout_response.py | 8 ++++---- src/onelogin/saml2/utils.py | 4 +++- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 5626e8ba..ff06da50 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -111,7 +111,7 @@ def process_response(self, request_id=None): OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND ) - def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None, lowercase_urlencoding=False): + def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None): """ Process the SAML Logout Response / Logout Request sent by the IdP. @@ -127,7 +127,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']: logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) - if not logout_response.is_valid(self.__request_data, request_id, lowercase_urlencoding=lowercase_urlencoding): + if not logout_response.is_valid(self.__request_data, request_id): self.__errors.append('invalid_logout_response') self.__error_reason = logout_response.get_error() elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS: @@ -137,7 +137,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']: logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest']) - if not logout_request.is_valid(self.__request_data, lowercase_urlencoding=lowercase_urlencoding): + if not logout_request.is_valid(self.__request_data): self.__errors.append('invalid_logout_request') self.__error_reason = logout_request.get_error() else: diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 96702b74..8c1a83d2 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -251,7 +251,7 @@ def get_session_indexes(request): session_indexes.append(session_index_node.text) return session_indexes - def is_valid(self, request_data, lowercase_urlencoding=False): + def is_valid(self, request_data): """ Checks if the Logout Request recieved is valid :param request_data: Request Data @@ -316,10 +316,10 @@ def is_valid(self, request_data, lowercase_urlencoding=False): else: sign_alg = get_data['SigAlg'] - signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding) + signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest') if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) - signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState')) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 7a48aac6..a9cce855 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -68,7 +68,7 @@ def get_status(self): status = entries[0].attrib['Value'] return status - def is_valid(self, request_data, request_id=None, lowercase_urlencoding=False): + def is_valid(self, request_data, request_id=None): """ Determines if the SAML LogoutResponse is valid :param request_id: The ID of the LogoutRequest sent by this SP to the IdP @@ -119,10 +119,10 @@ def is_valid(self, request_data, request_id=None, lowercase_urlencoding=False): else: sign_alg = get_data['SigAlg'] - signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse', lowercase_urlencoding=lowercase_urlencoding) + signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse') if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) - signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState')) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index fec7ec32..53ac28ff 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1127,13 +1127,15 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ return False @staticmethod - def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False): + def get_encoded_parameter(get_data, name, default=None): """Return an url encoded get parameter value Prefer to extract the original encoded value directly from query_string since url encoding is not canonical. The encoding used by ADFS 3.0 is not compatible with python's quote_plus (ADFS produces lower case hex numbers and quote_plus produces upper case hex numbers) """ + lowercase_urlencoding = get_data.get('lowercase_urlencoding', False) + if name not in get_data: return OneLogin_Saml2_Utils.case_sensitive_urlencode(default, lowercase_urlencoding) if 'query_string' in get_data: From 9c72fa3d20c75a9d283ba7e4082e9c0f3021b194 Mon Sep 17 00:00:00 2001 From: Max Leblanc Date: Thu, 26 May 2016 13:53:48 -0400 Subject: [PATCH 122/352] test --- src/onelogin/saml2/logout_request.py | 11 ++++++++--- src/onelogin/saml2/logout_response.py | 11 ++++++++--- src/onelogin/saml2/utils.py | 3 +-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 8c1a83d2..a2882ec6 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -272,6 +272,11 @@ def is_valid(self, request_data): else: get_data = {} + if 'lowercase_urlencoding' in request_data.keys(): + lowercase_urlencoding = request_data['lowercase_urlencoding'] + else: + lowercase_urlencoding = False + if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): @@ -316,10 +321,10 @@ def is_valid(self, request_data): else: sign_alg = get_data['SigAlg'] - signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest') + signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding) if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState')) - signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1)) + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index a9cce855..bcba30f5 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -82,6 +82,11 @@ def is_valid(self, request_data, request_id=None): idp_entity_id = idp_data['entityId'] get_data = request_data['get_data'] + if 'lowercase_urlencoding' in request_data.keys(): + lowercase_urlencoding = request_data['lowercase_urlencoding'] + else: + lowercase_urlencoding = False + if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): @@ -119,10 +124,10 @@ def is_valid(self, request_data, request_id=None): else: sign_alg = get_data['SigAlg'] - signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse') + signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse', lowercase_urlencoding=lowercase_urlencoding) if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState')) - signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1)) + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 53ac28ff..5fbac2d8 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1127,14 +1127,13 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ return False @staticmethod - def get_encoded_parameter(get_data, name, default=None): + def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False): """Return an url encoded get parameter value Prefer to extract the original encoded value directly from query_string since url encoding is not canonical. The encoding used by ADFS 3.0 is not compatible with python's quote_plus (ADFS produces lower case hex numbers and quote_plus produces upper case hex numbers) """ - lowercase_urlencoding = get_data.get('lowercase_urlencoding', False) if name not in get_data: return OneLogin_Saml2_Utils.case_sensitive_urlencode(default, lowercase_urlencoding) From 86aaca415ffb949c91437d5e5c88f5ab2ac7c75b Mon Sep 17 00:00:00 2001 From: Max Leblanc Date: Thu, 26 May 2016 14:31:45 -0400 Subject: [PATCH 123/352] Fixed pep8 --- 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 5fbac2d8..1d0b80e9 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1151,5 +1151,5 @@ def extract_raw_query_parameter(query_string, parameter, default=''): @staticmethod def case_sensitive_urlencode(to_encode, lowercase=False): - encoded=quote_plus(to_encode) + encoded = quote_plus(to_encode) return re.sub(r"%[A-F0-9]{2}", lambda m: m.group(0).lower(), encoded) if lowercase else encoded From 957b5872dafd62e0067a53ef3b4322c9918e9cca Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 27 May 2016 22:31:02 +0200 Subject: [PATCH 124/352] Fix #143 READMEs suggest wrong cert name --- demo-bottle/saml/certs/README | 4 ++-- demo-django/saml/certs/README | 4 ++-- demo-flask/saml/certs/README | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/demo-bottle/saml/certs/README b/demo-bottle/saml/certs/README index bcb87f11..03c13737 100644 --- a/demo-bottle/saml/certs/README +++ b/demo-bottle/saml/certs/README @@ -3,9 +3,9 @@ Take care of this folder that could contain private key. Be sure that this folde Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: * sp.key Private Key - * sp.cert Public cert + * sp.crt Public cert Also you can use other cert to sign the metadata of the SP using the: * metadata.key - * metadata.cert + * metadata.crt diff --git a/demo-django/saml/certs/README b/demo-django/saml/certs/README index bcb87f11..03c13737 100644 --- a/demo-django/saml/certs/README +++ b/demo-django/saml/certs/README @@ -3,9 +3,9 @@ Take care of this folder that could contain private key. Be sure that this folde Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: * sp.key Private Key - * sp.cert Public cert + * sp.crt Public cert Also you can use other cert to sign the metadata of the SP using the: * metadata.key - * metadata.cert + * metadata.crt diff --git a/demo-flask/saml/certs/README b/demo-flask/saml/certs/README index bcb87f11..03c13737 100644 --- a/demo-flask/saml/certs/README +++ b/demo-flask/saml/certs/README @@ -3,9 +3,9 @@ Take care of this folder that could contain private key. Be sure that this folde Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: * sp.key Private Key - * sp.cert Public cert + * sp.crt Public cert Also you can use other cert to sign the metadata of the SP using the: * metadata.key - * metadata.cert + * metadata.crt From f527bc10f25c5675e5ea947382fb71f6715da938 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 3 Jun 2016 21:21:47 +0200 Subject: [PATCH 125/352] Add parameter to the demos --- demo-bottle/index.py | 4 +++- demo-django/demo/views.py | 2 ++ demo-flask/index.py | 2 ++ src/onelogin/saml2/logout_request.py | 3 +-- src/onelogin/saml2/logout_response.py | 3 +-- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/demo-bottle/index.py b/demo-bottle/index.py index 2763f66c..45b234c3 100644 --- a/demo-bottle/index.py +++ b/demo-bottle/index.py @@ -30,13 +30,15 @@ def init_saml_auth(req): def prepare_bottle_request(req): url_data = urlparse(req.url) return { + 'https': 'on' if req.urlparts.scheme == 'https' else 'off', 'http_host': req.get_header('host'), 'server_port': url_data.port, 'script_name': req.fullpath, 'get_data': req.query, 'post_data': req.forms, 'query_string': req.query_string, - 'https': 'on' if req.urlparts.scheme == 'https' else 'off' + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True } diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index 13881f65..859ff9df 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -24,6 +24,8 @@ def prepare_django_request(request): 'server_port': request.META['SERVER_PORT'], 'get_data': request.GET.copy(), 'post_data': request.POST.copy(), + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True, 'query_string': request.META['QUERY_STRING'] } return result diff --git a/demo-flask/index.py b/demo-flask/index.py index 9c9abf42..9dee0bc8 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -29,6 +29,8 @@ def prepare_flask_request(request): 'script_name': request.path, 'get_data': request.args.copy(), 'post_data': request.form.copy(), + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True, 'query_string': request.query_string } diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index a2882ec6..dc519cec 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -261,6 +261,7 @@ def is_valid(self, request_data): :rtype: boolean """ self.__error = None + lowercase_urlencoding = False try: dom = fromstring(self.__logout_request) @@ -274,8 +275,6 @@ def is_valid(self, request_data): if 'lowercase_urlencoding' in request_data.keys(): lowercase_urlencoding = request_data['lowercase_urlencoding'] - else: - lowercase_urlencoding = False if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index bcba30f5..a0f6f25d 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -77,6 +77,7 @@ def is_valid(self, request_data, request_id=None): :rtype: boolean """ self.__error = None + lowercase_urlencoding = False try: idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] @@ -84,8 +85,6 @@ def is_valid(self, request_data, request_id=None): if 'lowercase_urlencoding' in request_data.keys(): lowercase_urlencoding = request_data['lowercase_urlencoding'] - else: - lowercase_urlencoding = False if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) From bab7412ab5aa8e66af95477fc5869350198c3d94 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 3 Jun 2016 21:50:07 +0200 Subject: [PATCH 126/352] . --- demo-bottle/index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo-bottle/index.py b/demo-bottle/index.py index 45b234c3..f6b5d78a 100644 --- a/demo-bottle/index.py +++ b/demo-bottle/index.py @@ -36,9 +36,9 @@ def prepare_bottle_request(req): 'script_name': req.fullpath, 'get_data': req.query, 'post_data': req.forms, - 'query_string': req.query_string, # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 - # 'lowercase_urlencoding': True + # 'lowercase_urlencoding': True, + 'query_string': req.query_string } From 9d312a87123b2cae42973e73c0cb5ea5ae6f6cd6 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 4 Jun 2016 00:02:17 +0200 Subject: [PATCH 127/352] Release 2.1.8 --- changelog.md | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 695d5e38..132e4db4 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # python-saml changelog +### 2.1.8 (Jun 02, 2016) +* Fix Metadata XML (RequestedAttribute) +* Fix Windows specific Unix date formatting bug. +* Docs for OSx instlltion of libsecxml1 +* Fix SHA384 Constant URI +* [#142](https://github.com/onelogin/python-saml/pull/142) Refactor of settings.py to make it a little more readable. +* Bugfix for ADFS lowercase signatures +* READMEs suggested wrong cert name + ### 2.1.7 (May 14, 2016) * [#117](https://github.com/onelogin/python-saml/pull/117) AttributeConsumingService support * [#114](https://github.com/onelogin/python-saml/pull/114) Compare Assertion InResponseTo if not None diff --git a/setup.py b/setup.py index 0499d50a..609941f6 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.1.7', + version='2.1.8', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', From 86cb7ef1b819b179d09f77bd6a60df88cc715c46 Mon Sep 17 00:00:00 2001 From: coolbootscoder Date: Fri, 17 Jun 2016 13:53:25 -0400 Subject: [PATCH 128/352] Changing xmlsec.initialize --- src/onelogin/saml2/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index ff06da50..a04ed50d 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -433,7 +433,7 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND ) - xmlsec.initialize() + xmlsec.initialize('openssl') dsig_ctx = xmlsec.DSigCtx() dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) From 367d8458d79941fd0e4896f730fca0391d06f849 Mon Sep 17 00:00:00 2001 From: coolbootscoder Date: Fri, 17 Jun 2016 13:54:54 -0400 Subject: [PATCH 129/352] Changing xmlsec.initialize --- 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 1d0b80e9..cb15e9e7 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -626,7 +626,7 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None): xml = name_id_container.toxml() elem = fromstring(xml) - xmlsec.initialize() + xmlsec.initialize('openssl') if debug: xmlsec.set_error_callback(print_xmlsec_errors) From d4c82a465dcbafc40e4d8a6e026f6a204858f11e Mon Sep 17 00:00:00 2001 From: coolbootscoder Date: Fri, 17 Jun 2016 14:28:24 -0400 Subject: [PATCH 130/352] Work-around for xmlsec.initialize problem --- src/onelogin/saml2/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index cb15e9e7..eed639eb 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -34,6 +34,9 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.errors import OneLogin_Saml2_Error +if not globals().get('xmlsec_setup', False): + xmlsec.initialize() + globals()['xmlsec_setup'] = True def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg): """ From dfd2bc0fd52f31d8b59cfbda9fcdc4b3b96fd505 Mon Sep 17 00:00:00 2001 From: coolbootscoder Date: Fri, 17 Jun 2016 14:29:41 -0400 Subject: [PATCH 131/352] Work-around for xmlsec.initialize problem --- src/onelogin/saml2/auth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index a04ed50d..0cbeeaa7 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -14,14 +14,14 @@ from base64 import b64encode from urllib import quote_plus -import dm.xmlsec.binding as xmlsec +# import dm.xmlsec.binding as xmlsec 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, xmlsec from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request @@ -433,7 +433,7 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND ) - xmlsec.initialize('openssl') + # xmlsec.initialize() # already done in utils module dsig_ctx = xmlsec.DSigCtx() dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) From 5b1410b3b2c30ca3b08ab13ae5573a7942dd5600 Mon Sep 17 00:00:00 2001 From: coolbootscoder Date: Fri, 17 Jun 2016 14:30:27 -0400 Subject: [PATCH 132/352] Update utils.py --- 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 eed639eb..a02a65ef 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -629,7 +629,7 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None): xml = name_id_container.toxml() elem = fromstring(xml) - xmlsec.initialize('openssl') + # xmlsec.initialize('openssl') # done when the module is loaded if debug: xmlsec.set_error_callback(print_xmlsec_errors) From d9760c00cdb4b46c0f91b20e90c24b5976386233 Mon Sep 17 00:00:00 2001 From: coolbootscoder Date: Fri, 17 Jun 2016 14:38:08 -0400 Subject: [PATCH 133/352] Update utils.py --- src/onelogin/saml2/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index a02a65ef..665a215d 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -738,7 +738,7 @@ def decrypt_element(encrypted_data, key, debug=False): elif isinstance(encrypted_data, basestring): encrypted_data = fromstring(str(encrypted_data)) - xmlsec.initialize() + # xmlsec.initialize() # Initialized at the start of the module if debug: xmlsec.set_error_callback(print_xmlsec_errors) @@ -813,7 +813,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant else: raise Exception('Error parsing xml string') - xmlsec.initialize() + # xmlsec.initialize() # Initialized at the start of the module if debug: xmlsec.set_error_callback(print_xmlsec_errors) @@ -919,7 +919,7 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid else: raise Exception('Error parsing xml string') - xmlsec.initialize() + # xmlsec.initialize() # Initialized at the start of the module if debug: xmlsec.set_error_callback(print_xmlsec_errors) @@ -985,7 +985,7 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha else: raise Exception('Error parsing xml string') - xmlsec.initialize() + # xmlsec.initialize() # Initialized at the start of the module if debug: xmlsec.set_error_callback(print_xmlsec_errors) @@ -1038,7 +1038,7 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger :type: bool """ try: - xmlsec.initialize() + # xmlsec.initialize() # Initialized at the start of the module if debug: xmlsec.set_error_callback(print_xmlsec_errors) @@ -1103,7 +1103,7 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ :type: bool """ try: - xmlsec.initialize() + # xmlsec.initialize() # Initialized at the start of the module if debug: xmlsec.set_error_callback(print_xmlsec_errors) From b65749d84fd9dcd6fb428d5548d0d1445b86d6e6 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 27 Jun 2016 12:24:55 +0200 Subject: [PATCH 134/352] On this commit we changed the way we decrypt an Assertion and added 2 new validations in order to avoid Signature wrapping attacks: - Extra validations at the validateSignedElements method to check that the ref URIs and IDs are unique and consistent. - Validate the document (encrypted and decrypted version) against the scheme. --- src/onelogin/saml2/response.py | 70 ++++++++++++++++--- src/onelogin/saml2/utils.py | 1 - .../src/OneLogin/saml2_tests/response_test.py | 2 +- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 77453b86..de94228b 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -86,16 +86,19 @@ def is_valid(self, request_data, request_id=None): sp_data = self.__settings.get_sp_data() sp_entity_id = sp_data.get('entityId', '') - sign_nodes = self.__query('//ds:Signature') - - signed_elements = [] - for sign_node in sign_nodes: - signed_elements.append(sign_node.getparent().tag) + signed_elements = self.process_signed_elements() if self.__settings.is_strict(): + no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): - raise Exception('Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd') + raise Exception(no_valid_xml_msg) + + # If encrypted, check also the decrypted document + if self.encrypted: + res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.decrypted_document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) + if not isinstance(res, Document): + raise Exception(no_valid_xml_msg) security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) @@ -385,6 +388,53 @@ def validate_num_assertions(self): assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:Assertion') return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1 + def process_signed_elements(self): + """ + Verifies the signature nodes: + - Checks that are Response or Assertion + - Check that IDs and reference URI are unique and consistent. + + :returns: The signed elements tag names + :rtype: list + """ + sign_nodes = self.__query('//ds:Signature') + + signed_elements = [] + verified_seis = [] + verified_ids = [] + response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP + assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML + + 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) + + if not sign_node.getparent().get('ID'): + raise Exception('Signed Element must contain an ID. SAML Response rejected') + + id_value = sign_node.getparent().get('ID') + if id_value in verified_ids: + raise Exception('Duplicated ID. SAML Response rejected') + verified_ids.append(id_value) + + # Check that reference URI matches the parent ID and no duplicate References or IDs + ref = OneLogin_Saml2_Utils.query(sign_node, './/ds:Reference') + if ref: + ref = ref[0] + if ref.get('URI'): + sei = ref.get('URI')[1:] + + if sei != id_value: + raise Exception('Found an invalid Signed Element. SAML Response rejected') + + if sei in verified_seis: + raise Exception('Duplicated Reference URI. SAML Response rejected') + verified_seis.append(sei) + + signed_elements.append(signed_element) + return signed_elements + def validate_timestamps(self): """ Verifies that the document is valid according to Conditions Element @@ -413,10 +463,7 @@ def __query_assertion(self, xpath_expr): :returns: The queried nodes :rtype: list """ - if self.encrypted: - assertion_expr = '/saml:EncryptedAssertion/saml:Assertion' - else: - assertion_expr = '/saml:Assertion' + assertion_expr = '/saml:Assertion' signature_expr = '/ds:Signature/ds:SignedInfo/ds:Reference' signed_assertion_query = '/samlp:Response' + assertion_expr + signature_expr assertion_reference_nodes = self.__query(signed_assertion_query) @@ -493,7 +540,8 @@ def __decrypt_assertion(self, dom): keyinfo.append(encrypted_key[0]) encrypted_data = encrypted_data_nodes[0] - OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug) + decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug) + dom.replace(encrypted_assertion_nodes[0], decrypted) return dom def get_error(self): diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 1d0b80e9..f8415ef2 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -926,7 +926,6 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature') if not len(signature_nodes) > 0: - signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:EncryptedAssertion/saml:Assertion/ds:Signature') signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature') if len(signature_nodes) == 1: diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 435142a8..08602847 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -352,7 +352,7 @@ def testOnlyRetrieveAssertionWithIDThatMatchesSignatureReference(self): nameid = response.get_nameid() self.assertNotEqual('root@example.com', nameid) except: - self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error()) + self.assertEqual('Invalid Signature Element {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor SAML Response rejected', response.get_error()) def testDoesNotAllowSignatureWrappingAttack(self): """ From 2461c36228505d9894d453a5f3a5ff9405224327 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 27 Jun 2016 12:26:21 +0200 Subject: [PATCH 135/352] Release 2.1.9 --- README.md | 10 ++++++++-- changelog.md | 4 ++++ setup.py | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 47af42d2..724f6160 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,14 @@ This version supports Python2, exists an alternative version compatible with Pyt #### Warning #### -`Please if you are using python-saml < v2.1.6. Update it! -v2.1.6 includes a security patch that will prevent signature wrapping attacks, older versions are vulnerable.` :exclamation: +Update python-saml to 2.1.9, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. + +python-saml < v2.1.6 is vulnerable and allows signature wrapping! + + +#### Security Guidelines #### + +If you believe you have discovered a security vulnerability in this toolkit, please report it at https://www.onelogin.com/security with a description. We follow responsible disclosure guidelines, and will work with you to quickly find a resolution. Why add SAML support to my software? diff --git a/changelog.md b/changelog.md index 132e4db4..4e518024 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # python-saml changelog +### 2.1.9 (Jun 27, 2016) +* Change the decrypt assertion process. +* Add 2 extra validations to prevent Signature wrapping attacks. + ### 2.1.8 (Jun 02, 2016) * Fix Metadata XML (RequestedAttribute) * Fix Windows specific Unix date formatting bug. diff --git a/setup.py b/setup.py index 609941f6..e38926a1 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.1.8', + version='2.1.9', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', From e01b1462715ac105bb6132e570be6134c26458dc Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 28 Jun 2016 21:05:35 +0200 Subject: [PATCH 136/352] Document the wantAssertionsEncrypted parameter --- README.md | 4 ++++ demo-bottle/saml/advanced_settings.json | 3 ++- demo-django/saml/advanced_settings.json | 1 + demo-flask/saml/advanced_settings.json | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 724f6160..fe1c9069 100644 --- a/README.md +++ b/README.md @@ -360,6 +360,10 @@ In addition to the required settings data (idp, sp), there is extra information // and elements received by this SP to be signed. "wantMessagesSigned": false, + // this SP to be encrypted. + 'wantAssertionsEncrypted' => false, + + // Indicates a requirement for the elements received by // Indicates a requirement for the elements received by // this SP to be signed. [Metadata of the SP will offer this info] "wantAssertionsSigned": false, diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json index 78e5662c..5b9396ff 100644 --- a/demo-bottle/saml/advanced_settings.json +++ b/demo-bottle/saml/advanced_settings.json @@ -9,7 +9,8 @@ "wantAssertionsSigned": false, "wantNameId" : true, "wantNameIdEncrypted": false, - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + "wantAssertionsEncrypted": false, + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, "contactPerson": { "technical": { diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json index 04bd0fba..ed0ba4ab 100644 --- a/demo-django/saml/advanced_settings.json +++ b/demo-django/saml/advanced_settings.json @@ -9,6 +9,7 @@ "wantAssertionsSigned": false, "wantNameId" : true, "wantNameIdEncrypted": false, + "wantAssertionsEncrypted": false, "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, "contactPerson": { diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json index 04bd0fba..ed0ba4ab 100644 --- a/demo-flask/saml/advanced_settings.json +++ b/demo-flask/saml/advanced_settings.json @@ -9,6 +9,7 @@ "wantAssertionsSigned": false, "wantNameId" : true, "wantNameIdEncrypted": false, + "wantAssertionsEncrypted": false, "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, "contactPerson": { From 3ade98c643fc5eabe73de8aac2452feee0377c6a Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 28 Jun 2016 21:07:49 +0200 Subject: [PATCH 137/352] typo --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fe1c9069..d49b922f 100644 --- a/README.md +++ b/README.md @@ -360,14 +360,14 @@ In addition to the required settings data (idp, sp), there is extra information // and elements received by this SP to be signed. "wantMessagesSigned": false, - // this SP to be encrypted. - 'wantAssertionsEncrypted' => false, - - // Indicates a requirement for the elements received by // Indicates a requirement for the elements received by // this SP to be signed. [Metadata of the SP will offer this info] "wantAssertionsSigned": false, + // Indicates a requirement for the + // elements received by this SP to be encrypted. + 'wantAssertionsEncrypted' => false, + // Indicates a requirement for the NameID element on the SAMLResponse // received by this SP to be present. "wantNameId": true, From 040a4a69a68139b867858d226c008d539c174c22 Mon Sep 17 00:00:00 2001 From: coolbootscoder Date: Sat, 2 Jul 2016 18:54:58 -0400 Subject: [PATCH 138/352] PEP8 changes Added a new line for PIP8 requirement. --- src/onelogin/saml2/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 665a215d..c93f1da1 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -38,6 +38,7 @@ xmlsec.initialize() globals()['xmlsec_setup'] = True + def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg): """ Auxiliary method. It override the default xmlsec debug message. From 0ca31c7c73670eede6d197318af175eaab770dbd Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Sun, 3 Jul 2016 10:45:25 -0400 Subject: [PATCH 139/352] Fix some spelling and grammar issues in README.md Also remove trailing whitespace. --- README.md | 100 +++++++++++++++++++++++++++--------------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index d49b922f..642a131a 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ ![Python versions](https://img.shields.io/pypi/pyversions/python-saml.svg) Add SAML support to your Python software using this library. -Forget those complicated libraries and use that open source library provided +Forget those complicated libraries and use the open source library provided and supported by OneLogin Inc. -This version supports Python2, exists an alternative version compatible with Python 3: [python3-saml](https://github.com/onelogin/python3-saml) +This version supports Python2. There is a separate version that supports +Python3: [python3-saml](https://github.com/onelogin/python3-saml). #### Warning #### @@ -18,20 +19,18 @@ Update python-saml to 2.1.9, this version includes a security patch that contain python-saml < v2.1.6 is vulnerable and allows signature wrapping! - #### Security Guidelines #### If you believe you have discovered a security vulnerability in this toolkit, please report it at https://www.onelogin.com/security with a description. We follow responsible disclosure guidelines, and will work with you to quickly find a resolution. - Why add SAML support to my software? ------------------------------------ SAML is an XML-based standard for web browser single sign-on and is defined by -the OASIS Security Services Technical Committee. The standard has been around +the OASIS Security Services Technical Committee. The standard has been around since 2002, but lately it is becoming popular due its advantages: - * **Usability** - One-click access from portals or intranets, deep linking, + * **Usability** - One-click access from portals or intranets, deep linking, password elimination and automatically renewing sessions make life easier for the user. * **Security** - Based on strong digital signatures for authentication and @@ -44,21 +43,21 @@ since 2002, but lately it is becoming popular due its advantages: * **IT Friendly** - SAML simplifies life for IT because it centralizes authentication, provides greater visibility and makes directory integration easier. - * **Opportunity** - B2B cloud vendor should support SAML to facilitate the + * **Opportunity** - B2B cloud vendor should support SAML to facilitate the integration of their product. General description ------------------- -OneLogin's SAML Python toolkit let you build a SP (Service Provider) over -your Python application and connect it to any IdP (Identity Provider). +OneLogin's SAML Python toolkit lets you turn you Python application into an SP +(Service Provider) that can connect to a IdP (Identity Provider). Supports: * SSO and SLO (SP-Initiated and IdP-Initiated). * Assertion and nameId encryption. - * Assertion signature. - * Message signature: AuthNRequest, LogoutRequest, LogoutResponses. + * Assertion signatures. + * Message signatures: AuthNRequest, LogoutRequest, LogoutResponses. * Enable an Assertion Consumer Service endpoint. * Enable a Single Logout Service endpoint. * Publish the SP metadata (which can be signed). @@ -68,7 +67,7 @@ Key features: * **saml2int** - Implements the SAML 2.0 Web Browser SSO Profile. * **Session-less** - Forget those common conflicts between the SP and the final app, the toolkit delegate session in the final app. - * **Easy to use** - Programmer will be allowed to code high-level and + * **Easy to use** - Programmer will be allowed to code high-level and low-level programming, 2 easy to use APIs are available. * **Tested** - Thoroughly tested. * **Popular** - OneLogin's customers use it. Add easy support to your django/flask/bottle web projects. @@ -77,7 +76,7 @@ Key features: Installation ------------ -### Dependences ### +### Dependencies ### * python 2.7 * [dm.xmlsec.binding](https://pypi.python.org/pypi/dm.xmlsec.binding) Cython/lxml based binding for the XML security library (depends on python-dev libxml2-dev libxmlsec1-dev) @@ -87,13 +86,14 @@ Installation Review the setup.py file to know the version of the library that python-saml is using -### OSX Dependences ### +### OSX Dependencies ### + * python 2.7 * libxmlsec1 - + ```sh - # using brew - brew install libxmlsec1 +# using brew +$ brew install libxmlsec1 ``` @@ -106,7 +106,7 @@ The toolkit is hosted on github. You can download it from: * Lastest release: https://github.com/onelogin/python-saml/releases/latest * Master repo: https://github.com/onelogin/python-saml/tree/master -Copy the core of the library (src/onelogin/saml2 folder) and merge the setup.py inside the python application. (each application has its structure so take your time to locate the Python SAML toolkit in the best place). +Copy the core of the library (src/onelogin/saml2 folder) and merge the setup.py inside the python application. (each application has its structure so take your time to locate the Python SAML toolkit in the best place). #### Option 2. Download from pypi #### @@ -114,7 +114,7 @@ The toolkit is hosted in pypi, you can find the python-saml package at https://p You can install it executing: ``` - pip install python-saml +$ pip install python-saml ``` If you want to know how a project can handle python packages review this [guide](https://packaging.python.org/en/latest/tutorial.html) and review this [sampleproject](https://github.com/pypa/sampleproject) @@ -123,7 +123,7 @@ If you want to know how a project can handle python packages review this [guide] Security warning ---------------- -In production, the **strict** parameter MUST be set as **"true"**. Otherwise +In production, the **strict** parameter MUST be set as **"true"**. Otherwise your environment is not secure and will be exposed to attacks. @@ -217,7 +217,7 @@ This is the settings.json file: ```javascript { - // If strict is True, then the Python Toolkit will reject unsigned + // If strict is True, then the Python Toolkit will reject unsigned // or unencrypted messages if it expects them to be signed or encrypted. // Also it will reject the messages if the SAML standard is not strictly // followed. Destination, NameId, Conditions ... are validated too. @@ -236,11 +236,11 @@ This is the settings.json file: // URL Location where the from the IdP will be returned "url": "https:///?acs", // SAML protocol binding to be used when returning the - // message. OneLogin Toolkit supports this endpoint for the + // message. OneLogin Toolkit supports this endpoint for the // HTTP-POST binding only. "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, - // If you need to specify requested attributes, set a + // If you need to specify requested attributes, set a // attributeConsumingService. nameFormat, attributeValue and // friendlyName can be omitted "attributeConsumingService": { @@ -257,7 +257,7 @@ This is the settings.json file: ] }, // Specifies info about where and how the message MUST be - // returned to the requester, in this case our SP. + // returned to the requester, in this case our SP. "singleLogoutService": { // URL Location where the from the IdP will be returned "url": "https:///?sls", @@ -282,7 +282,7 @@ This is the settings.json file: "entityId": "https://app.onelogin.com/saml/metadata/", // SSO endpoint info of the IdP. (Authentication Request protocol) "singleSignOnService": { - // URL Target of the IdP where the Authentication Request Message + // URL Target of the IdP where the Authentication Request Message // will be sent. "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", // SAML protocol binding to be used when returning the @@ -302,16 +302,16 @@ This is the settings.json file: // Public x509 certificate of the IdP "x509cert": "" /* - * Instead of use the whole x509cert you can use a fingerprint in order to + * Instead of using the whole x509cert you can use a fingerprint in order to * validate a SAMLResponse, but you will need it to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding. - * + * * (openssl x509 -noout -fingerprint -in "idp.crt" to generate it, * or add for example the -sha256 , -sha384 or -sha512 parameter) * * If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to * let the toolkit know which algorithm was used. Possible values: sha1, sha256, sha384 or sha512 * 'sha1' is the default value. - * + * * Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you * will need to provide the whole x509cert. */ @@ -321,7 +321,7 @@ This is the settings.json file: } ``` -In addition to the required settings data (idp, sp), there is extra information that could be defined at advanced_settings.json +In addition to the required settings data (idp, sp), extra settings can be defined in `advanced_settings.json`: ```javascript { @@ -334,15 +334,15 @@ In addition to the required settings data (idp, sp), there is extra information // will be encrypted. "nameIdEncrypted": false, - // Indicates whether the messages sent by this SP + // Indicates whether the messages sent by this SP // will be signed. [Metadata of the SP will offer this info] "authnRequestsSigned": false, - // Indicates whether the messages sent by this SP + // Indicates whether the messages sent by this SP // will be signed. "logoutRequestSigned": false, - // Indicates whether the messages sent by this SP + // Indicates whether the messages sent by this SP // will be signed. "logoutResponseSigned": false, @@ -368,7 +368,7 @@ In addition to the required settings data (idp, sp), there is extra information // elements received by this SP to be encrypted. 'wantAssertionsEncrypted' => false, - // Indicates a requirement for the NameID element on the SAMLResponse + // Indicates a requirement for the NameID element on the SAMLResponse // received by this SP to be present. "wantNameId": true, @@ -428,7 +428,7 @@ In addition to the required settings data (idp, sp), there is extra information } ``` -In the security section, you can set the way that the SP will handle the messages and assertions. Contact the admin of the IdP and ask him what the IdP expects, and decide what validations will handle the SP and what requirements the SP will have and communicate them to the IdP's admin too. +In the security section, you can set the way that the SP will handle the messages and assertions. Contact the admin of the IdP and ask them what the IdP expects, and decide what validations will handle the SP and what requirements the SP will have and communicate them to the IdP's admin too. Once we know what kind of data could be configured, let's talk about the way settings are handled within the toolkit. @@ -557,7 +557,7 @@ 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. +Related to the SP there are 3 important endpoints: The metadata view, the ACS view and the SLS view. The toolkit provides examples of those views in the demos, but lets see an example. ***SP Metadata*** @@ -580,7 +580,7 @@ The get_sp_metadata will return the metadata signed or not based on the security Before the XML metadata is exposed, a check takes place to ensure that the info to be provided is valid. -Instead of use the Auth object, you can directly use +Instead of using the Auth object, you can directly use ``` saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp_validation_only=True) ``` @@ -598,7 +598,7 @@ errors = auth.get_errors() if not errors: if auth.is_authenticated(): request.session['samlUserdata'] = auth.get_attributes() - if 'RelayState' in req['post_data'] and + if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: auth.redirect_to(req['post_data']['RelayState']) else: @@ -636,7 +636,7 @@ If we execute print attributes we could get: "mail": ["Doe"], "groups": ["users", "members"] } -``` +``` Each attribute name can be used as a key to obtain the value. Every attribute is a list of values. A single-valued attribute is a listy of a single element. @@ -679,7 +679,7 @@ if not logout_response.is_valid(self.__request_data, request_id): elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS: self.__errors.append('logout_not_success') elif not keep_local_session: - OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) + OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) ``` If the SLS endpoints receives an Logout Request, the request is validated, the session is closed and a Logout Response is sent to the SLS endpoint of the IdP. @@ -723,7 +723,7 @@ In order to send a Logout Request to the IdP: The Logout Request will be sent signed or unsigned based on the security info of the advanced_settings.json ('logoutRequestSigned'). -The IdP will return the Logout Response through the user's client to the Single Logout Service of the SP. +The IdP will return the Logout Response through the user's client to the Single Logout Service of the SP. We can set a 'return_to' url parameter to the logout function and that will be converted as a 'RelayState' parameter: @@ -734,7 +734,7 @@ auth.logout(return_to=target_url) Also there are 2 optional parameters that can be set: -* name_id. That will be used to build the LogoutRequest. If not name_id parameter is set and the auth object processed a +* name_id. That will be used to build the LogoutRequest. If not name_id parameter is set and the auth object processed a SAML Response with a NameId, then this NameId will be used. * session_index. SessionIndex that identifies the session of the user. @@ -758,7 +758,7 @@ auth = OneLogin_Saml2_Auth(req) # Initialize the SP SAML instance if 'sso' in request.args: # SSO action (SP-SSO initited). Will send an AuthNRequest to the IdP return redirect(auth.login()) -elif 'sso2' in request.args: # Another SSO init action +elif 'sso2' in request.args: # Another SSO init action return_to = '%sattrs/' % request.host_url # but set a custom RelayState URL return redirect(auth.login(return_to)) elif 'slo' in request.args: # SLO action. Will sent a Logout Request to IdP @@ -766,13 +766,13 @@ elif 'slo' in request.args: # SLO action. Will sent a Logout elif 'acs' in request.args: # Assertion Consumer Service auth.process_response() # Process the Response of the IdP errors = auth.get_errors() # This method receives an array with the errors - if len(errors) == 0: # that could took place during the process + if len(errors) == 0: # that could took place during the process if not auth.is_authenticated(): # This check if the response was ok and the user msg = "Not authenticated" # data retrieved or not (user authenticated) else: request.session['samlUserdata'] = auth.get_attributes() # Retrieves user data self_url = OneLogin_Saml2_Utils.get_self_url(req) - if 'RelayState' in request.form and self_url != request.form['RelayState']: + if 'RelayState' in request.form and self_url != request.form['RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) # Redirect if there is a relayState else: # If there is user data we save that to print it later. msg = '' @@ -875,7 +875,7 @@ SAML 2 Logout Response class * ***get_status*** Gets the Status of the Logout Response. * ***is_valid*** Determines if the SAML LogoutResponse is valid * ***build*** Creates a Logout Response object. -* ***get_response*** Returns 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. @@ -915,7 +915,7 @@ Configuration of the OneLogin Python Toolkit A class that contains functionality related to the metadata of the SP -* ***builder*** Generates the metadata of the SP based on the settings. +* ***builder*** Generates the metadata of the SP based on the settings. * ***sign_metadata*** Signs the metadata with the key/cert provided. * ***add_x509_key_descriptors*** Adds the x509 descriptors (sign/encriptation) to the metadata @@ -1039,7 +1039,7 @@ The flask project contains: ####SP setup#### -The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-flask it used the first method. +The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-flask it used the first method. In the index.py file we define the app.config['SAML_PATH'], that will target to the 'saml' folder. We require it in order to load the settings files. @@ -1079,7 +1079,7 @@ To run the demo you need to install the requirements first. Load your virtualenv and execute: ``` pip install -r demo-django/requirements.txt -``` +``` This will install django and its dependences. Once it has finished, you have to complete the configuration of the toolkit. Later, with the virtualenv loaded, you can run the demo like this: @@ -1112,9 +1112,9 @@ The django project contains: ####SP setup#### -The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-django it used the first method. +The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-django it used the first method. -After set the SAML_FOLDER in the demo/settings.py, the settings of the python toolkit will be loaded on the django web. +After set the SAML_FOLDER in the demo/settings.py, the settings of the python toolkit will be loaded on the django web. First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. From 543f2af063972f4561dc77b417a3f6abec6f323b Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Sun, 3 Jul 2016 10:41:53 -0400 Subject: [PATCH 140/352] Fix crash in flask demo error handling --- demo-flask/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-flask/index.py b/demo-flask/index.py index 9dee0bc8..ac2c1320 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -121,7 +121,7 @@ def metadata(): resp = make_response(metadata, 200) resp.headers['Content-Type'] = 'text/xml' else: - resp = make_response(errors.join(', '), 500) + resp = make_response(', '.join(errors), 500) return resp From 40bae020d49be8be2941b3a839f149c4891b37f8 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Tue, 5 Jul 2016 11:32:55 -0400 Subject: [PATCH 141/352] Update LICENSE to include MIT rather than BSD license Apparently the MIT license was indented for this project as per the setup.py file but somehow a copy of the 3-clause BSD license was included in the LICENSE file. --- LICENSE | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/LICENSE b/LICENSE index 5504f2bb..c0826de8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,24 +1,23 @@ Copyright (c) 2011-2014, OneLogin, Inc. -All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL ONELOGIN, INC. BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 3c77f552594f39fcf34c9a95ce8f7dccd93b8fcc Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 7 Jul 2016 17:13:23 +0200 Subject: [PATCH 142/352] Update copyright on LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index c0826de8..dbbca9c6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2014, OneLogin, Inc. +Copyright (c) 2010-2016 OneLogin, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation From e9165c8e757c22ed281de95d2c9cb61732d8e695 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 19 Jul 2016 16:53:29 +0200 Subject: [PATCH 143/352] Remove unnecesary comments --- src/onelogin/saml2/auth.py | 4 ---- src/onelogin/saml2/utils.py | 14 -------------- 2 files changed, 18 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 0cbeeaa7..f613f0b8 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -14,8 +14,6 @@ from base64 import b64encode from urllib import quote_plus -# import dm.xmlsec.binding as xmlsec - from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.response import OneLogin_Saml2_Response from onelogin.saml2.errors import OneLogin_Saml2_Error @@ -433,8 +431,6 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND ) - # xmlsec.initialize() # already done in utils module - dsig_ctx = xmlsec.DSigCtx() dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 77ee75cf..f04046b7 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -630,8 +630,6 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None): xml = name_id_container.toxml() elem = fromstring(xml) - # xmlsec.initialize('openssl') # done when the module is loaded - if debug: xmlsec.set_error_callback(print_xmlsec_errors) @@ -739,8 +737,6 @@ def decrypt_element(encrypted_data, key, debug=False): elif isinstance(encrypted_data, basestring): encrypted_data = fromstring(str(encrypted_data)) - # xmlsec.initialize() # Initialized at the start of the module - if debug: xmlsec.set_error_callback(print_xmlsec_errors) @@ -814,8 +810,6 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant else: raise Exception('Error parsing xml string') - # xmlsec.initialize() # Initialized at the start of the module - if debug: xmlsec.set_error_callback(print_xmlsec_errors) @@ -920,8 +914,6 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid else: raise Exception('Error parsing xml string') - # xmlsec.initialize() # Initialized at the start of the module - if debug: xmlsec.set_error_callback(print_xmlsec_errors) @@ -985,8 +977,6 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha else: raise Exception('Error parsing xml string') - # xmlsec.initialize() # Initialized at the start of the module - if debug: xmlsec.set_error_callback(print_xmlsec_errors) @@ -1038,8 +1028,6 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger :type: bool """ try: - # xmlsec.initialize() # Initialized at the start of the module - if debug: xmlsec.set_error_callback(print_xmlsec_errors) @@ -1103,8 +1091,6 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ :type: bool """ try: - # xmlsec.initialize() # Initialized at the start of the module - if debug: xmlsec.set_error_callback(print_xmlsec_errors) From b1d20e7f855c567be37408478c5cc25ffa0717d2 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 27 Jul 2016 10:40:43 +0200 Subject: [PATCH 144/352] Relax isodate requirement --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e38926a1..e157f95a 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ test_suite='tests', install_requires=[ 'dm.xmlsec.binding==1.3.2', - 'isodate==0.5.0', + 'isodate>=0.5.0', 'defusedxml==0.4.1', ], extras_require={ From 393f015859c97015e1f24a86c45ceb5826088ab1 Mon Sep 17 00:00:00 2001 From: Oluwafemi Sule Date: Sun, 31 Jul 2016 11:33:30 +0100 Subject: [PATCH 145/352] Fix typographical errors in docstring --- src/onelogin/saml2/auth.py | 24 +++++++++++------------ src/onelogin/saml2/authn_request.py | 6 +++--- src/onelogin/saml2/idp_metadata_parser.py | 6 +++--- src/onelogin/saml2/logout_request.py | 6 +++--- src/onelogin/saml2/logout_response.py | 4 ++-- src/onelogin/saml2/metadata.py | 2 +- src/onelogin/saml2/response.py | 2 +- src/onelogin/saml2/settings.py | 2 +- src/onelogin/saml2/utils.py | 24 +++++++++++------------ 9 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index f613f0b8..1240c738 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -30,7 +30,7 @@ class OneLogin_Saml2_Auth(object): This class implements the SP SAML instance. Defines the methods that you can invoke in your application in - order to add SAML support (initiates sso, initiates slo, processes a + order to add SAML support (initiates SSO, initiates SLO, processes a SAML Response, a Logout Request or a Logout Response). """ @@ -80,7 +80,7 @@ def process_response(self, request_id=None): """ Process the SAML Response sent by the IdP. - :param request_id: Is an optional argumen. Is the ID of the AuthNRequest sent by this SP to the IdP. + :param request_id: Is an optional argument. Is the ID of the AuthNRequest sent by this SP to the IdP. :type request_id: string :raises: OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND, when a POST with a SAMLResponse is not found @@ -119,7 +119,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string - :returns: Redirection url + :returns: Redirection URL """ self.__errors = [] @@ -168,14 +168,14 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ def redirect_to(self, url=None, parameters={}): """ - Redirects the user to the url past by parameter or to the url that we defined in our SSO Request. + Redirects the user to the URL passed by parameter or to the URL that we defined in our SSO Request. :param url: The target URL to redirect the user :type url: string - :param parameters: Extra parameters to be passed as part of the url + :param parameters: Extra parameters to be passed as part of the URL :type parameters: dict - :returns: Redirection url + :returns: Redirection URL """ if url is None and 'RelayState' in self.__request_data['get_data']: url = self.__request_data['get_data']['RelayState'] @@ -272,16 +272,16 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_ :param return_to: Optional argument. The target URL the user should be redirected to after login. :type return_to: string - :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'. + :param force_authn: Optional argument. When true the AuthNRequest will set the ForceAuthn='true'. :type force_authn: bool - :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. + :param is_passive: Optional argument. When true the AuthNRequest will set the Ispassive='true'. :type is_passive: bool - :param set_nameid_policy: Optional argument. When true the AuthNReuqest will set a nameIdPolicy element. + :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element. :type set_nameid_policy: bool - :returns: Redirection url + :returns: Redirection URL :rtype: string """ authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy) @@ -355,7 +355,7 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None): def get_sso_url(self): """ - Gets the SSO url. + Gets the SSO URL. :returns: An URL, the SSO endpoint of the IdP :rtype: string @@ -365,7 +365,7 @@ def get_sso_url(self): def get_slo_url(self): """ - Gets the SLO url. + Gets the SLO URL. :returns: An URL, the SLO endpoint of the IdP :rtype: string diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index ea0200e8..25162c51 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -29,13 +29,13 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol :param settings: OSetting data :type return_to: OneLogin_Saml2_Settings - :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'. + :param force_authn: Optional argument. When true the AuthNRequest will set the ForceAuthn='true'. :type force_authn: bool - :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. + :param is_passive: Optional argument. When true the AuthNRequest will set the Ispassive='true'. :type is_passive: bool - :param set_nameid_policy: Optional argument. When true the AuthNReuqest will set a nameIdPolicy element. + :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element. :type set_nameid_policy: bool """ self.__settings = settings diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 44c3d4d5..80b38ec1 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -20,13 +20,13 @@ class OneLogin_Saml2_IdPMetadataParser(object): """ - A class that contains methods related to obtain and parse metadata from IdP + A class that contain methods related to obtaining and parsing metadata from IdP """ @staticmethod def get_metadata(url): """ - Get the metadata XML from the provided URL + Gets the metadata XML from the provided URL :param url: Url where the XML of the Identity Provider Metadata is published. :type url: string @@ -55,7 +55,7 @@ def get_metadata(url): @staticmethod def parse_remote(url, **kwargs): """ - Get the metadata XML from the provided URL and parse it, returning a dict with extracted data + Gets the metadata XML from the provided URL and parse it, returning a dict with extracted data :param url: Url where the XML of the Identity Provider Metadata is published. :type url: string diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index dc519cec..ab79be0d 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -118,7 +118,7 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= def get_request(self, deflate=True): """ - Returns the Logout Request defated, base64encoded + Returns the Logout Request deflated, base64encoded :param deflate: It makes the deflate process optional :type: bool :return: Logout Request maybe deflated and base64 encoded @@ -253,7 +253,7 @@ def get_session_indexes(request): def is_valid(self, request_data): """ - Checks if the Logout Request recieved is valid + Checks if the Logout Request received is valid :param request_data: Request Data :type request_data: dict @@ -343,6 +343,6 @@ def is_valid(self, request_data): def get_error(self): """ - After execute a validation process, if fails this method returns the cause + After executing a validation process, if it fails this method returns the cause """ return self.__error diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index a0f6f25d..5420ebbe 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -93,7 +93,7 @@ def is_valid(self, request_data, request_id=None): security = self.__settings.get_security_data() - # Check if the InResponseTo of the Logout Response matchs the ID of the Logout Request (requestId) if provided + # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'): in_response_to = self.document.documentElement.getAttribute('InResponseTo') if request_id != in_response_to: @@ -207,6 +207,6 @@ def get_response(self, deflate=True): def get_error(self): """ - After execute a validation process, if fails this method returns the cause + After executing a validation process, if it fails this method returns the cause """ return self.__error diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index c8c40b95..8a2ddf1c 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -220,7 +220,7 @@ def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.R @staticmethod def add_x509_key_descriptors(metadata, cert=None): """ - Adds the x509 descriptors (sign/encriptation) to the metadata + Adds the x509 descriptors (sign/encryption) to the metadata The same cert will be used for sign/encrypt :param metadata: SAML Metadata XML diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index de94228b..61224b16 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -546,6 +546,6 @@ def __decrypt_assertion(self, dom): def get_error(self): """ - After execute a validation process, if fails this method returns the cause + After executing a validation process, if it fails this method returns the cause """ return self.__error diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 4470cc91..8e3b151a 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -112,7 +112,7 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals def __load_paths(self, base_path=None): """ - Sets the paths of the different folders + Set the paths of the different folders """ if base_path is None: base_path = dirname(dirname(dirname(abspath(__file__)))) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index f04046b7..c1d0eff6 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -41,7 +41,7 @@ def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg): """ - Auxiliary method. It override the default xmlsec debug message. + Auxiliary method. It overrides the default xmlsec debug message. """ info = [] @@ -80,7 +80,7 @@ def decode_base64_and_inflate(value): @staticmethod def deflate_and_base64_encode(value): """ - Deflates and the base64 encodes a string + Deflates and then base64 encodes a string :param value: The string to deflate and encode :type value: string :returns: The deflated and encoded string @@ -137,13 +137,13 @@ def format_cert(cert, heads=True): """ Returns a x509 cert (adding header & footer if required). - :param cert: A x509 unformated cert + :param cert: A x509 unformatted cert :type: string :param heads: True if we want to include head and footer :type: boolean - :returns: Formated cert + :returns: Formatted cert :rtype: string """ x509_cert = cert.replace('\x0D', '') @@ -170,7 +170,7 @@ def format_private_key(key, heads=True): :param heads: True if we want to include head and footer :type: boolean - :returns: Formated private key + :returns: Formatted private key :rtype: string """ private_key = key.replace('\x0D', '') @@ -576,12 +576,12 @@ def calculate_x509_fingerprint(x509_cert, alg='sha1'): @staticmethod def format_finger_print(fingerprint): """ - Formates a fingerprint. + Formats a fingerprint. :param fingerprint: fingerprint :type: string - :returns: Formated fingerprint + :returns: Formatted fingerprint :rtype: string """ formated_fingerprint = fingerprint.replace(':', '') @@ -1012,7 +1012,7 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger :param xml: The element we should validate :type: Document - :param cert: The pubic cert + :param cert: The public cert :type: string :param fingerprint: The fingerprint of the public cert @@ -1072,7 +1072,7 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger @staticmethod def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False): """ - Validates signed bynary data (Used to validate GET Signature). + Validates signed binary data (Used to validate GET Signature). :param signed_query: The element we should validate :type: string @@ -1081,7 +1081,7 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ :param signature: The signature that will be validate :type: string - :param cert: The pubic cert + :param cert: The public cert :type: string :param algorithm: Signature algorithm @@ -1117,8 +1117,8 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ @staticmethod def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False): - """Return an url encoded get parameter value - Prefer to extract the original encoded value directly from query_string since url + """Return a URL encoded get parameter value + Prefer to extract the original encoded value directly from query_string since URL encoding is not canonical. The encoding used by ADFS 3.0 is not compatible with python's quote_plus (ADFS produces lower case hex numbers and quote_plus produces upper case hex numbers) From 9ffd521537e012ee428a0724a2fdbc7e440784b7 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 4 Aug 2016 17:16:45 +0200 Subject: [PATCH 146/352] Fix __build_signature method. If relay_state is null not be part of the SignQuery --- src/onelogin/saml2/auth.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 1240c738..648e9868 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -150,8 +150,8 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ parameters = {'SAMLResponse': logout_response} if 'RelayState' in self.__request_data['get_data']: parameters['RelayState'] = self.__request_data['get_data']['RelayState'] - else: - parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) + # else: + # parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) security = self.__settings.get_security_data() if 'logoutResponseSigned' in security and security['logoutResponseSigned']: @@ -434,12 +434,10 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On dsig_ctx = xmlsec.DSigCtx() dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) - saml_data_str = '%s=%s' % (saml_type, quote_plus(saml_data)) - relay_state_str = 'RelayState=%s' % quote_plus(relay_state) - alg_str = 'SigAlg=%s' % quote_plus(sign_algorithm) - - sign_data = [saml_data_str, relay_state_str, alg_str] - msg = '&'.join(sign_data) + msg = '%s=%s' % (saml_type, quote_plus(saml_data)) + if relay_state is not None: + msg += '&RelayState=%s' % quote_plus(relay_state) + msg += '&SigAlg=%s' % quote_plus(sign_algorithm) # Sign the metadata with our private key. sign_algorithm_transform_map = { From 5674f9073e1b50c762b7dafbdc7f2fb1b657b712 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 6 Aug 2016 16:26:01 +0200 Subject: [PATCH 147/352] Remove download stats from README (not working anymore) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 642a131a..7ac999c9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Build Status](https://api.travis-ci.org/onelogin/python-saml.png?branch=master)](http://travis-ci.org/onelogin/python-saml) [![Coverage Status](https://coveralls.io/repos/onelogin/python-saml/badge.png)](https://coveralls.io/r/onelogin/python-saml) [![PyPi Version](https://img.shields.io/pypi/v/python-saml.svg)](https://pypi.python.org/pypi/python-saml) -![PyPi Downloads](https://img.shields.io/pypi/dm/python-saml.svg) ![Python versions](https://img.shields.io/pypi/pyversions/python-saml.svg) Add SAML support to your Python software using this library. From 87d4c18865e4997061ec62fd0e8d1e070b92e4e7 Mon Sep 17 00:00:00 2001 From: Jesse Shapiro Date: Tue, 23 Aug 2016 13:41:11 -0700 Subject: [PATCH 148/352] Adding hooks to retrieve most recent decrypted and sent documents; updating readme and adding tests --- README.md | 2 ++ src/onelogin/saml2/auth.py | 27 ++++++++++++++++-- src/onelogin/saml2/authn_request.py | 8 ++++++ src/onelogin/saml2/response.py | 12 ++++++++ ...d_valid_encrypted_assertion.xml.base64.xml | 7 +++++ ...d_valid_encrypted_assertion.xml.base64.xml | 7 +++++ tests/src/OneLogin/saml2_tests/auth_test.py | 27 ++++++++++++++++++ .../saml2_tests/authn_request_test.py | 28 +++++++++++++++++++ .../src/OneLogin/saml2_tests/response_test.py | 15 +++++++++- 9 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.xml create mode 100644 tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml.base64.xml diff --git a/README.md b/README.md index 7ac999c9..68a1e9fc 100644 --- a/README.md +++ b/README.md @@ -823,6 +823,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 XML request +* ***get_last_response_xml*** Returns the most recently-decrypted XML response ####OneLogin_Saml2_Auth - authn_request.py#### diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 648e9868..e4693a7d 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -13,6 +13,7 @@ from base64 import b64encode from urllib import quote_plus +from lxml import etree from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.response import OneLogin_Saml2_Response @@ -57,6 +58,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_xml = None + self.__last_response_xml = None def get_settings(self): """ @@ -90,7 +93,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_xml = response.get_xml_document() if response.is_valid(self.__request_data, request_id): self.__attributes = response.get_attributes() self.__nameid = response.get_nameid() @@ -290,7 +293,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_ saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} - + self.__last_request_xml = authn_request.get_request_as_xml() if return_to is not None: parameters['RelayState'] = return_to else: @@ -451,3 +454,23 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On signature = dsig_ctx.signBinary(str(msg), sign_algorithm_transform) return b64encode(signature) + + def get_last_response_xml(self): + """ + Retrieves the decrypted XML of the last SAML response + + :returns: SAML response XML + :rtype: string|None + """ + if self.__last_response_xml: + return etree.tostring(self.__last_response_xml, pretty_print=True) + + def get_last_request_xml(self): + """ + Retrieves the raw XML sent in the last SAML request + + :returns: SAML request XML + :rtype: string|None + """ + if self.__last_request_xml: + return self.__last_request_xml diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 25162c51..3fa4d0f6 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -149,3 +149,11 @@ def get_id(self): :rtype: string """ return self.__id + + def get_request_as_xml(self): + """ + Return the XML document 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/response.py b/src/onelogin/saml2/response.py index 61224b16..d4703c67 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -549,3 +549,15 @@ 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/tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.xml b/tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.xml new file mode 100644 index 00000000..0237994f --- /dev/null +++ b/tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.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.base64.xml b/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml.base64.xml new file mode 100644 index 00000000..fbc5942f --- /dev/null +++ b/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml.base64.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/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 2d296e3c..ce987936 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -896,6 +896,33 @@ def testBuildResponseSignature(self): except Exception as e: self.assertIn("Trying to sign the SAMLResponse but can't load the SP private key", e.message) + def testGetLastDecryptedResponse(self): + settings = self.loadSettingsJSON() + 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', 'pretty_decrypted_valid_encrypted_assertion.xml.base64.xml')) + self.assertEqual(auth.get_last_response_xml(), decrypted_response) + + def testGetLastSentRequest(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' + ' >\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()) if __name__ == '__main__': if is_running_under_teamcity(): diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 4b241a56..4f298710 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -64,6 +64,34 @@ def testCreateRequest(self): self.assertRegexpMatches(inflated, '^ Date: Mon, 29 Aug 2016 09:47:43 -0400 Subject: [PATCH 149/352] Making certain methods raise exceptions when used inside application --- src/onelogin/saml2/response.py | 28 +- src/onelogin/saml2/utils.py | 281 ++++++++++--------- tests/src/OneLogin/saml2_tests/utils_test.py | 31 ++ 3 files changed, 203 insertions(+), 137 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 61224b16..3734502e 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -16,7 +16,7 @@ from xml.dom.minidom import Document 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 class OneLogin_Saml2_Response(object): @@ -90,13 +90,21 @@ def is_valid(self, request_data, request_id=None): if self.__settings.is_strict(): no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' - res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) + res = OneLogin_Saml2_Utils.validate_xml( + etree.tostring(self.document), + 'saml-schema-protocol-2.0.xsd', + self.__settings.is_debug_active() + ) if not isinstance(res, Document): raise Exception(no_valid_xml_msg) # If encrypted, check also the decrypted document if self.encrypted: - res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.decrypted_document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) + res = OneLogin_Saml2_Utils.validate_xml( + etree.tostring(self.decrypted_document), + 'saml-schema-protocol-2.0.xsd', + self.__settings.is_debug_active() + ) if not isinstance(res, Document): raise Exception(no_valid_xml_msg) @@ -123,8 +131,7 @@ def is_valid(self, request_data, request_id=None): raise Exception('There is no AttributeStatement on the Response') # Validates Assertion timestamps - if not self.validate_timestamps(): - raise Exception('Timing issues (please check your clock settings)') + self.validate_timestamps(raise_exceptions=True) encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute') if encrypted_attributes_nodes: @@ -212,8 +219,7 @@ def is_valid(self, request_data, request_id=None): document_to_validate = self.decrypted_document else: document_to_validate = self.document - if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg): - raise Exception('Signature validation failed. SAML Response rejected') + OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg, raise_exceptions=True) else: raise Exception('No Signature found. SAML Response rejected') @@ -435,10 +441,14 @@ def process_signed_elements(self): signed_elements.append(signed_element) return signed_elements + @return_false_on_exception def validate_timestamps(self): """ Verifies that the document is valid according to Conditions Element + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean + :returns: True if the condition is valid, False otherwise :rtype: bool """ @@ -448,9 +458,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 c1d0eff6..2610f3dc 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -25,6 +25,7 @@ from uuid import uuid4 from xml.dom.minidom import Document, Element from defusedxml.minidom import parseString +from functools import wraps import zlib @@ -39,6 +40,24 @@ globals()['xmlsec_setup'] = True +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 + + def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg): """ Auxiliary method. It overrides the default xmlsec debug message. @@ -866,6 +885,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant return newdoc.saveXML(newdoc.firstChild) @staticmethod + @return_false_on_exception def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): """ Validates a signature (Message or Assertion). @@ -887,53 +907,54 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid :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') - elif isinstance(xml, etree._Element): - elem = xml - elif isinstance(xml, Document): - xml = xml.toxml() - elem = fromstring(str(xml)) - elif isinstance(xml, Element): - xml.setAttributeNS( - unicode(OneLogin_Saml2_Constants.NS_SAMLP), - 'xmlns:samlp', - unicode(OneLogin_Saml2_Constants.NS_SAMLP) - ) - xml.setAttributeNS( - unicode(OneLogin_Saml2_Constants.NS_SAML), - 'xmlns:saml', - unicode(OneLogin_Saml2_Constants.NS_SAML) - ) - xml = xml.toxml() - elem = fromstring(str(xml)) - elif isinstance(xml, basestring): - elem = fromstring(str(xml)) - else: - raise Exception('Error parsing xml string') + if xml is None or xml == '': + raise Exception('Empty string supplied as input') + elif isinstance(xml, etree._Element): + elem = xml + elif isinstance(xml, Document): + xml = xml.toxml() + elem = fromstring(str(xml)) + elif isinstance(xml, Element): + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_SAMLP), + 'xmlns:samlp', + unicode(OneLogin_Saml2_Constants.NS_SAMLP) + ) + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_SAML), + 'xmlns:saml', + unicode(OneLogin_Saml2_Constants.NS_SAML) + ) + xml = xml.toxml() + elem = fromstring(str(xml)) + elif isinstance(xml, basestring): + elem = fromstring(str(xml)) + else: + raise Exception('Error parsing xml string') - if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + if debug: + xmlsec.set_error_callback(print_xmlsec_errors) - xmlsec.addIDs(elem, ["ID"]) + xmlsec.addIDs(elem, ["ID"]) - signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature') + signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature') - if not len(signature_nodes) > 0: - signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature') + if not len(signature_nodes) > 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature') - if len(signature_nodes) == 1: - signature_node = signature_nodes[0] + if len(signature_nodes) == 1: + signature_node = signature_nodes[0] - return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug) - else: - return False - except Exception: - return False + 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. @@ -955,53 +976,53 @@ 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') - elif isinstance(xml, etree._Element): - elem = xml - elif isinstance(xml, Document): - xml = xml.toxml() - elem = fromstring(str(xml)) - elif isinstance(xml, Element): - xml.setAttributeNS( - unicode(OneLogin_Saml2_Constants.NS_MD), - 'xmlns:md', - unicode(OneLogin_Saml2_Constants.NS_MD) - ) - xml = xml.toxml() - elem = fromstring(str(xml)) - elif isinstance(xml, basestring): - elem = fromstring(str(xml)) - else: - raise Exception('Error parsing xml string') + if xml is None or xml == '': + raise Exception('Empty string supplied as input') + elif isinstance(xml, etree._Element): + elem = xml + elif isinstance(xml, Document): + xml = xml.toxml() + elem = fromstring(str(xml)) + elif isinstance(xml, Element): + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_MD), + 'xmlns:md', + unicode(OneLogin_Saml2_Constants.NS_MD) + ) + xml = xml.toxml() + elem = fromstring(str(xml)) + elif isinstance(xml, basestring): + elem = fromstring(str(xml)) + else: + raise Exception('Error parsing xml string') - if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + if debug: + xmlsec.set_error_callback(print_xmlsec_errors) - xmlsec.addIDs(elem, ["ID"]) + xmlsec.addIDs(elem, ["ID"]) - signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature') + signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature') - if len(signature_nodes) == 0: - signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature') + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature') - if len(signature_nodes) == 0: - signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature') - signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature') + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature') + signature_nodes += OneLogin_Saml2_Utils.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: + 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. @@ -1026,50 +1047,54 @@ 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 debug: - xmlsec.set_error_callback(print_xmlsec_errors) + if debug: + xmlsec.set_error_callback(print_xmlsec_errors) - xmlsec.addIDs(elem, ["ID"]) + xmlsec.addIDs(elem, ["ID"]) - if (cert is None or cert == '') and fingerprint: - x509_certificate_nodes = OneLogin_Saml2_Utils.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 == '') and fingerprint: + x509_certificate_nodes = OneLogin_Saml2_Utils.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_Utils.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')) + # Check if Reference URI is empty + # reference_elem = OneLogin_Saml2_Utils.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 == '': - return False + if cert is None or cert == '': + raise Exception('Could not validate node signature: No certificate provided.') - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) - if validatecert: - mngr = xmlsec.KeysMngr() - mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted) - dsig_ctx = xmlsec.DSigCtx(mngr) - else: - dsig_ctx = xmlsec.DSigCtx() - dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + if validatecert: + mngr = xmlsec.KeysMngr() + mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted) + dsig_ctx = xmlsec.DSigCtx(mngr) + else: + dsig_ctx = xmlsec.DSigCtx() + dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) - file_cert.close() + file_cert.close() - dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) + dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) + try: dsig_ctx.verify(signature_node) - return True except Exception: - return False + raise Exception('Signature validation failed. SAML Response rejected') + return True @staticmethod + @return_false_on_exception def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False): """ Validates signed binary data (Used to validate GET Signature). @@ -1089,31 +1114,31 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ :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 debug: - xmlsec.set_error_callback(print_xmlsec_errors) + if debug: + xmlsec.set_error_callback(print_xmlsec_errors) - dsig_ctx = xmlsec.DSigCtx() + dsig_ctx = xmlsec.DSigCtx() - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) - dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) - file_cert.close() + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + file_cert.close() - # Sign the metadata with our private key. - sign_algorithm_transform_map = { - OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1, - OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1, - OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256, - OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384, - OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512 - } - sign_algorithm_transform = sign_algorithm_transform_map.get(algorithm, xmlsec.TransformRsaSha1) - - dsig_ctx.verifyBinary(signed_query, sign_algorithm_transform, signature) - return True - except Exception: - return False + # Sign the metadata with our private key. + sign_algorithm_transform_map = { + OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1, + OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1, + OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256, + OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384, + OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512 + } + sign_algorithm_transform = sign_algorithm_transform_map.get(algorithm, xmlsec.TransformRsaSha1) + + dsig_ctx.verifyBinary(signed_query, sign_algorithm_transform, signature) + return True @staticmethod def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False): diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index ebd221e8..08e617a1 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -856,11 +856,18 @@ def testValidateSign(self): except Exception as e: self.assertEqual('Error parsing xml string', e.message) + with self.assertRaisesRegexp(Exception, 'Empty string supplied as input'): + OneLogin_Saml2_Utils.validate_sign('', cert, raise_exceptions=True) + with self.assertRaisesRegexp(Exception, 'Error parsing xml string'): + OneLogin_Saml2_Utils.validate_sign(1, cert, raise_exceptions=True) + # expired cert xml_metadata_signed = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings1.xml')) self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True, raise_exceptions=True) xml_metadata_signed_2 = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings2.xml')) self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, cert_2)) @@ -872,6 +879,8 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True, raise_exceptions=True) # modified cert other_cert_path = join(dirname(__file__), '..', '..', '..', 'certs') @@ -880,6 +889,10 @@ def testValidateSign(self): f.close() self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x)) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, raise_exceptions=True) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True, raise_exceptions=True) xml_response_msg_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_message_response2.xml.base64'))) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed_2, cert_2)) @@ -893,6 +906,8 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True, raise_exceptions=True) xml_response_assert_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response2.xml.base64'))) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed_2, cert_2)) @@ -904,6 +919,8 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True, raise_exceptions=True) xml_response_double_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'double_signed_response2.xml.base64'))) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed_2, cert_2)) @@ -917,32 +934,46 @@ def testValidateSign(self): dom.firstChild.getAttributeNode('ID').nodeValue = u'_34fg27g212d63k1f923845324475802ac0fc24530b' # Reference validation failed self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom, cert_2)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(dom, cert_2, raise_exceptions=True) invalid_fingerprint = 'afe71c34ef740bc87434be13a2263d31271da1f9' # Wrong fingerprint self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint, raise_exceptions=True) dom_2 = parseString(xml_response_double_signed_2) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2)) dom_2.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp' # Modified message self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2, raise_exceptions=True) # Try to validate directly the Assertion dom_3 = parseString(xml_response_double_signed_2) assert_elem_3 = dom_3.firstChild.firstChild.nextSibling.nextSibling.nextSibling self.assertFalse(OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2, raise_exceptions=True) # Wrong scheme no_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_signature.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_signed, cert)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(no_signed, cert, raise_exceptions=True) no_key = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_key, cert)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(no_key, cert, raise_exceptions=True) # Signature Wrapping attack wrapping_attack1 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'signature_wrapping_attack.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert, raise_exceptions=True) if __name__ == '__main__': From 9289d2632dbcadcefa25bda9c981e39a67f28b2c Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 20 Sep 2016 12:14:04 +0200 Subject: [PATCH 150/352] Fix 157. Support multiple attributeValues on RequestedAttribute --- README.md | 2 +- src/onelogin/saml2/metadata.py | 25 +++++++++++-------- .../src/OneLogin/saml2_tests/metadata_test.py | 22 ++++++++++++++++ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7ac999c9..d4a43275 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ This is the settings.json file: "isRequired": false, "nameFormat": "", "friendlyName": "", - "attributeValue": "" + "attributeValue": [] } ] }, diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 8a2ddf1c..903b9548 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -77,7 +77,6 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N organization = {} str_attribute_consuming_service = '' - if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']): attr_cs_desc_str = '' if "serviceDescription" in sp['attributeConsumingService']: @@ -94,16 +93,22 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']: req_attr_nameformat_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName'] if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']: - req_attr_isrequired_str = " isRequired=\"%s\"" % 'true' if req_attribs['isRequired'] else 'false' + req_attr_isrequired_str = " isRequired=\"%s\"" % req_attribs['isRequired'] + if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']: - req_attr_aux_str = """ > - """ + + requested_attribute = """ """, metadata) + def testBuilderAttributeConsumingServiceWithMultipleAttributeValue(self): + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings5.json')) + sp_data = settings.get_sp_data() + security = settings.get_security_data() + organization = settings.get_organization() + contacts = settings.get_contacts() + + metadata = OneLogin_Saml2_Metadata.builder( + sp_data, security['authnRequestsSigned'], + security['wantAssertionsSigned'], None, None, contacts, + organization + ) + self.assertIn(""" + Test Service + Test Service + admin + + + """, metadata) + def testSignMetadata(self): """ Tests the signMetadata method of the OneLogin_Saml2_Metadata From b063d96937726f74ce6b19e26f0c888391133caf Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 20 Sep 2016 12:20:51 +0200 Subject: [PATCH 151/352] Fix pep8 --- src/onelogin/saml2/metadata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 903b9548..cb92bcf7 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -102,9 +102,9 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N for attrValue in req_attribs['attributeValue']: req_attr_aux_str += """ """ From 6415eca2b880f86be91bd78b13ee5a78cf2f4860 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 20 Sep 2016 18:35:08 +0200 Subject: [PATCH 152/352] Forgot the setting file --- tests/settings/settings5.json | 65 +++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/settings/settings5.json diff --git a/tests/settings/settings5.json b/tests/settings/settings5.json new file mode 100644 index 00000000..e399d217 --- /dev/null +++ b/tests/settings/settings5.json @@ -0,0 +1,65 @@ +{ + "strict": false, + "debug": false, + "custom_base_path": "../../../tests/data/customPath/", + "sp": { + "entityId": "http://pytoolkit.com:8000/metadata/", + "assertionConsumerService": { + "url": "http://pytoolkit.com:8000/?acs" + }, + "attributeConsumingService": { + "isDefault": false, + "serviceName": "Test Service", + "serviceDescription": "Test Service", + "requestedAttributes": [ { + "name": "userType", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", + "isRequired": false, + "attributeValue": ["userType","admin"] + }, + { + "name": "urn:oid:0.9.2342.19200300.100.1.1", + "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "friendlyName": "uid", + "isRequired": false + } + ] + }, + "singleLogoutService": { + "url": "http://pytoolkit.com:8000/?sls" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + }, + "idp": { + "entityId": "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php", + "singleSignOnService": { + "url": "http://pitbulk.no-ip.org/SSOService.php" + }, + "singleLogoutService": { + "url": "http://pitbulk.no-ip.org/SingleLogoutService.php" + }, + "x509cert": "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m" + }, + "security": { + "authnRequestsSigned": false, + "wantAssertionsSigned": false, + "signMetadata": false + }, + "contactPerson": { + "technical": { + "givenName": "technical_name", + "emailAddress": "technical@example.com" + }, + "support": { + "givenName": "support_name", + "emailAddress": "support@example.com" + } + }, + "organization": { + "en-US": { + "name": "sp_test", + "displayname": "SP test", + "url": "http://sp.example.com" + } + } +} From f965e5ba1e30ebca1bd200901155c9882b5a5de9 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 21 Sep 2016 14:00:00 +0200 Subject: [PATCH 153/352] Fix minor bug with AttributeConsumingService. Update Travis config to test python 2.7.6 and 2.7.12 --- .travis.yml | 3 ++- src/onelogin/saml2/metadata.py | 5 +++-- tests/src/OneLogin/saml2_tests/metadata_test.py | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0ddaacc3..b287ebb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: - - '2.7' + - '2.7.6' + - '2.7.12' install: - sudo apt-get update -qq diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index cb92bcf7..f2932067 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -96,12 +96,13 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N req_attr_isrequired_str = " isRequired=\"%s\"" % req_attribs['isRequired'] if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']: - req_attr_aux_str = "" if isinstance(req_attribs['attributeValue'], basestring): req_attribs['attributeValue'] = [req_attribs['attributeValue']] + + req_attr_aux_str = ">" for attrValue in req_attribs['attributeValue']: req_attr_aux_str += """ - %(attributeValue)s""" % \ { 'attributeValue': attrValue } diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 0fd3a18a..6d7ca494 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -180,9 +180,9 @@ def testBuilderAttributeConsumingServiceWithMultipleAttributeValue(self): self.assertIn(""" Test Service Test Service - admin + + userType + admin """, metadata) From abcbdd8ae60a7e279602eca35c0a890af25185b6 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 21 Sep 2016 14:20:03 +0200 Subject: [PATCH 154/352] Deactivate Travis on python 2.7.12 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b287ebb1..41d26230 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: python python: - '2.7.6' - - '2.7.12' + - '2.7.9' +# - '2.7.12' install: - sudo apt-get update -qq From 6f36e597df97072e5b5f2800596eb835adf447da Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 21 Sep 2016 14:27:51 +0200 Subject: [PATCH 155/352] . --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 41d26230..4f275e8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - '2.7.6' - - '2.7.9' # - '2.7.12' install: From 4e73f1756ab0461ed67edfdcaeb17c0d58671d84 Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric Date: Wed, 21 Sep 2016 10:50:07 +0200 Subject: [PATCH 156/352] Add support for non-ascii fields in settings. --- src/onelogin/saml2/metadata.py | 4 +- src/onelogin/saml2/utils.py | 32 ++++++------- tests/settings/settings6.json | 47 +++++++++++++++++++ tests/src/OneLogin/saml2_tests/auth_test.py | 18 ++++++- .../src/OneLogin/saml2_tests/settings_test.py | 18 ++++++- tests/src/OneLogin/saml2_tests/utils_test.py | 12 +++++ 6 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 tests/settings/settings6.json diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index f2932067..6ac03c27 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -173,7 +173,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N contacts_info.append(contact) str_contacts = '\n'.join(contacts_info) + '\n' - metadata = """ + metadata = u""" ', metadata) self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata) + def testGetUnicodeSPMetadata(self): + """ + Tests the getSPMetadata method of the OneLogin_Saml2_Settings + Case unicode metadata + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings6.json')) + metadata = settings.get_sp_metadata() + + self.assertIn('', metadata) + self.assertIn(u'Sérvïçé prövïdér', metadata) + self.assertIn(u'Téçhnïçäl Nämé', metadata) + self.assertIn(u'Süppört Nämé', metadata) + def testGetSPMetadataSigned(self): """ Tests the getSPMetadata method of the OneLogin_Saml2_Settings diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index ebd221e8..cb0a2a3d 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -39,6 +39,18 @@ def file_contents(self, filename): f.close() return content + def testDeflateBase64Roundtrip(self): + """ + Tests deflate_and_base64_encode and decode_base64_and_inflate methods of OneLogin_Saml2_Utils + """ + body = 'Some random string.' + encoded = OneLogin_Saml2_Utils.deflate_and_base64_encode(body) + self.assertEqual(OneLogin_Saml2_Utils.decode_base64_and_inflate(encoded), body) + + unicode_body = u'Sömé rändöm nön-äsçïï strïng.' + unicode_encoded = OneLogin_Saml2_Utils.deflate_and_base64_encode(unicode_body) + self.assertEqual(OneLogin_Saml2_Utils.decode_base64_and_inflate(unicode_encoded), unicode_body) + def testValidateXML(self): """ Tests the validate_xml method of the OneLogin_Saml2_Utils From aeb25be9aff1313ec87c2f9b19687fb76088813f Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 6 Oct 2016 10:21:44 +0200 Subject: [PATCH 157/352] =?UTF-8?q?Several=20security=20improvements:=20-?= =?UTF-8?q?=20Conditions=20element=20required=20and=20unique.=20-=20AuthnS?= =?UTF-8?q?tatement=20element=20required=20and=20unique.=20-=20SPNameQuali?= =?UTF-8?q?fier=20must=20math=20the=20SP=20EntityID=20-=20Reject=20saml:At?= =?UTF-8?q?tribute=20element=20with=20same=20=E2=80=9CName=E2=80=9D=20attr?= =?UTF-8?q?ibute=20-=20Reject=20empty=20nameID=20-=20Require=20Issuer=20el?= =?UTF-8?q?ement.=20(Must=20match=20IdP=20EntityID).=20-=20Destination=20v?= =?UTF-8?q?alue=20can't=20be=20blank=20(if=20present=20must=20match=20ACS?= =?UTF-8?q?=20URL).=20-=20Check=20that=20the=20EncryptedAssertion=20elemen?= =?UTF-8?q?t=20only=20contains=201=20Assertion=20element.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/onelogin/saml2/response.py | 71 ++++++++-- src/onelogin/saml2/utils.py | 15 +- .../invalids/duplicated_attributes.xml.base64 | 1 + .../invalids/empty_destination.xml.base64 | 1 + .../invalids/empty_nameid.xml.base64 | 1 + .../invalids/no_authnstatement.xml.base64 | 1 + .../invalids/no_conditions.xml.base64 | 1 + .../invalids/no_issuer_assertion.xml.base64 | 1 + .../invalids/no_issuer_response.xml.base64 | 1 + .../invalids/wrong_spnamequalifier.xml.base64 | 1 + .../response_encrypted_nameid.xml.base64 | 2 +- .../data/responses/valid_response.xml.base64 | 2 +- .../src/OneLogin/saml2_tests/response_test.py | 130 ++++++++++++++++-- tests/src/OneLogin/saml2_tests/utils_test.py | 4 +- 14 files changed, 202 insertions(+), 30 deletions(-) create mode 100644 tests/data/responses/invalids/duplicated_attributes.xml.base64 create mode 100644 tests/data/responses/invalids/empty_destination.xml.base64 create mode 100644 tests/data/responses/invalids/empty_nameid.xml.base64 create mode 100644 tests/data/responses/invalids/no_authnstatement.xml.base64 create mode 100644 tests/data/responses/invalids/no_conditions.xml.base64 create mode 100644 tests/data/responses/invalids/no_issuer_assertion.xml.base64 create mode 100644 tests/data/responses/invalids/no_issuer_response.xml.base64 create mode 100644 tests/data/responses/invalids/wrong_spnamequalifier.xml.base64 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 61224b16..b8aac5b3 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -114,24 +114,32 @@ def is_valid(self, request_data, request_id=None): if security.get('wantNameIdEncrypted', False): encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') - if len(encrypted_nameid_nodes) == 0: + if len(encrypted_nameid_nodes) != 1: raise Exception('The NameID of the Response is not encrypted and the SP require it') - # 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') + # Checks that a Conditions element exists + if not self.check_one_condition(): + 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)') + # Checks that an AuthnStatement element exists and is unique + if not self.check_one_authnstatement(): + raise Exception('The Assertion must include an AuthnStatement element') + + # 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') + 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') # Checks destination - destination = self.document.get('Destination', '') + destination = self.document.get('Destination', None) if destination: if not destination.startswith(current_url): # TODO: Review if following lines are required, since we can control the @@ -139,6 +147,8 @@ def is_valid(self, request_data, request_id=None): # 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)) + elif destination == '': + raise Exception('The response has an empty Destination value') # Checks audience valid_audiences = self.get_audiences() @@ -242,6 +252,26 @@ def check_status(self): status_exception_msg += ' -> ' + status_msg raise Exception(status_exception_msg) + def check_one_condition(self): + """ + Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique. + """ + condition_nodes = self.__query_assertion('/saml:Conditions') + if len(condition_nodes) == 1: + return True + else: + return False + + def check_one_authnstatement(self): + """ + Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique. + """ + authnstatement_nodes = self.__query_assertion('/saml:AuthnStatement') + if len(authnstatement_nodes) == 1: + return True + else: + return False + def get_audiences(self): """ Gets the audiences @@ -262,12 +292,16 @@ def get_issuers(self): issuers = [] message_issuer_nodes = self.__query('/samlp:Response/saml:Issuer') - if message_issuer_nodes: + if len(message_issuer_nodes) == 1: issuers.append(message_issuer_nodes[0].text) + else: + raise Exception('Issuer of the Response not found or multiple.') assertion_issuer_nodes = self.__query_assertion('/saml:Issuer') - if assertion_issuer_nodes: + if len(assertion_issuer_nodes) == 1: issuers.append(assertion_issuer_nodes[0].text) + else: + raise Exception('Issuer of the Assertion not found or multiple.') return list(set(issuers)) @@ -296,10 +330,19 @@ def get_nameid_data(self): if security.get('wantNameId', True): raise Exception('Not NameID found in the assertion of the Response') else: + if self.__settings.is_strict() and not nameid.text: + raise Exception('An empty NameID value found') + nameid_data = {'Value': nameid.text} for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: value = nameid.get(attr, None) if value: + if self.__settings.is_strict() and attr == 'SPNameQualifier': + 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.') + nameid_data[attr] = value return nameid_data @@ -355,6 +398,9 @@ def get_attributes(self): attribute_nodes = self.__query_assertion('/saml:AttributeStatement/saml:Attribute') 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') + values = [] for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP['saml']): # Remove any whitespace (which may be present where attributes are @@ -386,7 +432,14 @@ def validate_num_assertions(self): """ encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:EncryptedAssertion') assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:Assertion') - return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1 + + valid = len(encrypted_assertion_nodes) + len(assertion_nodes) == 1 + + if (self.encrypted): + assertion_nodes = OneLogin_Saml2_Utils.query(self.decrypted_document, '//saml:Assertion') + valid = valid and len(assertion_nodes) == 1 + + return valid def process_signed_elements(self): """ diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index ee89d59f..fbacf0ef 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -694,23 +694,22 @@ def get_status(dom): status = {} status_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status') - if len(status_entry) == 0: - raise Exception('Missing Status on response') + if len(status_entry) != 1: + raise Exception('Missing valid Status on response') code_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode', status_entry[0]) - if len(code_entry) == 0: - raise Exception('Missing Status Code on response') + if len(code_entry) != 1: + raise Exception('Missing valid Status Code on response') code = code_entry[0].values()[0] status['code'] = code + status['msg'] = '' message_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusMessage', status_entry[0]) if len(message_entry) == 0: subcode_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode', status_entry[0]) - if len(subcode_entry) > 0: + if len(subcode_entry) == 1: status['msg'] = subcode_entry[0].values()[0] - else: - status['msg'] = '' - else: + elif len(message_entry) == 1: status['msg'] = message_entry[0].text return status diff --git a/tests/data/responses/invalids/duplicated_attributes.xml.base64 b/tests/data/responses/invalids/duplicated_attributes.xml.base64 new file mode 100644 index 00000000..a571b6d3 --- /dev/null +++ b/tests/data/responses/invalids/duplicated_attributes.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQ0OTkyZWJiLTRiMzgtZTQzMi1kYjgyLTk5NTI0MTBkOWFhYiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDI6MzFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzE5MWMwM2U2OGQ3MWQ5Nzk2ZjVlMDdlNjI2MmNhNGFkODgzYTc0YjEiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQ0OTkyZWJiLTRiMzgtZTQzMi1kYjgyLTk5NTI0MTBkOWFhYiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+Z3ZScnJneHBBZHlsSUEvMnNyRm1KZCtqaXM4PTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5LZHA4VDhybndQY0JVb2hjcVBNMGVpTlhwTWgzbGMrZXBIVERIcUxFbk9Kcmd1NS9qaitpN0VhQW1nTzBSSlRraERFWTBWOEZuZVQ0dm92Y0FiZzlmYk04ZlRPMWxYODJ3SW1zRWRxMkwzU0U4NHFCdWFDbURWNVlvMDdDSGJRT1FqYWV0VGt0SnVvRjA4QWQ2bCs1aFJPL3BKeG1yRXlHKzRLaWhGWUJ1dWs9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDgwYmFhZWY2LTI5MmItODc0Ny1jZmNhLWRlMWVlM2YxYTQxNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDI6MzFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng4MGJhYWVmNi0yOTJiLTg3NDctY2ZjYS1kZTFlZTNmMWE0MTUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmFSOU00ZXdOczN1K25KYVFDRDI2WjBBd0Q2TT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+NGQ4WEo1bXBOaW1vQkhkenNXZi9aemxVTlE3SmlVeEl4K1B5TjRuM0EvbWExcGwvQ0FPSUtOUzZ0clR6STg5N1ZjbGxneFhhTTljUFZqOUhLYU9aRW4wSE5Qa2FWR3VjeVVPVzFUd2dWdnJVdkNNQXVRTzdRZ21aekd1SVhsblVKS3FpTDRZMThNT1M1VGpLaExoSG4xbGE4TEFucmRVVEJobUx5eGtjZjhVPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50Ij5fMjEyNmRkMTliOGE5YTI4MjM4ZDg4ZmRjNzM4NWU2MDk5NTAwNGE3NzgyPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA5LTIyVDE5OjAyOjMxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fMTkxYzAzZTY4ZDcxZDk3OTZmNWUwN2U2MjYyY2E0YWQ4ODNhNzRiMSIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAzLTIxVDEzOjQyOjAxWiIgTm90T25PckFmdGVyPSIyMDIzLTA5LTIyVDE5OjAyOjMxWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDE6MDlaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAzLTIxVDIxOjQyOjMxWiIgU2Vzc2lvbkluZGV4PSJfZTY1NzhkNmFmOTdiOWY3ZjA2NzJkODUwZDI5ZGI0YWRkMWEyODZkYzI0Ij48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0Mjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj53YWEyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/invalids/empty_destination.xml.base64 b/tests/data/responses/invalids/empty_destination.xml.base64 new file mode 100644 index 00000000..352988ce --- /dev/null +++ b/tests/data/responses/invalids/empty_destination.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDc2ZWY5MjAxLTY4OGItYzJkZC1mY2Q2LTQxMzEyNzE3ODk0OSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDc2ZWY5MjAxLTY4OGItYzJkZC1mY2Q2LTQxMzEyNzE3ODk0OSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+TVJEd3dSTXZtalQ1VEhLUTBCNWRUNDVBNWhNPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5wRFlrTFNKM2Z3TUQ0cnJNbWF4cFUwQkZVemZHQlVwNklURmovejNOTnFrQmdaTzdBMGIvQlFGbVBOQ202UE82NGdYNmVySGhhMVQ3aW5PTGRIY2crT0Q2Z2h2R0lpbGJzM1RjUkRwUmVTVkpZVWRiUS9jVk85aC9VdWNielBqZ3gyb3dpakk2aVh1dXhYcmpVeHEzYS9DbHcyTGJiVWJHMCtmQStud0ZuOVE9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDk0Y2Q5YTMzLWQyOWMtMTMyMi1kYzMzLTFkOGU0ZDJiNTQzNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng5NGNkOWEzMy1kMjljLTEzMjItZGMzMy0xZDhlNGQyYjU0MzUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPnBYMkV3c1pVVUdCTGhYSTBVOVVMc3d0S3hDYz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+TW5aVU04U0VmN3RVMlc1VGwvb0ZYTVBJYTZUVlcvUTczRmJUNUcxdW14eHZFRkM1UDlsR08reFVkdlBBTXdkTGc1aEN0R29QenB6amxCSnVFemhJU3VYblNZdkVCbllqdGJKVzcxcU9iM25WcTFjYVZtZXRhQjk4aUZzTDFvS0FWTVZ0Q0VST2E1SFpoT3VtQWJONU5qeHYvcUJlYW1lK0ExaStjV0FNaW13PTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/invalids/empty_nameid.xml.base64 b/tests/data/responses/invalids/empty_nameid.xml.base64 new file mode 100644 index 00000000..72350d1f --- /dev/null +++ b/tests/data/responses/invalids/empty_nameid.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQ0MTM5Y2JkLWE2NTQtOWM1Mi00Njk3LTdjMDVkMzAyM2QyZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQ0MTM5Y2JkLWE2NTQtOWM1Mi00Njk3LTdjMDVkMzAyM2QyZiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+VEVFTFhxT0tmZVRqSFI5aUhPb2hrQWlCSDVVPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT51ZW1SeWgyQkcyTXBsbG5kWFNsV0tiaEgzZTRNQVd0VHNJYS9waWJndXZaRmhSTTVJNzUrRkFxYkl4UFVoWDlGYjlOTWRVRzdacWJJS2J0aitLZGxCdVlYaDdTdEIyQWMwY1VzamFQTHVLa2RTc0IzUzdESXFYRThmcEdNeHBSblNNZDZWc1RXM2RId3FYaTJiZklYblBDM0N0RjMwWUhXditwR081MFpCcjg9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDVhMTU1NmIwLTE1NmYtZjNhNS04OGUyLTc1MzRkNjdiNjg0MyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng1YTE1NTZiMC0xNTZmLWYzYTUtODhlMi03NTM0ZDY3YjY4NDMiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPnhpTEtIa05OcllPWTdWOFhkSjVET3pQNFp0ND08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+WHZDRURGdDBJM1VXWlMwN3JWa1VmNTA0Mjg3ZHJTbEI2bDBSdS9OTWMzZFlIT2E1V0NCNXZRanpGVURMSFZSQWlueWR0WXh3ejRTN1NKd081V3RKVFdTOStQNU9SMnpRTjRpYVpnclVGRm5xV0FDZW4rUTMzaXZVaFY0elVTcDU0cjVVdUxLNE96UnVhNmhlWUYrM0Y5TXZMK3VPV2hFZVc3NXZjODk0VXlVPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIi8+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW5AeWFjby5lczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/invalids/no_authnstatement.xml.base64 b/tests/data/responses/invalids/no_authnstatement.xml.base64 new file mode 100644 index 00000000..d116b7b5 --- /dev/null +++ b/tests/data/responses/invalids/no_authnstatement.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGRmNWRiN2JiLTYwZDgtMWZhNi00OTBhLWFjMWMyZThjYWFhMiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGRmNWRiN2JiLTYwZDgtMWZhNi00OTBhLWFjMWMyZThjYWFhMiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+bGJTZmtFR0JsNmZEN0JBc1prU25wYmQyNGJFPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT56Y3YwSitsZ0V4R2tjSVVKYVp5ajdvZkFrY1VZc3dvckpiei9xdEo0WDBmSEtMYXB1eE0xYmlEbnJMTm5wUXhNSkJ3K092WG9sdWdHdVZBeEVyYmE5NTV2QlFtQTRCZXRZS0tKR09XcTkyMWpxKzVhdThtOWQzM2M1UTR6cDYzZld4UnRKV3AyVU05UnZ0aWd6enk2WWg0SE5yNVNkdUhzd1FJeFM2ZEQ2Lzg9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjM2OjMxWiIgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/invalids/no_conditions.xml.base64 b/tests/data/responses/invalids/no_conditions.xml.base64 new file mode 100644 index 00000000..4b73a83e --- /dev/null +++ b/tests/data/responses/invalids/no_conditions.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGQyMjJkZWI1LTZkMjktNWFiZC05NmM0LWFlOTk5ODZhYmVkNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGQyMjJkZWI1LTZkMjktNWFiZC05NmM0LWFlOTk5ODZhYmVkNSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+Sm03Qm5JTEJ3V2h2TW1ZTjd4WG01dDR0ZEZVPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5XRGw3K2RMclN4Ym95bTNWZzBXWTBrLzVDNWZxaDNNUWZPcXQraExDTXMwKzl3ekY4SHduWlJwLzRCMlJGOVBiUVAzc1d6VUY5QWNWeUErUFM4bU5aUnRzRzN4amFabE5BMWV3ZlQ3blFHZ1EvUkxLckhHeW9Bc3VaT0pLTDNqVjJiOGFSTE8rdSsrcmdoZUZSWm1wTkxVanBFTkdFZ3ZWc3ptcGN5aHFCd2c9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/invalids/no_issuer_assertion.xml.base64 b/tests/data/responses/invalids/no_issuer_assertion.xml.base64 new file mode 100644 index 00000000..46094a6f --- /dev/null +++ b/tests/data/responses/invalids/no_issuer_assertion.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDBlNmM5NjUzLTEwNjgtYzhjNS1iNzVjLWU2OTA1ZTE0M2Q0NCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDBlNmM5NjUzLTEwNjgtYzhjNS1iNzVjLWU2OTA1ZTE0M2Q0NCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+KzhLSEl6dHh6SXNzMzNZZzlzRTVjTDlBRFpBPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5yK21CNi9NU3pSS0VGNi9NZGY4M29QeE9ZelFWQ2IvUVIvNlVieG10cmVqbnRFRnN2ZFZSckhmMmd5TUUyZTBGd21ta3JQbEtzcHl2ZDhXbVN2ckV0T0pZaERLRWRYUThtUnRmZWgvY1M4M3pFYmRGSG9ubTd2YkJiU2VxSDBIN2g3S1UxSStqeEwyZVRpQWlubkpHeWhhVHNmaVAxNzdXZmlXVmQ4SHBOY289PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW5AeWFjby5lczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/invalids/no_issuer_response.xml.base64 b/tests/data/responses/invalids/no_issuer_response.xml.base64 new file mode 100644 index 00000000..0e498d44 --- /dev/null +++ b/tests/data/responses/invalids/no_issuer_response.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZnhmMTA1MTkwNy0wZDZjLWI0NjctZjBiNC1kMDI4YTU4ZjNmNzIiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ij48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiBJRD0icGZ4NGFhZGFlMTQtMmY5MC0xZDI1LWJlOTAtYjdjMzI3NzdkODU5IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDRhYWRhZTE0LTJmOTAtMWQyNS1iZTkwLWI3YzMyNzc3ZDg1OSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+T1R6Slg2cmNnUXdnM3dsOEZGMUZkUWFYY1QwPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5GQlRWMXVGVk1WQ0NXelNvdTFxK3kvMzRZVVp1RnlLUzFyaktEREV0aHNVV0ZnVU10S3pQcU9VOFc2enN2MmdZaG0xQ09qd01yenFZUG5WTGViWmtQZ0VNYUlRZW9DR1M0M0pqYllzWk9sakgxZWo5Z3Z6SDM3NHBZMUd6UUx1QXllYmxlL3B4ZmZSMEY5NklYbnFjbjFySnJQM1puR0k1RGcxV3BpbVphWTQ9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPjQ5Mjg4MjYxNWFjZjMxYzgwOTZiNjI3MjQ1ZDc2YWU1MzAzNmMwOTA8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ii8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDItMTlUMDE6MzY6MzFaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/invalids/wrong_spnamequalifier.xml.base64 b/tests/data/responses/invalids/wrong_spnamequalifier.xml.base64 new file mode 100644 index 00000000..48e1fbff --- /dev/null +++ b/tests/data/responses/invalids/wrong_spnamequalifier.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDEzNzFkZmU1LTdlMmYtMTdiNy1hODE1LTVmNWU5YTRiNjVkYSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDEzNzFkZmU1LTdlMmYtMTdiNy1hODE1LTVmNWU5YTRiNjVkYSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+c3lrMGNHOHAxOGpSMUtINThKNnR3Z0JNYlhzPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT4yaytTbmhjWWhSV0FLQWdLR2hyMVpZN1ZsSWtWSytsbEFKZncxVnllKzRkWFpZTGh0TkwrMUJ3bWRlVHlLY1BEYjNWSmZtNXRzRGFWRDNtTHVUd2E5L0EvUCt0ZnY0d0t6YVQrdmJvTDl0RVFnNFAwR3hmaWRzbkYrQUM0Z2lLb0VBRE5RbmlaamExR1hhM3VOdi85TEVYV1h4YmZaVGJPNmxLNnlhbGR6UkU9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0id3Jvbmctc3AtZW50aXR5aWQiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjM2OjMxWiIgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDA5OjM3OjAxWiIgU2Vzc2lvbkluZGV4PSJfNjI3M2Q3N2I4Y2RlMGMzMzNlYzc5ZDIyYTlmYTAwMDNiOWZlMmQ3NWNiIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdEBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/response_encrypted_nameid.xml.base64 b/tests/data/responses/response_encrypted_nameid.xml.base64 index 6b1c9856..d3bcbb6e 100644 --- a/tests/data/responses/response_encrypted_nameid.xml.base64 +++ b/tests/data/responses/response_encrypted_nameid.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGNlNWNjMjE0LTI1OGMtNjNkYy1iM2UxLWQ4YmI3ZGQ1ZWM1ZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGNlNWNjMjE0LTI1OGMtNjNkYy1iM2UxLWQ4YmI3ZGQ1ZWM1ZiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+aWptQ20xSDcxUE44TENNWWprbmx2YUVLMDB3PTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5SeDNCODBrSUJnZm1UeU43Sy9HRXFNL0VmQWgwRVllV1NJN2RxdzBzdTRQUDdKclhjcng0N2ZwSjlLYWpRWlBTOU5CcVdjZlhJeVNVdGR5c1l4SUU3VzIvOHd0ZFozakVzbzZLRWlreDBTTlA0Z0RkUzNyNzVLMVNMb3NBZklVSmg2L0lUbll1Q2Y5UFl3RHNXVSt2WU9la2xnWC9xT3lkbm5sKzQ1QmpTd2M9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc1NmNjMjc5LWFmMjEtZDE5Mi1mMGVkLTEyNmRjZTYwMzBkNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng3NTZjYzI3OS1hZjIxLWQxOTItZjBlZC0xMjZkY2U2MDMwZDUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPks0MkdwcnJwREpLNmUvRlhpazBZYjl0cVpIVT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+Z1pYSVkxWEw2UitNYzBzYWE5NzdSRzZEOXYzdUNkYUtXTUF5S0pUZUQ5UHIrL0NrWFNDbjJpbERXbUQyaldEVUVGNEZnZWtBMEt6STZQamRLR3VwRmVuZW96a2pFcHVmU0ozRkhpSXNxRnM5OGdONWZvZEEzRm16RjBLS2dScTRJaVRSd216UG5xT080eE8rQlhNQkoyTkFPVENaYnRyb3RsYnVBVlFUT3lzPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0xMFQxNzo0MzozN1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48c2FtbDpFbmNyeXB0ZWRJRD48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgeG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3RyaXBsZWRlcy1jYmMiLz48ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleT48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLTFfNSIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+Mkx5Ry9wb1RrZUFsVHAvNmpEWUhGQ0JFbU9wUmZGUE5qZ2R5WDRWYTY5RDFkOXMycUIvcHB0UUg0UE8wZjl3cjVsbm9hWUhKME9CWUJJRjdnS1lEOVcvOEpVTUhLN1lNS2svandJcnhmWDNpVlRMQ1VMaTRoSWdRVUhRckR0cXg0eFNsNTJBckcrYzRxcDhNQk5ydTZ5T2lqSzB0NkRnemJGZURlNExxb0ZjPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPg0KICAgPHhlbmM6Q2lwaGVyRGF0YT4NCiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPkJNWHRJZENXM2N6S0h5dk1tTGswMW9MWXNPY0NRcmFQblFzT3ZWN3pKL2Zzbnc3cG1pR2V5TlBsZFJQS2VRSVk1M2hJYTR4WUR5bmpUcHNHUERXZFpDUWo4US83R0R1TlMrSnArdUY2WWZSRzd1bEIyanBhU0xBVkRBRUt3eXVUVTBrZHhnTEw0L1BTZFVTQ1B1SURUMS94UURuZ2F6Q2I5RTNtY1ZsajZSODlxano0R1A5cVBlSC95WmZoV1EwYzRpNUs5NnZWOXFud09ZTUtSWjhHdFBLemNyK2RsbFhSSHlHQ09nOGtyUjd3Y0QxR1BvaHZmdVhvdm5xR3hMKzVPZExrMjVRbDJ4Y1Q1cUxNZnhGaEVxRDNteHVIVmN3WTwveGVuYzpDaXBoZXJWYWx1ZT4NCiAgIDwveGVuYzpDaXBoZXJEYXRhPg0KPC94ZW5jOkVuY3J5cHRlZERhdGE+PC9zYW1sOkVuY3J5cHRlZElEPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAzLTA5VDEyOjIzOjA3WiIgTm90T25PckFmdGVyPSIyMDIzLTA5LTEwVDE3OjQzOjM3WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAzLTA5VDIwOjIzOjM3WiIgU2Vzc2lvbkluZGV4PSJfOTQ0YmZjYWNiMGQ4MzJiMTJlNGJjZjc3NGUwMmJiZTVmNjQ1NWM2ODAzIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdEBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+d2FhMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQyN2JmMGJiLTI3MmMtMGFkMC05MGYyLTVmN2ZkOWJhYzQ2OCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQyN2JmMGJiLTI3MmMtMGFkMC05MGYyLTVmN2ZkOWJhYzQ2OCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+eHRBaE5WaUtVa3VmcUR1eUNHRDBVVE9SbVdBPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5yL0E3SHhVVC9mZ1pXMVBGVkdHOXJTbW5sQU1QVjdnQm4zTjBVdFJ1V0VtTmlaQTVNUUVCSE9JeTA0dFFuSmRXYksrKzAxemh0TmdqRlhGeGduaE5VWFdKdVF4Rnp3SzdvaTRJbHc0amZjQ2tpSFJLVWxubGQ4U3Q1YVBGMUE5bFlWbWxtU0d5dUgzSnlrSjNnVjNaeVJRMjhDc0IvQnBtdi9mbmhleHBhYzQ9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDczY2ZlZWM0LWZkOTMtMzE1OC05ZDI1LWVmZDgzYWM4MjQyYiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng3M2NmZWVjNC1mZDkzLTMxNTgtOWQyNS1lZmQ4M2FjODI0MmIiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPjJpdzJPZUpRSjA1ellQTkIvSUp3TXdETFRjdz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+cFFiaUFReWEvKzVxbkhKWWlPQ0dFaUt5RHNSZThKdTlBUlJiVlFlUkd5NWF4VlU5aDYxV0tYSWcwcDI5NlFQMEJBem0rWmt5cXErTUdSVWE4OFNpaWFlbWRma3dRZnd1TS8rTk1FTkRqQmgrR2tob0RmcjN1ZDAzWjNDcHA3dDdTSldWVnF5NGZZVUtzNllaYmJRTG5PVjZPVVphb3R6eFBzUXVSVGlsWmVrPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA0My0wOS0xMFQxNzo0MzozN1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48c2FtbDpFbmNyeXB0ZWRJRD48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgeG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI2FlczEyOC1jYmMiLz48ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleT48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLTFfNSIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+T0J0UmN6b2dyZWc1VmpOS29zL2JOaEEvMjZ1MnFiWko5ZzBvbWpsRGphWEp5N2FJRkJ4WTMyV3ZvcjY3T0VhNm5VV3dUcFpVT3FWR2NLQ1puMGtCWVZJUjFjSGJsTUdhRUNLdEthU25wdUR5UXZ4UDA1Ulp5cG5nUVJ3ZXdSZ0VaZkE5NmhCM2w1bmgzMFNqYmh4U09IbXk0Qi9lWEx1UHlOUWhiMEdadEFzPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPg0KICAgPHhlbmM6Q2lwaGVyRGF0YT4NCiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPnNwZm8yNndVUlVCc3RnYzdBbG1PM25nem91OUFqUVU0NFhUOUt2ZWcvVTh6Q1FzNU43WXRyazZwREs2Tktic2gvQ1RMaFZKUy9kRFNwd3hzRy9QRDU0WlUrcmdKNGtndGt5YWduSzhydnBwNDJuYjc1YzBzUE1KK0M4dW42VHExeFBRRjlkcEJaVU1sZDI2WUJKQ0d6TG0vcmVDUlpiSk5iNEx3TmRjd3pQTnc4cnJsb1Z2SWhyQjlvWS80a3hwdExFemJmczVHcklxZXZNQnZjeWxpQVhBbFE1QitEZUdPWkhtODJnQmFBVHdxcXNuWkRsbmMyTUtiUWJyVXdLUTZ5NXdPcVByUmxXbFBJUnRLbzRZc2R3PT08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICA8L3hlbmM6Q2lwaGVyRGF0YT4NCjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDpFbmNyeXB0ZWRJRD48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0wOVQxMjoyMzowN1oiIE5vdE9uT3JBZnRlcj0iMjA0My0wOS0xMFQxNzo0MzozN1oiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMy0wOVQxMjoyMzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNDMtMDMtMDlUMjA6MjM6MzdaIiBTZXNzaW9uSW5kZXg9Il85NDRiZmNhY2IwZDgzMmIxMmU0YmNmNzc0ZTAyYmJlNWY2NDU1YzY4MDMiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj53YWEyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/valid_response.xml.base64 b/tests/data/responses/valid_response.xml.base64 index c727dc13..42ff8eb4 100644 --- a/tests/data/responses/valid_response.xml.base64 +++ b/tests/data/responses/valid_response.xml.base64 @@ -1 +1 @@ -PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmpBZ290RjBKK1JLMS9LODd3MjRNTUMyK1pScz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+cFhmM3Z3T1p2dGZtZjdNTWNPU0cwMzkyU213bnBJb0FqZ2dzVmErUlNJRE1Td0tTckwzcWw3SnlZQjVTaXZxL0xYODlUYXF5WTJ4MFBnTWl4YXY0bjFHMTFDM3NtbFJBTXJEZTZ2UnRJbUpVc2xTR2s5N3pQaHlvUStKNW9nUVBkNlZsTVR6OEtXemZxdE9QRGY1ZGwyWXlKcG9rZU9OVE0xemM0SkdNM0dBPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmhucFQ5Y3VlNnRCSzRJWk9qNTBlTW0zbUNnST08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+ckEwZkxaN0trbDNKd3NMS1hEd2FGY2c2TG5LSFJ6SVVyeDZtTlgxVG85ek1DdkNVRC9vUFlWYUVlTXRueFlZNDZmc21hTnZzS2l1Z2l4ZFZ6TEwzOEttQVk4dU1UZXpoR3Z6MlZlSFVvcjR1NGh4ZlFsc090MmZ1ekFFNkRicXZJVkswd2p4M1JZMDhYL3VPQ1I5dVBjcHk0QkJzUTN4UGw0UXZpcEpqU05NPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPjQ5Mjg4MjYxNWFjZjMxYzgwOTZiNjI3MjQ1ZDc2YWU1MzAzNmMwOTA8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ii8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDItMTlUMDE6MzY6MzFaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGNjMzE1NjhiLWM0NmQtZmY3NS1iYTJlLTUzMDM0ODQ5ODBkYSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGNjMzE1NjhiLWM0NmQtZmY3NS1iYTJlLTUzMDM0ODQ5ODBkYSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+cTZnVllaZUJmV21TelhzUUNEVUFYc2hyNjVRPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5ES041ZWVwbm5CU3NvNVd3VHFwZ1lxQ2hsUXA1YXlVYUUrWlVVZ09CK2t5RDJEOUMxRjlSblVTK1VTa2hJQ2dDWVNTamtqQWE4OTZNNzgzdFFMd3dwcGZBSG1NMWpUcTRUVm1xKzlQOTZyQ29LUzUwR0FiZHVOUXFlSTl2T1EraTlXRWVkcFFFeWFsNEJNbS9pTkxHNE00Lzl5alRrVTU2OUdOOGZBOUJSb1E9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDA4YzJhNmJiLTdlZTQtOGRjMi04ZmUyLWYwNTVlZWQ5M2RlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZngwOGMyYTZiYi03ZWU0LThkYzItOGZlMi1mMDU1ZWVkOTNkZTQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkVnQzhKSU1zL2VHaXdiSTBVc3AvWGRYUEtnOD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+MW9JZFJ1bUJDaUNOdXVtYzVYNkhQYWI1L2xXZjZqR0dOR0dGOWxKRWxOc2lOYThpK3dSdkEveVF1YUFvMmp5bGNnV1pMWmZscjc2VnYrYVF0ZFA0LzNDR0djall5cUxWc0k0SS9iZjdXakk0dW1nMXl6aU5zNzMrelUzQThKK21LV013Q1J0eEZibm9BcnNyZzVFcVdOT2MrYkdWMEFsYnFCZTF4RllGcGVnPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 08602847..91a98278 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -114,6 +114,25 @@ def testReturnNameId(self): except Exception as e: self.assertIn('Not NameID found in the assertion of the Response', e.message) + 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: + response_8.get_nameid() + self.assertTrue(False) + except Exception as e: + self.assertIn('The SPNameQualifier value mistmatch the SP entityID value.', e.message) + + xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) + response_9 = OneLogin_Saml2_Response(settings, xml_6) + try: + response_9.get_nameid() + self.assertTrue(False) + except Exception as e: + self.assertIn('An empty NameID value found', e.message) + def testGetNameIdData(self): """ Tests the get_nameid_data method of the OneLogin_Saml2_Response @@ -135,7 +154,7 @@ def testGetNameIdData(self): expected_nameid_data_2 = { 'Value': '2de11defd199f8d5bb63f9b7deb265ba5c675c10', 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', - 'SPNameQualifier': 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php' + 'SPNameQualifier': 'http://stuff.com/endpoints/metadata.php' } nameid_data_2 = response_2.get_nameid_data() self.assertEqual(expected_nameid_data_2, nameid_data_2) @@ -185,6 +204,25 @@ def testGetNameIdData(self): except Exception as e: self.assertIn('Not NameID found in the assertion of the Response', e.message) + 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: + response_8.get_nameid_data() + self.assertTrue(False) + except Exception as e: + self.assertIn('The SPNameQualifier value mistmatch the SP entityID value.', e.message) + + xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) + response_9 = OneLogin_Saml2_Response(settings, xml_6) + try: + response_9.get_nameid_data() + self.assertTrue(False) + except Exception as e: + self.assertIn('An empty NameID value found', e.message) + def testCheckStatus(self): """ Tests the check_status method of the OneLogin_Saml2_Response @@ -214,6 +252,45 @@ def testCheckStatus(self): except Exception as e: self.assertIn('The status code of the Response was not Success, was Responder -> something_is_wrong', e.message) + def testCheckOneCondition(self): + """ + Tests the check_one_condition method of SamlResponse + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_conditions.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertFalse(response.check_one_condition()) + + self.assertTrue(response.is_valid(self.get_request_data())) + 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()) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertTrue(response_2.check_one_condition()) + + + def testCheckOneAuthnStatement(self): + """ + Tests the check_one_authnstatement method of SamlResponse + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_authnstatement.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertFalse(response.check_one_authnstatement()) + + self.assertTrue(response.is_valid(self.get_request_data())) + 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()) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertTrue(response_2.check_one_authnstatement()) + def testGetAudiences(self): """ Tests the get_audiences method of the OneLogin_Saml2_Response @@ -237,9 +314,9 @@ def testQueryAssertions(self): OneLogin_Saml2_Response using the get_issuers call """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + xml = self.file_contents(join(self.data_path, 'responses', 'adfs_response.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - self.assertEqual(['https://app.onelogin.com/saml/metadata/13590'], response.get_issuers()) + self.assertEqual(['http://login.example.com/issuer'], response.get_issuers()) xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) response_2 = OneLogin_Saml2_Response(settings, xml_2) @@ -270,9 +347,9 @@ def testGetIssuers(self): Tests the get_issuers method of the OneLogin_Saml2_Response """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + xml = self.file_contents(join(self.data_path, 'responses', 'adfs_response.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - self.assertEqual(['https://app.onelogin.com/saml/metadata/13590'], response.get_issuers()) + self.assertEqual(['http://login.example.com/issuer'], response.get_issuers()) xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) response_2 = OneLogin_Saml2_Response(settings, xml_2) @@ -282,6 +359,20 @@ def testGetIssuers(self): response_3 = OneLogin_Saml2_Response(settings, xml_3) self.assertEqual(['http://idp.example.com/', 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_3.get_issuers()) + 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: + issuers = response_4.get_issuers() + except Exception as e: + self.assertIn('Issuer of the Response not found or multiple.', e.message) + + 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: + issuers = response_5.get_issuers() + except Exception as e: + self.assertIn('Issuer of the Assertion not found or multiple.', e.message) + def testGetSessionIndex(self): """ Tests the get_session_index method of the OneLogin_Saml2_Response @@ -637,6 +728,20 @@ def testIsInValidEncAttrs(self): except Exception as e: self.assertEqual('There is an EncryptedAttribute in the Response and this SP not support them', e.message) + def testIsInValidDuplicatedAttrs(self): + """ + Tests the getAttributes method of the OneLogin_Saml2_Response + Case duplicated Attrs + """ + 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: + attributes = response.get_attributes() + self.assertFalse(True) + except Exception as e: + self.assertEqual('Found an Attribute element with duplicated Name', e.message) + def testIsInValidDestination(self): """ Tests the is_valid method of the OneLogin_Saml2_Response class @@ -653,18 +758,25 @@ def testIsInValidDestination(self): self.assertFalse(response_2.is_valid(self.get_request_data())) self.assertIn('The response was received at', response_2.get_error()) + # Empty Destination dom = parseString(b64decode(message)) dom.firstChild.setAttribute('Destination', '') message_2 = b64encode(dom.toxml()) response_3 = OneLogin_Saml2_Response(settings, message_2) self.assertFalse(response_3.is_valid(self.get_request_data())) - self.assertIn('A valid SubjectConfirmation was not found on this Response', response_3.get_error()) + self.assertIn('The response has an empty Destination value', response_3.get_error()) - dom.firstChild.removeAttribute('Destination') - message_3 = b64encode(dom.toxml()) + 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.assertIn('A valid SubjectConfirmation was not found on this Response', response_4.get_error()) + self.assertEquals('The response has an empty Destination value', response_4.get_error()) + + # No Destination + dom.firstChild.removeAttribute('Destination') + message_4 = b64encode(dom.toxml()) + response_5 = OneLogin_Saml2_Response(settings, message_4) + self.assertFalse(response_5.is_valid(self.get_request_data())) + self.assertIn('A valid SubjectConfirmation was not found on this Response', response_5.get_error()) def testIsInValidAudience(self): """ diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index cb0a2a3d..61edf0d5 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -438,7 +438,7 @@ def testGetStatus(self): status_inv = OneLogin_Saml2_Utils.get_status(dom_inv) self.assertEqual(status_inv, 42) except Exception as e: - self.assertEqual('Missing Status on response', e.message) + self.assertEqual('Missing valid Status on response', e.message) xml_inv2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_status_code.xml.base64')) xml_inv2 = b64decode(xml_inv2) @@ -448,7 +448,7 @@ def testGetStatus(self): status_inv2 = OneLogin_Saml2_Utils.get_status(dom_inv2) self.assertEqual(status_inv2, 42) except Exception as e: - self.assertEqual('Missing Status Code on response', e.message) + self.assertEqual('Missing valid Status Code on response', e.message) def testParseDuration(self): """ From 33095935f55e69fe3fa4874244b99fe6ca2d516b Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 7 Oct 2016 14:39:43 +0200 Subject: [PATCH 158/352] pep8 & pyflakes --- tests/src/OneLogin/saml2_tests/response_test.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 91a98278..9ec754de 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -271,7 +271,6 @@ def testCheckOneCondition(self): response_2 = OneLogin_Saml2_Response(settings, xml_2) self.assertTrue(response_2.check_one_condition()) - def testCheckOneAuthnStatement(self): """ Tests the check_one_authnstatement method of SamlResponse @@ -362,14 +361,14 @@ 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: - issuers = response_4.get_issuers() + response_4.get_issuers() except Exception as e: self.assertIn('Issuer of the Response not found or multiple.', e.message) 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: - issuers = response_5.get_issuers() + response_5.get_issuers() except Exception as e: self.assertIn('Issuer of the Assertion not found or multiple.', e.message) @@ -737,7 +736,7 @@ def testIsInValidDuplicatedAttrs(self): xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'duplicated_attributes.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) try: - attributes = response.get_attributes() + response.get_attributes() self.assertFalse(True) except Exception as e: self.assertEqual('Found an Attribute element with duplicated Name', e.message) From 80fab33ca6af711f3fbd90faff939b39f90a80e8 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 14 Oct 2016 01:15:15 +0200 Subject: [PATCH 159/352] Improve Signature validation process --- src/onelogin/saml2/response.py | 64 ++++++++++++++----- src/onelogin/saml2/utils.py | 17 +++-- .../saml2_tests/signed_response_test.py | 6 +- 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index b8aac5b3..8ea2f2f7 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -88,6 +88,9 @@ def is_valid(self, request_data, request_id=None): signed_elements = self.process_signed_elements() + has_signed_response = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements + has_signed_assertion = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML in signed_elements + if self.__settings.is_strict(): no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) @@ -200,32 +203,26 @@ def is_valid(self, request_data, request_id=None): if not any_subject_confirmation: raise Exception('A valid SubjectConfirmation was not found on this Response') - if security.get('wantAssertionsSigned', False) and ('{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML) not in signed_elements: + if security.get('wantAssertionsSigned', False) and not has_signed_assertion: raise Exception('The Assertion of the Response is not signed and the SP require it') - if security.get('wantMessagesSigned', False) and ('{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP) not in signed_elements: + if security.get('wantMessagesSigned', False) and not has_signed_response: raise Exception('The Message of the Response is not signed and the SP require it') - if len(signed_elements) > 0: - if len(signed_elements) > 2: - raise Exception('Too many Signatures found. SAML Response rejected') + if not signed_elements or (not has_signed_response and not has_signed_assertion): + raise Exception('No Signature found. SAML Response rejected') + else: cert = idp_data.get('x509cert', None) fingerprint = idp_data.get('certFingerprint', None) fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) # If find a Signature on the Response, validates it checking the original response - if '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements: - document_to_validate = self.document - # Otherwise validates the assertion (decrypted assertion if was encrypted) - else: - if self.encrypted: - document_to_validate = self.decrypted_document - else: - document_to_validate = self.document - if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg): + if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH): + 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): raise Exception('Signature validation failed. SAML Response rejected') - else: - raise Exception('No Signature found. SAML Response rejected') return True except Exception as err: @@ -291,7 +288,7 @@ def get_issuers(self): """ issuers = [] - message_issuer_nodes = self.__query('/samlp:Response/saml:Issuer') + message_issuer_nodes = OneLogin_Saml2_Utils.query(self.document, '/samlp:Response/saml:Issuer') if len(message_issuer_nodes) == 1: issuers.append(message_issuer_nodes[0].text) else: @@ -486,8 +483,41 @@ def process_signed_elements(self): 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') return signed_elements + def validate_signed_elements(self, signed_elements): + """ + Verifies that the document has the expected signed nodes. + """ + if len(signed_elements) > 2: + return False + + response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP + assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML + + if (response_tag in signed_elements and signed_elements.count(response_tag) > 1) or \ + (assertion_tag in signed_elements and signed_elements.count(assertion_tag) > 1) or \ + (response_tag not in signed_elements and assertion_tag not in signed_elements): + return False + + # Check that the signed elements found here, are the ones that will be verified + # by OneLogin_Saml2_Utils.validate_sign + if response_tag in signed_elements: + expected_signature_nodes = OneLogin_Saml2_Utils.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.') + + 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.') + + return True + def validate_timestamps(self): """ Verifies that the document is valid according to Conditions Element diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index fbacf0ef..d7abf1cc 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -65,6 +65,9 @@ class OneLogin_Saml2_Utils(object): """ + RESPONSE_SIGNATURE_XPATH = '/samlp:Response/ds:Signature' + ASSERTION_SIGNATURE_XPATH = '/samlp:Response/saml:Assertion/ds:Signature' + @staticmethod def decode_base64_and_inflate(value): """ @@ -865,7 +868,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant return newdoc.saveXML(newdoc.firstChild) @staticmethod - def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): + def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None): """ Validates a signature (Message or Assertion). @@ -886,6 +889,9 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid :param debug: Activate the xmlsec debug :type: bool + + :param xpath: The xpath of the signed element + :type: string """ try: if xml is None or xml == '': @@ -918,10 +924,13 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid xmlsec.addIDs(elem, ["ID"]) - signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature') + if xpath: + signature_nodes = OneLogin_Saml2_Utils.query(elem, xpath) + else: + signature_nodes = OneLogin_Saml2_Utils.query(elem, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH) - if not len(signature_nodes) > 0: - signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature') + if len(signature_nodes) == 0: + signature_nodes = OneLogin_Saml2_Utils.query(elem, OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH) if len(signature_nodes) == 1: signature_node = signature_nodes[0] diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py index cdbba1fa..d630f00f 100644 --- a/tests/src/OneLogin/saml2_tests/signed_response_test.py +++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py @@ -50,7 +50,11 @@ def testResponseAndAssertionSigned(self): Tests the getNameId method of the OneLogin_Saml2_Response Case valid signed response, signed assertion """ - settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings_info = self.loadSettingsJSON() + settings_info['idp']['entityId'] = "https://federate.example.net/saml/saml2/idp/metadata.php" + settings_info['sp']['entityId'] = "hello.com" + + settings = OneLogin_Saml2_Settings(settings_info) message = self.file_contents(join(self.data_path, 'responses', 'simple_saml_php.xml')) response = OneLogin_Saml2_Response(settings, b64encode(message)) From 48addfdb3215933b8ae31532da0e214697477dec Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 14 Oct 2016 16:53:16 +0200 Subject: [PATCH 160/352] Release 2.2.0 --- README.md | 4 ++-- changelog.md | 21 +++++++++++++++++++++ setup.py | 6 +++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d4a43275..85ca137e 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml). #### Warning #### -Update python-saml to 2.1.9, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. +Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. -python-saml < v2.1.6 is vulnerable and allows signature wrapping! +python-saml < v2.2.0 is vulnerable and allows signature wrapping! #### Security Guidelines #### diff --git a/changelog.md b/changelog.md index 4e518024..b78c9f3c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,26 @@ # python-saml changelog +### 2.2.0 (Oct 14, 2016) +* Several security improvements: + * Conditions element required and unique. + * AuthnStatement element required and unique. + * SPNameQualifier must math the SP EntityID + * Reject saml:Attribute element with same “Name” attribute + * Reject empty nameID + * Require Issuer element. (Must match IdP EntityID). + * Destination value can't be blank (if present must match ACS URL). + * Check that the EncryptedAssertion element only contains 1 Assertion element. +* Improve Signature validation process +* [#149](https://github.com/onelogin/python-saml/pull/149) Work-around for xmlsec.initialize +* [#151](https://github.com/onelogin/python-saml/pull/151) Fix flask demo error handling and improve documentation +* +* [#152](https://github.com/onelogin/python-saml/pull/152) Update LICENSE to include MIT rather than BSD license +* [#155](https://github.com/onelogin/python-saml/pull/155) Fix typographical errors in docstring +* Fix RequestedAttribute Issue +* Fix __build_signature method. If relay_state is null not be part of the SignQuery +* [#164](https://github.com/onelogin/python-saml/pull/164) Add support for non-ascii fields in settings + + ### 2.1.9 (Jun 27, 2016) * Change the decrypt assertion process. * Add 2 extra validations to prevent Signature wrapping attacks. diff --git a/setup.py b/setup.py index e157f95a..faa5708f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.1.9', + version='2.2.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', @@ -22,9 +22,9 @@ author_email='support@onelogin.com', license='MIT', url='https://github.com/onelogin/python-saml', - packages=['onelogin','onelogin/saml2'], + packages=['onelogin', 'onelogin/saml2'], include_package_data=True, - package_data = { + package_data={ 'onelogin/saml2/schemas': ['*.xsd'], }, package_dir={ From 54e5be1d8bb7f769c09e5de6b9f05ad6b6fe7e51 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 25 Oct 2016 14:07:57 +0200 Subject: [PATCH 161/352] Fix #167, related to #136 --- changelog.md | 1 - src/onelogin/saml2/metadata.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index b78c9f3c..40b03d4f 100644 --- a/changelog.md +++ b/changelog.md @@ -13,7 +13,6 @@ * Improve Signature validation process * [#149](https://github.com/onelogin/python-saml/pull/149) Work-around for xmlsec.initialize * [#151](https://github.com/onelogin/python-saml/pull/151) Fix flask demo error handling and improve documentation -* * [#152](https://github.com/onelogin/python-saml/pull/152) Update LICENSE to include MIT rather than BSD license * [#155](https://github.com/onelogin/python-saml/pull/155) Fix typographical errors in docstring * Fix RequestedAttribute Issue diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 6ac03c27..fba83145 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -93,7 +93,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']: req_attr_nameformat_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName'] if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']: - req_attr_isrequired_str = " isRequired=\"%s\"" % req_attribs['isRequired'] + req_attr_isrequired_str = " isRequired=\"%s\"" % 'true' if req_attribs['isRequired'] else 'false' if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']: if isinstance(req_attribs['attributeValue'], basestring): From 4b1c5cc2445aa51e1319e754e7eac8806883aa39 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 17 Nov 2016 03:37:43 +0100 Subject: [PATCH 162/352] Update README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 85ca137e..395e688f 100644 --- a/README.md +++ b/README.md @@ -719,6 +719,15 @@ auth.process_slo(keep_local_session=keepLocalSession); #### Initiate SLO #### In order to send a Logout Request to the IdP: +```python +from onelogin.saml2.auth import OneLogin_Saml2_Auth + +req = prepare_request_for_toolkit(request) +auth = OneLogin_Saml2_Auth(req) # Constructor of the SP, loads settings.json + # and advanced_settings.json + +auth.logout() # Method that builds and sends the LogoutRequest +``` The Logout Request will be sent signed or unsigned based on the security info of the advanced_settings.json ('logoutRequestSigned'). From a088a3ea4ad5ce18ba5f026daba6ed7c7344a66e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 29 Nov 2016 17:18:48 +0100 Subject: [PATCH 163/352] Extend get_last_request_xml and get_last_response_xml to retrieve also Logout messages. Refactoring --- README.md | 6 +-- src/onelogin/saml2/auth.py | 34 +++++++------ src/onelogin/saml2/authn_request.py | 4 +- src/onelogin/saml2/logout_request.py | 9 ++++ src/onelogin/saml2/logout_response.py | 9 ++++ ...> decrypted_valid_encrypted_assertion.xml} | 0 ...y_decrypted_valid_encrypted_assertion.xml} | 0 tests/src/OneLogin/saml2_tests/auth_test.py | 6 ++- .../saml2_tests/authn_request_test.py | 4 +- .../saml2_tests/expensify_test.py.txt | 50 +++++++++++++++++++ .../src/OneLogin/saml2_tests/response_test.py | 2 +- 11 files changed, 100 insertions(+), 24 deletions(-) rename tests/data/responses/{decrypted_valid_encrypted_assertion.xml.base64.xml => decrypted_valid_encrypted_assertion.xml} (100%) rename tests/data/responses/{pretty_decrypted_valid_encrypted_assertion.xml.base64.xml => pretty_decrypted_valid_encrypted_assertion.xml} (100%) create mode 100644 tests/src/OneLogin/saml2_tests/expensify_test.py.txt diff --git a/README.md b/README.md index 4e9f7a29..41bda4a8 100644 --- a/README.md +++ b/README.md @@ -827,13 +827,13 @@ 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. +* ***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. * ***set_strict*** Set the strict mode active/disable. -* ***get_last_request_xml*** Returns the most recently-constructed XML request -* ***get_last_response_xml*** Returns the most recently-decrypted XML response +* ***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#### diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index e4693a7d..1d2d5cc6 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -58,8 +58,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_xml = None - self.__last_response_xml = None + self.__last_request = None + self.__last_response = None def get_settings(self): """ @@ -93,7 +93,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_xml = response.get_xml_document() + self.__last_response = response.get_xml_document() if response.is_valid(self.__request_data, request_id): self.__attributes = response.get_attributes() self.__nameid = response.get_nameid() @@ -128,6 +128,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']: logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) + self.__logout_response = logout_response.get_xml() if not logout_response.is_valid(self.__request_data, request_id): self.__errors.append('invalid_logout_response') self.__error_reason = logout_response.get_error() @@ -138,6 +139,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']: logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest']) + self.__last_request = logout_request.get_xml() if not logout_request.is_valid(self.__request_data): self.__errors.append('invalid_logout_request') self.__error_reason = logout_request.get_error() @@ -148,6 +150,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.__logout_response = response_builder.get_xml() logout_response = response_builder.get_response() parameters = {'SAMLResponse': logout_response} @@ -288,12 +291,11 @@ 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() + parameters = {'SAMLRequest': saml_request} - self.__last_request_xml = authn_request.get_request_as_xml() if return_to is not None: parameters['RelayState'] = return_to else: @@ -339,9 +341,8 @@ 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 - saml_request = logout_request.get_request() parameters = {'SAMLRequest': logout_request.get_request()} @@ -455,15 +456,21 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On signature = dsig_ctx.signBinary(str(msg), sign_algorithm_transform) return b64encode(signature) - def get_last_response_xml(self): + def get_last_response_xml(self, pretty_print_if_possible=False): """ - Retrieves the decrypted XML of the last SAML response + 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 """ - if self.__last_response_xml: - return etree.tostring(self.__last_response_xml, pretty_print=True) + response = None + if self.__last_response: + 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): """ @@ -472,5 +479,4 @@ def get_last_request_xml(self): :returns: SAML request XML :rtype: string|None """ - if self.__last_request_xml: - return self.__last_request_xml + return self.__last_request or None diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 3fa4d0f6..636a6888 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -150,9 +150,9 @@ def get_id(self): """ return self.__id - def get_request_as_xml(self): + def get_xml(self): """ - Return the XML document that will be sent as part of the request + Returns the XML that will be sent as part of the request :return: XML request body :rtype: string """ diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index ab79be0d..e22b94cd 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -130,6 +130,15 @@ def get_request(self, deflate=True): request = 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 5420ebbe..47c318d7 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -205,6 +205,15 @@ def get_response(self, deflate=True): response = b64encode(self.__logout_response) return response + 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 + def get_error(self): """ After executing a validation process, if it fails this method returns the cause diff --git a/tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.xml b/tests/data/responses/decrypted_valid_encrypted_assertion.xml similarity index 100% rename from tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.xml rename to tests/data/responses/decrypted_valid_encrypted_assertion.xml diff --git a/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml.base64.xml b/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml similarity index 100% rename from tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml.base64.xml rename to tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index a83e10b8..0d59da9d 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -916,8 +916,10 @@ def testGetLastDecryptedResponse(self): 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', 'pretty_decrypted_valid_encrypted_assertion.xml.base64.xml')) - self.assertEqual(auth.get_last_response_xml(), decrypted_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) + decrypted_response = self.file_contents(join(self.data_path, 'responses', 'pretty_decrypted_valid_encrypted_assertion.xml')) + self.assertEqual(auth.get_last_response_xml(True), decrypted_response) def testGetLastSentRequest(self): settings = self.loadSettingsJSON() diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 4f298710..b26e2f82 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -80,7 +80,7 @@ def testGetRequestXML(self): } authn_request = OneLogin_Saml2_Authn_Request(settings) - inflated = authn_request.get_request_as_xml() + inflated = authn_request.get_xml() self.assertRegexpMatches(inflated, '^ Date: Wed, 30 Nov 2016 16:03:51 +0100 Subject: [PATCH 164/352] Add more test. Fix bug getting last response on process_slo --- src/onelogin/saml2/auth.py | 6 +- .../pretty_signed_message_response.xml | 48 +++++++++++++++ tests/src/OneLogin/saml2_tests/auth_test.py | 61 +++++++++++++++++-- .../saml2_tests/authn_request_test.py | 12 ++-- .../saml2_tests/logout_request_test.py | 21 +++++++ .../saml2_tests/logout_response_test.py | 25 ++++++++ .../src/OneLogin/saml2_tests/response_test.py | 13 ++-- 7 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 tests/data/responses/pretty_signed_message_response.xml diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 1d2d5cc6..ab8fb2af 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -128,7 +128,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']: logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) - self.__logout_response = logout_response.get_xml() + self.__last_response = logout_response.get_xml() if not logout_response.is_valid(self.__request_data, request_id): self.__errors.append('invalid_logout_response') self.__error_reason = logout_response.get_error() @@ -150,7 +150,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.__logout_response = response_builder.get_xml() + self.__last_response = response_builder.get_xml() logout_response = response_builder.get_response() parameters = {'SAMLResponse': logout_response} @@ -465,7 +465,7 @@ def get_last_response_xml(self, pretty_print_if_possible=False): :rtype: string|None """ response = None - if self.__last_response: + if self.__last_response is not None: if isinstance(self.__last_response, basestring): response = self.__last_response else: 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 0d59da9d..1afaa32f 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -910,18 +910,26 @@ def testBuildResponseSignature(self): except Exception as e: self.assertIn("Trying to sign the SAMLResponse but can't load the SP private key", e.message) - def testGetLastDecryptedResponse(self): + 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) - decrypted_response = self.file_contents(join(self.data_path, 'responses', 'pretty_decrypted_valid_encrypted_assertion.xml')) - self.assertEqual(auth.get_last_response_xml(True), 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 testGetLastSentRequest(self): + def testGetLastAuthnRequest(self): settings = self.loadSettingsJSON() auth = OneLogin_Saml2_Auth({'http_host': 'localhost', 'script_name': 'thing'}, old_settings=settings) auth.login() @@ -940,6 +948,51 @@ def testGetLastSentRequest(self): ) 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>\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()) + + if __name__ == '__main__': if is_running_under_teamcity(): runner = TeamcityTestRunner() diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index b26e2f82..6aee012d 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -64,25 +64,25 @@ def testCreateRequest(self): self.assertRegexpMatches(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, b64encode(request)) + self.assertEqual(request, logout_request_processed.get_xml()) + if __name__ == '__main__': if is_running_under_teamcity(): diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index d69c739c..4624e872 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -385,6 +385,31 @@ def testIsValid(self): response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) self.assertTrue(response_3.is_valid(request_data)) + 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>\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()) + if __name__ == '__main__': if is_running_under_teamcity(): diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 9b709c18..df781245 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -60,18 +60,23 @@ def testConstruct(self): self.assertIsInstance(response_enc, OneLogin_Saml2_Response) - def test_get_decrypted_xml(self): + def test_get_xml_document(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', 'valid_encrypted_assertion.xml.base64')) + + 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.get_xml_document()), decrypted) + self.assertEqual(etree.tostring(response_2.get_xml_document()), decrypted) def testReturnNameId(self): """ From 367d6dc64e3c2f9b325856e279d8788fd2ff3ac6 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 2 Dec 2016 01:27:47 +0100 Subject: [PATCH 165/352] Improved inResponse validation on Responses --- src/onelogin/saml2/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 50b80685..10908741 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -108,7 +108,7 @@ def is_valid(self, request_data, request_id=None): # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided in_response_to = self.document.get('InResponseTo', None) - if in_response_to and request_id: + 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)) From 5cd77ca49f76d32fcd433816f643e643bb891818 Mon Sep 17 00:00:00 2001 From: Charles McBride Date: Fri, 2 Dec 2016 11:03:45 -0600 Subject: [PATCH 166/352] Fix invalid JSON in readme.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 41bda4a8..cc21560d 100644 --- a/README.md +++ b/README.md @@ -365,7 +365,7 @@ In addition to the required settings data (idp, sp), extra settings can be defin // Indicates a requirement for the // elements received by this SP to be encrypted. - 'wantAssertionsEncrypted' => false, + "wantAssertionsEncrypted": false, // Indicates a requirement for the NameID element on the SAMLResponse // received by this SP to be present. @@ -382,16 +382,16 @@ In addition to the required settings data (idp, sp), extra settings can be defin // Set to false and no AuthContext will be sent in the AuthNRequest, // Set true or don't present this parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' // Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'), - 'requestedAuthnContext': true, + "requestedAuthnContext": true, // Allows the authn comparison parameter to be set, defaults to 'exact' if the setting is not present. - 'requestedAuthnContextComparison': 'exact', + "requestedAuthnContextComparison": "exact", // In some environment you will need to set how long the published metadata of the Service Provider gonna be valid. // is possible to not set the 2 following parameters (or set to null) and default values will be set (2 days, 1 week) // Provide the desired Timestamp, for example 2015-06-26T20:00:00Z - 'metadataValidUntil': null, + "metadataValidUntil": null, // Provide the desired duration, for example PT518400S (6 days) - 'metadataCacheDuration': null, + "metadataCacheDuration": null, // Algorithm that the toolkit will use on signing process. Options: // 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' @@ -399,7 +399,7 @@ In addition to the required settings data (idp, sp), extra settings can be defin // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' - 'signatureAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, // Contact information template, it is recommended to supply From 963f297f63c01108dd0e2e71b509987a9c44b3cb Mon Sep 17 00:00:00 2001 From: Andrew Thal Date: Mon, 12 Dec 2016 15:41:13 -0500 Subject: [PATCH 167/352] Fix attributeConsumingService serviceName format in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc21560d..bb1d9e5a 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ This is the settings.json file: // attributeConsumingService. nameFormat, attributeValue and // friendlyName can be omitted "attributeConsumingService": { - "ServiceName": "SP test", + "serviceName": "SP test", "serviceDescription": "Test Service", "requestedAttributes": [ { From 0cd21ec9acda7018e513e8a0e28c3711b513a0a7 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 21 Dec 2016 10:22:13 +0100 Subject: [PATCH 168/352] Add option to raise exceptions on SAML message validation --- src/onelogin/saml2/logout_request.py | 7 +++++-- src/onelogin/saml2/logout_response.py | 8 ++++++-- src/onelogin/saml2/response.py | 4 +++- .../OneLogin/saml2_tests/logout_request_test.py | 16 ++++++++++++++++ .../saml2_tests/logout_response_test.py | 17 +++++++++++++++++ tests/src/OneLogin/saml2_tests/response_test.py | 12 ++++++++++++ 6 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 131c69cc..99339e54 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -260,12 +260,13 @@ 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, raise_exceptions=False): """ Checks if the Logout Request received is valid :param request_data: Request Data :type request_data: dict - + :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 """ @@ -348,6 +349,8 @@ def is_valid(self, request_data): debug = self.__settings.is_debug_active() if debug: print err.__str__() + if raise_exceptions: + raise err return False def get_error(self): diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 3df89064..498272e0 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -68,11 +68,13 @@ 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, 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 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 """ @@ -89,7 +91,7 @@ def is_valid(self, request_data, request_id=None): if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): - raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') + raise Exception('Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd') security = self.__settings.get_security_data() @@ -142,6 +144,8 @@ def is_valid(self, request_data, request_id=None): debug = self.__settings.is_debug_active() if debug: print err.__str__() + if raise_exceptions: + raise err return False def __query(self, query): diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 620aff71..bd72ac74 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -51,7 +51,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, raise_exceptions=False): """ Validates the response object. @@ -239,6 +239,8 @@ def is_valid(self, request_data, request_id=None): debug = self.__settings.is_debug_active() if debug: print err.__str__() + if raise_exceptions: + raise err return False def check_status(self): diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index df115ee0..cbdfdb33 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -312,6 +312,22 @@ def testIsValid(self): logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertTrue(logout_request5.is_valid(request_data)) + def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): + request = OneLogin_Saml2_Utils.deflate_and_base64_encode('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.assertRaisesRegexp(Exception, "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd"): + logout_request.is_valid(request_data, raise_exceptions=True) + def testIsValidSign(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 89d7bbf9..f0a1553b 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -228,6 +228,23 @@ def testIsInValidDestination(self): response_4 = OneLogin_Saml2_Logout_Response(settings, message_4) self.assertTrue(response_4.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.assertRaisesRegexp(Exception, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd"): + response.is_valid(request_data, raise_exceptions=True) + def testIsInValidSign(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 2f34efd8..2bd2aa7c 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1227,6 +1227,18 @@ def testIsValidEnc(self): response_7.is_valid(request_data) self.assertEqual('No Signature found. SAML Response rejected', response_7.get_error()) + def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): + message = b64encode('invalid') + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings.set_strict(True) + + response = OneLogin_Saml2_Response(settings, message) + + self.assertFalse(response.is_valid(self.get_request_data())) + + with self.assertRaisesRegexp(Exception, "Unsupported SAML version"): + response.is_valid(self.get_request_data(), raise_exceptions=True) + def testIsValidSign(self): """ Tests the is_valid method of the OneLogin_Saml2_Response From bc7bac63b72de309fd3361b6a83595da9fdf57c5 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 22 Dec 2016 09:06:17 +0100 Subject: [PATCH 169/352] Implement a more specific exception class for handling some validation errors. Improve tests --- src/onelogin/saml2/auth.py | 2 +- src/onelogin/saml2/errors.py | 77 +++++- src/onelogin/saml2/logout_request.py | 36 ++- src/onelogin/saml2/logout_response.py | 36 ++- src/onelogin/saml2/response.py | 230 ++++++++++++++---- src/onelogin/saml2/settings.py | 5 +- src/onelogin/saml2/utils.py | 55 +++-- tests/src/OneLogin/saml2_tests/auth_test.py | 11 +- .../saml2_tests/logout_request_test.py | 11 +- .../saml2_tests/logout_response_test.py | 7 +- .../src/OneLogin/saml2_tests/response_test.py | 36 +-- tests/src/OneLogin/saml2_tests/utils_test.py | 31 +-- 12 files changed, 410 insertions(+), 127 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index ab8fb2af..096440eb 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -432,7 +432,7 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On 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 ) dsig_ctx = xmlsec.DSigCtx() diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 63d98744..5a859fc1 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): """ @@ -51,3 +53,76 @@ 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 + + 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(message, basestring) + assert isinstance(code, int) + + if errors is not None: + message = message % errors + + Exception.__init__(self, message) + self.code = code \ No newline at end of file diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 99339e54..785c7c64 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -17,6 +17,7 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError class OneLogin_Saml2_Logout_Request(object): @@ -179,7 +180,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_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_data_nodes) == 1: @@ -191,7 +195,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 @@ -289,7 +296,10 @@ def is_valid(self, request_data, raise_exceptions=False): if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): - 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() @@ -318,11 +328,17 @@ def is_valid(self, request_data, raise_exceptions=False): # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom) 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 + ) if 'Signature' in get_data: if 'SigAlg' not in get_data: @@ -336,11 +352,17 @@ def is_valid(self, request_data, raise_exceptions=False): signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) if 'x509cert' not in idp_data or not idp_data['x509cert']: - raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') + raise OneLogin_Saml2_Error( + 'In order to validate the sign on the Logout Request, the x509cert of the IdP is required', + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) cert = idp_data['x509cert'] if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): - raise Exception('Signature validation failed. Logout Request rejected') + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Request rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True except Exception as err: diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 498272e0..f7fd486a 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -17,6 +17,7 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError class OneLogin_Saml2_Logout_Response(object): @@ -91,7 +92,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): - raise Exception('Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd') + raise OneLogin_Saml2_ValidationError( + 'Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd', + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) security = self.__settings.get_security_data() @@ -99,12 +103,18 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'): in_response_to = self.document.documentElement.getAttribute('InResponseTo') if request_id != in_response_to: - 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) @@ -113,11 +123,17 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): destination = self.document.documentElement.getAttribute('Destination') if destination != '': if 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 + ) if 'Signature' in get_data: if 'SigAlg' not in get_data: @@ -131,11 +147,17 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) if 'x509cert' not in idp_data or not idp_data['x509cert']: - raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') + raise OneLogin_Saml2_Error( + 'In order to validate the sign on the Logout Response, the x509cert of the IdP is required', + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) cert = idp_data['x509cert'] if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): - raise Exception('Signature validation failed. Logout Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True # pylint: disable=R0801 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index bd72ac74..04522b6d 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -17,6 +17,7 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError class OneLogin_Saml2_Response(object): @@ -61,6 +62,9 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP :type request_id: string + :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 """ @@ -68,15 +72,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() @@ -99,7 +112,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): self.__settings.is_debug_active() ) if not isinstance(res, Document): - 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: @@ -109,7 +125,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): self.__settings.is_debug_active() ) if not isinstance(res, Document): - 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) @@ -118,35 +137,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.get('wantAssertionsEncrypted', False): - 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.get('wantNameIdEncrypted', False): 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) @@ -156,25 +196,40 @@ 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 @@ -208,30 +263,46 @@ 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.get('wantAssertionsSigned', False) 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.get('wantMessagesSigned', False) 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) fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) # If find a Signature on the Response, validates it checking the original response - if has_signed_response: - # Raise exception if response signature is invalid - OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, raise_exceptions=True) + 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 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: - # Raise exception if assertion signature is invalid - OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=True) + 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 OneLogin_Saml2_ValidationError( + 'Signature validation failed. SAML Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True except Exception as err: @@ -258,7 +329,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): """ @@ -303,13 +377,19 @@ def get_issuers(self): if len(message_issuer_nodes) == 1: issuers.append(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.append(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)) @@ -336,10 +416,16 @@ def get_nameid_data(self): 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']: @@ -349,7 +435,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 @@ -407,7 +496,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']): @@ -469,14 +561,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 @@ -487,17 +588,26 @@ 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_exceptions=True): - raise Exception('Found an unexpected Signature Element. SAML Response rejected') + 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 @@ -527,12 +637,18 @@ def validate_signed_elements(self, signed_elements): if response_tag in signed_elements: expected_signature_nodes = OneLogin_Saml2_Utils.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 @@ -553,9 +669,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): @@ -621,7 +743,10 @@ def __decrypt_assertion(self, dom): 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_Utils.query(dom, '/samlp:Response/saml:EncryptedAssertion') if encrypted_assertion_nodes: @@ -629,15 +754,24 @@ def __decrypt_assertion(self, dom): if encrypted_data_nodes: keyinfo = OneLogin_Saml2_Utils.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 8e3b151a..b596e493 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -104,7 +104,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 16363a4d..ddde9d60 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -33,7 +33,8 @@ from dm.xmlsec.binding.tmpl import EncData, Signature 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 + if not globals().get('xmlsec_setup', False): xmlsec.initialize() @@ -652,8 +653,10 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None): xml = name_id_container.toxml() elem = fromstring(xml) + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) # Load the public cert mngr = xmlsec.KeysMngr() @@ -717,11 +720,17 @@ def get_status(dom): status_entry = OneLogin_Saml2_Utils.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_Utils.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 @@ -758,8 +767,10 @@ def decrypt_element(encrypted_data, key, debug=False): elif isinstance(encrypted_data, basestring): encrypted_data = fromstring(str(encrypted_data)) + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) mngr = xmlsec.KeysMngr() @@ -831,8 +842,10 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant else: raise Exception('Error parsing xml string') + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) # Sign the metadata with our private key. sign_algorithm_transform_map = { @@ -941,8 +954,10 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid else: raise Exception('Error parsing xml string') + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) xmlsec.addIDs(elem, ["ID"]) @@ -959,7 +974,7 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid 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 @@ -1008,8 +1023,10 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha else: raise Exception('Error parsing xml string') + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) xmlsec.addIDs(elem, ["ID"]) @@ -1059,8 +1076,10 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean """ + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) xmlsec.addIDs(elem, ["ID"]) @@ -1080,7 +1099,10 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger # 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.') + raise OneLogin_Saml2_Error( + 'Could not validate node signature: No certificate provided.', + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) @@ -1095,10 +1117,9 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger file_cert.close() dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) - try: - dsig_ctx.verify(signature_node) - except Exception: - raise Exception('Signature validation failed. SAML Response rejected') + + dsig_ctx.verify(signature_node) + return True @staticmethod @@ -1126,8 +1147,10 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean """ + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) dsig_ctx = xmlsec.DSigCtx() diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index cdd996e4..d7e50949 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -16,6 +16,7 @@ from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request +from onelogin.saml2.errors import OneLogin_Saml2_Error class OneLogin_Saml2_Auth_Test(unittest.TestCase): @@ -142,7 +143,7 @@ def testProcessNoResponse(self): """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) - with self.assertRaisesRegexp(Exception, 'SAML Response not found'): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML Response not found'): auth.process_response() self.assertEqual(auth.get_errors(), ['invalid_binding']) @@ -257,7 +258,7 @@ def testProcessNoSLO(self): Case No Message, An exception is throw """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) - with self.assertRaisesRegexp(Exception, 'SAML LogoutRequest/LogoutResponse not found'): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML LogoutRequest/LogoutResponse not found'): auth.process_slo(True) def testProcessSLOResponseInvalid(self): @@ -783,7 +784,7 @@ def testLogoutNoSLO(self): del settings_info['idp']['singleLogoutService'] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) - with self.assertRaisesRegexp(Exception, 'The IdP does not support Single Log Out'): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'The IdP does not support Single Log Out'): # The Header of the redirect produces an Exception auth.logout('http://example.com/returnto') @@ -872,7 +873,7 @@ 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.assertRaisesRegexp(Exception, "Trying to sign the SAMLRequest but can't load the SP private key"): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLRequest but can't load the SP private key"): auth2.build_request_signature(message, relay_state) def testBuildResponseSignature(self): @@ -891,7 +892,7 @@ 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.assertRaisesRegexp(Exception, "Trying to sign the SAMLResponse but can't load the SP private key"): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLResponse but can't load the SP private key"): auth2.build_response_signature(message, relay_state) def testGetLastSAMLResponse(self): diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index cbdfdb33..8c484b94 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -15,6 +15,7 @@ from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError class OneLogin_Saml2_Logout_Request_Test(unittest.TestCase): @@ -113,7 +114,7 @@ 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.assertRaisesRegexp(Exception, 'Key is required in order to decrypt the NameID'): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Key is required in order to decrypt the NameID'): OneLogin_Saml2_Logout_Request.get_nameid_data(request_2) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) @@ -130,11 +131,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(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid_data(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(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid_data(inv_request) def testGetNameId(self): @@ -146,7 +147,7 @@ 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.assertRaisesRegexp(Exception, 'Key is required in order to decrypt the NameID'): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Key is required in order to decrypt the NameID'): OneLogin_Saml2_Logout_Request.get_nameid(request_2) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) @@ -325,7 +326,7 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): self.assertFalse(logout_request.is_valid(request_data)) - with self.assertRaisesRegexp(Exception, "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd"): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd"): logout_request.is_valid(request_data, raise_exceptions=True) def testIsValidSign(self): diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index f0a1553b..3f86df19 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -15,6 +15,7 @@ from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.errors import OneLogin_Saml2_ValidationError class OneLogin_Saml2_Logout_Response_Test(unittest.TestCase): @@ -211,7 +212,7 @@ def testIsInValidDestination(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) self.assertFalse(response_2.is_valid(request_data)) - self.assertIn('The LogoutRequest was received at', response_2.get_error()) + self.assertIn('The LogoutResponse was received at', response_2.get_error()) # Empty destination dom = parseString(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) @@ -242,7 +243,7 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): self.assertFalse(response.is_valid(request_data)) - with self.assertRaisesRegexp(Exception, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd"): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd"): response.is_valid(request_data, raise_exceptions=True) def testIsInValidSign(self): @@ -356,7 +357,7 @@ def testIsValid(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) self.assertFalse(response_2.is_valid(request_data)) - self.assertIn('The LogoutRequest was received at', response_2.get_error()) + self.assertIn('The LogoutResponse was received at', response_2.get_error()) plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 2bd2aa7c..2803cd3e 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -17,6 +17,7 @@ from onelogin.saml2.response import OneLogin_Saml2_Response from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.errors import OneLogin_Saml2_ValidationError class OneLogin_Saml2_Response_Test(unittest.TestCase): @@ -99,14 +100,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(OneLogin_Saml2_ValidationError, 'Not NameID 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(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): response_5.get_nameid() json_settings['security']['wantNameId'] = False @@ -120,7 +121,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(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): response_7.get_nameid() json_settings['strict'] = True @@ -128,12 +129,12 @@ def testReturnNameId(self): xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64')) response_8 = OneLogin_Saml2_Response(settings, xml_5) - with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value'): response_8.get_nameid() xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) response_9 = OneLogin_Saml2_Response(settings, xml_6) - with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): response_9.get_nameid() def testGetNameIdData(self): @@ -174,14 +175,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(OneLogin_Saml2_ValidationError, 'Not NameID 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(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): response_5.get_nameid_data() json_settings['security']['wantNameId'] = False @@ -195,7 +196,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(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): response_7.get_nameid_data() json_settings['strict'] = True @@ -203,13 +204,13 @@ def testGetNameIdData(self): xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64')) response_8 = OneLogin_Saml2_Response(settings, xml_5) - with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value.'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value.'): response_8.get_nameid_data() self.assertTrue(False) xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) response_9 = OneLogin_Saml2_Response(settings, xml_6) - with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): response_9.get_nameid_data() def testCheckStatus(self): @@ -227,12 +228,12 @@ 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) - with self.assertRaisesRegexp(Exception, 'The status code of the Response was not Success, was Responder'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder'): response_2.check_status() 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) - with self.assertRaisesRegexp(Exception, 'The status code of the Response was not Success, was Responder -> something_is_wrong'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder -> something_is_wrong'): response_3.check_status() def testCheckOneCondition(self): @@ -343,12 +344,12 @@ 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) - with self.assertRaisesRegexp(Exception, 'Issuer of the Response not found or multiple.'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Issuer of the Response not found or multiple.'): response_4.get_issuers() xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_assertion.xml.base64')) response_5 = OneLogin_Saml2_Response(settings, xml_5) - with self.assertRaisesRegexp(Exception, 'Issuer of the Assertion not found or multiple.'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Issuer of the Assertion not found or multiple.'): response_5.get_issuers() def testGetSessionIndex(self): @@ -692,7 +693,7 @@ def testIsInValidDuplicatedAttrs(self): xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'duplicated_attributes.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) self.assertTrue(response.is_valid(self.get_request_data())) - with self.assertRaisesRegexp(Exception, 'Found an Attribute element with duplicated Name'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Found an Attribute element with duplicated Name'): response.get_attributes() def testIsInValidDestination(self): @@ -1105,9 +1106,8 @@ def testIsInValidCert(self): settings = OneLogin_Saml2_Settings(settings_info) xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - self.assertFalse(response.is_valid(self.get_request_data())) - self.assertIn('failed to load key from file', response.get_error()) + self.assertIn('Signature validation failed. SAML Response rejected', response.get_error()) def testIsInValidCert2(self): """ @@ -1236,7 +1236,7 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): self.assertFalse(response.is_valid(self.get_request_data())) - with self.assertRaisesRegexp(Exception, "Unsupported SAML version"): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Unsupported SAML version"): response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsValidSign(self): diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 7498096f..63439f68 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -15,6 +15,7 @@ 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.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError class OneLogin_Saml2_Utils_Test(unittest.TestCase): @@ -426,14 +427,14 @@ def testGetStatus(self): xml_inv = b64decode(xml_inv) dom_inv = etree.fromstring(xml_inv) - with self.assertRaisesRegexp(Exception, 'Missing valid Status on response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Missing Status on response'): OneLogin_Saml2_Utils.get_status(dom_inv) 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.assertRaisesRegexp(Exception, 'Missing valid Status Code on response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Missing Status Code on response'): OneLogin_Saml2_Utils.get_status(dom_inv2) def testParseDuration(self): @@ -837,7 +838,7 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True, raise_exceptions=True) xml_metadata_signed_2 = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings2.xml')) @@ -850,7 +851,7 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True, raise_exceptions=True) # modified cert @@ -860,9 +861,9 @@ def testValidateSign(self): f.close() self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x)) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('signature verification failed', 2)"): OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, raise_exceptions=True) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True, raise_exceptions=True) xml_response_msg_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_message_response2.xml.base64'))) @@ -877,7 +878,7 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True, raise_exceptions=True) xml_response_assert_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response2.xml.base64'))) @@ -890,7 +891,7 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True, raise_exceptions=True) xml_response_double_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'double_signed_response2.xml.base64'))) @@ -905,13 +906,13 @@ def testValidateSign(self): dom.firstChild.getAttributeNode('ID').nodeValue = u'_34fg27g212d63k1f923845324475802ac0fc24530b' # Reference validation failed self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom, cert_2)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(dom, cert_2, raise_exceptions=True) invalid_fingerprint = 'afe71c34ef740bc87434be13a2263d31271da1f9' # Wrong fingerprint self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Could not validate node signature: No certificate provided.'): OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint, raise_exceptions=True) dom_2 = parseString(xml_response_double_signed_2) @@ -919,31 +920,31 @@ def testValidateSign(self): dom_2.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp' # Modified message self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('signature verification failed', 2)"): OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2, raise_exceptions=True) # Try to validate directly the Assertion dom_3 = parseString(xml_response_double_signed_2) assert_elem_3 = dom_3.firstChild.firstChild.nextSibling.nextSibling.nextSibling self.assertFalse(OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."): OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2, raise_exceptions=True) # Wrong scheme no_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_signature.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_signed, cert)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."): OneLogin_Saml2_Utils.validate_sign(no_signed, cert, raise_exceptions=True) no_key = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_key, cert)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(no_key, cert, raise_exceptions=True) # Signature Wrapping attack wrapping_attack1 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'signature_wrapping_attack.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."): OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert, raise_exceptions=True) From a0b20152b88687e079030354a5d7334fbbcd81a0 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 22 Dec 2016 09:54:21 +0100 Subject: [PATCH 170/352] Fix pep8 --- src/onelogin/saml2/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 5a859fc1..ceb2730e 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -125,4 +125,4 @@ def __init__(self, message, code=0, errors=None): message = message % errors Exception.__init__(self, message) - self.code = code \ No newline at end of file + self.code = code From 731d01f3dfdbf5631af5529b4d775a93045ba96f Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 30 Dec 2016 00:58:13 +0100 Subject: [PATCH 171/352] Minor test fixes --- src/onelogin/saml2/errors.py | 1 + src/onelogin/saml2/logout_request.py | 8 ++++++-- tests/src/OneLogin/saml2_tests/response_test.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index ceb2730e..10c43d11 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -109,6 +109,7 @@ class OneLogin_Saml2_ValidationError(Exception): DUPLICATED_ATTRIBUTE_NAME_FOUND = 41 INVALID_SIGNATURE = 42 WRONG_NUMBER_OF_SIGNATURES = 43 + RESPONSE_EXPIRED = 44 def __init__(self, message, code=0, errors=None): """ diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 785c7c64..c49995e3 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -309,7 +309,10 @@ def is_valid(self, request_data, raise_exceptions=False): if dom.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.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 dom.get('Destination', None): @@ -322,7 +325,8 @@ def is_valid(self, request_data, raise_exceptions=False): { 'currentURL': current_url, 'destination': destination, - } + }, + OneLogin_Saml2_ValidationError.WRONG_DESTINATION ) # Check issuer diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 2803cd3e..ce940984 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -61,7 +61,7 @@ def testConstruct(self): self.assertIsInstance(response_enc, OneLogin_Saml2_Response) - def test_get_xml_document(self): + def testGetXMLDocument(self): """ Tests that we can retrieve the raw text of an encrypted XML response without going through intermediate steps From 3b94b705cb2840a0faa22c3e4bd015d58a043eaf Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 30 Dec 2016 17:04:52 +0100 Subject: [PATCH 172/352] Some minor improvements based on suggestions --- src/onelogin/saml2/errors.py | 2 ++ src/onelogin/saml2/logout_request.py | 2 +- src/onelogin/saml2/response.py | 2 +- src/onelogin/saml2/utils.py | 9 ++++++++- .../src/OneLogin/saml2_tests/logout_request_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/response_test.py | 12 ++++++------ 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 10c43d11..cd5e2fdc 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 c49995e3..93eabbf3 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -196,7 +196,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 04522b6d..70c4bdb5 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -417,7 +417,7 @@ def get_nameid_data(self): 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 ddde9d60..6fa42e2d 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1118,7 +1118,14 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) - 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. %s', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE, + err.__str__() + ) 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 8c484b94..b58404cc 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -131,11 +131,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(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key) inv_request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'no_nameId.xml')) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid_data(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 ce940984..593bb12b 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -100,14 +100,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(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, '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(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): response_5.get_nameid() json_settings['security']['wantNameId'] = False @@ -121,7 +121,7 @@ def testReturnNameId(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): response_7.get_nameid() json_settings['strict'] = True @@ -175,14 +175,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(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, '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(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): response_5.get_nameid_data() json_settings['security']['wantNameId'] = False @@ -196,7 +196,7 @@ def testGetNameIdData(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): response_7.get_nameid_data() json_settings['strict'] = True From cd8ea02ceadf3df1d91d42b2d58441611ca31e55 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 31 Dec 2016 10:00:29 +0100 Subject: [PATCH 173/352] 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 cd5e2fdc..532844fe 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -103,7 +103,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 70c4bdb5..e1b44f0f 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -763,7 +763,7 @@ def __decrypt_assertion(self, dom): 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 c3e696e013885714ea25e6f830129fc434b7dc44 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 2 Jan 2017 17:22:38 +0100 Subject: [PATCH 174/352] Minor fix on docs --- README.md | 7 +++++-- src/onelogin/saml2/response.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bb1d9e5a..cecfc25b 100644 --- a/README.md +++ b/README.md @@ -833,7 +833,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#### @@ -842,7 +842,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#### @@ -861,6 +861,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*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it). ####OneLogin_Saml2_LogoutRequest - logout_request.py#### @@ -875,6 +876,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#### @@ -887,6 +889,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/response.py b/src/onelogin/saml2/response.py index e1b44f0f..69346bf1 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -793,10 +793,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 667b132ca30df5672ac25e15d560a5a708b6074e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 3 Jan 2017 17:10:01 +0100 Subject: [PATCH 175/352] Fix name --- 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 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 532844fe..b85016a6 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -99,7 +99,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 93eabbf3..8dea50ef 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -341,7 +341,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 ) if 'Signature' in get_data: diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index f7fd486a..2653b210 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -132,7 +132,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 ) if 'Signature' in get_data: diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 69346bf1..a9843dc6 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -271,7 +271,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if security.get('wantAssertionsSigned', False) 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.get('wantMessagesSigned', False) and not has_signed_response: From 74305d53c6cd92e0ffd64289111b7e0dd98a0a03 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 3 Jan 2017 22:23:48 +0100 Subject: [PATCH 176/352] 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 a9843dc6..051e1ccf 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -271,13 +271,13 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if security.get('wantAssertionsSigned', False) 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.get('wantMessagesSigned', False) 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): From 7e31bb8cb92bc9b0fdfcb9384dad889e5709f310 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 10 Jan 2017 10:28:44 +0100 Subject: [PATCH 177/352] Add CVE reference to the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cecfc25b..0c88f538 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml). #### Warning #### -Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. +Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json) python-saml < v2.2.0 is vulnerable and allows signature wrapping! From a00e79403d53c7429a21aed53cbf71b8800bd6e2 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 11 Jan 2017 13:43:14 +0100 Subject: [PATCH 178/352] Release 2.2.1 --- changelog.md | 8 ++++++++ setup.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 40b03d4f..5346c467 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # python-saml changelog +### 2.2.1 (Jan 11, 2017) +* [#175]((https://github.com/onelogin/python-saml/pull/175) Optionally raise detailed exceptions vs. returning False. +Implement a more specific exception class for handling some validation errors. Improve/Fix tests +* [#171](https://github.com/onelogin/python-saml/pull/171) Add hooks to retrieve last-sent and last-received requests and responses +* Improved inResponse validation on Responses +* [#173](https://github.com/onelogin/python-saml/pull/173) Fix attributeConsumingService serviceName format in README + + ### 2.2.0 (Oct 14, 2016) * Several security improvements: * Conditions element required and unique. diff --git a/setup.py b/setup.py index faa5708f..dfbd4b8e 100644 --- a/setup.py +++ b/setup.py @@ -9,10 +9,10 @@ setup( name='python-saml', - version='2.2.0', + version='2.2.1', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Operating System :: OS Independent', From 7e4d502c4b64ac6075577197a0096de89fb1ad01 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 6 Mar 2017 13:28:13 +0100 Subject: [PATCH 179/352] Make the Issuer on the Response Optional --- src/onelogin/saml2/errors.py | 2 +- src/onelogin/saml2/response.py | 15 ++++++++------- tests/src/OneLogin/saml2_tests/response_test.py | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index b85016a6..0675fb4a 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -94,7 +94,7 @@ class OneLogin_Saml2_ValidationError(Exception): WRONG_DESTINATION = 24 EMPTY_DESTINATION = 25 WRONG_AUDIENCE = 26 - ISSUER_NOT_FOUND_IN_RESPONSE = 27 + ISSUER_MULTIPLE_IN_RESPONSE = 27 ISSUER_NOT_FOUND_IN_ASSERTION = 28 WRONG_ISSUER = 29 SESSION_EXPIRED = 30 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 051e1ccf..49e4f734 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -374,13 +374,14 @@ def get_issuers(self): issuers = [] message_issuer_nodes = OneLogin_Saml2_Utils.query(self.document, '/samlp:Response/saml:Issuer') - if len(message_issuer_nodes) == 1: - issuers.append(message_issuer_nodes[0].text) - else: - raise OneLogin_Saml2_ValidationError( - 'Issuer of the Response not found or multiple.', - OneLogin_Saml2_ValidationError.ISSUER_NOT_FOUND_IN_RESPONSE - ) + if len(message_issuer_nodes) > 0: + if len(message_issuer_nodes) == 1: + issuers.append(message_issuer_nodes[0].text) + else: + raise OneLogin_Saml2_ValidationError( + 'Issuer of the Response is multiple.', + OneLogin_Saml2_ValidationError.ISSUER_MULTIPLE_IN_RESPONSE + ) assertion_issuer_nodes = self.__query_assertion('/saml:Issuer') if len(assertion_issuer_nodes) == 1: diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 593bb12b..c1538617 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -344,8 +344,8 @@ 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) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Issuer of the Response not found or multiple.'): - response_4.get_issuers() + response_4.get_issuers() + self.assertEqual(['https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_4.get_issuers()) xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_assertion.xml.base64')) response_5 = OneLogin_Saml2_Response(settings, xml_5) From 616f9fd2027ecc156765575ff57fa8a8aa8d9ea6 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 6 Mar 2017 13:33:58 +0100 Subject: [PATCH 180/352] Validate serial number as string to work around libxml2 limitation --- src/onelogin/saml2/schemas/xmldsig-core-schema.xsd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd b/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd index dd5254bb..6f5acc75 100644 --- a/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd +++ b/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd @@ -188,7 +188,7 @@ - + From c3bad3fd361185aa30f16e24e185831902bc06d5 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 6 Mar 2017 13:35:34 +0100 Subject: [PATCH 181/352] Fix pep8 issue --- tests/src/OneLogin/saml2_tests/response_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index c1538617..bba964e4 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -345,7 +345,7 @@ 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) response_4.get_issuers() - self.assertEqual(['https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_4.get_issuers()) + self.assertEqual(['https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_4.get_issuers()) xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_assertion.xml.base64')) response_5 = OneLogin_Saml2_Response(settings, xml_5) From 162156770e01ee598ac54c56195c71faea47cfa9 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 6 Mar 2017 20:47:27 +0100 Subject: [PATCH 182/352] Minor typos on Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0c88f538..b173844a 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ since 2002, but lately it is becoming popular due its advantages: General description ------------------- -OneLogin's SAML Python toolkit lets you turn you Python application into an SP -(Service Provider) that can connect to a IdP (Identity Provider). +OneLogin's SAML Python toolkit lets you turn your Python application into a SP +(Service Provider) that can be connected to an IdP (Identity Provider). Supports: From a5423c366c7ad2089150c152a62cf9433706b419 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 11 Mar 2017 01:19:51 +0100 Subject: [PATCH 183/352] Fix #181 Add DigestMethod support. (Add sign_algorithm and digest_algorithm parameters to sign_metadata and add_sign --- README.md | 9 +++++- demo-bottle/saml/advanced_settings.json | 3 +- demo-django/saml/advanced_settings.json | 3 +- demo-flask/saml/advanced_settings.json | 3 +- src/onelogin/saml2/metadata.py | 7 +++-- src/onelogin/saml2/settings.py | 8 +++++- src/onelogin/saml2/utils.py | 18 ++++++++++-- .../src/OneLogin/saml2_tests/metadata_test.py | 21 ++++++++++++++ tests/src/OneLogin/saml2_tests/utils_test.py | 28 +++++++++++++++++++ 9 files changed, 91 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b173844a..0cdd9de7 100644 --- a/README.md +++ b/README.md @@ -399,7 +399,14 @@ In addition to the required settings data (idp, sp), extra settings can be defin // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + + // Algorithm that the toolkit will use on digest process. Options: + // 'http://www.w3.org/2000/09/xmldsig#sha1' + // 'http://www.w3.org/2001/04/xmlenc#sha256' + // 'http://www.w3.org/2001/04/xmldsig-more#sha384' + // 'http://www.w3.org/2001/04/xmlenc#sha512' + 'digestAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#sha1 }, // Contact information template, it is recommended to supply diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json index 5b9396ff..47ec8c28 100644 --- a/demo-bottle/saml/advanced_settings.json +++ b/demo-bottle/saml/advanced_settings.json @@ -10,7 +10,8 @@ "wantNameId" : true, "wantNameIdEncrypted": false, "wantAssertionsEncrypted": false, - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" }, "contactPerson": { "technical": { diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json index ed0ba4ab..7efb5d1b 100644 --- a/demo-django/saml/advanced_settings.json +++ b/demo-django/saml/advanced_settings.json @@ -10,7 +10,8 @@ "wantNameId" : true, "wantNameIdEncrypted": false, "wantAssertionsEncrypted": false, - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" }, "contactPerson": { "technical": { diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json index ed0ba4ab..7efb5d1b 100644 --- a/demo-flask/saml/advanced_settings.json +++ b/demo-flask/saml/advanced_settings.json @@ -10,7 +10,8 @@ "wantNameId" : true, "wantNameIdEncrypted": false, "wantAssertionsEncrypted": false, - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" }, "contactPerson": { "technical": { diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index fba83145..d8bc0c75 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -202,7 +202,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N return metadata @staticmethod - def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): + def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1, digest_algorithm=OneLogin_Saml2_Constants.SHA1): """ Signs the metadata with the key/cert provided @@ -218,10 +218,13 @@ def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.R :param sign_algorithm: Signature algorithm method :type sign_algorithm: string + :param digest_algorithm: Digest algorithm method + :type digest_algorithm: string + :returns: Signed Metadata :rtype: string """ - return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm) + return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm, digest_algorithm) @staticmethod def add_x509_key_descriptors(metadata, cert=None): diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index b596e493..362819c5 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -283,6 +283,9 @@ def __add_default_values(self): # Signature Algorithm self.__security.setdefault('signatureAlgorithm', OneLogin_Saml2_Constants.RSA_SHA1) + # Digest Algorithm + self.__security.setdefault('digestAlgorithm', OneLogin_Saml2_Constants.SHA1) + # AttributeStatement required by default self.__security.setdefault('wantAttributeStatement', True) @@ -639,7 +642,10 @@ def get_sp_metadata(self): cert_metadata_file ) - metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key_metadata, cert_metadata) + signature_algorithm = self.__security['signatureAlgorithm'] + digest_algorithm = self.__security['digestAlgorithm'] + + metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key_metadata, cert_metadata, signature_algorithm, digest_algorithm) return metadata diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 6fa42e2d..00fefae3 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -797,7 +797,7 @@ def write_temp_file(content): return f_temp @staticmethod - def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): + def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1, digest_algorithm=OneLogin_Saml2_Constants.SHA1): """ Adds signature key and senders certificate to an element (Message or Assertion). @@ -816,6 +816,12 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant :param sign_algorithm: Signature algorithm method :type sign_algorithm: string + + :param digest_algorithm: Digest algorithm method + :type digest_algorithm: string + + :returns: Signed XML + :rtype: string """ if xml is None or xml == '': raise Exception('Empty string supplied as input') @@ -866,7 +872,15 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant else: elem[0].insert(0, signature) - ref = signature.addReference(xmlsec.TransformSha1) + digest_algorithm_transform_map = { + OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1, + OneLogin_Saml2_Constants.SHA256: xmlsec.TransformSha256, + OneLogin_Saml2_Constants.SHA384: xmlsec.TransformSha384, + OneLogin_Saml2_Constants.SHA512: xmlsec.TransformSha512 + } + digest_algorithm_transform = digest_algorithm_transform_map.get(digest_algorithm, xmlsec.TransformSha1) + + ref = signature.addReference(digest_algorithm_transform) ref.addTransform(xmlsec.TransformEnveloped) ref.addTransform(xmlsec.TransformExclC14N) diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 85c951cd..02e551e7 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -14,6 +14,7 @@ from onelogin.saml2.metadata import OneLogin_Saml2_Metadata from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.constants import OneLogin_Saml2_Constants class OneLogin_Saml2_Metadata_Test(unittest.TestCase): @@ -222,12 +223,32 @@ def testSignMetadata(self): self.assertIn('', signed_metadata) self.assertIn('', signed_metadata) + self.assertIn('', signed_metadata) self.assertIn('\n', signed_metadata) with self.assertRaisesRegexp(Exception, 'Empty string supplied as input'): OneLogin_Saml2_Metadata.sign_metadata('', key, cert) + signed_metadata_2 = OneLogin_Saml2_Metadata.sign_metadata(metadata, key, cert, OneLogin_Saml2_Constants.RSA_SHA256, OneLogin_Saml2_Constants.SHA384) + self.assertIn('', signed_metadata_2) + + self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', signed_metadata_2) + + self.assertIn('', signed_metadata_2) + self.assertIn('', signed_metadata_2) + self.assertIn('', signed_metadata_2) + self.assertIn('\n', signed_metadata_2) + def testAddX509KeyDescriptors(self): """ Tests the addX509KeyDescriptors method of the OneLogin_Saml2_Metadata diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 63439f68..459b0996 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -810,6 +810,34 @@ def testAddSign(self): with self.assertRaisesRegexp(Exception, 'Error parsing xml string'): OneLogin_Saml2_Utils.add_sign(1, key, cert) + def testAddSignCheckAlg(self): + """ + Tests the add_sign method of the OneLogin_Saml2_Utils + Case: Review signature & digest algorithm + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + key = settings.get_sp_key() + cert = settings.get_sp_cert() + + xml_authn = b64decode(self.file_contents(join(self.data_path, 'requests', 'authn_request.xml.base64'))) + xml_authn_signed = OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert) + self.assertIn('', xml_authn_signed) + self.assertIn('', xml_authn_signed) + self.assertIn('', xml_authn_signed) + self.assertIn('', xml_authn_signed) + + xml_authn_signed_2 = OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert, False, OneLogin_Saml2_Constants.RSA_SHA256, OneLogin_Saml2_Constants.SHA384) + self.assertIn('', xml_authn_signed_2) + self.assertIn('', xml_authn_signed_2) + self.assertIn('', xml_authn_signed_2) + self.assertIn('', xml_authn_signed_2) + + xml_authn_signed_3 = OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert, False, OneLogin_Saml2_Constants.RSA_SHA384, OneLogin_Saml2_Constants.SHA512) + self.assertIn('', xml_authn_signed_3) + self.assertIn('', xml_authn_signed_3) + self.assertIn('', xml_authn_signed_3) + self.assertIn('', xml_authn_signed_3) + def testValidateSign(self): """ Tests the validate_sign method of the OneLogin_Saml2_Utils From d00ee51e96a2cd25870e4fa9ed7819b1bfa7a38f Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 11 Mar 2017 01:25:42 +0100 Subject: [PATCH 184/352] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0cdd9de7..ee30cc5a 100644 --- a/README.md +++ b/README.md @@ -406,7 +406,7 @@ In addition to the required settings data (idp, sp), extra settings can be defin // 'http://www.w3.org/2001/04/xmlenc#sha256' // 'http://www.w3.org/2001/04/xmldsig-more#sha384' // 'http://www.w3.org/2001/04/xmlenc#sha512' - 'digestAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#sha1 + "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" }, // Contact information template, it is recommended to supply From 52e95da0c613fdef56c0ce4ddcfdea11d0ff7f4b Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 11 Mar 2017 18:48:41 +0100 Subject: [PATCH 185/352] Fix #184. Be able to provide a NameIDFormat to LogoutRequest --- src/onelogin/saml2/auth.py | 11 +++ src/onelogin/saml2/logout_request.py | 14 ++- src/onelogin/saml2/response.py | 13 +++ tests/src/OneLogin/saml2_tests/auth_test.py | 92 +++++++++++++++++++ .../src/OneLogin/saml2_tests/response_test.py | 58 ++++++++++++ 5 files changed, 185 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 096440eb..a0389d67 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -52,6 +52,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path) self.__attributes = [] self.__nameid = None + self.__nameid_format = None self.__session_index = None self.__session_expiration = None self.__authenticated = False @@ -97,6 +98,7 @@ def process_response(self, request_id=None): if response.is_valid(self.__request_data, request_id): self.__attributes = response.get_attributes() self.__nameid = response.get_nameid() + self.__nameid_format = response.get_nameid_format() self.__session_index = response.get_session_index() self.__session_expiration = response.get_session_not_on_or_after() self.__authenticated = True @@ -214,6 +216,15 @@ def get_nameid(self): """ return self.__nameid + def get_nameid_format(self): + """ + Returns the nameID Format. + + :returns: NameID Format + :rtype: string|None + """ + return self.__nameid_format + def get_session_index(self): """ Returns the SessionIndex from the AuthnStatement. diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 8dea50ef..a5e6181d 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -29,7 +29,7 @@ class OneLogin_Saml2_Logout_Request(object): """ - def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None): + def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None): """ Constructs the Logout Request object. @@ -47,6 +47,9 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= :param nq: IDP Name Qualifier :type: string + + :param name_id_format: The NameID Format that will be set in the LogoutRequest. + :type: string """ self.__settings = settings self.__error = None @@ -67,7 +70,10 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= cert = idp_data['x509cert'] if name_id is not None: - nameIdFormat = sp_data['NameIDFormat'] + if name_id_format is not None: + nameIdFormat = name_id_format + else: + nameIdFormat = sp_data['NameIDFormat'] spNameQualifier = None else: name_id = idp_data['entityId'] @@ -78,7 +84,9 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= name_id, spNameQualifier, nameIdFormat, - cert + cert, + False, + nq ) if session_index: diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 49e4f734..93f650a3 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -457,6 +457,19 @@ def get_nameid(self): nameid_value = nameid_data['Value'] return nameid_value + def get_nameid_format(self): + """ + Gets the NameID Format provided by the SAML Response from the IdP + + :returns: NameID Format + :rtype: string|None + """ + nameid_format = None + nameid_data = self.get_nameid_data() + if nameid_data and 'Format' in nameid_data.keys(): + nameid_format = nameid_data['Format'] + return nameid_format + def get_session_not_on_or_after(self): """ Gets the SessionNotOnOrAfter from the AuthnStatement diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index d7e50949..135dc99c 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -857,6 +857,98 @@ def testSetStrict(self): with self.assertRaises(AssertionError): auth.set_strict('42') + def testIsAuthenticated(self): + """ + Tests the is_authenticated method of the OneLogin_Saml2_Auth + """ + request_data = self.get_request() + del request_data['get_data'] + message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + auth.process_response() + self.assertFalse(auth.is_authenticated()) + + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + auth.process_response() + self.assertTrue(auth.is_authenticated()) + + def testGetNameId(self): + """ + Tests the get_nameid method of the OneLogin_Saml2_Auth + """ + settings = self.loadSettingsJSON() + request_data = self.get_request() + del request_data['get_data'] + message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_response() + self.assertFalse(auth.is_authenticated()) + self.assertEqual(auth.get_nameid(), None) + + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_response() + self.assertTrue(auth.is_authenticated()) + self.assertEqual("492882615acf31c8096b627245d76ae53036c090", auth.get_nameid()) + + settings_2 = self.loadSettingsJSON('settings2.json') + message = self.file_contents(join(self.data_path, 'responses', 'signed_message_encrypted_assertion2.xml.base64')) + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_2) + auth.process_response() + self.assertTrue(auth.is_authenticated()) + self.assertEqual("25ddd7d34a7d79db69167625cda56a320adf2876", auth.get_nameid()) + + def testGetNameIdFormat(self): + """ + Tests the get_nameid_format method of the OneLogin_Saml2_Auth + """ + settings = self.loadSettingsJSON() + request_data = self.get_request() + del request_data['get_data'] + message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_response() + self.assertFalse(auth.is_authenticated()) + self.assertEqual(auth.get_nameid_format(), None) + + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_response() + self.assertTrue(auth.is_authenticated()) + self.assertEqual("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", auth.get_nameid_format()) + + settings_2 = self.loadSettingsJSON('settings2.json') + message = self.file_contents(join(self.data_path, 'responses', 'signed_message_encrypted_assertion2.xml.base64')) + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_2) + auth.process_response() + self.assertTrue(auth.is_authenticated()) + self.assertEqual("urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", auth.get_nameid_format()) + def testBuildRequestSignature(self): """ Tests the build_request_signature method of the OneLogin_Saml2_Auth diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index bba964e4..8c499e9e 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -137,6 +137,64 @@ def testReturnNameId(self): with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): response_9.get_nameid() + def testReturnNameIdFormat(self): + """ + Tests the get_nameid_format method of the OneLogin_Saml2_Response + """ + json_settings = self.loadSettingsJSON() + + settings = OneLogin_Saml2_Settings(json_settings) + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response.get_nameid_format()) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', response_2.get_nameid_format()) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) + response_3 = OneLogin_Saml2_Response(settings, xml_3) + self.assertEqual('urn:oasis:names:tc:SAML:2.0:nameid-format:transient', response_3.get_nameid_format()) + + 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(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): + response_4.get_nameid_format() + + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + + response_5 = OneLogin_Saml2_Response(settings, xml_4) + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): + response_5.get_nameid_format() + + json_settings['security']['wantNameId'] = False + settings = OneLogin_Saml2_Settings(json_settings) + + response_6 = OneLogin_Saml2_Response(settings, xml_4) + nameid_6 = response_6.get_nameid_format() + self.assertIsNone(nameid_6) + + del json_settings['security']['wantNameId'] + settings = OneLogin_Saml2_Settings(json_settings) + + response_7 = OneLogin_Saml2_Response(settings, xml_4) + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): + response_7.get_nameid_format() + + 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) + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value'): + response_8.get_nameid_format() + + xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) + response_9 = OneLogin_Saml2_Response(settings, xml_6) + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): + response_9.get_nameid_format() + def testGetNameIdData(self): """ Tests the get_nameid_data method of the OneLogin_Saml2_Response From d318d3f961727bc8c3efc1e340cef9424a18d3d8 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 11 Mar 2017 20:37:27 +0100 Subject: [PATCH 186/352] Another test --- tests/src/OneLogin/saml2_tests/logout_request_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index b58404cc..56494e19 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -138,6 +138,10 @@ def testGetNameIdData(self): with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid_data(inv_request) + logout_request = OneLogin_Saml2_Logout_Request(settings, None, expected_name_id_data['Value'], None, expected_name_id_data['Value'], expected_name_id_data['Format']) + dom = parseString(logout_request.get_xml()) + name_id_data_3 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom) + def testGetNameId(self): """ Tests the get_nameid of the OneLogin_Saml2_LogoutRequest From 2fd0105a511f7600b4f9b52afaa1b232fc1d2bab Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 11 Mar 2017 21:12:28 +0100 Subject: [PATCH 187/352] Fix test --- tests/src/OneLogin/saml2_tests/logout_request_test.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 56494e19..5b5cd56b 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -134,13 +134,21 @@ def testGetNameIdData(self): with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key) + idp_data = settings.get_idp_data() + expected_name_id_data = { + 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress', + 'NameQualifier': idp_data['entityId'], + 'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69' + } + inv_request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'no_nameId.xml')) with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid_data(inv_request) - logout_request = OneLogin_Saml2_Logout_Request(settings, None, expected_name_id_data['Value'], None, expected_name_id_data['Value'], expected_name_id_data['Format']) + logout_request = OneLogin_Saml2_Logout_Request(settings, None, expected_name_id_data['Value'], None, idp_data['entityId'], expected_name_id_data['Format']) dom = parseString(logout_request.get_xml()) name_id_data_3 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom) + self.assertEqual(expected_name_id_data, name_id_data_3) def testGetNameId(self): """ From c3019b9e2a9cacfe5a7a0d864fee9135e5f369c9 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 12 Mar 2017 11:24:09 +0100 Subject: [PATCH 188/352] More NameID Format improvements --- src/onelogin/saml2/auth.py | 10 ++++++++-- src/onelogin/saml2/logout_request.py | 17 +++++++++++++++++ tests/src/OneLogin/saml2_tests/auth_test.py | 15 +++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index a0389d67..6e56467a 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -318,7 +318,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_ parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) return self.redirect_to(self.get_sso_url(), parameters) - def logout(self, return_to=None, name_id=None, session_index=None, nq=None): + def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None): """ Initiates the SLO process. @@ -334,6 +334,9 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None): :param nq: IDP Name Qualifier :type: string + :param name_id_format: The NameID Format that will be set in the LogoutRequest. + :type: string + :returns: Redirection url """ slo_url = self.get_slo_url() @@ -345,12 +348,15 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None): if name_id is None and self.__nameid is not None: name_id = self.__nameid + if name_id_format is None and self.__nameid_format is not None: + name_id_format = self.__nameid_format logout_request = OneLogin_Saml2_Logout_Request( self.__settings, name_id=name_id, session_index=session_index, - nq=nq + nq=nq, + name_id_format=name_id_format ) self.__last_request = logout_request.get_xml() self.__last_request_id = logout_request.id diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index a5e6181d..44dd4a52 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -231,6 +231,23 @@ def get_nameid(request, key=None): name_id = OneLogin_Saml2_Logout_Request.get_nameid_data(request, key) return name_id['Value'] + @staticmethod + def get_nameid_format(request, key=None): + """ + Gets the NameID Format of the Logout Request Message + :param request: Logout Request Message + :type request: string|DOMDocument + :param key: The SP key + :type key: string + :return: Name ID Value + :rtype: string + """ + name_id_format = None + name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(request, key) + if name_id_data and 'Format' in name_id_data.keys(): + name_id_format = name_id_data['Format'] + return name_id_format + @staticmethod def get_issuer(request): """ diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 135dc99c..9c42c0bc 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -826,6 +826,7 @@ def testLogoutNameID(self): auth.process_response() name_id_from_response = auth.get_nameid() + name_id_format_from_response = auth.get_nameid_format() target_url = auth.logout() parsed_query = parse_qs(urlparse(target_url)[4]) @@ -833,7 +834,21 @@ def testLogoutNameID(self): logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]) name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request) + name_id_format_from_request = OneLogin_Saml2_Logout_Request.get_nameid_format(logout_request) self.assertEqual(name_id_from_response, name_id_from_request) + self.assertEqual(name_id_format_from_response, name_id_format_from_request) + + new_name_id = "new_name_id" + new_name_id_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + target_url_2 = auth.logout(name_id=new_name_id, name_id_format=new_name_id_format) + parsed_query = parse_qs(urlparse(target_url_2)[4]) + self.assertIn('SAMLRequest', parsed_query) + logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]) + + name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request) + name_id_format_from_request = OneLogin_Saml2_Logout_Request.get_nameid_format(logout_request) + self.assertEqual(new_name_id, name_id_from_request) + self.assertEqual(new_name_id_format, name_id_format_from_request) def testSetStrict(self): """ From 37edd009764874b22900228a31ce461ff853f8a7 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 8 Apr 2017 15:12:11 +0200 Subject: [PATCH 189/352] Allows underscores in URL hosts --- src/onelogin/saml2/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 362819c5..1c17ec3e 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -25,7 +25,7 @@ # Released under a BSD 3-Clause License url_regex = re.compile( r'^(?:[a-z0-9\.\-]*)://' # scheme is validated separately - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... + r'(?:(?:[A-Z0-9_](?:[A-Z0-9-_]{0,61}[A-Z0-9_])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... r'localhost|' # localhost... r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 From cebc37b67ffa440b4e0e98f5852045b7fd1b6ca3 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 16 Apr 2017 18:07:24 +0200 Subject: [PATCH 190/352] Add Pyramid demo example --- README.md | 79 +++++++++ demo_pyramid/.coveragerc | 3 + demo_pyramid/.gitignore | 22 +++ demo_pyramid/CHANGES.txt | 4 + demo_pyramid/MANIFEST.in | 2 + demo_pyramid/README.txt | 29 ++++ demo_pyramid/demo_pyramid/__init__.py | 19 +++ .../demo_pyramid/saml/advanced_settings.json | 33 ++++ demo_pyramid/demo_pyramid/saml/certs/README | 11 ++ demo_pyramid/demo_pyramid/saml/settings.json | 30 ++++ .../demo_pyramid/static/pyramid-16x16.png | Bin 0 -> 1319 bytes demo_pyramid/demo_pyramid/static/pyramid.png | Bin 0 -> 12901 bytes demo_pyramid/demo_pyramid/static/theme.css | 154 ++++++++++++++++++ .../demo_pyramid/templates/attrs.jinja2 | 31 ++++ .../demo_pyramid/templates/index.jinja2 | 55 +++++++ .../demo_pyramid/templates/layout.jinja2 | 64 ++++++++ demo_pyramid/demo_pyramid/views.py | 126 ++++++++++++++ demo_pyramid/development.ini | 59 +++++++ demo_pyramid/production.ini | 53 ++++++ demo_pyramid/setup.py | 45 +++++ 20 files changed, 819 insertions(+) create mode 100644 demo_pyramid/.coveragerc create mode 100644 demo_pyramid/.gitignore create mode 100644 demo_pyramid/CHANGES.txt create mode 100644 demo_pyramid/MANIFEST.in create mode 100644 demo_pyramid/README.txt create mode 100644 demo_pyramid/demo_pyramid/__init__.py create mode 100644 demo_pyramid/demo_pyramid/saml/advanced_settings.json create mode 100644 demo_pyramid/demo_pyramid/saml/certs/README create mode 100644 demo_pyramid/demo_pyramid/saml/settings.json create mode 100644 demo_pyramid/demo_pyramid/static/pyramid-16x16.png create mode 100644 demo_pyramid/demo_pyramid/static/pyramid.png create mode 100644 demo_pyramid/demo_pyramid/static/theme.css create mode 100644 demo_pyramid/demo_pyramid/templates/attrs.jinja2 create mode 100644 demo_pyramid/demo_pyramid/templates/index.jinja2 create mode 100644 demo_pyramid/demo_pyramid/templates/layout.jinja2 create mode 100644 demo_pyramid/demo_pyramid/views.py create mode 100644 demo_pyramid/development.ini create mode 100644 demo_pyramid/production.ini create mode 100644 demo_pyramid/setup.py diff --git a/README.md b/README.md index ee30cc5a..275dd752 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,12 @@ This folder contains a Bottle project that will be used as demo to show how to a This folder contains a Flask project that will be used as demo to show how to add SAML support to the Flask Framework. 'index.py' is the main flask file that has all the code, this file uses the templates stored at the 'templates' folder. In the 'saml' folder we found the 'certs' folder to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). + +#### demo_pyramid #### + +This folder contains a Pyramid project that will be used as demo to show how to add SAML support to the [Pyramid Web Framework](http://docs.pylonsproject.org/projects/pyramid/en/latest/). '\_\_init__.py' is the main file that configures the app and its routes, 'views.py' is where all the logic and SAML handling takes place, and the templates are stored in the 'templates' folder. The 'saml' folder is the same as in the other two demos. + + #### setup.py #### Setup script is the centre of all activity in building, distributing, and installing modules. @@ -1145,3 +1151,76 @@ Once the SP is configured, the metadata of the SP is published at the /metadata ####How it works#### This demo works very similar to the flask-demo (We did it intentionally). + + +### Demo Pyramid ### + +Unlike the other two projects, you don't need a pre-existing virtualenv to get +up and running here, since Pyramid comes from the +[buildout](http://www.buildout.org/en/latest/) school of thought. + +To run the demo you need to install Pyramid, the requirements, etc.: +``` + cd demo_pyramid + python -m venv env + env/bin/pip install --upgrade pip setuptools + env/bin/pip install -e ".[testing]" +``` + +Next, edit the settings in `demo_pyramid/saml/settings.json`. (Pyramid runs on +port 6543 by default.) + +Now you can run the demo like this: +``` + env/bin/pserve development.ini +``` + +If that worked, the demo is now running at http://localhost:6543. + +####Content#### + +The Pyramid project contains: + + +* ***\_\_init__.py*** is the main Pyramid file that configures the app and its routes. + +* ***views.py*** is where all the SAML handling takes place. + +* ***templates*** is the folder where Pyramid stores the templates of the project. It was implemented a layout.jinja2 template that is extended by index.jinja2 and attrs.jinja2, the templates of our simple demo that shows messages, user attributes when available and login and logout links. + +* ***saml*** is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). + + +####SP setup#### + +The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In demo_pyramid the first method is used. + +In the views.py file we define the SAML_PATH, which will target the 'saml' folder. We require it in order to load the settings files. + +First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. + +####IdP setup#### + +Once the SP is configured, the metadata of the SP is published at the /metadata/ url. Based on that info, configure the IdP. + +####How it works#### + +1. First time you access to the main view 'http://localhost:6543', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view). + + 2. When you click: + + 2.1 in the first link, we access to /?sso (index view). An AuthNRequest is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view. + + 2.2 in the second link we access to /?attrs (attrs view), we will expetience have the same process described at 2.1 with the diference that as RelayState is set the attrs url. + + 3. The SAML Response is processed in the ACS /?acs, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the RelayState view. a) / or b) /?attrs + + 4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality. + + The single log out funcionality could be tested by 2 ways. + + 5.1 SLO Initiated by SP. Click on the "logout" link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint /?sls of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP. + + 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, /?sls). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP. + +Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET parameters are used to know the action that must be done. diff --git a/demo_pyramid/.coveragerc b/demo_pyramid/.coveragerc new file mode 100644 index 00000000..fd429eb0 --- /dev/null +++ b/demo_pyramid/.coveragerc @@ -0,0 +1,3 @@ +[run] +source = demo_pyramid +omit = demo_pyramid/test* diff --git a/demo_pyramid/.gitignore b/demo_pyramid/.gitignore new file mode 100644 index 00000000..bd31ad13 --- /dev/null +++ b/demo_pyramid/.gitignore @@ -0,0 +1,22 @@ +*.egg +*.egg-info +*.pyc +*$py.class +*~ +.coverage +coverage.xml +build/ +dist/ +.tox/ +nosetests.xml +env*/ +tmp/ +.cache/* +Data.fs* +*.sublime-project +*.sublime-workspace +.*.sw? +.sw? +.DS_Store +coverage +test diff --git a/demo_pyramid/CHANGES.txt b/demo_pyramid/CHANGES.txt new file mode 100644 index 00000000..14b902fd --- /dev/null +++ b/demo_pyramid/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version. diff --git a/demo_pyramid/MANIFEST.in b/demo_pyramid/MANIFEST.in new file mode 100644 index 00000000..3b3962e4 --- /dev/null +++ b/demo_pyramid/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include demo_pyramid *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 diff --git a/demo_pyramid/README.txt b/demo_pyramid/README.txt new file mode 100644 index 00000000..13d16ded --- /dev/null +++ b/demo_pyramid/README.txt @@ -0,0 +1,29 @@ +demo_pyramid +=============================== + +Getting Started +--------------- + +- Change directory into your newly created project. + + cd demo_pyramid + +- Create a Python virtual environment. + + python -m venv env + +- Upgrade packaging tools. + + env/bin/pip install --upgrade pip setuptools + +- Install the project in editable mode with its testing requirements. + + env/bin/pip install -e ".[testing]" + +- Run your project's tests. + + env/bin/pytest + +- Run your project. + + env/bin/pserve development.ini diff --git a/demo_pyramid/demo_pyramid/__init__.py b/demo_pyramid/demo_pyramid/__init__.py new file mode 100644 index 00000000..805e51d1 --- /dev/null +++ b/demo_pyramid/demo_pyramid/__init__.py @@ -0,0 +1,19 @@ +from pyramid.config import Configurator +from pyramid.session import SignedCookieSessionFactory + + +session_factory = SignedCookieSessionFactory('onelogindemopytoolkit') + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(settings=settings) + config.set_session_factory(session_factory) + config.include('pyramid_jinja2') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('index', '/') + config.add_route('attrs', '/attrs/') + config.add_route('metadata', '/metadata/') + config.scan() + return config.make_wsgi_app() diff --git a/demo_pyramid/demo_pyramid/saml/advanced_settings.json b/demo_pyramid/demo_pyramid/saml/advanced_settings.json new file mode 100644 index 00000000..3115e17e --- /dev/null +++ b/demo_pyramid/demo_pyramid/saml/advanced_settings.json @@ -0,0 +1,33 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": false, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": false, + "wantAssertionsSigned": false, + "wantNameId" : true, + "wantNameIdEncrypted": false, + "wantAssertionsEncrypted": false, + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" + }, + "contactPerson": { + "technical": { + "givenName": "technical_name", + "emailAddress": "technical@example.com" + }, + "support": { + "givenName": "support_name", + "emailAddress": "support@example.com" + } + }, + "organization": { + "en-US": { + "name": "sp_test", + "displayname": "SP test", + "url": "http://sp.example.com" + } + } +} \ No newline at end of file diff --git a/demo_pyramid/demo_pyramid/saml/certs/README b/demo_pyramid/demo_pyramid/saml/certs/README new file mode 100644 index 00000000..03c13737 --- /dev/null +++ b/demo_pyramid/demo_pyramid/saml/certs/README @@ -0,0 +1,11 @@ +Take care of this folder that could contain private key. Be sure that this folder never is published. + +Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: + + * sp.key Private Key + * sp.crt Public cert + +Also you can use other cert to sign the metadata of the SP using the: + + * metadata.key + * metadata.crt diff --git a/demo_pyramid/demo_pyramid/saml/settings.json b/demo_pyramid/demo_pyramid/saml/settings.json new file mode 100644 index 00000000..ec40b674 --- /dev/null +++ b/demo_pyramid/demo_pyramid/saml/settings.json @@ -0,0 +1,30 @@ +{ + "strict": true, + "debug": true, + "sp": { + "entityId": "https:///metadata/", + "assertionConsumerService": { + "url": "https:///?acs", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + "singleLogoutService": { + "url": "https:///?sls", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "x509cert": "", + "privateKey": "" + }, + "idp": { + "entityId": "https://app.onelogin.com/saml/metadata/", + "singleSignOnService": { + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "singleLogoutService": { + "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "" + } +} diff --git a/demo_pyramid/demo_pyramid/static/pyramid-16x16.png b/demo_pyramid/demo_pyramid/static/pyramid-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..979203112e76ba4cfdb8cd6f108f4275e987d99a GIT binary patch literal 1319 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xd_B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxR5#hc&_u!9QqR!T z(8R(}N5ROz&{*HVSl`fC*U-qyz|zXlQ~?TIxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr7KMf`)Hma%LWg zuL;)R>ucqiS6q^qmz?V9Vygr+LN7Bj#mdRl*wM}0&C<--)xyxw)!5S9(bdAp)YZVy zz`)Yd%><^`B|o_|H#M&WrZ)wl*Ab^)P+G_>0NU)5T9jFqn&MWJpQ`}&vsET;x0vHJ z52`l>w_7Z5>eUB2MjsTjNHGl)0wy026P|8?9C*r4%>yR)B4E1CQ{KVEz`!`m)5S5Q z;#SXOUk#T)k>lxKj4~&v95M;sSJp8lNikLV)bX~~P1`qaNLZPZ<6^QgASli8jmk<% zZdJ~1%}JBkHhu^^q;ghbe{lMjv_0X)ug#yI+xz_S{hiM(g*saf_f6Pt#!wis>2u+! z{;RjXUlmP?^Kq|yJMn}2uJ~!L3EU-*{+zn6Ya6$jYueOB4G#Lx+=R;oM4sqe*Sq0$ zQ2Nf{-BFQxlX@Dog;`l=SkyQh`W)n22F}BoCTQYYC=p8g*kiK(1`FEnD$cY`NZX8<>GJicU)2$Vj5iRl-7k*od z3K)m{Gsp@4JM)eDo7z=gtG;>hu{QvcXxLU8eD@D+$BKJ@RM`zyYKvG z-8a3ur|aAMt1Vr-yH|CEDauQLkO+_f002lzQdIf%fB4Ui0QY*V)U3(^0FZ<%L_`#& zL`1-fj&^1i)}{b}Bq%eIA9uoG!laCfFcwoR zb`OTl9xm%u?u}UK6Z+-0LfvF1uNzRlu;BSs+a-xXQEAzvn#Z125}lrEE$o@!cYog? z@lko^ANF`uyQDsu%o2*s(%P^-sbKEJ1>90;Svb?qHr@sbgo4>hFv21pO(baNe1U?G_am z$%uaYhJupCKc=2k-Lpftu1m0%A~@dHZKRf6W*s6Qm&D`7K|3 zP8#?(KABe7=AZNd-k*6CTcqHJ?f3yA6ws8mf*wHc;}7VpNW)zn=9RJ4PSI>0zxN+V zk#)jtw`7ILRrYRCqD>sB@)+LaZvtH~TpCmeT z5;T(}&;kNeCnT`+Is{plpj-ki?E!QC9#b�i5=5IxreNAbVsKKM4p@aIXvt)VjX~ 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 have the following attributes:

+ + + + + + {% for attr in attributes %} + + + {% endfor %} + +
NameValues
{{ attr.0 }}
    + {% for val in attr.1 %} +
  • {{ val }}
  • + {% endfor %} +
+ {% else %} + + {% endif %} + Logout +{% else %} + Login and access again to this page +{% endif %} + +{% endblock %} diff --git a/demo_pyramid/demo_pyramid/templates/index.jinja2 b/demo_pyramid/demo_pyramid/templates/index.jinja2 new file mode 100644 index 00000000..65bcedd6 --- /dev/null +++ b/demo_pyramid/demo_pyramid/templates/index.jinja2 @@ -0,0 +1,55 @@ +{% extends "layout.jinja2" %} + +{% block content %} + +
+

Pyramid Starter project

+

Welcome to demo_pyramid, a Pyramid application generated by
Cookiecutter.

+
+ +{% if errors %} + +{% endif %} + +{% if not_auth_warn %} + +{% endif %} + +{% if success_slo %} + +{% endif %} + +{% if paint_logout %} + {% if attributes %} + + + + + + {% for attr in attributes %} + + + {% endfor %} + +
NameValues
{{ attr.0 }}
    + {% for val in attr.1 %} +
  • {{ val }}
  • + {% endfor %} +
+ {% else %} + + {% endif %} + Logout +{% else %} + Login Login and access to attrs page +{% endif %} + +{% endblock %} diff --git a/demo_pyramid/demo_pyramid/templates/layout.jinja2 b/demo_pyramid/demo_pyramid/templates/layout.jinja2 new file mode 100644 index 00000000..57ad87c0 --- /dev/null +++ b/demo_pyramid/demo_pyramid/templates/layout.jinja2 @@ -0,0 +1,64 @@ + + + + + + + + + + + Cookiecutter Starter project for the Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+ +
+ +
+
+
+ + + + + + + + diff --git a/demo_pyramid/demo_pyramid/views.py b/demo_pyramid/demo_pyramid/views.py new file mode 100644 index 00000000..86814a99 --- /dev/null +++ b/demo_pyramid/demo_pyramid/views.py @@ -0,0 +1,126 @@ +import os + +from pyramid.httpexceptions import (HTTPFound, HTTPInternalServerError, HTTPOk,) +from pyramid.view import view_config + +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.utils import OneLogin_Saml2_Utils + +SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml') + + +def init_saml_auth(req): + auth = OneLogin_Saml2_Auth(req, custom_base_path=SAML_PATH) + return auth + + +def prepare_pyramid_request(request): + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields + return { + 'https': 'on' if request.scheme == 'https' else 'off', + 'http_host': request.host, + 'server_port': request.server_port, + 'script_name': request.path, + 'get_data': request.GET.copy(), + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True, + 'post_data': request.POST.copy(), + } + + +@view_config(route_name='index', renderer='templates/index.jinja2') +def index(request): + req = prepare_pyramid_request(request) + auth = init_saml_auth(req) + errors = [] + error_reason = "" + not_auth_warn = False + success_slo = False + attributes = False + paint_logout = False + + session = request.session + + if 'sso' in request.GET: + return HTTPFound(auth.login()) + elif 'sso2' in request.GET: + return_to = '%s/attrs/' % request.host_url + return HTTPFound(auth.login(return_to)) + elif 'slo' in request.GET: + name_id = None + session_index = None + if 'samlNameId' in session: + name_id = session['samlNameId'] + if 'samlSessionIndex' in session: + session_index = session['samlSessionIndex'] + + return HTTPFound(auth.logout(name_id=name_id, session_index=session_index)) + elif 'acs' in request.GET: + auth.process_response() + errors = auth.get_errors() + not_auth_warn = not auth.is_authenticated() + if len(errors) == 0: + session['samlUserdata'] = auth.get_attributes() + session['samlNameId'] = auth.get_nameid() + session['samlSessionIndex'] = auth.get_session_index() + self_url = OneLogin_Saml2_Utils.get_self_url(req) + if 'RelayState' in request.POST and self_url != request.POST['RelayState']: + return HTTPFound(auth.redirect_to(request.POST['RelayState'])) + else: + error_reason = auth.get_last_error_reason() + elif 'sls' in request.GET: + dscb = lambda: session.clear() + url = auth.process_slo(delete_session_cb=dscb) + errors = auth.get_errors() + if len(errors) == 0: + if url is not None: + return HTTPFound(url) + else: + success_slo = True + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return { + 'errors': errors, + 'error_reason': error_reason, + 'not_auth_warn': not_auth_warn, + 'success_slo': success_slo, + 'attributes': attributes, + 'paint_logout': paint_logout, + } + + +@view_config(route_name='attrs', renderer='templates/attrs.jinja2') +def attrs(request): + paint_logout = False + attributes = False + + session = request.session + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return { + 'paint_logout': paint_logout, + 'attributes': attributes, + } + + +@view_config(route_name='metadata', renderer='html') +def metadata(request): + req = prepare_pyramid_request(request) + auth = init_saml_auth(req) + settings = auth.get_settings() + metadata = settings.get_sp_metadata() + errors = settings.validate_metadata(metadata) + + if len(errors) == 0: + resp = HTTPOk(body=metadata, headers={'Content-Type': 'text/xml'}) + else: + resp = HTTPInternalServerError(body=', '.join(errors)) + return resp diff --git a/demo_pyramid/development.ini b/demo_pyramid/development.ini new file mode 100644 index 00000000..64042ee7 --- /dev/null +++ b/demo_pyramid/development.ini @@ -0,0 +1,59 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:demo_pyramid + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = 127.0.0.1:6543 [::1]:6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, demo_pyramid + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_demo_pyramid] +level = DEBUG +handlers = +qualname = demo_pyramid + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/demo_pyramid/production.ini b/demo_pyramid/production.ini new file mode 100644 index 00000000..84b482ae --- /dev/null +++ b/demo_pyramid/production.ini @@ -0,0 +1,53 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:demo_pyramid + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = *:6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, demo_pyramid + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_demo_pyramid] +level = WARN +handlers = +qualname = demo_pyramid + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/demo_pyramid/setup.py b/demo_pyramid/setup.py new file mode 100644 index 00000000..a30a8ddd --- /dev/null +++ b/demo_pyramid/setup.py @@ -0,0 +1,45 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'waitress', + 'xmlsec', + 'isodate', + 'python-saml', +] + +setup( + name='demo_pyramid', + version='0.0', + description='demo_pyramid', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + 'Programming Language :: Python', + 'Framework :: Pyramid', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=requires, + entry_points={ + 'paste.app_factory': [ + 'main = demo_pyramid:main', + ], + }, +) From 2b5091c91c5d8b3841bea66953d22ae974109b8a Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 21 Apr 2017 18:11:07 +1000 Subject: [PATCH 191/352] #190 Checking the status of response before assertion count Failed Responses don't have assertions and the error hides that the status is not success --- src/onelogin/saml2/response.py | 6 +++--- tests/src/OneLogin/saml2_tests/response_test.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 93f650a3..ba524c2b 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -84,6 +84,9 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): OneLogin_Saml2_ValidationError.MISSING_ID ) + # Checks that the response has the SUCCESS status + self.check_status() + # Checks that the response only has one assertion if not self.validate_num_assertions(): raise OneLogin_Saml2_ValidationError( @@ -91,9 +94,6 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS ) - # Checks that the response has the SUCCESS status - self.check_status() - idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data.get('entityId', '') sp_data = self.__settings.get_sp_data() diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 8c499e9e..cea605d1 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1391,6 +1391,17 @@ def testIsValidWithoutInResponseTo(self): })) + def testStatusCheckBeforeAssertionCheck(self): + """ + Tests the status of a response is checked before the assertion count. As failed statuses will have no assertions + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder'): + response_2.is_valid(self.get_request_data(), raise_exceptions=True) + + if __name__ == '__main__': if is_running_under_teamcity(): runner = TeamcityTestRunner() From b670d9e091fc85b8b9da7d590fe8a4574c7e55a8 Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 21 Apr 2017 18:11:07 +1000 Subject: [PATCH 192/352] #190 Checking the status of response before assertion count Failed Responses don't have assertions and the error hides that the status is not success --- 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 cea605d1..7e040ec5 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1396,10 +1396,10 @@ def testStatusCheckBeforeAssertionCheck(self): Tests the status of a response is checked before the assertion count. As failed statuses will have no assertions """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64')) - response_2 = OneLogin_Saml2_Response(settings, xml_2) + xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder'): - response_2.is_valid(self.get_request_data(), raise_exceptions=True) + response.is_valid(self.get_request_data(), raise_exceptions=True) if __name__ == '__main__': From ba9fdfee8729e6f040be07e28c5efc037c8e4f13 Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 21 Apr 2017 18:24:09 +1000 Subject: [PATCH 193/352] #190 Complying to pep8 --- tests/src/OneLogin/saml2_tests/response_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 7e040ec5..d7ac4673 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1390,7 +1390,6 @@ def testIsValidWithoutInResponseTo(self): 'script_name': 'newonelogin/demo1/index.php?acs' })) - def testStatusCheckBeforeAssertionCheck(self): """ Tests the status of a response is checked before the assertion count. As failed statuses will have no assertions From a9d16fdd0cf0984f744a6f77f814e77e6a9f5834 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 3 May 2017 10:02:47 +0200 Subject: [PATCH 194/352] Fix Readme titles --- README.md | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 275dd752..8a8f7786 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ the classes and methods that are described in a later section. This folder contains a Django project that will be used as demo to show how to add SAML support to the Django Framework. 'demo' is the main folder of the django project (with its settings.py, views.py, urls.py), 'templates' is the django templates of the project and 'saml' is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). -***Notice about certs*** +*** Notice about certs *** SAML requires a x.509 cert to sign and encrypt elements like NameID, Message, Assertion, Metadata. @@ -572,7 +572,7 @@ auth.get_last_request_id() Related to the SP there are 3 important endpoints: The metadata view, the ACS view and the SLS view. The toolkit provides examples of those views in the demos, but lets see an example. -***SP Metadata*** +*** SP Metadata *** This code will provide the XML metadata file of our SP, based on the info that we provided in the settings files. @@ -598,7 +598,7 @@ saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp ``` to get the settings object and with the sp_validation_only=True parameter we will avoid the IdP Settings validation. -***Attribute Consumer Service(ACS)*** +*** Attribute Consumer Service(ACS) *** This code handles the SAML response that the IdP forwards to the SP through the user's client. @@ -664,7 +664,7 @@ print auth.get_attribute('cn') Before trying to get an attribute, check that the user is authenticated. If the user isn't authenticated, an empty dict will be returned. For example, if we call to get_attributes before a auth.process_response, the get_attributes() will return an empty dict. -***Single Logout Service (SLS)*** +*** Single Logout Service (SLS) *** This code handles the Logout Request and the Logout Responses. @@ -765,7 +765,7 @@ If a match on the LogoutResponse ID and the LogoutRequest ID to be sent is requi auth.get_last_request_id() ``` -####Example of a view that initiates the SSO request and handles the response (is the acs target)#### +#### 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. @@ -820,7 +820,7 @@ else: Described below are the main classes and methods that can be invoked from the SAML2 library. -####OneLogin_Saml2_Auth - auth.py#### +#### OneLogin_Saml2_Auth - auth.py #### Main class of OneLogin Python Toolkit @@ -848,7 +848,7 @@ Main class of OneLogin Python Toolkit * ***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 had an encrypted assertion, decrypts it. -####OneLogin_Saml2_Auth - authn_request.py#### +#### OneLogin_Saml2_Auth - authn_request.py #### SAML 2 Authentication Request class @@ -857,7 +857,7 @@ SAML 2 Authentication Request class * ***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#### +#### OneLogin_Saml2_Response - response.py #### SAML 2 Authentication Response class @@ -876,7 +876,7 @@ SAML 2 Authentication Response class * ***get_error*** After execute a validation process, if fails this method returns the cause * ***get_xml_document*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it). -####OneLogin_Saml2_LogoutRequest - logout_request.py#### +#### OneLogin_Saml2_LogoutRequest - logout_request.py #### SAML 2 Logout Request class @@ -891,7 +891,7 @@ SAML 2 Logout Request class * ***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#### +#### OneLogin_Saml2_LogoutResponse - logout_response.py #### SAML 2 Logout Response class @@ -905,7 +905,7 @@ SAML 2 Logout Response class * ***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#### +#### OneLogin_Saml2_Settings - settings.py #### Configuration of the OneLogin Python Toolkit @@ -937,7 +937,7 @@ Configuration of the OneLogin Python Toolkit * ***is_strict*** Returns if the 'strict' mode is active. * ***is_debug_active*** Returns if the debug is active. -####OneLogin_Saml2_Metadata - metadata.py#### +#### OneLogin_Saml2_Metadata - metadata.py #### A class that contains functionality related to the metadata of the SP @@ -945,7 +945,7 @@ A class that contains functionality related to the metadata of the SP * ***sign_metadata*** Signs the metadata with the key/cert provided. * ***add_x509_key_descriptors*** Adds the x509 descriptors (sign/encriptation) to the metadata -####OneLogin_Saml2_Utils - utils.py#### +#### OneLogin_Saml2_Utils - utils.py #### Auxiliary class that contains several methods @@ -981,7 +981,7 @@ Auxiliary class that contains several methods * ***def get_encoded_parameter*** Return an url encoded get parameter value * ***extract_raw_query_parameter*** -####OneLogin_Saml2_IdPMetadataParser - idp_metadata_parser.py#### +#### OneLogin_Saml2_IdPMetadataParser - idp_metadata_parser.py #### A class that contains methods to obtain and parse metadata from IdP @@ -1006,7 +1006,7 @@ how it deployed. New demos using other python frameworks are welcome as a contri We said that this toolkit includes a django application demo and a flask applicacion demo, lets see how fast is deploy them. -***Virtualenv*** +*** Virtualenv *** The use of a [virtualenv](http://virtualenv.readthedocs.org/en/latest/) is highly recommended. @@ -1051,7 +1051,7 @@ Now, with the virtualenv loaded, you can run the demo like this: You'll have the demo running at http://localhost:8000 -####Content#### +#### Content #### The flask project contains: @@ -1063,7 +1063,7 @@ The flask project contains: * ***saml*** Is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). -####SP setup#### +#### SP setup #### The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-flask it used the first method. @@ -1071,11 +1071,11 @@ In the index.py file we define the app.config['SAML_PATH'], that will target to First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. -####IdP setup#### +#### IdP setup #### Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP. -####How it works#### +#### How it works #### 1. First time you access to the main view 'http://localhost:8000', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view). @@ -1120,7 +1120,7 @@ Note that many of the configuration files expect HTTPS. This is not required by If you want to integrate a production django application, take a look on this SAMLServiceProviderBackend that uses our toolkit to add SAML support: https://github.com/KristianOellegaard/django-saml-service-provider -####Content#### +#### Content #### The django project contains: @@ -1136,7 +1136,7 @@ The django project contains: * ***templates***. Is the folder where django stores the templates of the project. It was implemented a base.html template that is extended by index.html and attrs.html, the templates of our simple demo that shows messages, user attributes when available and login and logout links. -####SP setup#### +#### SP setup #### The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-django it used the first method. @@ -1144,11 +1144,11 @@ After set the SAML_FOLDER in the demo/settings.py, the settings of the python to First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. -####IdP setup#### +#### IdP setup #### Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP. -####How it works#### +#### How it works #### This demo works very similar to the flask-demo (We did it intentionally). @@ -1177,7 +1177,7 @@ Now you can run the demo like this: If that worked, the demo is now running at http://localhost:6543. -####Content#### +#### Content #### The Pyramid project contains: @@ -1191,7 +1191,7 @@ The Pyramid project contains: * ***saml*** is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). -####SP setup#### +#### SP setup #### The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In demo_pyramid the first method is used. @@ -1203,7 +1203,7 @@ First we need to edit the saml/settings.json, configure the SP part and review t Once the SP is configured, the metadata of the SP is published at the /metadata/ url. Based on that info, configure the IdP. -####How it works#### +#### How it works #### 1. First time you access to the main view 'http://localhost:6543', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view). From 5c1d869c48e2fa89921ee83f352292c67f50149e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 3 May 2017 10:03:37 +0200 Subject: [PATCH 195/352] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a8f7786..30d6c650 100644 --- a/README.md +++ b/README.md @@ -1199,7 +1199,7 @@ In the views.py file we define the SAML_PATH, which will target the 'saml' folde First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. -####IdP setup#### +#### IdP setup #### Once the SP is configured, the metadata of the SP is published at the /metadata/ url. Based on that info, configure the IdP. From 5e17c19cbdf0833d15e7d60a85d1a6924ff6e519 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 9 May 2017 17:53:19 +0200 Subject: [PATCH 196/352] Be able to relax SSL Certificate verification when retrieving idp metadata --- src/onelogin/saml2/idp_metadata_parser.py | 15 +++++++++++---- .../OneLogin/saml2_tests/signed_response_test.py | 3 +++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 80b38ec1..fc9689cb 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -10,6 +10,7 @@ """ import urllib2 +import ssl from copy import deepcopy from defusedxml.lxml import fromstring @@ -24,7 +25,7 @@ class OneLogin_Saml2_IdPMetadataParser(object): """ @staticmethod - def get_metadata(url): + def get_metadata(url, validate_cert=True): """ Gets the metadata XML from the provided URL @@ -35,7 +36,13 @@ def get_metadata(url): :rtype: string """ valid = False - response = urllib2.urlopen(url) + if validate_cert: + response = urllib2.urlopen(url) + else: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + response = urllib2.urlopen(url, context=ctx) xml = response.read() if xml: @@ -53,7 +60,7 @@ def get_metadata(url): return xml @staticmethod - def parse_remote(url, **kwargs): + def parse_remote(url, validate_cert=True, **kwargs): """ Gets the metadata XML from the provided URL and parse it, returning a dict with extracted data @@ -63,7 +70,7 @@ def parse_remote(url, **kwargs): :returns: settings dict with extracted data :rtype: dict """ - idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url) + idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url, validate_cert) return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata, **kwargs) @staticmethod diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py index d630f00f..50a4431f 100644 --- a/tests/src/OneLogin/saml2_tests/signed_response_test.py +++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py @@ -44,6 +44,9 @@ def testResponseSignedAssertionNot(self): response = OneLogin_Saml2_Response(settings, b64encode(message)) self.assertEquals('someone@example.org', response.get_nameid()) + from onelogin.saml2.utils import OneLogin_Saml2_Utils + assertion_nodes = OneLogin_Saml2_Utils.query(response.document, '//saml:Assertion') + self.assertEquals(len(assertion_nodes), 1) def testResponseAndAssertionSigned(self): """ From 37d6190367b1400fc2be73d73fc80f9944bda6b8 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 11 May 2017 12:52:11 +0200 Subject: [PATCH 197/352] Delete expensify_test.py.txt --- .../saml2_tests/expensify_test.py.txt | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 tests/src/OneLogin/saml2_tests/expensify_test.py.txt diff --git a/tests/src/OneLogin/saml2_tests/expensify_test.py.txt b/tests/src/OneLogin/saml2_tests/expensify_test.py.txt deleted file mode 100644 index c02d164d..00000000 --- a/tests/src/OneLogin/saml2_tests/expensify_test.py.txt +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2014, OneLogin, Inc. -# All rights reserved. - -from base64 import b64decode -import json -from lxml import etree -from os.path import dirname, join, exists -import unittest -from teamcity import is_running_under_teamcity -from teamcity.unittestpy import TeamcityTestRunner -from xml.dom.minidom import Document, parseString - -from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.settings import OneLogin_Saml2_Settings -from onelogin.saml2.utils import OneLogin_Saml2_Utils - - -class OneLogin_Saml2_Utils_Test(unittest.TestCase): - data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') - - def file_contents(self, filename): - f = open(filename, 'r') - content = f.read() - f.close() - return content - - - def testExpesify(self): - xml = self.file_contents(join(self.data_path, 'responses', 'expensify.xml')) - cert = """-----BEGIN CERTIFICATE----- -MIIC5jCCAc6gAwIBAgIQejzcQ7LOo7NDphYfVqKB8jANBgkqhkiG9w0BAQsFADAv -MS0wKwYDVQQDEyRBREZTIFNpZ25pbmcgLSBhZGZzLmZpbmlzaG1hc3Rlci5jb20w -HhcNMTYwMzMwMjA1NjM2WhcNMTcwMzMwMjA1NjM2WjAvMS0wKwYDVQQDEyRBREZT -IFNpZ25pbmcgLSBhZGZzLmZpbmlzaG1hc3Rlci5jb20wggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDrnGb3jm22UR0CqYQK55vj5dVMQIitpl5jUHuFgou0 -SsFfI5VJY5KNXQXCJoduXlxl12dRUQrd0h22TCLBWoneuDcor0UV4bRl7QyQWjuX -M6pEDG8JsNWJEmnoiKGVmnfsaVgKTFZxO+Kydk/GRJauOqAyU7igABDpazMMHjjL -c9iZ6tHjFjtWrUay3Hu3aaZheQWgzjnENUprgnH3zm5NQT5Dbd72z5TKrb4bbv08 -oInf52dnBNiSz0wa8uZRJPJJH2rGJmWUIJqp2fnhCxysMgf+ny5IfOkpcG4Cb/fk -d4hoSmY5Bb/dKuD9/pTpmoylcinBtb3bMGASkulyQmENAgMBAAEwDQYJKoZIhvcN -AQELBQADggEBAEx5l+gRoxUSY1nu6O2Cmu8WpnrFlEoNsko+Z/T34NYIm/2uaX6y -kHoDSptgplwjtjE6lE0Zygm5R59BAU3tqGG1uHu4G7w++rcE6nnYunC3tZ8iKuzb -VmkyUFjuf0AaWo/j/pl7zDJV1NHg6zm/NrqKhx4+Sr/LpVdBMOQmU9leqKsfVtkJ -ktTOx1ChiEqu7R0yfKuaZwSlUfgnVtVS7yvLvk0KIbxhZe9BGdtJ03+XHYE0TeVN -SpHh6Q1mylYurMX3WLMW5cQ+AcOWiDob2FuMq6wNWoUwOCvUwTu9NFQvuyzFpUrn -wFgrDzL8lRc9nNA/iZh+pBIu+BM3/wWZLp8= ------END CERTIFICATE-----""" - self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml, cert, debug=True)) From dc015e0ba469caabe6e9095f81cf2c71d5fe4847 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 11 May 2017 13:26:02 +0200 Subject: [PATCH 198/352] Be able to register future SP x509cert on the settings and publish it on SP metadata --- README.md | 14 ++++++ demo-bottle/saml/certs/README | 5 +- demo-django/saml/certs/README | 5 +- demo-flask/saml/certs/README | 5 +- demo_pyramid/demo_pyramid/saml/certs/README | 5 +- src/onelogin/saml2/settings.py | 29 +++++++++++ tests/settings/settings7.json | 50 +++++++++++++++++++ .../saml2_tests/expensify_test.py.txt | 50 ------------------- .../src/OneLogin/saml2_tests/settings_test.py | 32 ++++++++++++ .../saml2_tests/signed_response_test.py | 3 -- 10 files changed, 137 insertions(+), 61 deletions(-) create mode 100644 tests/settings/settings7.json delete mode 100644 tests/src/OneLogin/saml2_tests/expensify_test.py.txt diff --git a/README.md b/README.md index 30d6c650..de0210f8 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,9 @@ Or also we can provide those data in the setting file at the 'x509cert' and the Sometimes we could need a signature on the metadata published by the SP, in this case we could use the x.509 cert previously mentioned or use a new x.509 cert: metadata.crt and metadata.key. +Use `sp_new.crt` if you are in a key rollover process and you want to +publish that x509certificate on Service Provider metadata. + If you want to create self-signed certs, you can do it at the https://www.samltool.com/self_signed_certs.php service, or using the command: ```bash @@ -279,6 +282,15 @@ This is the settings.json file: // the certs folder. But we can also provide them with the following parameters "x509cert": "", "privateKey": "" + + /* + * Key rollover + * If you plan to update the SP x509cert and privateKey + * you can define here the new x509cert and it will be + * published on the SP metadata so Identity Providers can + * read them and get ready for rollover. + */ + // 'x509certNew': '', }, // Identity Provider Data that we want connected with our SP. @@ -924,6 +936,7 @@ Configuration of the OneLogin Python Toolkit * ***check_sp_certs*** Checks if the x509 certs of the SP exists and are valid. * ***get_sp_key*** Returns the x509 private key of the SP. * ***get_sp_cert*** Returns the x509 public cert of the SP. +* ***get_sp_cert_new*** Returns the future x509 public cert of the SP. * ***get_idp_cert*** Returns the x509 public cert of the IdP. * ***get_sp_data*** Gets the SP data. * ***get_idp_data*** Gets the IdP data. @@ -932,6 +945,7 @@ Configuration of the OneLogin Python Toolkit * ***get_organization*** Gets organization data. * ***format_idp_cert*** Formats the IdP cert. * ***format_sp_cert*** Formats the SP cert. +* ***format_sp_cert_new*** Formats the SP cert new. * ***format_sp_key*** Formats the private key. * ***set_strict*** Activates or deactivates the strict mode. * ***is_strict*** Returns if the 'strict' mode is active. diff --git a/demo-bottle/saml/certs/README b/demo-bottle/saml/certs/README index 03c13737..7cf0c143 100644 --- a/demo-bottle/saml/certs/README +++ b/demo-bottle/saml/certs/README @@ -2,8 +2,9 @@ Take care of this folder that could contain private key. Be sure that this folde Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: - * sp.key Private Key - * sp.crt Public cert + * sp.key Private Key + * sp.crt Public cert + * sp_new.crt Future Public cert Also you can use other cert to sign the metadata of the SP using the: diff --git a/demo-django/saml/certs/README b/demo-django/saml/certs/README index 03c13737..7cf0c143 100644 --- a/demo-django/saml/certs/README +++ b/demo-django/saml/certs/README @@ -2,8 +2,9 @@ Take care of this folder that could contain private key. Be sure that this folde Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: - * sp.key Private Key - * sp.crt Public cert + * sp.key Private Key + * sp.crt Public cert + * sp_new.crt Future Public cert Also you can use other cert to sign the metadata of the SP using the: diff --git a/demo-flask/saml/certs/README b/demo-flask/saml/certs/README index 03c13737..7cf0c143 100644 --- a/demo-flask/saml/certs/README +++ b/demo-flask/saml/certs/README @@ -2,8 +2,9 @@ Take care of this folder that could contain private key. Be sure that this folde Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: - * sp.key Private Key - * sp.crt Public cert + * sp.key Private Key + * sp.crt Public cert + * sp_new.crt Future Public cert Also you can use other cert to sign the metadata of the SP using the: diff --git a/demo_pyramid/demo_pyramid/saml/certs/README b/demo_pyramid/demo_pyramid/saml/certs/README index 03c13737..7cf0c143 100644 --- a/demo_pyramid/demo_pyramid/saml/certs/README +++ b/demo_pyramid/demo_pyramid/saml/certs/README @@ -2,8 +2,9 @@ Take care of this folder that could contain private key. Be sure that this folde Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: - * sp.key Private Key - * sp.crt Public cert + * sp.key Private Key + * sp.crt Public cert + * sp_new.crt Future Public cert Also you can use other cert to sign the metadata of the SP using the: diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 1c17ec3e..8322eaf0 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -111,6 +111,8 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals self.format_idp_cert() self.format_sp_cert() + if 'x509certNew' in self.__sp: + self.format_sp_cert_new() self.format_sp_key() def __load_paths(self, base_path=None): @@ -522,6 +524,23 @@ def get_sp_cert(self): return cert or None + def get_sp_cert_new(self): + """ + Returns the x509 public of the SP planned + to be used soon instead the other public cert + + :returns: SP public cert new + :rtype: string or None + """ + cert = self.__sp.get('x509certNew') + cert_file_name = self.__paths['cert'] + 'sp_new.crt' + + if not cert and exists(cert_file_name): + with open(cert_file_name) as f: + cert = f.read() + + return cert or None + def get_idp_cert(self): """ Returns the x509 public cert of the IdP. @@ -590,6 +609,10 @@ def get_sp_metadata(self): self.__security['metadataCacheDuration'], self.get_contacts(), self.get_organization() ) + + cert_new = self.get_sp_cert_new() + metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert_new) + cert = self.get_sp_cert() metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert) @@ -705,6 +728,12 @@ def format_sp_cert(self): """ self.__sp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__sp['x509cert']) + def format_sp_cert_new(self): + """ + Formats the SP cert. + """ + self.__sp['x509certNew'] = OneLogin_Saml2_Utils.format_cert(self.__sp['x509certNew']) + def format_sp_key(self): """ Formats the private key. diff --git a/tests/settings/settings7.json b/tests/settings/settings7.json new file mode 100644 index 00000000..e573624b --- /dev/null +++ b/tests/settings/settings7.json @@ -0,0 +1,50 @@ +{ + "strict": false, + "debug": false, + "custom_base_path": "../../../tests/data/customPath/", + "sp": { + "entityId": "http://stuff.com/endpoints/metadata.php", + "assertionConsumerService": { + "url": "http://stuff.com/endpoints/endpoints/acs.php" + }, + "singleLogoutService": { + "url": "http://stuff.com/endpoints/endpoints/sls.php" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "privateKey": "MIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABAoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0VJ5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8JK2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq66PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6rCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4ukXSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLcPQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg==", + "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo", + "x509certNew": "MIICVDCCAb2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBHMQswCQYDVQQGEwJ1czEQMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMTcwNDA3MDgzMDAzWhcNMjcwNDA1MDgzMDAzWjBHMQswCQYDVQQGEwJ1czEQMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIGA1UEAwwLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKhPS4/0azxbQekHHewQGKD7Pivr3CDpsrKxY3xlVanxj427OwzOb5KUVzsDEazumt6sZFY8HfidsjXY4EYA4ZzyL7ciIAR5vlAsIYN9nJ4AwVDnN/RjVwj+TN6BqWPLpVIpHc6Dl005HyE0zJnk1DZDn2tQVrIzbD3FhCp7YeotAgMBAAGjUDBOMB0GA1UdDgQWBBRYZx4thASfNvR/E7NsCF2IaZ7wIDAfBgNVHSMEGDAWgBRYZx4thASfNvR/E7NsCF2IaZ7wIDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBACz4aobx9aG3kh+rNyrlgM3K6dYfnKG1/YH5sJCAOvg8kDr0fQAQifH8lFVWumKUMoAe0bFTfwWtp/VJ8MprrEJth6PFeZdczpuv+fpLcNj2VmNVJqvQYvS4m36OnBFh1QFZW8UrbFIfdtm2nuZ+twSKqfKwjLdqcoX0p39h7Uw/" + }, + "idp": { + "entityId": "http://idp.example.com/", + "singleSignOnService": { + "url": "http://idp.example.com/SSOService.php" + }, + "singleLogoutService": { + "url": "http://idp.example.com/SingleLogoutService.php" + }, + "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo" + }, + "security": { + "authnRequestsSigned": false, + "wantAssertionsSigned": false, + "signMetadata": false + }, + "contactPerson": { + "technical": { + "givenName": "technical_name", + "emailAddress": "technical@example.com" + }, + "support": { + "givenName": "support_name", + "emailAddress": "support@example.com" + } + }, + "organization": { + "en-US": { + "name": "sp_test", + "displayname": "SP test", + "url": "http://sp.example.com" + } + } +} \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/expensify_test.py.txt b/tests/src/OneLogin/saml2_tests/expensify_test.py.txt deleted file mode 100644 index c02d164d..00000000 --- a/tests/src/OneLogin/saml2_tests/expensify_test.py.txt +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2014, OneLogin, Inc. -# All rights reserved. - -from base64 import b64decode -import json -from lxml import etree -from os.path import dirname, join, exists -import unittest -from teamcity import is_running_under_teamcity -from teamcity.unittestpy import TeamcityTestRunner -from xml.dom.minidom import Document, parseString - -from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.settings import OneLogin_Saml2_Settings -from onelogin.saml2.utils import OneLogin_Saml2_Utils - - -class OneLogin_Saml2_Utils_Test(unittest.TestCase): - data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') - - def file_contents(self, filename): - f = open(filename, 'r') - content = f.read() - f.close() - return content - - - def testExpesify(self): - xml = self.file_contents(join(self.data_path, 'responses', 'expensify.xml')) - cert = """-----BEGIN CERTIFICATE----- -MIIC5jCCAc6gAwIBAgIQejzcQ7LOo7NDphYfVqKB8jANBgkqhkiG9w0BAQsFADAv -MS0wKwYDVQQDEyRBREZTIFNpZ25pbmcgLSBhZGZzLmZpbmlzaG1hc3Rlci5jb20w -HhcNMTYwMzMwMjA1NjM2WhcNMTcwMzMwMjA1NjM2WjAvMS0wKwYDVQQDEyRBREZT -IFNpZ25pbmcgLSBhZGZzLmZpbmlzaG1hc3Rlci5jb20wggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDrnGb3jm22UR0CqYQK55vj5dVMQIitpl5jUHuFgou0 -SsFfI5VJY5KNXQXCJoduXlxl12dRUQrd0h22TCLBWoneuDcor0UV4bRl7QyQWjuX -M6pEDG8JsNWJEmnoiKGVmnfsaVgKTFZxO+Kydk/GRJauOqAyU7igABDpazMMHjjL -c9iZ6tHjFjtWrUay3Hu3aaZheQWgzjnENUprgnH3zm5NQT5Dbd72z5TKrb4bbv08 -oInf52dnBNiSz0wa8uZRJPJJH2rGJmWUIJqp2fnhCxysMgf+ny5IfOkpcG4Cb/fk -d4hoSmY5Bb/dKuD9/pTpmoylcinBtb3bMGASkulyQmENAgMBAAEwDQYJKoZIhvcN -AQELBQADggEBAEx5l+gRoxUSY1nu6O2Cmu8WpnrFlEoNsko+Z/T34NYIm/2uaX6y -kHoDSptgplwjtjE6lE0Zygm5R59BAU3tqGG1uHu4G7w++rcE6nnYunC3tZ8iKuzb -VmkyUFjuf0AaWo/j/pl7zDJV1NHg6zm/NrqKhx4+Sr/LpVdBMOQmU9leqKsfVtkJ -ktTOx1ChiEqu7R0yfKuaZwSlUfgnVtVS7yvLvk0KIbxhZe9BGdtJ03+XHYE0TeVN -SpHh6Q1mylYurMX3WLMW5cQ+AcOWiDob2FuMq6wNWoUwOCvUwTu9NFQvuyzFpUrn -wFgrDzL8lRc9nNA/iZh+pBIu+BM3/wWZLp8= ------END CERTIFICATE-----""" - self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml, cert, debug=True)) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 0069c0c8..b23f22f0 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -165,6 +165,21 @@ def testGetSPCert(self): settings_3 = OneLogin_Saml2_Settings(settings_data, custom_base_path=custom_base_path) self.assertIsNone(settings_3.get_sp_cert()) + def testGetSPCertNew(self): + """ + Tests the get_sp_cert_new method of the OneLogin_Saml2_Settings + """ + settings_data = self.loadSettingsJSON() + cert = "-----BEGIN CERTIFICATE-----\nMIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMC\nTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYD\nVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG\n9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4\nMTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xi\nZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2Zl\naWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5v\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LO\nNoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHIS\nKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d\n1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8\nBUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7n\nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2Qar\nQ4/67OZfHd7R+POBXhophSMv1ZOo\n-----END CERTIFICATE-----\n" + settings = OneLogin_Saml2_Settings(settings_data) + self.assertEqual(cert, settings.get_sp_cert()) + self.assertIsNone(settings.get_sp_cert_new()) + + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings7.json')) + cert_new = "-----BEGIN CERTIFICATE-----\nMIICVDCCAb2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBHMQswCQYDVQQGEwJ1czEQ\nMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIGA1UEAwwLZXhh\nbXBsZS5jb20wHhcNMTcwNDA3MDgzMDAzWhcNMjcwNDA1MDgzMDAzWjBHMQswCQYD\nVQQGEwJ1czEQMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIG\nA1UEAwwLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKhP\nS4/0azxbQekHHewQGKD7Pivr3CDpsrKxY3xlVanxj427OwzOb5KUVzsDEazumt6s\nZFY8HfidsjXY4EYA4ZzyL7ciIAR5vlAsIYN9nJ4AwVDnN/RjVwj+TN6BqWPLpVIp\nHc6Dl005HyE0zJnk1DZDn2tQVrIzbD3FhCp7YeotAgMBAAGjUDBOMB0GA1UdDgQW\nBBRYZx4thASfNvR/E7NsCF2IaZ7wIDAfBgNVHSMEGDAWgBRYZx4thASfNvR/E7Ns\nCF2IaZ7wIDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBACz4aobx9aG3\nkh+rNyrlgM3K6dYfnKG1/YH5sJCAOvg8kDr0fQAQifH8lFVWumKUMoAe0bFTfwWt\np/VJ8MprrEJth6PFeZdczpuv+fpLcNj2VmNVJqvQYvS4m36OnBFh1QFZW8UrbFIf\ndtm2nuZ+twSKqfKwjLdqcoX0p39h7Uw/\n-----END CERTIFICATE-----\n" + self.assertEqual(cert, settings.get_sp_cert()) + self.assertEqual(cert_new, settings.get_sp_cert_new()) + def testGetSPKey(self): """ Tests the get_sp_key method of the OneLogin_Saml2_Settings @@ -337,6 +352,23 @@ def testGetSPMetadata(self): self.assertIn('', metadata) self.assertIn('', metadata) self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata) + self.assertEquals(2, metadata.count(' Date: Fri, 12 May 2017 02:10:33 +0200 Subject: [PATCH 199/352] Be able to register more than 1 Identity Provider x509cert, linked with an specific use (signing or encryption) --- README.md | 40 ++++++++++++- src/onelogin/saml2/logout_request.py | 29 ++++++++-- src/onelogin/saml2/logout_response.py | 22 +++++-- src/onelogin/saml2/response.py | 8 ++- src/onelogin/saml2/settings.py | 26 ++++++++- src/onelogin/saml2/utils.py | 17 +++++- tests/settings/settings8.json | 58 +++++++++++++++++++ .../saml2_tests/logout_request_test.py | 47 +++++++++++++-- .../saml2_tests/logout_response_test.py | 26 +++++++-- .../src/OneLogin/saml2_tests/response_test.py | 18 ++++-- .../src/OneLogin/saml2_tests/settings_test.py | 14 ++++- 11 files changed, 274 insertions(+), 31 deletions(-) create mode 100644 tests/settings/settings8.json diff --git a/README.md b/README.md index de0210f8..d96dc7d8 100644 --- a/README.md +++ b/README.md @@ -332,8 +332,24 @@ This is the settings.json file: * Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you * will need to provide the whole x509cert. */ - // 'certFingerprint' => '', - // 'certFingerprintAlgorithm' => 'sha1', + // 'certFingerprint': '', + // 'certFingerprintAlgorithm': 'sha1', + + /* In some scenarios the IdP uses different certificates for + * signing/encryption, or is under key rollover phase and + * more than one certificate is published on IdP metadata. + * In order to handle that the toolkit offers that parameter. + * (when used, 'x509cert' and 'certFingerprint' values are + * ignored). + */ + // 'x509certMulti': { + // 'signing': [ + // '' + // ], + // 'encryption': [ + // '' + // ] + // } } } ``` @@ -827,6 +843,25 @@ else: print ', '.join(errors) ``` +### SP Key rollover ### + +If you plan to update the SP x509cert and privateKey you can define the new x509cert as $settings['sp']['x509certNew'] and it will be +published on the SP metadata so Identity Providers can read them and get ready for rollover. + + +### IdP with multiple certificates ### + +In some scenarios the IdP uses different certificates for +signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata. + +In order to handle that the toolkit offers the $settings['idp']['x509certMulti'] parameter. + +When that parameter is used, 'x509cert' and 'certFingerprint' values will be ignored by the toolkit. + +The 'x509certMulti' is an array with 2 keys: +- 'signing'. An array of certs that will be used to validate IdP signature +- 'encryption' An array with one unique cert that will be used to encrypt data to be sent to the IdP + ### Main classes and methods ### @@ -944,6 +979,7 @@ Configuration of the OneLogin Python Toolkit * ***get_contacts*** Gets contacts data. * ***get_organization*** Gets organization data. * ***format_idp_cert*** Formats the IdP cert. +* ***format_idp_cert_multi*** Formats all registered IdP certs. * ***format_sp_cert*** Formats the SP cert. * ***format_sp_cert_new*** Formats the SP cert new. * ***format_sp_key*** Formats the private key. diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 44dd4a52..99c97198 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -67,7 +67,13 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= cert = None if 'nameIdEncrypted' in security and security['nameIdEncrypted']: - cert = idp_data['x509cert'] + exists_multix509enc = 'x509certMulti' in idp_data and \ + 'encryption' in idp_data['x509certMulti'] and \ + idp_data['x509certMulti']['encryption'] + if exists_multix509enc: + cert = idp_data['x509certMulti']['encryption'][0] + else: + cert = idp_data['x509cert'] if name_id is not None: if name_id_format is not None: @@ -380,19 +386,32 @@ def is_valid(self, request_data, raise_exceptions=False): signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) - if 'x509cert' not in idp_data or not idp_data['x509cert']: + exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert'] + exists_multix509sign = 'x509certMulti' in idp_data and \ + 'signing' in idp_data['x509certMulti'] and \ + idp_data['x509certMulti']['signing'] + + if not (exists_x509cert or exists_multix509sign): raise OneLogin_Saml2_Error( 'In order to validate the sign on the Logout Request, the x509cert of the IdP is required', OneLogin_Saml2_Error.CERT_NOT_FOUND ) - cert = idp_data['x509cert'] - - if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): + if exists_multix509sign: + for cert in idp_data['x509certMulti']['signing']: + if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): + return True raise OneLogin_Saml2_ValidationError( 'Signature validation failed. Logout Request rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE ) + else: + cert = idp_data['x509cert'] + if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Request rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True except Exception as err: # pylint: disable=R0801sign_alg diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 2653b210..f37a3585 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -146,18 +146,32 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) - if 'x509cert' not in idp_data or not idp_data['x509cert']: + exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert'] + exists_multix509sign = 'x509certMulti' in idp_data and \ + 'signing' in idp_data['x509certMulti'] and \ + idp_data['x509certMulti']['signing'] + + if not (exists_x509cert or exists_multix509sign): raise OneLogin_Saml2_Error( 'In order to validate the sign on the Logout Response, the x509cert of the IdP is required', OneLogin_Saml2_Error.CERT_NOT_FOUND ) - cert = idp_data['x509cert'] - - if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): + if exists_multix509sign: + for cert in idp_data['x509certMulti']['signing']: + if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): + return True raise OneLogin_Saml2_ValidationError( 'Signature validation failed. Logout Response rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE ) + else: + cert = idp_data['x509cert'] + + if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True # pylint: disable=R0801 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index ba524c2b..a97ac860 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -290,15 +290,19 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): fingerprint = idp_data.get('certFingerprint', None) fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) + multicerts = None + if 'x509certMulti' in idp_data and 'signing' in idp_data['x509certMulti'] and idp_data['x509certMulti']['signing']: + multicerts = idp_data['x509certMulti']['signing'] + # 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): + if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False): 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): + if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. SAML Response rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 8322eaf0..01bd5087 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -114,6 +114,8 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals if 'x509certNew' in self.__sp: self.format_sp_cert_new() self.format_sp_key() + if 'x509certMulti' in self.__idp: + self.format_idp_cert_multi() def __load_paths(self, base_path=None): """ @@ -361,14 +363,21 @@ def check_idp_settings(self, settings): exists_x509 = bool(idp.get('x509cert')) exists_fingerprint = bool(idp.get('certFingerprint')) + exists_multix509sign = 'x509certMulti' in idp and \ + 'signing' in idp['x509certMulti'] and \ + idp['x509certMulti']['signing'] + exists_multix509enc = 'x509certMulti' in idp and \ + 'encryption' in idp['x509certMulti'] and \ + idp['x509certMulti']['encryption'] + want_assert_sign = bool(security.get('wantAssertionsSigned')) want_mes_signed = bool(security.get('wantMessagesSigned')) nameid_enc = bool(security.get('nameIdEncrypted')) if (want_assert_sign or want_mes_signed) and \ - not(exists_x509 or exists_fingerprint): + not(exists_x509 or exists_fingerprint or exists_multix509sign): errors.append('idp_cert_or_fingerprint_not_found_and_required') - if nameid_enc and not exists_x509: + if nameid_enc and not (exists_x509 or exists_multix509enc): errors.append('idp_cert_not_found_and_required') return errors @@ -722,6 +731,19 @@ def format_idp_cert(self): """ self.__idp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509cert']) + def format_idp_cert_multi(self): + """ + Formats the Multple IdP certs. + """ + if 'x509certMulti' in self.__idp: + if 'signing' in self.__idp['x509certMulti']: + for idx in range(len(self.__idp['x509certMulti']['signing'])): + self.__idp['x509certMulti']['signing'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['signing'][idx]) + + if 'encryption' in self.__idp['x509certMulti']: + for idx in range(len(self.__idp['x509certMulti']['encryption'])): + self.__idp['x509certMulti']['encryption'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['encryption'][idx]) + def format_sp_cert(self): """ Formats the SP cert. diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 00fefae3..826c8a87 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -915,7 +915,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant @staticmethod @return_false_on_exception - def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None): + def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None, multicerts=None): """ Validates a signature (Message or Assertion). @@ -940,6 +940,9 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid :param xpath: The xpath of the signed element :type: string + :param multicerts: Multiple public certs + :type: list + :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean """ @@ -986,7 +989,17 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid if len(signature_nodes) == 1: signature_node = signature_nodes[0] - return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) + if not multicerts: + return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) + else: + # If multiple certs are provided, I may ignore cert and + # fingerprint provided by the method and just check the + # certs multicerts + fingerprint = fingerprintalg = None + for cert in multicerts: + if OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, False, raise_exceptions=False): + return True + raise OneLogin_Saml2_ValidationError('Signature validation failed. SAML Response rejected.') else: raise OneLogin_Saml2_ValidationError('Expected exactly one signature node; got {}.'.format(len(signature_nodes)), OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES) diff --git a/tests/settings/settings8.json b/tests/settings/settings8.json new file mode 100644 index 00000000..ce30e498 --- /dev/null +++ b/tests/settings/settings8.json @@ -0,0 +1,58 @@ +{ + "strict": false, + "debug": false, + "custom_base_path": "../../../tests/data/customPath/", + "sp": { + "entityId": "http://stuff.com/endpoints/metadata.php", + "assertionConsumerService": { + "url": "http://stuff.com/endpoints/endpoints/acs.php" + }, + "singleLogoutService": { + "url": "http://stuff.com/endpoints/endpoints/sls.php" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "privateKey": "MIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABAoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0VJ5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8JK2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq66PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6rCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4ukXSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLcPQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg==", + "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo" + }, + "idp": { + "entityId": "http://idp.example.com/", + "singleSignOnService": { + "url": "http://idp.example.com/SSOService.php" + }, + "singleLogoutService": { + "url": "http://idp.example.com/SingleLogoutService.php" + }, + "x509cert": "", + "x509certMulti": { + "signing": [ + "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m", + "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo" + ], + "encryption": [ + "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo" + ] + } + }, + "security": { + "authnRequestsSigned": false, + "wantAssertionsSigned": false, + "signMetadata": false + }, + "contactPerson": { + "technical": { + "givenName": "technical_name", + "emailAddress": "technical@example.com" + }, + "support": { + "givenName": "support_name", + "emailAddress": "support@example.com" + } + }, + "organization": { + "en-US": { + "name": "sp_test", + "displayname": "SP test", + "url": "http://sp.example.com" + } + } +} \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 5b5cd56b..b0bfdf92 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -22,15 +22,13 @@ class OneLogin_Saml2_Logout_Request_Test(unittest.TestCase): data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') - def loadSettingsJSON(self): - filename = join(self.settings_path, 'settings1.json') + def loadSettingsJSON(self, name='settings1.json'): + filename = join(self.settings_path, name) if exists(filename): stream = open(filename, 'r') settings = json.load(stream) stream.close() return settings - else: - raise Exception('Settings json file does not exist') def file_contents(self, filename): f = open(filename, 'r') @@ -57,6 +55,27 @@ def testConstructor(self): inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(payload) self.assertRegexpMatches(inflated, '^') + def testCreateDeflatedSAMLLogoutRequestURLParameter(self): """ Tests the OneLogin_Saml2_LogoutRequest Constructor. @@ -426,6 +445,26 @@ def testIsValidSign(self): self.assertFalse(logout_request7.is_valid(request_data)) self.assertEqual('In order to validate the sign on the Logout Request, the x509cert of the IdP is required', logout_request7.get_error()) + def testIsValidSignUsingX509certMulti(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': { + 'SAMLRequest': 'fZJNa+MwEIb/itHdiTz6sC0SQyEsBPoB27KHXoIsj7cGW3IlGfLzV7G7kN1DL2KYmeedmRcdgp7GWT26326JP/FzwRCz6zTaoNbKkSzeKqfDEJTVEwYVjXp9eHpUsKNq9i4640Zyh3xP6BDQx8FZkp1PR3KpqexAl72QmpUCS8SW01IiZz2TVVGD4X1VQYlAsl/oQyKPJAklPIQFzzZEbWNK0YLnlOVA3wqpQCoB7yQ7pWsGq+NKfcQ4q/0+xKXvd8ZNe7Td7AYbw10UxrCbP2aSPbv4Yl/8Qx/R3+SB5bTOoXiDQvFNvjnc7lXrIr75kh+6eYdXPc0jrkMO+/umjXhOtpxP2Q/nJx2/9+uWGbq8X1tV9NqGAW0kzaVvoe1AAJeCSWqYaUVRM2SilKKuqDTpFSlszdcK29RthVm9YriZebYdXpsLdhVAB7VJzif3haYMqqTVcl0JMBR4y+s2zak3sf/4v8l/vlHzBw==', + 'RelayState': '_1037fbc88ec82ce8e770b2bed1119747bb812a07e6', + 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', + 'Signature': 'Ouxo9BV6zmq4yrgamT9EbSKy/UmvSxGS8z26lIMgKOEP4LFR/N23RftdANmo4HafrzSfA0YTXwhKDqbOByS0j+Ql8OdQOes7vGioSjo5qq/Bi+5i6jXwQfphnfcHAQiJL4gYVIifkhhHRWpvYeiysF1Y9J02me0izwazFmoRXr4=' + } + } + settings_info = self.loadSettingsJSON('settings8.json') + settings_info['strict'] = False + settings = OneLogin_Saml2_Settings(settings_info) + logout_request = OneLogin_Saml2_Logout_Request(settings, request_data['get_data']['SAMLRequest']) + self.assertTrue(logout_request.is_valid(request_data)) + def testGetXML(self): """ Tests that we can get the logout request XML directly without diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 3f86df19..f54690ed 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -22,15 +22,13 @@ class OneLogin_Saml2_Logout_Response_Test(unittest.TestCase): data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') - def loadSettingsJSON(self): - filename = join(self.settings_path, 'settings1.json') + def loadSettingsJSON(self, name='settings1.json'): + filename = join(self.settings_path, name) if exists(filename): stream = open(filename, 'r') settings = json.load(stream) stream.close() return settings - else: - raise Exception('Settings json file does not exist') def file_contents(self, filename): f = open(filename, 'r') @@ -367,6 +365,26 @@ def testIsValid(self): response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) self.assertTrue(response_3.is_valid(request_data)) + def testIsValidSignUsingX509certMulti(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': { + 'SAMLResponse': 'fZHbasJAEIZfJey9ZrNZc1gSodRSBKtQxYveyGQz1kCyu2Q24OM3jS21UHo3p++f4Z+CoGud2th3O/hXJGcNYXDtWkNqapVs6I2yQA0pAx2S8lrtH142Ssy5cr31VtuW3SH/E0CEvW+sYcF6VbLTIktFLMWZgxQR8DSP85wDB4GJGMOqShYVaoBUsOCIPY1kyUahEScacG3Ig/FjiUdyxuOZ4IcoUVGq4vSNBSsk3xjwE3Xx3qkwJD+cz3NtuxBN7WxjPN1F1NLcXdwob77tONiS7bZPm93zenvCqopxgVJmuU50jREsZF4noKWAOuNZJbNznnBky+LTDDVd2S+/dje1m+MVOtfidEER3g8Vt2fsPfiBfmePtsbgCO2A/9tL07TaD1ojEQuXtw0/ouFfD19+AA==', + 'RelayState': 'http://stuff.com/endpoints/endpoints/index.php', + 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', + 'Signature': 'OV9c4R0COSjN69fAKCpV7Uj/yx6/KFxvbluVCzdK3UuortpNMpgHFF2wYNlMSG9GcYGk6p3I8nB7Z+1TQchMWZOlO/StjAqgtZhtpiwPcWryNuq8vm/6hnJ3zMDhHTS7F8KG4qkCXmJ9sQD3Y31UNcuygBwIbNakvhDT5Qo9Nsw=' + } + } + settings_info = self.loadSettingsJSON('settings8.json') + settings_info['strict'] = False + settings = OneLogin_Saml2_Settings(settings_info) + logout_response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) + self.assertTrue(logout_response.is_valid(request_data)) + def testGetXML(self): """ Tests that we can get the logout response XML directly without diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index d7ac4673..5d6bebf2 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -22,12 +22,10 @@ class OneLogin_Saml2_Response_Test(unittest.TestCase): data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') - def loadSettingsJSON(self, filename=None): - if filename: - filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', filename) - else: - filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', 'settings1.json') + def loadSettingsJSON(self, name='settings1.json'): + filename = join(self.settings_path, name) if exists(filename): stream = open(filename, 'r') settings = json.load(stream) @@ -1354,6 +1352,16 @@ def testIsValidSign(self): # Modified message self.assertFalse(response_9.is_valid(self.get_request_data())) + def testIsValidSignUsingX509certMulti(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case Using x509certMulti + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings8.json')) + xml = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertTrue(response.is_valid(self.get_request_data())) + def testIsValidSignWithEmptyReferenceURI(self): settings_info = self.loadSettingsJSON() del settings_info['idp']['x509cert'] diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index b23f22f0..df08a6a7 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -356,7 +356,7 @@ def testGetSPMetadata(self): self.assertEquals(1, metadata.count(' Date: Fri, 12 May 2017 16:53:41 +0200 Subject: [PATCH 200/352] Allow metadata to be retrieved from source containing data for multiple entities --- README.md | 20 +++++++ src/onelogin/saml2/idp_metadata_parser.py | 22 ++++++-- .../metadata/idp_multiple_descriptors.xml | 53 +++++++++++++++++++ .../saml2_tests/idp_metadata_parser_test.py | 38 +++++++++++++ 4 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 tests/data/metadata/idp_multiple_descriptors.xml diff --git a/README.md b/README.md index d96dc7d8..7ad159f3 100644 --- a/README.md +++ b/README.md @@ -503,6 +503,23 @@ json_data_file.close() auth = OneLogin_Saml2_Auth(req, settings_data) ``` +#### Metadata Based Configuration + +The method above requires a little extra work to manually specify attributes about the IdP. (And your SP application) + +There's an easier method -- use a metadata exchange. Metadata is just an XML file that defines the capabilities of both the IdP and the SP application. It also contains the X.509 public key certificates which add to the trusted relationship. The IdP administrator can also configure custom settings for an SP based on the metadata. + +Using ````parse_remote```` IdP metadata can be obtained and added to the settings withouth further ado. + +`` +idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://example.com/auth/saml2/idp/metadata') +`` + +If the Metadata contains several entities, the relevant EntityDescriptor can be specified when retrieving the settings from the IdpMetadataParser by its Entity Id value: + +idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(https://example.com/metadatas, entity_id='idp_entity_id') + + #### How load the library #### In order to use the toolkit library you need to import the file that contains the class that you will need @@ -1042,6 +1059,9 @@ A class that contains methods to obtain and parse metadata from IdP For more info, look at the source code; each method is documented and details about what does and how to use it are provided. Make sure to also check the doc folder where HTML documentation about the classes and methods is provided. + + + Demos included in the toolkit ----------------------------- diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index fc9689cb..28154820 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -60,24 +60,29 @@ def get_metadata(url, validate_cert=True): return xml @staticmethod - def parse_remote(url, validate_cert=True, **kwargs): + def parse_remote(url, validate_cert=True, entity_id=None, **kwargs): """ Gets the metadata XML from the provided URL and parse it, returning a dict with extracted data :param url: Url where the XML of the Identity Provider Metadata is published. :type url: string + :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML + that contains multiple EntityDescriptor. + :type entity_id: string + :returns: settings dict with extracted data :rtype: dict """ idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url, validate_cert) - return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata, **kwargs) + return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata, entity_id=entity_id, **kwargs) @staticmethod def parse( idp_metadata, required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, - required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT): + required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, + entity_id=None): """ Parse the Identity Provider metadata and return a dict with extracted data. @@ -104,13 +109,22 @@ def parse( :type required_slo_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT or OneLogin_Saml2_Constants.BINDING_HTTP_POST + :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML + that contains multiple EntityDescriptor. + :type entity_id: string + :returns: settings dict with extracted data :rtype: dict """ data = {} dom = fromstring(idp_metadata) - entity_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:EntityDescriptor') + + entity_desc_path = '//md:EntityDescriptor' + if entity_id: + entity_desc_path += "[@entityID='%s']" % entity_id + + entity_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, entity_desc_path) idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = idp_x509_cert = None diff --git a/tests/data/metadata/idp_multiple_descriptors.xml b/tests/data/metadata/idp_multiple_descriptors.xml new file mode 100644 index 00000000..c77face7 --- /dev/null +++ b/tests/data/metadata/idp_multiple_descriptors.xml @@ -0,0 +1,53 @@ + + + + + + + + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + + + + + + + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + + + + + + + + + + + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + + + + + + + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + + + + + + \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index a9a46908..1c70f1b2 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -273,6 +273,44 @@ def test_parse_required_binding_all(self): self.assertEqual(expected_settings6_7, settings6) self.assertEqual(expected_settings6_7, settings7) + def test_parse_with_entity_id(self): + """ + Tests the parse method of the OneLogin_Saml2_IdPMetadataParser + Case: Provide entity_id to identify the desired IdPDescriptor from + EntitiesDescriptor + """ + xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_multiple_descriptors.xml')) + + # should find first descriptor + data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata) + self.assertEqual("https://foo.example.com/access/saml/idp.xml", data["idp"]["entityId"]) + + # should find desired descriptor + data2 = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata, entity_id="https://bar.example.com/access/saml/idp.xml") + self.assertEqual("https://bar.example.com/access/saml/idp.xml", data2["idp"]["entityId"]) + + expected_settings_json = """ + { + "sp": { + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + }, + "idp": { + "singleLogoutService": { + "url": "https://hello.example.com/access/saml/logout", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "entityId": "https://bar.example.com/access/saml/idp.xml", + "x509cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=", + "singleSignOnService": { + "url": "https://hello.example.com/access/saml/login", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + } + } + """ + expected_settings = json.loads(expected_settings_json) + self.assertEqual(expected_settings, data2) + def testMergeSettings(self): """ Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser From f267167d0f795c30b4ccb79bc719e86e23842b64 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 12 May 2017 19:21:57 +0200 Subject: [PATCH 201/352] Adapt IdP XML metadata parser to take care of multiple IdP certtificates and be able to inject the data obtained on the settings --- src/onelogin/saml2/idp_metadata_parser.py | 39 +++++- .../metadata/idp_metadata_multi_certs.xml | 75 ++++++++++ .../saml2_tests/idp_metadata_parser_test.py | 131 ++++++++++++++++-- 3 files changed, 227 insertions(+), 18 deletions(-) create mode 100644 tests/data/metadata/idp_metadata_multi_certs.xml diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 28154820..d4c69015 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -126,7 +126,7 @@ def parse( entity_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, entity_desc_path) - idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = idp_x509_cert = None + idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = certs = None if len(entity_descriptor_nodes) > 0: for entity_descriptor_node in entity_descriptor_nodes: @@ -157,9 +157,19 @@ def parse( if len(slo_nodes) > 0: idp_slo_url = slo_nodes[0].get('Location', None) - cert_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate") - if len(cert_nodes) > 0: - idp_x509_cert = cert_nodes[0].text + signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") + encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") + + if len(signing_nodes) > 0 or len(encryption_nodes) > 0: + certs = {} + if len(signing_nodes) > 0: + certs['signing'] = [] + for cert_node in signing_nodes: + certs['signing'].append(''.join(cert_node.text.split())) + if len(encryption_nodes) > 0: + certs['encryption'] = [] + for cert_node in encryption_nodes: + certs['encryption'].append(''.join(cert_node.text.split())) data['idp'] = {} @@ -176,8 +186,17 @@ def parse( data['idp']['singleLogoutService']['url'] = idp_slo_url data['idp']['singleLogoutService']['binding'] = required_slo_binding - if idp_x509_cert is not None: - data['idp']['x509cert'] = idp_x509_cert + if certs is not None: + if len(certs) == 1 or \ + (('signing' in certs and len(certs['signing']) == 1) and + ('encryption' in certs and len(certs['encryption']) == 1 and + certs['signing'][0] == certs['encryption'][0])): + if 'signing' in certs: + data['idp']['x509cert'] = certs['signing'][0] + else: + data['idp']['x509cert'] = certs['encryption'][0] + else: + data['idp']['x509certMulti'] = certs if want_authn_requests_signed is not None: data['security'] = {} @@ -211,6 +230,14 @@ def merge_settings(settings, new_metadata_settings): # Guarantee to not modify original data (`settings.copy()` would not # be sufficient, as it's just a shallow copy). result_settings = deepcopy(settings) + + # previously I will take care of cert stuff + if 'idp' in new_metadata_settings and 'idp' in result_settings: + if new_metadata_settings['idp'].get('x509cert', None) and result_settings['idp'].get('x509certMulti',None): + del result_settings['idp']['x509certMulti'] + if new_metadata_settings['idp'].get('x509certMulti', None) and result_settings['idp'].get('x509cert',None): + del result_settings['idp']['x509cert'] + # Merge `new_metadata_settings` into `result_settings`. dict_deep_merge(result_settings, new_metadata_settings) return result_settings diff --git a/tests/data/metadata/idp_metadata_multi_certs.xml b/tests/data/metadata/idp_metadata_multi_certs.xml new file mode 100644 index 00000000..f993f64a --- /dev/null +++ b/tests/data/metadata/idp_metadata_multi_certs.xml @@ -0,0 +1,75 @@ + + + + + + + MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF +BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj +aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW +T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy +MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz +Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV +BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo +3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw +tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx +VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5 +L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t +1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/ +BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB +pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD +VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL +DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC +FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B +AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM +GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c +hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB +vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37 +MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ +WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw== + + + + + + + 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 +WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw== + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index 1c70f1b2..53e9b2b0 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -71,19 +71,21 @@ def testParseRemote(self): self.assertTrue(data is not None and data is not {}) expected_settings_json = """ { - "sp": { - "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier" - }, - "idp": { - "entityId": "https://idp.testshib.org/idp/shibboleth", - "singleSignOnService": { - "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + "sp": { + "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier" + }, + "idp": { + "x509cert": "MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryhm3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEmlGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBnxoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTHot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQIDAQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQwEoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzROZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QPdRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOTMVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhORkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqXMLRKhDgdmA==", + "entityId": "https://idp.testshib.org/idp/shibboleth", + "singleSignOnService": { + "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } } - } } """ expected_settings = json.loads(expected_settings_json) + self.assertEqual(expected_settings, data) def testParse(self): @@ -109,7 +111,7 @@ def testParse(self): "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\\n1sE=", + "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE=", "entityId": "https://app.onelogin.com/saml/metadata/383123" }, "sp": { @@ -137,7 +139,8 @@ def test_parse_testshib_required_binding_sso_redirect(self): "singleSignOnService": { "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - } + }, + "x509cert": "MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryhm3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEmlGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBnxoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTHot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQIDAQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQwEoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzROZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QPdRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOTMVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhORkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqXMLRKhDgdmA==" } } """ @@ -169,6 +172,7 @@ def test_parse_testshib_required_binding_sso_post(self): "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier" }, "idp": { + "x509cert": "MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryhm3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEmlGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBnxoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTHot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQIDAQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQwEoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzROZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QPdRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOTMVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhORkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqXMLRKhDgdmA==", "entityId": "https://idp.testshib.org/idp/shibboleth", "singleSignOnService": { "url": "https://idp.testshib.org/idp/profile/SAML2/POST/SSO", @@ -311,6 +315,44 @@ def test_parse_with_entity_id(self): expected_settings = json.loads(expected_settings_json) self.assertEqual(expected_settings, data2) + def test_parse_multi_certs(self): + """ + Tests the parse method of the OneLogin_Saml2_IdPMetadataParser + Case: IdP metadata contains multiple certs + """ + xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_multi_certs.xml')) + data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata) + + expected_settings_json = """ + { + "sp": { + "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + }, + "idp": { + "singleLogoutService": { + "url": "https://idp.examle.com/saml/slo", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509certMulti": { + "signing": [ + "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==" + ], + "encryption": [ + "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" + ] + }, + "entityId": "https://idp.examle.com/saml/metadata", + "singleSignOnService": { + "url": "https://idp.examle.com/saml/sso", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + } + } + """ + expected_settings = json.loads(expected_settings_json) + self.assertEqual(expected_settings, data) + def testMergeSettings(self): """ Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser @@ -359,7 +401,7 @@ def testMergeSettings(self): "singleLogoutService": { "url": "http://idp.example.com/SingleLogoutService.php" }, - "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\\n1sE=" + "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE=" }, "sp": { "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", @@ -446,6 +488,71 @@ def testMergeSettings(self): expected_settings2 = json.loads(expected_settings2_json) self.assertEqual(expected_settings2, settings_result2) + # Test merging multiple certs + xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_multi_certs.xml')) + data3 = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata) + settings_result3 = OneLogin_Saml2_IdPMetadataParser.merge_settings(settings, data3) + expected_settings3_json = """ + { + "debug": false, + "strict": false, + "custom_base_path": "../../../tests/data/customPath/", + "sp": { + "singleLogoutService": { + "url": "http://stuff.com/endpoints/endpoints/sls.php" + }, + "assertionConsumerService": { + "url": "http://stuff.com/endpoints/endpoints/acs.php" + }, + "entityId": "http://stuff.com/endpoints/metadata.php", + "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + }, + "idp": { + "singleLogoutService": { + "url": "https://idp.examle.com/saml/slo", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509certMulti": { + "signing": [ + "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==" + ], + "encryption": [ + "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" + ] + }, + "entityId": "https://idp.examle.com/saml/metadata", + "singleSignOnService": { + "url": "https://idp.examle.com/saml/sso", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + }, + "security": { + "authnRequestsSigned": false, + "wantAssertionsSigned": false, + "signMetadata": false + }, + "contactPerson": { + "technical": { + "emailAddress": "technical@example.com", + "givenName": "technical_name" + }, + "support": { + "emailAddress": "support@example.com", + "givenName": "support_name" + } + }, + "organization": { + "en-US": { + "displayname": "SP test", + "url": "http://sp.example.com", + "name": "sp_test" + } + } + } + """ + expected_settings3 = json.loads(expected_settings3_json) + self.assertEqual(expected_settings3, settings_result3) if __name__ == '__main__': if is_running_under_teamcity(): From 21a53c36a503aef36baaf115908fec3f5102bfbf Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 12 May 2017 19:36:51 +0200 Subject: [PATCH 202/352] pep8 --- src/onelogin/saml2/idp_metadata_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index d4c69015..a239ff93 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -233,9 +233,9 @@ def merge_settings(settings, new_metadata_settings): # previously I will take care of cert stuff if 'idp' in new_metadata_settings and 'idp' in result_settings: - if new_metadata_settings['idp'].get('x509cert', None) and result_settings['idp'].get('x509certMulti',None): + if new_metadata_settings['idp'].get('x509cert', None) and result_settings['idp'].get('x509certMulti', None): del result_settings['idp']['x509certMulti'] - if new_metadata_settings['idp'].get('x509certMulti', None) and result_settings['idp'].get('x509cert',None): + if new_metadata_settings['idp'].get('x509certMulti', None) and result_settings['idp'].get('x509cert', None): del result_settings['idp']['x509cert'] # Merge `new_metadata_settings` into `result_settings`. From dffcbb37b0d20bc5d985c03c752908598821e4f9 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 15 May 2017 13:02:58 +0200 Subject: [PATCH 203/352] Add method description --- src/onelogin/saml2/idp_metadata_parser.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index a239ff93..5ff705b7 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -32,6 +32,9 @@ def get_metadata(url, validate_cert=True): :param url: Url where the XML of the Identity Provider Metadata is published. :type url: string + :param validate_cert: If the url uses https schema, that flag enables or not the verification of the associated certificate. + :type validate_cert: bool + :returns: metadata XML :rtype: string """ @@ -67,6 +70,9 @@ def parse_remote(url, validate_cert=True, entity_id=None, **kwargs): :param url: Url where the XML of the Identity Provider Metadata is published. :type url: string + :param validate_cert: If the url uses https schema, that flag enables or not the verification of the associated certificate. + :type validate_cert: bool + :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML that contains multiple EntityDescriptor. :type entity_id: string From c6b86c9c2d473197200a409c0315a20ef64f8c1a Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 15 May 2017 13:34:03 +0200 Subject: [PATCH 204/352] Fix bug when retrieving signing and encryption IdP certs --- src/onelogin/saml2/idp_metadata_parser.py | 4 ++-- .../src/OneLogin/saml2_tests/idp_metadata_parser_test.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 5ff705b7..281177a2 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -163,8 +163,8 @@ def parse( if len(slo_nodes) > 0: idp_slo_url = slo_nodes[0].get('Location', None) - signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") - encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") + signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") + encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") if len(signing_nodes) > 0 or len(encryption_nodes) > 0: certs = {} diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index 53e9b2b0..a6f53654 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -334,10 +334,10 @@ def test_parse_multi_certs(self): "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, "x509certMulti": { - "signing": [ + "encryption": [ "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==" ], - "encryption": [ + "signing": [ "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" ] @@ -513,10 +513,10 @@ def testMergeSettings(self): "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, "x509certMulti": { - "signing": [ + "encryption": [ "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==" ], - "encryption": [ + "signing": [ "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" ] From d5c9f7ba927d8a237ce59628970dca8937cd2406 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 15 May 2017 13:38:06 +0200 Subject: [PATCH 205/352] Minor improvement --- src/onelogin/saml2/idp_metadata_parser.py | 148 +++++++++++----------- 1 file changed, 73 insertions(+), 75 deletions(-) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 281177a2..7e2bb704 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -135,84 +135,82 @@ def parse( idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = certs = None if len(entity_descriptor_nodes) > 0: - for entity_descriptor_node in entity_descriptor_nodes: - idp_descriptor_nodes = OneLogin_Saml2_Utils.query(entity_descriptor_node, './md:IDPSSODescriptor') - if len(idp_descriptor_nodes) > 0: - idp_descriptor_node = idp_descriptor_nodes[0] - - idp_entity_id = entity_descriptor_node.get('entityID', None) - - want_authn_requests_signed = entity_descriptor_node.get('WantAuthnRequestsSigned', None) - - name_id_format_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, './md:NameIDFormat') - if len(name_id_format_nodes) > 0: - idp_name_id_format = name_id_format_nodes[0].text - - sso_nodes = OneLogin_Saml2_Utils.query( - idp_descriptor_node, - "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding - ) - - if len(sso_nodes) > 0: - idp_sso_url = sso_nodes[0].get('Location', None) - - slo_nodes = OneLogin_Saml2_Utils.query( - idp_descriptor_node, - "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding - ) - if len(slo_nodes) > 0: - idp_slo_url = slo_nodes[0].get('Location', None) - - signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") - encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") - - if len(signing_nodes) > 0 or len(encryption_nodes) > 0: - certs = {} - if len(signing_nodes) > 0: - certs['signing'] = [] - for cert_node in signing_nodes: - certs['signing'].append(''.join(cert_node.text.split())) - if len(encryption_nodes) > 0: - certs['encryption'] = [] - for cert_node in encryption_nodes: - certs['encryption'].append(''.join(cert_node.text.split())) - - data['idp'] = {} - - if idp_entity_id is not None: - data['idp']['entityId'] = idp_entity_id - - if idp_sso_url is not None: - data['idp']['singleSignOnService'] = {} - data['idp']['singleSignOnService']['url'] = idp_sso_url - data['idp']['singleSignOnService']['binding'] = required_sso_binding - - if idp_slo_url is not None: - data['idp']['singleLogoutService'] = {} - data['idp']['singleLogoutService']['url'] = idp_slo_url - data['idp']['singleLogoutService']['binding'] = required_slo_binding - - if certs is not None: - if len(certs) == 1 or \ - (('signing' in certs and len(certs['signing']) == 1) and - ('encryption' in certs and len(certs['encryption']) == 1 and - certs['signing'][0] == certs['encryption'][0])): - if 'signing' in certs: - data['idp']['x509cert'] = certs['signing'][0] - else: - data['idp']['x509cert'] = certs['encryption'][0] + entity_descriptor_node = entity_descriptor_nodes[0] + idp_descriptor_nodes = OneLogin_Saml2_Utils.query(entity_descriptor_node, './md:IDPSSODescriptor') + if len(idp_descriptor_nodes) > 0: + idp_descriptor_node = idp_descriptor_nodes[0] + + idp_entity_id = entity_descriptor_node.get('entityID', None) + + want_authn_requests_signed = entity_descriptor_node.get('WantAuthnRequestsSigned', None) + + name_id_format_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, './md:NameIDFormat') + if len(name_id_format_nodes) > 0: + idp_name_id_format = name_id_format_nodes[0].text + + sso_nodes = OneLogin_Saml2_Utils.query( + idp_descriptor_node, + "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding + ) + + if len(sso_nodes) > 0: + idp_sso_url = sso_nodes[0].get('Location', None) + + slo_nodes = OneLogin_Saml2_Utils.query( + idp_descriptor_node, + "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding + ) + if len(slo_nodes) > 0: + idp_slo_url = slo_nodes[0].get('Location', None) + + signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") + encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") + + if len(signing_nodes) > 0 or len(encryption_nodes) > 0: + certs = {} + if len(signing_nodes) > 0: + certs['signing'] = [] + for cert_node in signing_nodes: + certs['signing'].append(''.join(cert_node.text.split())) + if len(encryption_nodes) > 0: + certs['encryption'] = [] + for cert_node in encryption_nodes: + certs['encryption'].append(''.join(cert_node.text.split())) + + data['idp'] = {} + + if idp_entity_id is not None: + data['idp']['entityId'] = idp_entity_id + + if idp_sso_url is not None: + data['idp']['singleSignOnService'] = {} + data['idp']['singleSignOnService']['url'] = idp_sso_url + data['idp']['singleSignOnService']['binding'] = required_sso_binding + + if idp_slo_url is not None: + data['idp']['singleLogoutService'] = {} + data['idp']['singleLogoutService']['url'] = idp_slo_url + data['idp']['singleLogoutService']['binding'] = required_slo_binding + + if certs is not None: + if len(certs) == 1 or \ + (('signing' in certs and len(certs['signing']) == 1) and + ('encryption' in certs and len(certs['encryption']) == 1 and + certs['signing'][0] == certs['encryption'][0])): + if 'signing' in certs: + data['idp']['x509cert'] = certs['signing'][0] else: - data['idp']['x509certMulti'] = certs + data['idp']['x509cert'] = certs['encryption'][0] + else: + data['idp']['x509certMulti'] = certs - if want_authn_requests_signed is not None: - data['security'] = {} - data['security']['authnRequestsSigned'] = want_authn_requests_signed + if want_authn_requests_signed is not None: + data['security'] = {} + data['security']['authnRequestsSigned'] = want_authn_requests_signed - if idp_name_id_format: - data['sp'] = {} - data['sp']['NameIDFormat'] = idp_name_id_format - - break + if idp_name_id_format: + data['sp'] = {} + data['sp']['NameIDFormat'] = idp_name_id_format return data @staticmethod From 3d9245ab21477a7d1b5a6013ba7f1e6bfb295b3f Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 18 May 2017 12:27:12 +0200 Subject: [PATCH 206/352] #194 Publish KeyDescriptor[use=encryption] only when required --- src/onelogin/saml2/metadata.py | 21 ++++++++------ src/onelogin/saml2/settings.py | 6 ++-- .../src/OneLogin/saml2_tests/settings_test.py | 28 +++++++++++++++++-- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index d8bc0c75..7561ab6e 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -227,7 +227,7 @@ def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.R return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm, digest_algorithm) @staticmethod - def add_x509_key_descriptors(metadata, cert=None): + def add_x509_key_descriptors(metadata, cert=None, add_encryption=True): """ Adds the x509 descriptors (sign/encryption) to the metadata The same cert will be used for sign/encrypt @@ -238,6 +238,9 @@ def add_x509_key_descriptors(metadata, cert=None): :param cert: x509 cert :type cert: string + :param add_encryption: Determines if the KeyDescriptor[use="encryption"] should be added. + :type add_encryption: boolean + :returns: Metadata with KeyDescriptors :rtype: string """ @@ -265,18 +268,18 @@ def add_x509_key_descriptors(metadata, cert=None): sp_sso_descriptor = entity_descriptor.getElementsByTagName('md:SPSSODescriptor')[0] sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) - sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) + if add_encryption: + sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) signing = xml.getElementsByTagName('md:KeyDescriptor')[0] signing.setAttribute('use', 'signing') - - encryption = xml.getElementsByTagName('md:KeyDescriptor')[1] - encryption.setAttribute('use', 'encryption') - signing.appendChild(key_info) - encryption.appendChild(key_info.cloneNode(True)) - signing.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) - encryption.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) + + if add_encryption: + encryption = xml.getElementsByTagName('md:KeyDescriptor')[1] + encryption.setAttribute('use', 'encryption') + encryption.appendChild(key_info.cloneNode(True)) + encryption.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) return xml.toxml() diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 01bd5087..77a8592a 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -619,11 +619,13 @@ def get_sp_metadata(self): self.get_contacts(), self.get_organization() ) + add_encryption = self.__security['wantNameIdEncrypted'] or self.__security['wantAssertionsEncrypted'] + cert_new = self.get_sp_cert_new() - metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert_new) + metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert_new, add_encryption) cert = self.get_sp_cert() - metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert) + metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert, add_encryption) # Sign metadata if 'signMetadata' in self.__security and self.__security['signMetadata'] is not False: diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index df08a6a7..ab7fd97a 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -341,7 +341,10 @@ def testGetSPMetadata(self): Tests the getSPMetadata method of the OneLogin_Saml2_Settings Case unsigned metadata """ - settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings_info = self.loadSettingsJSON() + settings_info['security']['wantNameIdEncrypted'] = False + settings_info['security']['wantAssertionsEncrypted'] = False + settings = OneLogin_Saml2_Settings(settings_info) metadata = settings.get_sp_metadata() self.assertNotEqual(len(metadata), 0) @@ -352,20 +355,39 @@ def testGetSPMetadata(self): self.assertIn('', metadata) self.assertIn('', metadata) self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata) + self.assertEquals(1, metadata.count(' Date: Thu, 18 May 2017 13:01:51 +0200 Subject: [PATCH 207/352] Release 2.2.2 --- changelog.md | 18 +++++++++++++++++- setup.py | 2 +- .../src/OneLogin/saml2_tests/settings_test.py | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 5346c467..7d8b3173 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,23 @@ # python-saml changelog +### 2.2.2 (May 18, 2017) +* Be able to relax SSL Certificate verification when retrieving idp metadata +* [#195](https://github.com/onelogin/python-saml/pull/195) Be able to register future SP x509cert on the settings and publish it on SP metadata +* [#195](https://github.com/onelogin/python-saml/pull/195) Be able to register more than 1 Identity Provider x509cert, linked with an specific use (signing or encryption +* [#195](https://github.com/onelogin/python-saml/pull/195) Allow metadata to be retrieved from source containing data of multiple entities +* [#195](https://github.com/onelogin/python-saml/pull/195) Adapt IdP XML metadata parser to take care of multiple IdP certtificates and be able to inject the data obtained on the settings. +* [#194](https://github.com/onelogin/python-saml/pull/194) Publish KeyDescriptor[use=encryption] only when required +* [#190](https://github.com/onelogin/python-saml/pull/190) Checking the status of response before assertion count +* Add Pyramid demo example +* Allows underscores in URL hosts +* NameID Format improvements +* [#184](https://github.com/onelogin/python-saml/pull/184) Be able to provide a NameIDFormat to LogoutRequest +* [#180](https://github.com/onelogin/python-saml/pull/180) Add DigestMethod support. (Add sign_algorithm and digest_algorithm parameters to sign_metadata and add_sign) +* Validate serial number as string to work around libxml2 limitation +* Make the Issuer on the Response Optional + ### 2.2.1 (Jan 11, 2017) -* [#175]((https://github.com/onelogin/python-saml/pull/175) Optionally raise detailed exceptions vs. returning False. +* [#175](https://github.com/onelogin/python-saml/pull/175) Optionally raise detailed exceptions vs. returning False. Implement a more specific exception class for handling some validation errors. Improve/Fix tests * [#171](https://github.com/onelogin/python-saml/pull/171) Add hooks to retrieve last-sent and last-received requests and responses * Improved inResponse validation on Responses diff --git a/setup.py b/setup.py index dfbd4b8e..37a8fbd6 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.2.1', + version='2.2.2', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index ab7fd97a..bcdd7e1f 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -365,7 +365,7 @@ def testGetSPMetadata(self): metadata = settings.get_sp_metadata() self.assertEquals(2, metadata.count(' Date: Fri, 19 May 2017 09:45:48 -0400 Subject: [PATCH 208/352] https missing from request dict If you do not have the https set in the request dict, when auth.login is called it will not build the relaystate url properly if your relaystate url should be https. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ad159f3..6393a814 100644 --- a/README.md +++ b/README.md @@ -543,6 +543,7 @@ This parameter has the following scheme: ```javascript req = { + "https": "" "http_host": "", "script_name": "", "server_port": "", @@ -574,7 +575,7 @@ def prepare_from_flask_request(request): 'post_data': request.form.copy() } ``` - +The https dictionary entry should be set to on for https requests and off for http #### Initiate SSO #### From d9b0ce59010eba0def48515e12e5fdd1f8b45983 Mon Sep 17 00:00:00 2001 From: piotch Date: Wed, 31 May 2017 22:53:47 +0200 Subject: [PATCH 209/352] Upgrade to version 1.3.3 of dm.xmlsec.binding --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 37a8fbd6..26f73f1a 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ }, test_suite='tests', install_requires=[ - 'dm.xmlsec.binding==1.3.2', + 'dm.xmlsec.binding==1.3.3', 'isodate>=0.5.0', 'defusedxml==0.4.1', ], From 9dc6cbd116e28014ac8148f1ee9b71a09afac45b Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 31 May 2017 23:59:48 +0200 Subject: [PATCH 210/352] Fix pep8 --- src/onelogin/saml2/metadata.py | 2 +- tests/pep8.rc | 4 ++-- tests/src/OneLogin/saml2_tests/utils_test.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 7561ab6e..c431be48 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -268,7 +268,7 @@ def add_x509_key_descriptors(metadata, cert=None, add_encryption=True): sp_sso_descriptor = entity_descriptor.getElementsByTagName('md:SPSSODescriptor')[0] sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) - if add_encryption: + if add_encryption: sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) signing = xml.getElementsByTagName('md:KeyDescriptor')[0] diff --git a/tests/pep8.rc b/tests/pep8.rc index b60901e4..910e1eaa 100644 --- a/tests/pep8.rc +++ b/tests/pep8.rc @@ -1,3 +1,3 @@ [pep8] -ignore = E501 -max-line-length = 160 \ No newline at end of file +ignore = E501, E731 +max-line-length = 160 diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 459b0996..82dded99 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -655,11 +655,11 @@ def testDeleteLocalSession(self): OneLogin_Saml2_Utils.delete_local_session() self.assertEqual(1, local_session_test) - dscb = lambda: self.session_cear() + dscb = lambda: self.session_clear() OneLogin_Saml2_Utils.delete_local_session(dscb) self.assertEqual(0, local_session_test) - def session_cear(self): + def session_clear(self): """ Auxiliar method to test the delete_local_session method of the OneLogin_Saml2_Utils """ From 0799719409282c77672b846004cf9da2cb8de942 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 15 Jun 2017 20:00:15 +0200 Subject: [PATCH 211/352] Replaces some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml . Release 2.2.3 --- README.md | 2 ++ changelog.md | 4 ++++ setup.py | 2 +- src/onelogin/saml2/auth.py | 4 ++-- src/onelogin/saml2/response.py | 7 +++---- src/onelogin/saml2/utils.py | 6 +++--- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7ad159f3..cece876d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml). #### Warning #### +Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml + Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json) python-saml < v2.2.0 is vulnerable and allows signature wrapping! diff --git a/changelog.md b/changelog.md index 7d8b3173..a245715a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,8 @@ # python-saml changelog +### 2.2.3 (Jun 15, 2017) +* Replace some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml +* Update dm.xmlsec.binding requirement to 1.3.3 version + ### 2.2.2 (May 18, 2017) * Be able to relax SSL Certificate verification when retrieving idp metadata * [#195](https://github.com/onelogin/python-saml/pull/195) Be able to register future SP x509cert on the settings and publish it on SP metadata diff --git a/setup.py b/setup.py index 26f73f1a..7e9b9a83 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.2.2', + version='2.2.3', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 6e56467a..5a7f5a14 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -13,7 +13,7 @@ from base64 import b64encode from urllib import quote_plus -from lxml import etree +from defusedxml.lxml import tostring from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.response import OneLogin_Saml2_Response @@ -486,7 +486,7 @@ def get_last_response_xml(self, pretty_print_if_possible=False): if isinstance(self.__last_response, basestring): response = self.__last_response else: - response = etree.tostring(self.__last_response, pretty_print=pretty_print_if_possible) + response = tostring(self.__last_response, pretty_print=pretty_print_if_possible) return response def get_last_request_xml(self): diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index a97ac860..4d66f590 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -11,8 +11,7 @@ from base64 import b64decode from copy import deepcopy -from lxml import etree -from defusedxml.lxml import fromstring +from defusedxml.lxml import tostring, fromstring from xml.dom.minidom import Document from onelogin.saml2.constants import OneLogin_Saml2_Constants @@ -107,7 +106,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if self.__settings.is_strict(): no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' res = OneLogin_Saml2_Utils.validate_xml( - etree.tostring(self.document), + tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active() ) @@ -120,7 +119,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): # If encrypted, check also the decrypted document if self.encrypted: res = OneLogin_Saml2_Utils.validate_xml( - etree.tostring(self.decrypted_document), + tostring(self.decrypted_document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active() ) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 826c8a87..3d19835c 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -153,7 +153,7 @@ def validate_xml(xml, schema, debug=False): return 'invalid_xml' - return parseString(etree.tostring(dom, encoding='unicode').encode('utf-8')) + return parseString(tostring(dom, encoding='unicode').encode('utf-8')) @staticmethod def format_cert(cert, heads=True): @@ -680,7 +680,7 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None): edata = enc_ctx.encryptXml(enc_data, elem[0]) - newdoc = parseString(etree.tostring(edata, encoding='unicode').encode('utf-8')) + newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8')) if newdoc.hasChildNodes(): child = newdoc.firstChild @@ -897,7 +897,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant dsig_ctx.signKey = sign_key dsig_ctx.sign(signature) - newdoc = parseString(etree.tostring(elem, encoding='unicode').encode('utf-8')) + newdoc = parseString(tostring(elem, encoding='unicode').encode('utf-8')) signature_nodes = newdoc.getElementsByTagName("Signature") From 36cd9c5d303bb10d8176914e14e07dbb9196fd4d Mon Sep 17 00:00:00 2001 From: Florent PIGOUT Date: Fri, 16 Jun 2017 11:43:57 +0200 Subject: [PATCH 212/352] Fix typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cece876d..dd340b24 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml). #### Warning #### -Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml +Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recently, by the sanitized call provided by defusedxml Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json) From fac4767038591e046994aa9335fefda58368bde4 Mon Sep 17 00:00:00 2001 From: Florent PIGOUT Date: Fri, 23 Jun 2017 11:52:14 +0200 Subject: [PATCH 213/352] Get NameID when element decrypted twice --- changelog.md | 3 ++ src/onelogin/saml2/response.py | 3 +- src/onelogin/saml2/utils.py | 8 +++- tests/data/misc/sp4.key | 28 ++++++++++++ tests/src/OneLogin/saml2_tests/utils_test.py | 47 ++++++++++++++++++-- 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 tests/data/misc/sp4.key diff --git a/changelog.md b/changelog.md index a245715a..bb3f142b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,7 @@ # python-saml changelog +### 2.2.4 (unreleased) +* Get NameID when element decrypted twice + ### 2.2.3 (Jun 15, 2017) * Replace some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml * Update dm.xmlsec.binding requirement to 1.3.3 version diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 4d66f590..eb5f73a4 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -798,8 +798,9 @@ def __decrypt_assertion(self, dom): keyinfo.append(encrypted_key[0]) encrypted_data = encrypted_data_nodes[0] - decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug) + decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug=debug, inplace=True) dom.replace(encrypted_assertion_nodes[0], decrypted) + return dom def get_error(self): diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 3d19835c..19f1511a 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -10,6 +10,7 @@ """ import base64 +from copy import deepcopy from datetime import datetime import calendar from hashlib import sha1, sha256, sha384, sha512 @@ -746,7 +747,7 @@ def get_status(dom): return status @staticmethod - def decrypt_element(encrypted_data, key, debug=False): + def decrypt_element(encrypted_data, key, debug=False, inplace=False): """ Decrypts an encrypted element. @@ -759,6 +760,9 @@ def decrypt_element(encrypted_data, key, debug=False): :param debug: Activate the xmlsec debug :type: bool + :param inplace: update passed data with decrypted result + :type: bool + :returns: The decrypted element. :rtype: lxml.etree.Element """ @@ -766,6 +770,8 @@ def decrypt_element(encrypted_data, key, debug=False): encrypted_data = fromstring(str(encrypted_data.toxml())) elif isinstance(encrypted_data, basestring): encrypted_data = fromstring(str(encrypted_data)) + elif not inplace and isinstance(encrypted_data, etree._Element): + encrypted_data = deepcopy(encrypted_data) error_callback_method = None if debug: diff --git a/tests/data/misc/sp4.key b/tests/data/misc/sp4.key new file mode 100644 index 00000000..8be7be60 --- /dev/null +++ b/tests/data/misc/sp4.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD4ZrcXcjCBOQS7 +stUabuXPYnXKvcoJUrMVPRX1zfrXvpfghCrykbL1TKoqGfmEA9oNRoMBOmZCgLlK +eb0TfuEO/u1jf4rRFcK7U/dYEiX74bQgUnJUWTfFlhwPjxGhn9zDrc2tSpworJBV +amyBZIo5Beap5OJLote/Wqp1DZjNyEZ2m8m+lv8udmejmlo5RMoIzuG3VdH6ADC9 +LKF+QsXC/HRZBhLE/y+75/XrNODvX8eM8+9Xp21QlVF1EIZDfNQ2iHsA8GEpJDC5 +aomTW/xExBysejnwP2ROrfm3PIfP64EbB4G01f8eErlXeUD0oQ0gECgIXsJpfBkD +IWMHwx3/AgMBAAECggEAdbLNvFlJ7GDlAj75RJ4ZXAuOPrNw4LwDyON53U9tNP7F +HgfiBa/NuPdLhclq9geRMUsg1dsjCw3NPiGy2mL7JszaFJQhZXLHI1Xk1CE9SD0o +yUvniln/2CqJP0IOG6QQydM3qo24snkZpq9XnHPUHrLSGdwu8aHGUpAWRoJbzdzR +tBWBn6SlkuaE52vcGh7eMdKSICRCg2/gg6LIi89pkiI9tfozAL2LPcDTRGp3DA3w +U6OO8k+d1La4s9G0i22OGSwPxGerTHnBIzpeM/ivRwBypFy3EV9bbjQlheI53xAo +ZMmGeSnQ89MWgY64pnWrX862Mf1EZYTjumDe2dl1kQKBgQD9pBG2BbcQ8qieTf84 +92LeOYTPRdd0N+gdyDKKorRO772zgxBwpSwO285nzy/FKSnpJIDtuee6OFClnDor +Ui6lG1WPQeoSEdH1V10XkfSaoFOz7Hyv9H2dCLvW/VO9KYq07VAmQcvNZnqIW+tI +edSHcQ3I8tnw4CiFa0BPvdhk9wKBgQD6tiuN2NvuNFFLvwpBGp3hjGyn6siyXDyP +8IXQmP66NxKqcX/NafVO3bVh6VrPGd7PL1PloQZ5EBG2PPtRdf/g4aeZKZleCUXm +9OgMEOUqdbTP9TGrmgNPtNBx3jnhnX/GTy/7GK77YlXEVplezWaerwRM7NCFCtp2 +W6K1M961OQKBgQDDSznr2hirrvuP8GRMW4a/rrAI3DDZplZN4CCySDbm9IcvGgJl +iXgT9MDHg2q3t0sy3U18PYEkDEpkSZcsVfneXN6TEGCHCzuLWXovNM2O5VWtmrAi +1vCFIf1nuuRoKP1I89SbsFuYyogcSBIwWsX+h1ji2cJfSmlI2VzKSVW93wKBgQDA +sqwfRoMkP0oM8jUrfQ3Egm4xUiAYFxTlfXUcs7t13UaXgs08USifCYGUVAvcCoJa +tIHDiVS0UEmMzKpOHmghrM9oxbR/tpjnv21reMDrNbVX8ZnPz3ykEtHz816BrtC6 +17qFQJ+d0CMj2XvghfdOGC8yAQL0fzcSqbQRmmCe4QKBgFWY9fqHEKdG/UlxZfBB +C/QRNTJsrbZf9Ok/o1h6BHnK64xUc4elShEwV9IdC4QNW0UCr7WXoGLUkhfUphId +q//KUDNc7VrWj5URsZcGi7WMkqNm9kPkpeuh3iSvh3+q7tK0/yfuj9ZQOjKzQnit +VZBooJAJGdSqYgitpyxB71/n +-----END PRIVATE KEY----- diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 82dded99..cbc36d66 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -5,6 +5,7 @@ from base64 import b64decode import json +from defusedxml.lxml import fromstring from lxml import etree from os.path import dirname, join, exists import unittest @@ -718,15 +719,28 @@ def testDecryptElement(self): key2 = f.read() f.close() - with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"): - OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key2) + # sp.key and sp2.key are equivalent we should be able to decrypt the nameID again + decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key2) + self.assertIn('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag) + self.assertEqual('457bdb600de717891c77647b0806ce59c089d5b8', decrypted_nameid.text) key_3_file_name = join(self.data_path, 'misc', 'sp3.key') f = open(key_3_file_name, 'r') key3 = f.read() f.close() + + # sp.key and sp3.key are equivalent we should be able to decrypt the nameID again + decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key3) + self.assertIn('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag) + self.assertEqual('457bdb600de717891c77647b0806ce59c089d5b8', decrypted_nameid.text) + + key_4_file_name = join(self.data_path, 'misc', 'sp4.key') + f = open(key_4_file_name, 'r') + key4 = f.read() + f.close() + with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"): - OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key3) + OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key4) xml_nameid_enc_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_EncMethod.xml.base64'))) dom_nameid_enc_2 = parseString(xml_nameid_enc_2) @@ -744,6 +758,33 @@ def testDecryptElement(self): with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"): OneLogin_Saml2_Utils.decrypt_element(encrypted_data_3, key) + def testDecryptElementInplace(self): + """ + Tests the decrypt_element method of the OneLogin_Saml2_Utils with inplace=True + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + key = settings.get_sp_key() + + xml_nameid_enc = b64decode(self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64'))) + dom = fromstring(xml_nameid_enc) + encrypted_node = dom.xpath('//saml:EncryptedID/xenc:EncryptedData', namespaces=OneLogin_Saml2_Constants.NSMAP)[0] + + # can be decrypted twice when copy the node first + for _ in range(2): + decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=False) + self.assertIn('NameID', decrypted_nameid.tag) + self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text) + + # can only be decrypted once in place + decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=True) + self.assertIn('NameID', decrypted_nameid.tag) + self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text) + + # can't be decrypted twice since it has been dcrypted inplace + with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"): + OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=True) + def testAddSign(self): """ Tests the add_sign method of the OneLogin_Saml2_Utils From 295364013442d3460f03e7bc832bb94c50e3fbc9 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 26 Jul 2017 13:00:26 +0200 Subject: [PATCH 214/352] Fix minor typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ea9291a5..9bb01b16 100644 --- a/README.md +++ b/README.md @@ -865,7 +865,7 @@ else: ### SP Key rollover ### -If you plan to update the SP x509cert and privateKey you can define the new x509cert as $settings['sp']['x509certNew'] and it will be +If you plan to update the SP x509cert and privateKey you can define the new x509cert as settings['sp']['x509certNew'] and it will be published on the SP metadata so Identity Providers can read them and get ready for rollover. @@ -874,7 +874,7 @@ published on the SP metadata so Identity Providers can read them and get ready f In some scenarios the IdP uses different certificates for signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata. -In order to handle that the toolkit offers the $settings['idp']['x509certMulti'] parameter. +In order to handle that the toolkit offers the settings['idp']['x509certMulti'] parameter. When that parameter is used, 'x509cert' and 'certFingerprint' values will be ignored by the toolkit. From b5ff71417a2fde394a214087d9e404392ee52a0d Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 13 Sep 2017 12:29:29 +0200 Subject: [PATCH 215/352] Fix #204. On a LogoutRequest if the NameIdFormat is entity, NameQualifier and SPNameQualifier will be ommited. If the NameIdFormat is not entity and a NameQualifier is provided, then the SPNameQualifier will be also added. Update info related to LogoutRequest on the README --- README.md | 8 ++++++- src/onelogin/saml2/logout_request.py | 9 ++++++-- tests/src/OneLogin/saml2_tests/auth_test.py | 2 +- .../saml2_tests/logout_request_test.py | 22 ++++++++++++++++++- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9bb01b16..2e6842ee 100644 --- a/README.md +++ b/README.md @@ -801,11 +801,17 @@ target_url = 'https://example.com' auth.logout(return_to=target_url) ``` -Also there are 2 optional parameters that can be set: +Also there are 4 optional parameters that can be set: * name_id. That will be used to build the LogoutRequest. If not name_id parameter is set and the auth object processed a SAML Response with a NameId, then this NameId will be used. * session_index. SessionIndex that identifies the session of the user. +* nq. IDP Name Qualifier +* name_id_format. The NameID Format that will be set in the LogoutRequest + +If no name_id is provided, the LogoutRequest will contain a NameID with the entity Format. +If name_id is provided and no name_id_format is provided, the NameIDFormat of the settings will be used. +If nq is provided, the SPNameQualifier will be also attached to the NameId. 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 diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 99c97198..6e6a1903 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -80,10 +80,15 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= nameIdFormat = name_id_format else: nameIdFormat = sp_data['NameIDFormat'] - spNameQualifier = None else: - name_id = idp_data['entityId'] nameIdFormat = OneLogin_Saml2_Constants.NAMEID_ENTITY + + spNameQualifier = None + if nameIdFormat == OneLogin_Saml2_Constants.NAMEID_ENTITY: + name_id = idp_data['entityId'] + nq = None + elif nq is not None: + # We only gonna include SPNameQualifier if NameQualifier is provided spNameQualifier = sp_data['entityId'] name_id_obj = OneLogin_Saml2_Utils.generate_name_id( diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 9c42c0bc..f80d26e1 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -1047,7 +1047,7 @@ def testGetLastLogoutRequest(self): expectedFragment = ( ' Destination="http://idp.example.com/SingleLogoutService.php">\n' ' http://stuff.com/endpoints/metadata.php\n' - ' http://idp.example.com/\n' + ' http://idp.example.com/\n' ' \n ' ) self.assertIn(expectedFragment, auth.get_last_request_xml()) diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index b0bfdf92..d1eab57e 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -154,9 +154,11 @@ def testGetNameIdData(self): OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key) idp_data = settings.get_idp_data() + sp_data = settings.get_sp_data() expected_name_id_data = { 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress', 'NameQualifier': idp_data['entityId'], + 'SPNameQualifier': sp_data['entityId'], 'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69' } @@ -169,6 +171,24 @@ def testGetNameIdData(self): name_id_data_3 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom) self.assertEqual(expected_name_id_data, name_id_data_3) + expected_name_id_data = { + 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress', + 'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69' + } + logout_request = OneLogin_Saml2_Logout_Request(settings, None, expected_name_id_data['Value'], None, None, expected_name_id_data['Format']) + dom = parseString(logout_request.get_xml()) + name_id_data_4 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom) + self.assertEqual(expected_name_id_data, name_id_data_4) + + expected_name_id_data = { + 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity', + 'Value': 'http://idp.example.com/' + } + logout_request = OneLogin_Saml2_Logout_Request(settings) + dom = parseString(logout_request.get_xml()) + name_id_data_5 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom) + self.assertEqual(expected_name_id_data, name_id_data_5) + def testGetNameId(self): """ Tests the get_nameid of the OneLogin_Saml2_LogoutRequest @@ -478,7 +498,7 @@ def testGetXML(self): expectedFragment = ( 'Destination="http://idp.example.com/SingleLogoutService.php">\n' ' http://stuff.com/endpoints/metadata.php\n' - ' http://idp.example.com/\n' + ' http://idp.example.com/\n' ' \n ' ) self.assertIn(expectedFragment, logout_request_generated.get_xml()) From e70d0cd8f3c772ebd0b4606fd7ce02b5d3cce97d Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 14 Sep 2017 15:33:21 +0200 Subject: [PATCH 216/352] Be able to get at the auth object the last processed ID (response/assertion) and the last generated ID. Reset errorReason attribute of the auth object after each Process method --- README.md | 13 +++ src/onelogin/saml2/auth.py | 36 +++++++- src/onelogin/saml2/logout_response.py | 2 + src/onelogin/saml2/response.py | 30 +++++++ .../data/responses/valid_response.xml.base64 | 2 +- tests/src/OneLogin/saml2_tests/auth_test.py | 82 ++++++++++++++++++- .../src/OneLogin/saml2_tests/response_test.py | 49 +++++++++++ 7 files changed, 210 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2e6842ee..aa740fce 100644 --- a/README.md +++ b/README.md @@ -889,6 +889,13 @@ The 'x509certMulti' is an array with 2 keys: - 'encryption' An array with one unique cert that will be used to encrypt data to be sent to the IdP +### Replay attacks ### + + In order to avoid reply attacks, you can store the ID of the SAML messages already processed, to avoid processing them twice. Since the Messages expires and will be invalidated due that fact, you don't need to store those IDs longer than the time frame that you currently accepting. + + Get the ID of the last processed message/assertion with the get_last_message_id/get_last_assertion_id method of the Auth object. + + ### Main classes and methods ### Described below are the main classes and methods that can be invoked from the SAML2 library. @@ -920,6 +927,9 @@ Main class of OneLogin Python Toolkit * ***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 had an encrypted assertion, decrypts it. +* ***get_last_message_id*** The ID of the last Response SAML message processed. +* ***get_last_assertion_id*** The ID of the last assertion processed. +* ***get_last_assertion_not_on_or_after*** The NotOnOrAfter value of the valid SubjectConfirmationData node (if any) of the last assertion processed (is only calculated with strict = true) #### OneLogin_Saml2_Auth - authn_request.py #### @@ -948,6 +958,9 @@ SAML 2 Authentication Response class * ***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*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it). +* ***get_id*** the ID of the response +* ***get_assertion_id*** the ID of the assertion in the response +* ***get_assertion_not_on_or_after*** the NotOnOrAfter value of the valid SubjectConfirmationData if any #### OneLogin_Saml2_LogoutRequest - logout_request.py #### diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 5a7f5a14..bb564e4d 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -59,6 +59,9 @@ 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_message_id = None + self.__last_assertion_id = None + self.__last_assertion_not_on_or_after = None self.__last_request = None self.__last_response = None @@ -90,6 +93,7 @@ def process_response(self, request_id=None): :raises: OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND, when a POST with a SAMLResponse is not found """ self.__errors = [] + self.__error_reason = None if 'post_data' in self.__request_data and 'SAMLResponse' in self.__request_data['post_data']: # AuthnResponse -- HTTP_POST Binding @@ -101,6 +105,9 @@ def process_response(self, request_id=None): self.__nameid_format = response.get_nameid_format() self.__session_index = response.get_session_index() self.__session_expiration = response.get_session_not_on_or_after() + self.__last_message_id = response.get_id() + self.__last_assertion_id = response.get_assertion_id() + self.__last_assertion_not_on_or_after = response.get_assertion_not_on_or_after() self.__authenticated = True else: @@ -127,6 +134,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ :returns: Redirection URL """ self.__errors = [] + self.__error_reason = None if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']: logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) @@ -136,8 +144,10 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ self.__error_reason = logout_response.get_error() elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS: self.__errors.append('logout_not_success') - elif not keep_local_session: - OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) + else: + self.__last_message_id = logout_response.id + if not keep_local_session: + OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']: logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest']) @@ -150,6 +160,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) in_response_to = logout_request.id + self.__last_message_id = logout_request.id response_builder = OneLogin_Saml2_Logout_Response(self.__settings) response_builder.build(in_response_to) self.__last_response = response_builder.get_xml() @@ -241,6 +252,13 @@ def get_session_expiration(self): """ return self.__session_expiration + def get_last_assertion_not_on_or_after(self): + """ + The NotOnOrAfter value of the valid SubjectConfirmationData node + (if any) of the last assertion processed + """ + return self.__last_assertion_not_on_or_after + def get_errors(self): """ Returns a list with code errors if something went wrong @@ -282,6 +300,20 @@ def get_last_request_id(self): """ return self.__last_request_id + def get_last_message_id(self): + """ + :returns: The ID of the last Response SAML message processed. + :rtype: string + """ + return self.__last_message_id + + def get_last_assertion_id(self): + """ + :returns: The ID of the last assertion processed. + :rtype: string + """ + return self.__last_assertion_id + def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True): """ Initiates the SSO process. diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index f37a3585..eec5cc94 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -40,10 +40,12 @@ def __init__(self, settings, response=None): """ self.__settings = settings self.__error = None + self.id = None if response is not None: self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response) self.document = parseString(self.__logout_response) + self.id = self.document.documentElement.getAttribute('ID') def get_issuer(self): """ diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index eb5f73a4..d57f8c42 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -43,6 +43,7 @@ def __init__(self, settings, response): self.document = fromstring(self.response) self.decrypted_document = None self.encrypted = None + self.valid_scd_not_on_or_after = None # Quick check for the presence of EncryptedAssertion encrypted_assertion_nodes = self.__query('/samlp:Response/saml:EncryptedAssertion') @@ -258,6 +259,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time(nb) if parsed_nb > OneLogin_Saml2_Utils.now(): continue + + if nooa: + self.valid_scd_not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa) + any_subject_confirmation = True break @@ -487,6 +492,12 @@ def get_session_not_on_or_after(self): not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(authn_statement_nodes[0].get('SessionNotOnOrAfter')) return not_on_or_after + def get_assertion_not_on_or_after(self): + """ + Returns the NotOnOrAfter value of the valid SubjectConfirmationData node if any + """ + return self.valid_scd_not_on_or_after + def get_session_index(self): """ Gets the SessionIndex from the AuthnStatement @@ -820,3 +831,22 @@ def get_xml_document(self): return self.decrypted_document else: return self.document + + def get_id(self): + """ + :returns: the ID of the response + :rtype: string + """ + return self.document.get('ID', None) + + def get_assertion_id(self): + """ + :returns: the ID of the assertion in the response + :rtype: string + """ + if not self.validate_num_assertions(): + raise OneLogin_Saml2_ValidationError( + 'SAML Response must contain 1 assertion', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS + ) + return self.__query_assertion('')[0].get('ID', None) diff --git a/tests/data/responses/valid_response.xml.base64 b/tests/data/responses/valid_response.xml.base64 index 42ff8eb4..5a917f2f 100644 --- a/tests/data/responses/valid_response.xml.base64 +++ b/tests/data/responses/valid_response.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGNjMzE1NjhiLWM0NmQtZmY3NS1iYTJlLTUzMDM0ODQ5ODBkYSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGNjMzE1NjhiLWM0NmQtZmY3NS1iYTJlLTUzMDM0ODQ5ODBkYSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+cTZnVllaZUJmV21TelhzUUNEVUFYc2hyNjVRPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5ES041ZWVwbm5CU3NvNVd3VHFwZ1lxQ2hsUXA1YXlVYUUrWlVVZ09CK2t5RDJEOUMxRjlSblVTK1VTa2hJQ2dDWVNTamtqQWE4OTZNNzgzdFFMd3dwcGZBSG1NMWpUcTRUVm1xKzlQOTZyQ29LUzUwR0FiZHVOUXFlSTl2T1EraTlXRWVkcFFFeWFsNEJNbS9pTkxHNE00Lzl5alRrVTU2OUdOOGZBOUJSb1E9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDA4YzJhNmJiLTdlZTQtOGRjMi04ZmUyLWYwNTVlZWQ5M2RlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZngwOGMyYTZiYi03ZWU0LThkYzItOGZlMi1mMDU1ZWVkOTNkZTQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkVnQzhKSU1zL2VHaXdiSTBVc3AvWGRYUEtnOD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+MW9JZFJ1bUJDaUNOdXVtYzVYNkhQYWI1L2xXZjZqR0dOR0dGOWxKRWxOc2lOYThpK3dSdkEveVF1YUFvMmp5bGNnV1pMWmZscjc2VnYrYVF0ZFA0LzNDR0djall5cUxWc0k0SS9iZjdXakk0dW1nMXl6aU5zNzMrelUzQThKK21LV013Q1J0eEZibm9BcnNyZzVFcVdOT2MrYkdWMEFsYnFCZTF4RllGcGVnPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQyYmU0MGJmLTM5YzMtNzdmMC1jNmFlLThiZjJlMjNhMWEyZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQyYmU0MGJmLTM5YzMtNzdmMC1jNmFlLThiZjJlMjNhMWEyZSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+M1JNaTI0V0F2cjlnTHdWZ0NtUDlsM2NneCtFPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5FR2ZSVnRTblJqVkJwdkpMdjExNnhWcmovaDVVQnJlTDNnV0V3ZnNORHkrMU9iaC9XTHZlR01uS2xIN0draHU5eXNIUVkxYzRnSER4SVgyaXZtM3YzVFhqK0V3ZDVhMVE2dXgvbXZJSFRvSUR5SnFLL25LSUtVZFEwTWhIcHVXUnF1OEJoWGZQcnRGdWkzOHRhamJpNjFTUnMxN0ZJc3YvbFJLb1JScWJIYlU9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDU3ZGZkYTYwLWIyMTEtNGNkYS0wZjYzLTZkNWRlYjY5ZTViYiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng1N2RmZGE2MC1iMjExLTRjZGEtMGY2My02ZDVkZWI2OWU1YmIiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPndIVUpDalpLRWVtd3E2eGZzMkNIbUd3UXNIND08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+Y2VFd0NtbFQ2d3lpdGVLZE5JaVNFLzBoTG5rMkRweEh0K24vdzlhTHp4MmpneDJOTzBiUTFjb0xyYlBmc1A1SjhNYkNBQWRUb20yUkxaTUxIdTNwZjBHeXJ6cUUxTkhIMmthaGJiSWtKYnZJWkhhaE1JaFZNUW1RMzhJMDdQRC8wc3BjdkJqQS9lblk0SWtWR2VQdmUwV3FhaU5ZUGNOeDNaTDJPdENMZEtzPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index f80d26e1..c50fc6e8 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -118,7 +118,7 @@ def testGetSessionExpiration(self): self.assertIsNone(auth2.get_session_expiration()) auth2.process_response() - self.assertEqual(1392802621, auth2.get_session_expiration()) + self.assertEqual(2655106621, auth2.get_session_expiration()) def testGetLastErrorReason(self): """ @@ -1002,6 +1002,21 @@ def testBuildResponseSignature(self): with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLResponse but can't load the SP private key"): auth2.build_response_signature(message, relay_state) + def testGetLastRequestID(self): + settings_info = self.loadSettingsJSON() + request_data = self.get_request() + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) + + auth.login() + id1 = auth.get_last_request_id() + self.assertNotEqual(id1, None) + + auth.logout() + id2 = auth.get_last_request_id() + self.assertNotEqual(id2, None) + + self.assertNotEqual(id1, id2) + def testGetLastSAMLResponse(self): settings = self.loadSettingsJSON() message = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64')) @@ -1084,6 +1099,71 @@ def testGetLastLogoutResponse(self): auth.process_slo() self.assertEqual(response, auth.get_last_response_xml()) + def testGetInfoFromLastResponseReceived(self): + """ + Tests the get_last_message_id, get_last_assertion_id and get_last_assertion_not_on_or_after + of the OneLogin_Saml2_Auth class + """ + settings = self.loadSettingsJSON() + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + del request_data['get_data'] + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + + auth.process_response() + self.assertEqual(auth.get_last_message_id(), 'pfx42be40bf-39c3-77f0-c6ae-8bf2e23a1a2e') + self.assertEqual(auth.get_last_assertion_id(), 'pfx57dfda60-b211-4cda-0f63-6d5deb69e5bb') + self.assertIsNone(auth.get_last_assertion_not_on_or_after()) + + # NotOnOrAfter is only calculated with strict = true + # If invalid, response id and assertion id are not obtained + + settings['strict'] = True + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_response() + self.assertNotEqual(len(auth.get_errors()), 0) + self.assertIsNone(auth.get_last_message_id()) + self.assertIsNone(auth.get_last_assertion_id()) + self.assertIsNone(auth.get_last_assertion_not_on_or_after()) + + request_data['https'] = 'on' + request_data['http_host'] = 'pitbulk.no-ip.org' + request_data['script_name'] = '/newonelogin/demo1/index.php?acs' + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_response() + self.assertEqual(len(auth.get_errors()), 0) + self.assertEqual(auth.get_last_message_id(), 'pfx42be40bf-39c3-77f0-c6ae-8bf2e23a1a2e') + self.assertEqual(auth.get_last_assertion_id(), 'pfx57dfda60-b211-4cda-0f63-6d5deb69e5bb') + self.assertEqual(auth.get_last_assertion_not_on_or_after(), 2671081021) + + def testGetIdFromLogoutRequest(self): + """ + Tests the get_last_message_id of the OneLogin_Saml2_Auth class + Case Valid Logout request + """ + 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() + self.assertIn(auth.get_last_message_id(), 'ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e') + + def testGetIdFromLogoutResponse(self): + """ + Tests the get_last_message_id of the OneLogin_Saml2_Auth class + Case Valid Logout response + """ + settings = self.loadSettingsJSON() + 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.assertIn(auth.get_last_message_id(), '_f9ee61bd9dbf63606faa9ae3b10548d5b3656fb859') if __name__ == '__main__': if is_running_under_teamcity(): diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 5d6bebf2..f8c396e0 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1408,6 +1408,55 @@ def testStatusCheckBeforeAssertionCheck(self): with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder'): response.is_valid(self.get_request_data(), raise_exceptions=True) + def testGetId(self): + """ + Tests that we can retrieve the ID of the Response + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertEqual(response.get_id(), 'pfxc3d2b542-0f7e-8767-8e87-5b0dc6913375') + + def testGetAssertionId(self): + """ + Tests that we can retrieve the ID of the Assertion + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertEqual(response.get_assertion_id(), '_cccd6024116641fe48e0ae2c51220d02755f96c98d') + + def testGetAssertionNotOnOrAfter(self): + """ + Tests that we can retrieve the NotOnOrAfter value of + the valid SubjectConfirmationData + """ + settings_data = self.loadSettingsJSON() + request_data = self.get_request_data() + settings = OneLogin_Saml2_Settings(settings_data) + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, message) + self.assertIsNone(response.get_assertion_not_on_or_after()) + + response.is_valid(request_data) + self.assertIsNone(response.get_error()) + self.assertIsNone(response.get_assertion_not_on_or_after()) + + settings_data['strict'] = True + settings = OneLogin_Saml2_Settings(settings_data) + response = OneLogin_Saml2_Response(settings, message) + + response.is_valid(request_data) + self.assertNotEqual(response.get_error(), None) + self.assertIsNone(response.get_assertion_not_on_or_after()) + + request_data['https'] = 'on' + request_data['http_host'] = 'pitbulk.no-ip.org' + request_data['script_name'] = '/newonelogin/demo1/index.php?acs' + response.is_valid(request_data) + self.assertIsNone(response.get_error()) + self.assertEqual(response.get_assertion_not_on_or_after(), 2671081021) + if __name__ == '__main__': if is_running_under_teamcity(): From df3db495c2576c706900fbd3ede2488ba0edfb4c Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 14 Sep 2017 18:08:24 +0200 Subject: [PATCH 217/352] Fix issue on getting multiple certs when only sign or encryption certs --- src/onelogin/saml2/idp_metadata_parser.py | 4 +- .../idp_metadata_multi_signing_certs.xml | 75 +++++++++++++++++++ .../saml2_tests/idp_metadata_parser_test.py | 36 +++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 tests/data/metadata/idp_metadata_multi_signing_certs.xml diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 7e2bb704..097b3342 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -193,7 +193,9 @@ def parse( data['idp']['singleLogoutService']['binding'] = required_slo_binding if certs is not None: - if len(certs) == 1 or \ + if (len(certs) == 1 and + (('signing' in certs and len(certs['signing']) == 1) or + ('encryption' in certs and len(certs['encryption']) == 1))) or \ (('signing' in certs and len(certs['signing']) == 1) and ('encryption' in certs and len(certs['encryption']) == 1 and certs['signing'][0] == certs['encryption'][0])): diff --git a/tests/data/metadata/idp_metadata_multi_signing_certs.xml b/tests/data/metadata/idp_metadata_multi_signing_certs.xml new file mode 100644 index 00000000..0cba257a --- /dev/null +++ b/tests/data/metadata/idp_metadata_multi_signing_certs.xml @@ -0,0 +1,75 @@ + + + + + + + MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF +BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj +aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW +T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy +MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz +Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV +BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo +3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw +tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx +VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5 +L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t +1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/ +BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB +pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD +VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL +DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC +FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B +AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM +GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c +hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB +vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37 +MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ +WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw== + + + + + + + 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 +WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw== + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index a6f53654..58e2852e 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -353,6 +353,42 @@ def test_parse_multi_certs(self): expected_settings = json.loads(expected_settings_json) self.assertEqual(expected_settings, data) + def test_parse_multi_singing_certs(self): + """ + Tests the parse method of the OneLogin_Saml2_IdPMetadataParser + Case: IdP metadata contains multiple signing certs and no encryption certs + """ + xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_multi_signing_certs.xml')) + data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata) + + expected_settings_json = """ + { + "sp": { + "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + }, + "idp": { + "singleLogoutService": { + "url": "https://idp.examle.com/saml/slo", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509certMulti": { + "signing": [ + "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==", + "MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ==", + "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==" + ] + }, + "entityId": "https://idp.examle.com/saml/metadata", + "singleSignOnService": { + "url": "https://idp.examle.com/saml/sso", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + } + } + """ + expected_settings = json.loads(expected_settings_json) + self.assertEqual(expected_settings, data) + def testMergeSettings(self): """ Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser From 30cbe7c056f9e2a3892b3c61f79ac9086aa0631e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 14 Sep 2017 18:38:22 +0200 Subject: [PATCH 218/352] Allow empty nameid if setting wantNameId is false. Only raise Exceptions when strict mode is enabled --- src/onelogin/saml2/response.py | 7 ++-- .../src/OneLogin/saml2_tests/response_test.py | 32 +++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index d57f8c42..a5992617 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -421,16 +421,19 @@ def get_nameid_data(self): nameid_nodes = self.__query_assertion('/saml:Subject/saml:NameID') if nameid_nodes: nameid = nameid_nodes[0] + + is_strict = self.__settings.is_strict() + want_nameid = self.__settings.get_security_data().get('wantNameId', True) if nameid is None: security = self.__settings.get_security_data() - if security.get('wantNameId', True): + if is_strict and want_nameid: raise OneLogin_Saml2_ValidationError( 'NameID not found in the assertion of the Response', OneLogin_Saml2_ValidationError.NO_NAMEID ) else: - if self.__settings.is_strict() and not nameid.text: + if is_strict and want_nameid and not nameid.text: raise OneLogin_Saml2_ValidationError( 'An empty NameID value found', OneLogin_Saml2_ValidationError.EMPTY_NAMEID diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index f8c396e0..e811b003 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -82,6 +82,7 @@ def testReturnNameId(self): Tests the get_nameid method of the OneLogin_Saml2_Response """ json_settings = self.loadSettingsJSON() + json_settings['strict'] = True settings = OneLogin_Saml2_Settings(json_settings) xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) @@ -135,11 +136,18 @@ def testReturnNameId(self): with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): response_9.get_nameid() + json_settings['security']['wantNameId'] = False + settings = OneLogin_Saml2_Settings(json_settings) + + nameid_9 = response_9.get_nameid() + self.assertEqual(None, nameid_9) + def testReturnNameIdFormat(self): """ Tests the get_nameid_format method of the OneLogin_Saml2_Response """ json_settings = self.loadSettingsJSON() + json_settings['strict'] = True settings = OneLogin_Saml2_Settings(json_settings) xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) @@ -193,11 +201,18 @@ def testReturnNameIdFormat(self): with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): response_9.get_nameid_format() + json_settings['security']['wantNameId'] = False + settings = OneLogin_Saml2_Settings(json_settings) + + nameid_9 = response_9.get_nameid_format() + self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', nameid_9) + def testGetNameIdData(self): """ Tests the get_nameid_data method of the OneLogin_Saml2_Response """ json_settings = self.loadSettingsJSON() + json_settings['strict'] = True settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) @@ -231,8 +246,9 @@ 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(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): - response_4.get_nameid_data() + + nameid_data_4 = response_4.get_nameid_data() + self.assertEqual({}, nameid_data_4) json_settings['security']['wantNameId'] = True settings = OneLogin_Saml2_Settings(json_settings) @@ -262,13 +278,23 @@ def testGetNameIdData(self): response_8 = OneLogin_Saml2_Response(settings, xml_5) with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value.'): response_8.get_nameid_data() - self.assertTrue(False) xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) response_9 = OneLogin_Saml2_Response(settings, xml_6) with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): response_9.get_nameid_data() + json_settings['security']['wantNameId'] = False + settings = OneLogin_Saml2_Settings(json_settings) + + nameid_data_9 = response_9.get_nameid_data() + + expected_nameid_data_4 = { + 'Value': None, + 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + } + self.assertEqual(expected_nameid_data_4, nameid_data_9) + def testCheckStatus(self): """ Tests the check_status method of the OneLogin_Saml2_Response From ef91db1fb7c5b80555fe8365908a28e3838d5d4e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 15 Sep 2017 12:39:09 +0200 Subject: [PATCH 219/352] Improve previous commited tests --- src/onelogin/saml2/response.py | 4 +- .../src/OneLogin/saml2_tests/response_test.py | 269 +++++++++++++----- 2 files changed, 202 insertions(+), 71 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index a5992617..869a4e7a 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -425,8 +425,6 @@ def get_nameid_data(self): is_strict = self.__settings.is_strict() want_nameid = self.__settings.get_security_data().get('wantNameId', True) if nameid is None: - security = self.__settings.get_security_data() - if is_strict and want_nameid: raise OneLogin_Saml2_ValidationError( 'NameID not found in the assertion of the Response', @@ -443,7 +441,7 @@ def get_nameid_data(self): for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: value = nameid.get(attr, None) if value: - if self.__settings.is_strict() and attr == 'SPNameQualifier': + if is_strict and attr == 'SPNameQualifier': sp_data = self.__settings.get_sp_data() sp_entity_id = sp_data.get('entityId', '') if sp_entity_id != value: diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index e811b003..6e4c1df8 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -82,8 +82,7 @@ def testReturnNameId(self): Tests the get_nameid method of the OneLogin_Saml2_Response """ json_settings = self.loadSettingsJSON() - json_settings['strict'] = True - + json_settings['strict'] = False settings = OneLogin_Saml2_Settings(json_settings) xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) @@ -97,58 +96,98 @@ def testReturnNameId(self): response_3 = OneLogin_Saml2_Response(settings, xml_3) self.assertEqual('_68392312d490db6d355555cfbbd8ec95d746516f60', response_3.get_nameid()) + json_settings['strict'] = True + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + 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(OneLogin_Saml2_ValidationError, 'NameID not 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 + json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) - response_5 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): - response_5.get_nameid() + self.assertIsNone(response_5.get_nameid()) + json_settings['strict'] = False json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) - response_6 = OneLogin_Saml2_Response(settings, xml_4) - nameid_6 = response_6.get_nameid() - self.assertIsNone(nameid_6) + self.assertIsNone(response_6.get_nameid()) + + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_7 = OneLogin_Saml2_Response(settings, xml_4) + self.assertIsNone(response_7.get_nameid()) del json_settings['security']['wantNameId'] settings = OneLogin_Saml2_Settings(json_settings) + response_8 = OneLogin_Saml2_Response(settings, xml_4) + self.assertIsNone(response_8.get_nameid()) - response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): - response_7.get_nameid() + json_settings['strict'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_9 = OneLogin_Saml2_Response(settings, xml_4) + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): + response_9.get_nameid() + + json_settings['strict'] = False + settings = OneLogin_Saml2_Settings(json_settings) + xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64')) + response_10 = OneLogin_Saml2_Response(settings, xml_5) + self.assertEqual('test@example.com', response_10.get_nameid()) 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) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value'): - response_8.get_nameid() + response_11 = OneLogin_Saml2_Response(settings, xml_5) + with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value.'): + response_11.get_nameid() + + json_settings['strict'] = True + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) - response_9 = OneLogin_Saml2_Response(settings, xml_6) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): - response_9.get_nameid() + response_12 = OneLogin_Saml2_Response(settings, xml_6) + with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): + response_12.get_nameid() json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) + response_13 = OneLogin_Saml2_Response(settings, xml_6) + self.assertIsNone(response_13.get_nameid()) + + json_settings['strict'] = False + json_settings['security']['wantNameId'] = False + settings = OneLogin_Saml2_Settings(json_settings) + response_14 = OneLogin_Saml2_Response(settings, xml_6) + self.assertIsNone(response_14.get_nameid()) + + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_15 = OneLogin_Saml2_Response(settings, xml_6) + self.assertIsNone(response_15.get_nameid()) + + del json_settings['security']['wantNameId'] + settings = OneLogin_Saml2_Settings(json_settings) + response_16 = OneLogin_Saml2_Response(settings, xml_6) + self.assertIsNone(response_16.get_nameid()) - nameid_9 = response_9.get_nameid() - self.assertEqual(None, nameid_9) + json_settings['strict'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_17 = OneLogin_Saml2_Response(settings, xml_6) + with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): + response_17.get_nameid() def testReturnNameIdFormat(self): """ Tests the get_nameid_format method of the OneLogin_Saml2_Response """ json_settings = self.loadSettingsJSON() - json_settings['strict'] = True - + json_settings['strict'] = False settings = OneLogin_Saml2_Settings(json_settings) xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) @@ -162,59 +201,99 @@ def testReturnNameIdFormat(self): response_3 = OneLogin_Saml2_Response(settings, xml_3) self.assertEqual('urn:oasis:names:tc:SAML:2.0:nameid-format:transient', response_3.get_nameid_format()) + json_settings['strict'] = True + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + 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(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): response_4.get_nameid_format() - json_settings['security']['wantNameId'] = True + json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) - response_5 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): - response_5.get_nameid_format() + self.assertIsNone(response_5.get_nameid_format()) + json_settings['strict'] = False json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) - response_6 = OneLogin_Saml2_Response(settings, xml_4) - nameid_6 = response_6.get_nameid_format() - self.assertIsNone(nameid_6) + self.assertIsNone(response_6.get_nameid_format()) + + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_7 = OneLogin_Saml2_Response(settings, xml_4) + self.assertIsNone(response_7.get_nameid_format()) del json_settings['security']['wantNameId'] settings = OneLogin_Saml2_Settings(json_settings) + response_8 = OneLogin_Saml2_Response(settings, xml_4) + self.assertIsNone(response_8.get_nameid_format()) - response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): - response_7.get_nameid_format() + json_settings['strict'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_9 = OneLogin_Saml2_Response(settings, xml_4) + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): + response_9.get_nameid_format() + + json_settings['strict'] = False + settings = OneLogin_Saml2_Settings(json_settings) + xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64')) + response_10 = OneLogin_Saml2_Response(settings, xml_5) + self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response_10.get_nameid_format()) 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) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value'): - response_8.get_nameid_format() + response_11 = OneLogin_Saml2_Response(settings, xml_5) + with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value.'): + response_11.get_nameid_format() + + json_settings['strict'] = True + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) - response_9 = OneLogin_Saml2_Response(settings, xml_6) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): - response_9.get_nameid_format() + response_12 = OneLogin_Saml2_Response(settings, xml_6) + with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): + response_12.get_nameid_format() + + json_settings['security']['wantNameId'] = False + settings = OneLogin_Saml2_Settings(json_settings) + response_13 = OneLogin_Saml2_Response(settings, xml_6) + self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response_13.get_nameid_format()) + json_settings['strict'] = False json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) + response_14 = OneLogin_Saml2_Response(settings, xml_6) + self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response_14.get_nameid_format()) + + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_15 = OneLogin_Saml2_Response(settings, xml_6) + self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response_15.get_nameid_format()) + + del json_settings['security']['wantNameId'] + settings = OneLogin_Saml2_Settings(json_settings) + response_16 = OneLogin_Saml2_Response(settings, xml_6) + self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response_16.get_nameid_format()) - nameid_9 = response_9.get_nameid_format() - self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', nameid_9) + json_settings['strict'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_17 = OneLogin_Saml2_Response(settings, xml_6) + with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): + response_17.get_nameid_format() def testGetNameIdData(self): """ Tests the get_nameid_data method of the OneLogin_Saml2_Response """ json_settings = self.loadSettingsJSON() - json_settings['strict'] = True - - settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + json_settings['strict'] = False + settings = OneLogin_Saml2_Settings(json_settings) xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) expected_nameid_data = { @@ -244,56 +323,110 @@ def testGetNameIdData(self): nameid_data_3 = response_3.get_nameid_data() self.assertEqual(expected_nameid_data_3, nameid_data_3) + json_settings['strict'] = True + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + 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, 'NameID not found in the assertion of the Response'): + response_4.get_nameid_data() - nameid_data_4 = response_4.get_nameid_data() - self.assertEqual({}, nameid_data_4) - - json_settings['security']['wantNameId'] = True + json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) - response_5 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): - response_5.get_nameid_data() + nameid_data_5 = response_5.get_nameid_data() + self.assertEqual({}, nameid_data_5) + json_settings['strict'] = False json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) - response_6 = OneLogin_Saml2_Response(settings, xml_4) nameid_data_6 = response_6.get_nameid_data() self.assertEqual({}, nameid_data_6) + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_7 = OneLogin_Saml2_Response(settings, xml_4) + nameid_data_7 = response_7.get_nameid_data() + self.assertEqual({}, nameid_data_7) + del json_settings['security']['wantNameId'] settings = OneLogin_Saml2_Settings(json_settings) + response_8 = OneLogin_Saml2_Response(settings, xml_4) + nameid_data_8 = response_8.get_nameid_data() + self.assertEqual({}, nameid_data_8) - response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): - response_7.get_nameid_data() + json_settings['strict'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_9 = OneLogin_Saml2_Response(settings, xml_4) + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): + response_9.get_nameid_data() + + expected_nameid_data_4 = { + 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + 'SPNameQualifier': 'wrong-sp-entityid', + 'Value': 'test@example.com' + } + json_settings['strict'] = False + settings = OneLogin_Saml2_Settings(json_settings) + xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64')) + response_10 = OneLogin_Saml2_Response(settings, xml_5) + nameid_data_10 = response_10.get_nameid_data() + self.assertEqual(expected_nameid_data_4, nameid_data_10) 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) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value.'): - response_8.get_nameid_data() + response_11 = OneLogin_Saml2_Response(settings, xml_5) + with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value.'): + response_11.get_nameid_data() + + expected_nameid_data_5 = { + 'Value': None, + 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + } + + json_settings['strict'] = True + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) - response_9 = OneLogin_Saml2_Response(settings, xml_6) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): - response_9.get_nameid_data() + response_12 = OneLogin_Saml2_Response(settings, xml_6) + with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): + response_12.get_nameid_data() json_settings['security']['wantNameId'] = False settings = OneLogin_Saml2_Settings(json_settings) + response_13 = OneLogin_Saml2_Response(settings, xml_6) + nameid_data_13 = response_13.get_nameid_data() + nameid_data_13 = self.assertEqual(expected_nameid_data_5, nameid_data_13) - nameid_data_9 = response_9.get_nameid_data() + json_settings['strict'] = False + json_settings['security']['wantNameId'] = False + settings = OneLogin_Saml2_Settings(json_settings) + response_14 = OneLogin_Saml2_Response(settings, xml_6) + nameid_data_14 = response_14.get_nameid_data() + self.assertEqual(expected_nameid_data_5, nameid_data_14) - expected_nameid_data_4 = { - 'Value': None, - 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', - } - self.assertEqual(expected_nameid_data_4, nameid_data_9) + json_settings['security']['wantNameId'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_15 = OneLogin_Saml2_Response(settings, xml_6) + nameid_data_15 = response_15.get_nameid_data() + self.assertEqual(expected_nameid_data_5, nameid_data_15) + + del json_settings['security']['wantNameId'] + settings = OneLogin_Saml2_Settings(json_settings) + response_16 = OneLogin_Saml2_Response(settings, xml_6) + nameid_data_16 = response_16.get_nameid_data() + self.assertEqual(expected_nameid_data_5, nameid_data_16) + + json_settings['strict'] = True + settings = OneLogin_Saml2_Settings(json_settings) + response_17 = OneLogin_Saml2_Response(settings, xml_6) + with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): + response_17.get_nameid_data() def testCheckStatus(self): """ From b089e782ca954eedd2157dc5d5d7115ab1ed2fac Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 16 Sep 2017 08:30:45 +0200 Subject: [PATCH 220/352] Release 2.3.0 --- changelog.md | 9 +++++++-- setup.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index bb3f142b..7fc8e5a9 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,11 @@ # python-saml changelog -### 2.2.4 (unreleased) -* Get NameID when element decrypted twice +### 2.3.0 (Sep 15, 2017) +* [#205](https://github.com/onelogin/python-saml/pull/205) Improve decrypt method, Add an option to decrypt an element in place or copy it before decryption. +* [#204](https://github.com/onelogin/python-saml/pull/204) On a LogoutRequest if the NameIdFormat is entity, NameQualifier and SPNameQualifier will be ommited. If the NameIdFormat is not entity and a NameQualifier is provided, then the SPNameQualifier will be also added. +* Be able to get at the auth object the last processed ID (response/assertion) and the last generated ID. +* Reset errorReason attribute of the auth object before each Process method +* Fix issue on getting multiple certs when only sign or encryption certs +* Allow empty nameid if setting wantNameId is false. Only raise Exceptions when strict mode is enabled ### 2.2.3 (Jun 15, 2017) * Replace some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml diff --git a/setup.py b/setup.py index 7e9b9a83..39e1be11 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.2.3', + version='2.3.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', From 265d019451adac073ca8f470c757a05e9b447867 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 19 Oct 2017 12:10:39 +0200 Subject: [PATCH 221/352] Fix issue with LogoutRequest rejected by ADFS due NameID with unspecified format instead no format attribute --- src/onelogin/saml2/logout_request.py | 12 ++-- src/onelogin/saml2/utils.py | 5 +- .../saml2_tests/logout_request_test.py | 57 +++++++++++++++---- tests/src/OneLogin/saml2_tests/utils_test.py | 11 ++++ 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 6e6a1903..2efd8aa8 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -76,15 +76,13 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= cert = idp_data['x509cert'] if name_id is not None: - if name_id_format is not None: - nameIdFormat = name_id_format - else: - nameIdFormat = sp_data['NameIDFormat'] + if not name_id_format and sp_data['NameIDFormat'] != OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED: + name_id_format = sp_data['NameIDFormat'] else: - nameIdFormat = OneLogin_Saml2_Constants.NAMEID_ENTITY + name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY spNameQualifier = None - if nameIdFormat == OneLogin_Saml2_Constants.NAMEID_ENTITY: + if name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY: name_id = idp_data['entityId'] nq = None elif nq is not None: @@ -94,7 +92,7 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, spNameQualifier, - nameIdFormat, + name_id_format, cert, False, nq diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 19f1511a..51e5db18 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -612,7 +612,7 @@ def format_finger_print(fingerprint): return formated_fingerprint.lower() @staticmethod - def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None): + def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=None): """ Generates a nameID. @@ -646,7 +646,8 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None): name_id.setAttribute('SPNameQualifier', sp_nq) if nq is not None: name_id.setAttribute('NameQualifier', nq) - name_id.setAttribute('Format', sp_format) + if sp_format is not None: + name_id.setAttribute('Format', sp_format) name_id.appendChild(doc.createTextNode(value)) name_id_container.appendChild(name_id) diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index d1eab57e..6d78d1bc 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -55,15 +55,12 @@ def testConstructor(self): inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(payload) self.assertRegexpMatches(inflated, '^') - def testCreateDeflatedSAMLLogoutRequestURLParameter(self): + def testConstructorWithNameIdFormatOnSettings(self): """ Tests the OneLogin_Saml2_LogoutRequest Constructor. - The creation of a deflated SAML Logout Request + Case: Defines NameIDFormat from settings """ - settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings_info = self.loadSettingsJSON() + name_id = 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c' + name_id_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + settings_info['sp']['NameIDFormat'] = name_id_format + settings = OneLogin_Saml2_Settings(settings_info) + logout_request = OneLogin_Saml2_Logout_Request(settings, name_id=name_id) + logout_request_xml = OneLogin_Saml2_Utils.decode_base64_and_inflate(logout_request.get_request()) + name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(logout_request_xml) + expected_name_id_data = { + 'Value': name_id, + 'Format': name_id_format + } + self.assertEqual(expected_name_id_data, name_id_data) + + def testConstructorWithoutNameIdFormat(self): + """ + Tests the OneLogin_Saml2_LogoutRequest Constructor. + Case: Checks that NameIDFormat is not added + """ + settings_info = self.loadSettingsJSON() + name_id = 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c' + name_id_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' + settings_info['sp']['NameIDFormat'] = name_id_format + settings = OneLogin_Saml2_Settings(settings_info) + logout_request = OneLogin_Saml2_Logout_Request(settings, name_id=name_id) + logout_request_xml = OneLogin_Saml2_Utils.decode_base64_and_inflate(logout_request.get_request()) + name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(logout_request_xml) + expected_name_id_data = { + 'Value': name_id + } + self.assertEqual(expected_name_id_data, name_id_data) + + def testConstructorEncryptIdUsingX509certMulti(self): + """ + Tests the OneLogin_Saml2_LogoutRequest Constructor. + Case: Able to generate encryptedID with MultiCert + """ + settings_info = self.loadSettingsJSON('settings8.json') + settings_info['security']['nameIdEncrypted'] = True + settings = OneLogin_Saml2_Settings(settings_info) + logout_request = OneLogin_Saml2_Logout_Request(settings) parameters = {'SAMLRequest': logout_request.get_request()} @@ -92,6 +128,7 @@ def testCreateDeflatedSAMLLogoutRequestURLParameter(self): payload = exploded['SAMLRequest'][0] inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(payload) self.assertRegexpMatches(inflated, '^') def testGetIDFromSAMLLogoutRequest(self): """ diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index cbc36d66..f1f2aeb6 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -607,6 +607,17 @@ def testGenerateNameIdWithSPNameQualifier(self): expected_name_id_enc = '' self.assertIn(expected_name_id_enc, name_id_enc) + def testGenerateNameIdWithoutFormat(self): + """ + Tests the generateNameId method of the OneLogin_Saml2_Utils + """ + name_id_value = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde' + name_id_format = None + + name_id = OneLogin_Saml2_Utils.generate_name_id(name_id_value, None, name_id_format) + expected_name_id = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde' + self.assertEqual(name_id, expected_name_id) + def testGenerateNameIdWithoutSPNameQualifier(self): """ Tests the generateNameId method of the OneLogin_Saml2_Utils From 032a2c7339c27788e795814512607c78482dd2ff Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 10 Nov 2017 21:36:20 +0100 Subject: [PATCH 222/352] Fix signature position in the SP metadata --- src/onelogin/saml2/utils.py | 6 +++++- tests/src/OneLogin/saml2_tests/utils_test.py | 18 +++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 51e5db18..a9f49cd3 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -877,7 +877,11 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant issuer = issuer[0] issuer.addnext(signature) else: - elem[0].insert(0, signature) + entity_descriptor = OneLogin_Saml2_Utils.query(elem, '//md:EntityDescriptor') + if len(entity_descriptor) > 0: + elem.insert(0, signature) + else: + elem[0].insert(0, signature) digest_algorithm_transform_map = { OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1, diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index f1f2aeb6..0b9772f2 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -810,54 +810,54 @@ def testAddSign(self): res = parseString(xml_authn_signed) ds_signature = res.firstChild.firstChild.nextSibling.nextSibling - self.assertIn('ds:Signature', ds_signature.tagName) + self.assertEqual('ds:Signature', ds_signature.tagName) xml_authn_dom = parseString(xml_authn) xml_authn_signed_2 = OneLogin_Saml2_Utils.add_sign(xml_authn_dom, key, cert) self.assertIn('', xml_authn_signed_2) res_2 = parseString(xml_authn_signed_2) ds_signature_2 = res_2.firstChild.firstChild.nextSibling.nextSibling - self.assertIn('ds:Signature', ds_signature_2.tagName) + self.assertEqual('ds:Signature', ds_signature_2.tagName) xml_authn_signed_3 = OneLogin_Saml2_Utils.add_sign(xml_authn_dom.firstChild, key, cert) self.assertIn('', xml_authn_signed_3) res_3 = parseString(xml_authn_signed_3) ds_signature_3 = res_3.firstChild.firstChild.nextSibling.nextSibling - self.assertIn('ds:Signature', ds_signature_3.tagName) + self.assertEqual('ds:Signature', ds_signature_3.tagName) xml_authn_etree = etree.fromstring(xml_authn) xml_authn_signed_4 = OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert) self.assertIn('', xml_authn_signed_4) res_4 = parseString(xml_authn_signed_4) ds_signature_4 = res_4.firstChild.firstChild.nextSibling.nextSibling - self.assertIn('ds:Signature', ds_signature_4.tagName) + self.assertEqual('ds:Signature', ds_signature_4.tagName) xml_authn_signed_5 = OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert) self.assertIn('', xml_authn_signed_5) res_5 = parseString(xml_authn_signed_5) ds_signature_5 = res_5.firstChild.firstChild.nextSibling.nextSibling - self.assertIn('ds:Signature', ds_signature_5.tagName) + self.assertEqual('ds:Signature', ds_signature_5.tagName) xml_logout_req = b64decode(self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml.base64'))) xml_logout_req_signed = OneLogin_Saml2_Utils.add_sign(xml_logout_req, key, cert) self.assertIn('', xml_logout_req_signed) res_6 = parseString(xml_logout_req_signed) ds_signature_6 = res_6.firstChild.firstChild.nextSibling.nextSibling - self.assertIn('ds:Signature', ds_signature_6.tagName) + self.assertEqual('ds:Signature', ds_signature_6.tagName) xml_logout_res = b64decode(self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml.base64'))) xml_logout_res_signed = OneLogin_Saml2_Utils.add_sign(xml_logout_res, key, cert) self.assertIn('', xml_logout_res_signed) res_7 = parseString(xml_logout_res_signed) ds_signature_7 = res_7.firstChild.firstChild.nextSibling.nextSibling - self.assertIn('ds:Signature', ds_signature_7.tagName) + self.assertEqual('ds:Signature', ds_signature_7.tagName) xml_metadata = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml')) xml_metadata_signed = OneLogin_Saml2_Utils.add_sign(xml_metadata, key, cert) self.assertIn('', xml_metadata_signed) res_8 = parseString(xml_metadata_signed) - ds_signature_8 = res_8.firstChild.firstChild.nextSibling.firstChild.nextSibling - self.assertIn('ds:Signature', ds_signature_8.tagName) + ds_signature_8 = res_8.firstChild.firstChild.nextSibling + self.assertEqual('ds:Signature', ds_signature_8.tagName) with self.assertRaisesRegexp(Exception, 'Error parsing xml string'): OneLogin_Saml2_Utils.add_sign(1, key, cert) From c8717ff5014ab73dfe7cf7617c49925d9d1e12bd Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 17 Dec 2017 20:57:53 +0100 Subject: [PATCH 223/352] Add more tests to cover IdPMetadataParser --- ...tadata_different_sign_and_encrypt_cert.xml | 72 +++++++++++++++++++ ...dp_metadata_same_sign_and_encrypt_cert.xml | 71 ++++++++++++++++++ .../saml2_tests/idp_metadata_parser_test.py | 56 ++++++++++++++- 3 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 tests/data/metadata/idp_metadata_different_sign_and_encrypt_cert.xml create mode 100644 tests/data/metadata/idp_metadata_same_sign_and_encrypt_cert.xml diff --git a/tests/data/metadata/idp_metadata_different_sign_and_encrypt_cert.xml b/tests/data/metadata/idp_metadata_different_sign_and_encrypt_cert.xml new file mode 100644 index 00000000..df90353a --- /dev/null +++ b/tests/data/metadata/idp_metadata_different_sign_and_encrypt_cert.xml @@ -0,0 +1,72 @@ + + + + + + + MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD +VQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2 +MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +DApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u +ZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z +0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT +gf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m +Tr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF +zRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ +UAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG +A1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV +HSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw +DwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO +BgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu +AuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV +gG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ +sTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP +TbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu +QOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78 +1sE= + + + + + + + MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF +BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj +aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW +T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy +MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz +Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV +BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo +3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw +tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx +VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5 +L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t +1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/ +BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB +pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD +VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL +DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC +FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B +AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM +GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c +hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB +vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37 +MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ +WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw== + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + + + + Support + support@onelogin.com + + \ No newline at end of file diff --git a/tests/data/metadata/idp_metadata_same_sign_and_encrypt_cert.xml b/tests/data/metadata/idp_metadata_same_sign_and_encrypt_cert.xml new file mode 100644 index 00000000..e7fd250b --- /dev/null +++ b/tests/data/metadata/idp_metadata_same_sign_and_encrypt_cert.xml @@ -0,0 +1,71 @@ + + + + + + + MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD +VQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2 +MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +DApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u +ZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z +0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT +gf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m +Tr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF +zRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ +UAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG +A1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV +HSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw +DwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO +BgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu +AuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV +gG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ +sTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP +TbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu +QOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78 +1sE= + + + + + + + MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD +VQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2 +MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +DApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u +ZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z +0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT +gf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m +Tr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF +zRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ +UAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG +A1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV +HSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw +DwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO +BgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu +AuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV +gG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ +sTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP +TbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu +QOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78 +1sE= + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + + + + Support + support@onelogin.com + + \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index 58e2852e..e441e0f0 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -389,7 +389,61 @@ def test_parse_multi_singing_certs(self): expected_settings = json.loads(expected_settings_json) self.assertEqual(expected_settings, data) - def testMergeSettings(self): + def test_parse_multi_same_signing_and_encrypt_cert(self): + """ + Tests the parse method of the OneLogin_Saml2_IdPMetadataParser + Case: IdP metadata contains multiple signature cert and encrypt cert + that is the same + """ + xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_same_sign_and_encrypt_cert.xml')) + data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata) + + expected_settings_json = """ + { + "sp": { + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + }, + "idp": { + "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE=", + "entityId": "https://app.onelogin.com/saml/metadata/383123", + "singleSignOnService": { + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + } + } + """ + expected_settings = json.loads(expected_settings_json) + self.assertEqual(expected_settings, data) + + xml_idp_metadata_2 = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_different_sign_and_encrypt_cert.xml')) + data_2 = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata_2) + expected_settings_json_2 = """ + { + "sp": { + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + }, + "idp": { + "x509certMulti": { + "encryption": [ + "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==" + ], + "signing": [ + "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE=" + ] + }, + "entityId": "https://app.onelogin.com/saml/metadata/383123", + "singleSignOnService": { + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + } + } + """ + expected_settings_2 = json.loads(expected_settings_json_2) + self.assertEqual(expected_settings_2, data_2) + + def test_merge_settings(self): """ Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser """ From 6b9faf5c6d140635d3fefcadd5319ee128abc529 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 18 Dec 2017 10:19:02 +0100 Subject: [PATCH 224/352] Redefine NSMAP constant --- src/onelogin/saml2/constants.py | 27 ++++++++++++++++++--------- src/onelogin/saml2/response.py | 4 ++-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py index 2cd2e340..2bf885ef 100644 --- a/src/onelogin/saml2/constants.py +++ b/src/onelogin/saml2/constants.py @@ -50,6 +50,24 @@ class OneLogin_Saml2_Constants(object): NS_XENC = 'http://www.w3.org/2001/04/xmlenc#' NS_DS = 'http://www.w3.org/2000/09/xmldsig#' + # Namespace Prefixes + NS_PREFIX_SAML = 'saml' + NS_PREFIX_SAMLP = 'samlp' + NS_PREFIX_MD = 'md' + NS_PREFIX_XS = 'xs' + NS_PREFIX_XSI = 'xsi' + NS_PREFIX_XENC = 'xenc' + NS_PREFIX_DS = 'ds' + + # Prefix:Namespace Mappings + NSMAP = { + NS_PREFIX_SAMLP: NS_SAMLP, + NS_PREFIX_SAML: NS_SAML, + NS_PREFIX_DS: NS_DS, + NS_PREFIX_XENC: NS_XENC, + NS_PREFIX_MD: NS_MD + } + # Bindings BINDING_HTTP_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' BINDING_HTTP_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' @@ -79,15 +97,6 @@ class OneLogin_Saml2_Constants(object): STATUS_PARTIAL_LOGOUT = 'urn:oasis:names:tc:SAML:2.0:status:PartialLogout' STATUS_PROXY_COUNT_EXCEEDED = 'urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded' - # Namespaces - NSMAP = { - 'samlp': NS_SAMLP, - 'saml': NS_SAML, - 'md': NS_MD, - 'ds': NS_DS, - 'xenc': NS_XENC - } - # Sign & Crypto SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1' SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256' diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 869a4e7a..30def415 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -531,7 +531,7 @@ def get_attributes(self): ) values = [] - for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP['saml']): + for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]): # Remove any whitespace (which may be present where attributes are # nested inside NameID children). if attr.text: @@ -540,7 +540,7 @@ def get_attributes(self): values.append(text) # Parse any nested NameID children - for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP['saml']): + for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]): values.append({ 'NameID': { 'Format': nameid.get('Format'), From 4081893698abc46c41e2a5c4b91bcdd37d401a19 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 18 Dec 2017 19:08:31 +0100 Subject: [PATCH 225/352] Be able to invalidate a SAMLResponse if it contains InResponseTo value but no RequestId parameter provided at the is_valid method. See rejectUnsolicitedResponsesWithInResponseTo security parameter (By default deactivated) --- README.md | 5 ++ src/onelogin/saml2/response.py | 23 +++++---- src/onelogin/saml2/settings.py | 3 ++ .../src/OneLogin/saml2_tests/response_test.py | 48 +++++++++++++++++++ 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index aa740fce..10ade02f 100644 --- a/README.md +++ b/README.md @@ -414,6 +414,11 @@ In addition to the required settings data (idp, sp), extra settings can be defin // Indicates a requirement for the AttributeStatement element "wantAttributeStatement": true, + // Rejects SAML responses with a InResponseTo attribute when request_id + // not provided in the process_response method that later call the + // response is_valid method with that parameter. + "rejectUnsolicitedResponsesWithInResponseTo": false, + // Authentication context. // Set to false and no AuthContext will be sent in the AuthNRequest, // Set true or don't present this parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 30def415..fe6f977e 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -133,14 +133,19 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) - # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided 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 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 request_id is None and in_response_to is not None and security.get('rejectUnsolicitedResponsesWithInResponseTo', False): + raise OneLogin_Saml2_ValidationError( + 'The Response has an InResponseTo attribute: %s while no InResponseTo was expected' % in_response_to, + OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO + ) + + # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided + if request_id is not None and 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.get('wantAssertionsEncrypted', False): raise OneLogin_Saml2_ValidationError( @@ -244,7 +249,9 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): continue else: irt = sc_data.get('InResponseTo', None) - if in_response_to and irt and irt != in_response_to: + if (in_response_to is None and irt is not None and + security.get('rejectUnsolicitedResponsesWithInResponseTo', False)) or \ + in_response_to and irt and irt != in_response_to: continue recipient = sc_data.get('Recipient', None) if recipient and current_url not in recipient: diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 77a8592a..d5db3a05 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -280,6 +280,9 @@ def __add_default_values(self): # NameID element expected self.__security.setdefault('wantNameId', True) + # SAML responses with a InResponseTo attribute not rejected when requestId not passed + self.__security.setdefault('rejectUnsolicitedResponsesWithInResponseTo', False) + # Encrypt expected self.__security.setdefault('wantAssertionsEncrypted', False) self.__security.setdefault('wantNameIdEncrypted', False) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 6e4c1df8..8df0ddfd 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1173,6 +1173,54 @@ def testIsInValidRequestId(self): response.is_valid(request_data, valid_request_id) self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + def testRejectUnsolicitedResponsesWithInResponseTo(self): + settings_info = self.loadSettingsJSON() + settings_info['strict'] = True + settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = False + settings = OneLogin_Saml2_Settings(settings_info) + request_data = { + 'http_host': 'stuff.com', + 'script_name': 'endpoints/endpoints/acs.php' + } + + xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + + settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = True + settings = OneLogin_Saml2_Settings(settings_info) + response = OneLogin_Saml2_Response(settings, xml) + response.is_valid(request_data) + self.assertEqual('The Response has an InResponseTo attribute: _57bcbf70-7b1f-012e-c821-782bcb13bb38 while no InResponseTo was expected', response.get_error()) + + settings_info['idp']['entityId'] = 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php' + settings_info['sp']['entityId'] = 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php' + request_data = { + 'https': 'on', + 'http_host': 'pitbulk.no-ip.org', + 'script_name': 'newonelogin/demo1/index.php?acs' + } + not_on_or_after = datetime.strptime('2014-02-19T09:37:01Z', '%Y-%m-%dT%H:%M:%SZ') + not_on_or_after -= timedelta(seconds=150) + + # InResponseTo on the SubjectConfirmation only + xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_without_inresponseto.xml.base64')) + settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = False + settings = OneLogin_Saml2_Settings(settings_info) + response = OneLogin_Saml2_Response(settings, xml) + + with freeze_time(not_on_or_after): + self.assertTrue(response.is_valid(request_data)) + + settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = True + settings = OneLogin_Saml2_Settings(settings_info) + response = OneLogin_Saml2_Response(settings, xml) + + with freeze_time(not_on_or_after): + self.assertFalse(response.is_valid(request_data)) + self.assertEquals("A valid SubjectConfirmation was not found on this Response", response.get_error()) + def testIsInValidSignIssues(self): """ Tests the is_valid method of the OneLogin_Saml2_Response class From fad881b4432febea69d70691dfed51c93f0de10f Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 27 Feb 2018 14:12:18 +0100 Subject: [PATCH 226/352] Fix vulnerability CVE-2017-11427. Process text of nodes properly, ignoring comments --- src/onelogin/saml2/idp_metadata_parser.py | 6 +++--- src/onelogin/saml2/logout_request.py | 6 +++--- src/onelogin/saml2/logout_response.py | 2 +- src/onelogin/saml2/response.py | 21 ++++++++++--------- src/onelogin/saml2/utils.py | 9 ++++++-- .../response_node_text_attack.xml.base64 | 1 + .../src/OneLogin/saml2_tests/response_test.py | 13 ++++++++++++ 7 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 tests/data/responses/response_node_text_attack.xml.base64 diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 097b3342..51d446ff 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -146,7 +146,7 @@ def parse( name_id_format_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, './md:NameIDFormat') if len(name_id_format_nodes) > 0: - idp_name_id_format = name_id_format_nodes[0].text + idp_name_id_format = OneLogin_Saml2_Utils.element_text(name_id_format_nodes[0]) sso_nodes = OneLogin_Saml2_Utils.query( idp_descriptor_node, @@ -171,11 +171,11 @@ def parse( if len(signing_nodes) > 0: certs['signing'] = [] for cert_node in signing_nodes: - certs['signing'].append(''.join(cert_node.text.split())) + certs['signing'].append(''.join(OneLogin_Saml2_Utils.element_text(cert_node).split())) if len(encryption_nodes) > 0: certs['encryption'] = [] for cert_node in encryption_nodes: - certs['encryption'].append(''.join(cert_node.text.split())) + certs['encryption'].append(''.join(OneLogin_Saml2_Utils.element_text(cert_node).split())) data['idp'] = {} diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 2efd8aa8..14c49e29 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -218,7 +218,7 @@ def get_nameid_data(request, key=None): ) name_id_data = { - 'Value': name_id.text + 'Value': OneLogin_Saml2_Utils.element_text(name_id) } for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: if attr in name_id.attrib.keys(): @@ -276,7 +276,7 @@ def get_issuer(request): issuer = None issuer_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:Issuer') if len(issuer_nodes) == 1: - issuer = issuer_nodes[0].text + issuer = OneLogin_Saml2_Utils.element_text(issuer_nodes[0]) return issuer @staticmethod @@ -298,7 +298,7 @@ def get_session_indexes(request): session_indexes = [] session_index_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex') for session_index_node in session_index_nodes: - session_indexes.append(session_index_node.text) + session_indexes.append(OneLogin_Saml2_Utils.element_text(session_index_node)) return session_indexes def is_valid(self, request_data, raise_exceptions=False): diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index eec5cc94..0cd1e2be 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -56,7 +56,7 @@ def get_issuer(self): issuer = None issuer_nodes = self.__query('/samlp:LogoutResponse/saml:Issuer') if len(issuer_nodes) == 1: - issuer = issuer_nodes[0].text + issuer = OneLogin_Saml2_Utils.element_text(issuer_nodes[0]) return issuer def get_status(self): diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index fe6f977e..56a8476b 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -377,7 +377,7 @@ def get_audiences(self): :rtype: list """ audience_nodes = self.__query_assertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience') - return [node.text for node in audience_nodes if node.text is not None] + return [OneLogin_Saml2_Utils.element_text(node) for node in audience_nodes if OneLogin_Saml2_Utils.element_text(node) is not None] def get_issuers(self): """ @@ -391,7 +391,7 @@ def get_issuers(self): message_issuer_nodes = OneLogin_Saml2_Utils.query(self.document, '/samlp:Response/saml:Issuer') if len(message_issuer_nodes) > 0: if len(message_issuer_nodes) == 1: - issuers.append(message_issuer_nodes[0].text) + issuers.append(OneLogin_Saml2_Utils.element_text(message_issuer_nodes[0])) else: raise OneLogin_Saml2_ValidationError( 'Issuer of the Response is multiple.', @@ -400,7 +400,7 @@ def get_issuers(self): assertion_issuer_nodes = self.__query_assertion('/saml:Issuer') if len(assertion_issuer_nodes) == 1: - issuers.append(assertion_issuer_nodes[0].text) + issuers.append(OneLogin_Saml2_Utils.element_text(assertion_issuer_nodes[0])) else: raise OneLogin_Saml2_ValidationError( 'Issuer of the Assertion not found or multiple.', @@ -438,13 +438,13 @@ def get_nameid_data(self): OneLogin_Saml2_ValidationError.NO_NAMEID ) else: - if is_strict and want_nameid and not nameid.text: + if is_strict and want_nameid and not OneLogin_Saml2_Utils.element_text(nameid): raise OneLogin_Saml2_ValidationError( 'An empty NameID value found', OneLogin_Saml2_ValidationError.EMPTY_NAMEID ) - nameid_data = {'Value': nameid.text} + nameid_data = {'Value': OneLogin_Saml2_Utils.element_text(nameid)} for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: value = nameid.get(attr, None) if value: @@ -541,10 +541,11 @@ def get_attributes(self): for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]): # Remove any whitespace (which may be present where attributes are # nested inside NameID children). - if attr.text: - text = attr.text.strip() - if text: - values.append(text) + attr_text = OneLogin_Saml2_Utils.element_text(attr) + if attr_text: + attr_text = attr_text.strip() + if attr_text: + values.append(attr_text) # Parse any nested NameID children for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]): @@ -552,7 +553,7 @@ def get_attributes(self): 'NameID': { 'Format': nameid.get('Format'), 'NameQualifier': nameid.get('NameQualifier'), - 'value': nameid.text + 'value': OneLogin_Saml2_Utils.element_text(nameid) } }) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index a9f49cd3..a9ece8ba 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -156,6 +156,11 @@ def validate_xml(xml, schema, debug=False): return parseString(tostring(dom, encoding='unicode').encode('utf-8')) + @staticmethod + def element_text(node): + etree.strip_tags(node, etree.Comment) + return node.text + @staticmethod def format_cert(cert, heads=True): """ @@ -743,7 +748,7 @@ def get_status(dom): if len(subcode_entry) == 1: status['msg'] = subcode_entry[0].values()[0] elif len(message_entry) == 1: - status['msg'] = message_entry[0].text + status['msg'] = OneLogin_Saml2_Utils.element_text(message_entry[0]) return status @@ -1125,7 +1130,7 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger x509_certificate_nodes = OneLogin_Saml2_Utils.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_cert_value = OneLogin_Saml2_Utils.element_text(x509_certificate_node) 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) diff --git a/tests/data/responses/response_node_text_attack.xml.base64 b/tests/data/responses/response_node_text_attack.xml.base64 new file mode 100644 index 00000000..ba9f2f12 --- /dev/null +++ b/tests/data/responses/response_node_text_attack.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJHT1NBTUxSMTI5MDExNzQ1NzE3OTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgRGVzdGluYXRpb249IntyZWNpcGllbnR9Ij4NCiAgPHNhbWxwOlN0YXR1cz4NCiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBmeGE0NjU3NGRmLWIzYjAtYTA2YS0yM2M4LTYzNjQxMzE5ODc3MiIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzEzNTkwPC9zYW1sOklzc3Vlcj4NCiAgICA8ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgICAgIDxkczpTaWduZWRJbmZvPg0KICAgICAgICA8ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogICAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4YTQ2NTc0ZGYtYjNiMC1hMDZhLTIzYzgtNjM2NDEzMTk4NzcyIj4NCiAgICAgICAgICA8ZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPg0KICAgICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgICAgIDwvZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICA8ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz4NCiAgICAgICAgICA8ZHM6RGlnZXN0VmFsdWU+cEpRN01TL2VrNEtSUldHbXYvSDQzUmVIWU1zPTwvZHM6RGlnZXN0VmFsdWU+DQogICAgICAgIDwvZHM6UmVmZXJlbmNlPg0KICAgICAgPC9kczpTaWduZWRJbmZvPg0KICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlPnlpdmVLY1BkRHB1RE5qNnNoclEzQUJ3ci9jQTNDcnlEMnBoRy94TFpzektXeFU1L21sYUt0OGV3YlpPZEtLdnRPczJwSEJ5NUR1YTNrOTRBRnp4R3llbDVnT293bW95WEpyQU9ya1BPMHZsaTFWOG8zaFBQVVp3UmdTWDZROXBTMUNxUWdoS2lFYXNSeXlscXFKVWFQWXptT3pPRTgvWGxNa3dpV21PMD08L2RzOlNpZ25hdHVyZVZhbHVlPg0KICAgICAgPGRzOktleUluZm8+DQogICAgICAgIDxkczpYNTA5RGF0YT4NCiAgICAgICAgICA8ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUJyVENDQWFHZ0F3SUJBZ0lCQVRBREJnRUFNR2N4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUlV3RXdZRFZRUUhEQXhUWVc1MFlTQk5iMjVwWTJFeEVUQVBCZ05WQkFvTUNFOXVaVXh2WjJsdU1Sa3dGd1lEVlFRRERCQmhjSEF1YjI1bGJHOW5hVzR1WTI5dE1CNFhEVEV3TURNd09UQTVOVGcwTlZvWERURTFNRE13T1RBNU5UZzBOVm93WnpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RlRBVEJnTlZCQWNNREZOaGJuUmhJRTF2Ym1sallURVJNQThHQTFVRUNnd0lUMjVsVEc5bmFXNHhHVEFYQmdOVkJBTU1FR0Z3Y0M1dmJtVnNiMmRwYmk1amIyMHdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JBT2pTdTFmalB5OGQ1dzRReUwxemQ0aEl3MU1ra2ZmNFdZL1RMRzhPWmtVNVlUU1dtbUhQRDVrdllINXVvWFMvNnFRODFxWHBSMndWOENUb3daSlVMZzA5ZGRSZFJuOFFzcWoxRnlPQzVzbEUzeTJiWjJvRnVhNzJvZi80OWZwdWpuRlQ2S25RNjFDQk1xbERvVFFxT1Q2MnZHSjhuUDZNWld2QTZzeHF1ZDVBZ01CQUFFd0F3WUJBQU1CQUE9PTwvZHM6WDUwOUNlcnRpZmljYXRlPg0KICAgICAgICA8L2RzOlg1MDlEYXRhPg0KICAgICAgPC9kczpLZXlJbmZvPg0KICAgIDwvZHM6U2lnbmF0dXJlPg0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnN1cHBvcnQ8IS0tIGF0dGFjayEgLS0+QG9uZWxvZ2luLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiIFJlY2lwaWVudD0ie3JlY2lwaWVudH0iLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0xMS0xOFQyMTo1MjozN1oiIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+e2F1ZGllbmNlfTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMC0xMS0xOFQyMTo1NzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTAtMTEtMTlUMjE6NTc6MzdaIiBTZXNzaW9uSW5kZXg9Il81MzFjMzJkMjgzYmRmZjdlMDRlNDg3YmNkYmM0ZGQ4ZCI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InN1cm5hbWUiPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnM8IS0tIGF0dGFjayEgLS0+bWl0aDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iYW5vdGhlcl92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+dmFsdWUxPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnZhbHVlMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0icm9sZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+cm9sZTE8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9ImZpcnN0bmFtZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+Ym9iPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4gIA0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9ImF0dHJpYnV0ZV93aXRoX25pbF92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOm5pbD0idHJ1ZSIvPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJhdHRyaWJ1dGVfd2l0aF9uaWxzX2FuZF9lbXB0eV9zdHJpbmdzIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUvPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZT52YWx1ZVByZXNlbnQ8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOm5pbD0idHJ1ZSIvPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTpuaWw9IjEiLz4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 8df0ddfd..44e9ead8 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -648,6 +648,19 @@ def testDoesNotAllowSignatureWrappingAttack(self): self.assertFalse(response.is_valid(self.get_request_data())) self.assertEqual('test@onelogin.com', response.get_nameid()) + def testNodeTextAttack(self): + """ + Tests the get_nameid and get_attributes methods of the OneLogin_Saml2_Response + Test that the node text with comment attack (VU#475445) is not allowed + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'response_node_text_attack.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + nameid = response.get_nameid() + attributes = response.get_attributes() + self.assertEqual("smith", attributes.get('surname')[0]) + self.assertEqual('support@onelogin.com', nameid) + def testGetSessionNotOnOrAfter(self): """ Tests the get_session_not_on_or_after method of the OneLogin_Saml2_Response From 826f4f53812e87d953ed424fb972223c0edc4a45 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 27 Feb 2018 14:31:55 +0100 Subject: [PATCH 227/352] Improve how fingerprint is calcultated --- src/onelogin/saml2/utils.py | 32 +++++++++++-------- .../src/OneLogin/saml2_tests/response_test.py | 2 +- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index a9ece8ba..dafeaa73 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -557,9 +557,9 @@ def delete_local_session(callback=None): @staticmethod def calculate_x509_fingerprint(x509_cert, alg='sha1'): """ - Calculates the fingerprint of a x509cert. + Calculates the fingerprint of a formatted x509cert. - :param x509_cert: x509 cert + :param x509_cert: x509 cert formatted :type: string :param alg: The algorithm to build the fingerprint @@ -572,23 +572,27 @@ def calculate_x509_fingerprint(x509_cert, alg='sha1'): lines = x509_cert.split('\n') data = '' + inData = False for line in lines: # Remove '\r' from end of line if present. line = line.rstrip() - if line == '-----BEGIN CERTIFICATE-----': - # Delete junk from before the certificate. - data = '' - elif line == '-----END CERTIFICATE-----': - # Ignore data after the certificate. - break - elif line == '-----BEGIN PUBLIC KEY-----' or line == '-----BEGIN RSA PRIVATE KEY-----': - # This isn't an X509 certificate. - return None + if not inData: + if line == '-----BEGIN CERTIFICATE-----': + inData = True + elif line == '-----BEGIN PUBLIC KEY-----' or line == '-----BEGIN RSA PRIVATE KEY-----': + # This isn't an X509 certificate. + return None else: + if line == '-----END CERTIFICATE-----': + break + # Append the current line to the certificate data. data += line + if not data: + return None + decoded_data = base64.b64decode(data) if alg == 'sha512': @@ -1131,9 +1135,11 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger if len(x509_certificate_nodes) > 0: x509_certificate_node = x509_certificate_nodes[0] x509_cert_value = OneLogin_Saml2_Utils.element_text(x509_certificate_node) - x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg) + x509_cert_value_formatted = OneLogin_Saml2_Utils.format_cert(x509_cert_value) + x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value_formatted, fingerprintalg) + if fingerprint == x509_fingerprint_value: - cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) + cert = x509_cert_value_formatted # Check if Reference URI is empty # reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference') diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 44e9ead8..e6793b87 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1430,7 +1430,7 @@ def testIsValid2(self): self.assertTrue(response_2.is_valid(self.get_request_data())) settings_info_3 = self.loadSettingsJSON('settings2.json') - idp_cert = settings_info_3['idp']['x509cert'] + idp_cert = OneLogin_Saml2_Utils.format_cert(settings_info_3['idp']['x509cert']) settings_info_3['idp']['certFingerprint'] = OneLogin_Saml2_Utils.calculate_x509_fingerprint(idp_cert) settings_info_3['idp']['x509cert'] = '' settings_3 = OneLogin_Saml2_Settings(settings_info_3) From 27bce0081b180f90ac094ceefaa38183bfc47212 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 27 Feb 2018 16:30:15 +0100 Subject: [PATCH 228/352] Release 2.4.0 --- README.md | 4 ++++ changelog.md | 8 ++++++++ setup.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 10ade02f..1399d32b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml). #### Warning #### +Update python-saml to 2.4.0, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability. + +That version also change how calculate fingerprint method works, and will expect as input a formatted x509 certificate + Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recently, by the sanitized call provided by defusedxml Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json) diff --git a/changelog.md b/changelog.md index 7fc8e5a9..474a6549 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,12 @@ # python-saml changelog +### 2.4.0 (Feb 27, 2018) +* Fix vulnerability [CVE-2017-11427](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11427). Process text of nodes properly, ignoring comments +* Improve how fingerprint is calcultated +* Fix issue with LogoutRequest rejected by ADFS due NameID with unspecified format instead no format attribute +* Be able to invalidate a SAMLResponse if it contains InResponseTo value but no RequestId parameter provided at the is_valid method. See rejectUnsolicitedResponsesWithInResponseTo security parameter (By default deactivated) +* Fix signature position in the SP metadata +* Redefine NSMAP constant + ### 2.3.0 (Sep 15, 2017) * [#205](https://github.com/onelogin/python-saml/pull/205) Improve decrypt method, Add an option to decrypt an element in place or copy it before decryption. * [#204](https://github.com/onelogin/python-saml/pull/204) On a LogoutRequest if the NameIdFormat is entity, NameQualifier and SPNameQualifier will be ommited. If the NameIdFormat is not entity and a NameQualifier is provided, then the SPNameQualifier will be also added. diff --git a/setup.py b/setup.py index 39e1be11..ad531eb4 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.3.0', + version='2.4.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', From 875dad4643743917b3258af049b03aacc500b1d3 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 27 Feb 2018 17:31:00 +0100 Subject: [PATCH 229/352] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1399d32b..8d31d913 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml). Update python-saml to 2.4.0, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability. -That version also change how calculate fingerprint method works, and will expect as input a formatted x509 certificate +This version also changes how the calculate fingerprint method works, and will expect as input a formatted x509 certificate Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recently, by the sanitized call provided by defusedxml From 7ab94910ecc359e545417abecb1d10c52e8ee040 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 5 Apr 2018 17:03:44 +0200 Subject: [PATCH 230/352] Update defusedxml dependency. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ad531eb4..10d676b8 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ install_requires=[ 'dm.xmlsec.binding==1.3.3', 'isodate>=0.5.0', - 'defusedxml==0.4.1', + 'defusedxml>=0.4.1', ], extras_require={ 'test': ( From 2b283b5568e67d8139f46ad8e7426d4f37172d86 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 23 Apr 2018 18:26:28 +0200 Subject: [PATCH 231/352] Update copyright and license reference --- LICENSE | 2 +- docs/saml2/_modules/saml2/auth.html | 4 ++-- docs/saml2/_modules/saml2/authn_request.html | 4 ++-- docs/saml2/_modules/saml2/constants.html | 4 ++-- docs/saml2/_modules/saml2/errors.html | 4 ++-- docs/saml2/_modules/saml2/logout_request.html | 4 ++-- docs/saml2/_modules/saml2/logout_response.html | 4 ++-- docs/saml2/_modules/saml2/metadata.html | 4 ++-- docs/saml2/_modules/saml2/response.html | 4 ++-- docs/saml2/_modules/saml2/settings.html | 4 ++-- docs/saml2/_modules/saml2/utils.html | 4 ++-- setup.py | 4 ++-- src/onelogin/__init__.py | 4 ++-- src/onelogin/saml2/__init__.py | 4 ++-- src/onelogin/saml2/auth.py | 4 ++-- src/onelogin/saml2/authn_request.py | 4 ++-- src/onelogin/saml2/constants.py | 4 ++-- src/onelogin/saml2/errors.py | 4 ++-- src/onelogin/saml2/idp_metadata_parser.py | 4 ++-- src/onelogin/saml2/logout_request.py | 4 ++-- src/onelogin/saml2/logout_response.py | 4 ++-- src/onelogin/saml2/metadata.py | 4 ++-- src/onelogin/saml2/response.py | 4 ++-- src/onelogin/saml2/settings.py | 4 ++-- src/onelogin/saml2/utils.py | 4 ++-- tests/src/OneLogin/saml2_tests/auth_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/authn_request_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/error_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/logout_request_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/logout_response_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/metadata_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/response_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/settings_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/signed_response_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/utils_test.py | 4 ++-- 36 files changed, 71 insertions(+), 71 deletions(-) diff --git a/LICENSE b/LICENSE index dbbca9c6..1c8f814e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010-2016 OneLogin, Inc. +Copyright (c) 2010-2018 OneLogin, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/docs/saml2/_modules/saml2/auth.html b/docs/saml2/_modules/saml2/auth.html index 82d6e960..243bd4db 100644 --- a/docs/saml2/_modules/saml2/auth.html +++ b/docs/saml2/_modules/saml2/auth.html @@ -51,8 +51,8 @@

Navigation

Source code for saml2.auth

 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from base64 import b64encode
 from urllib import urlencode, quote
diff --git a/docs/saml2/_modules/saml2/authn_request.html b/docs/saml2/_modules/saml2/authn_request.html
index 674eae42..207ee7a6 100644
--- a/docs/saml2/_modules/saml2/authn_request.html
+++ b/docs/saml2/_modules/saml2/authn_request.html
@@ -51,8 +51,8 @@ 

Navigation

Source code for saml2.authn_request

 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from base64 import b64encode
 from datetime import datetime
diff --git a/docs/saml2/_modules/saml2/constants.html b/docs/saml2/_modules/saml2/constants.html
index 3b098931..306ec1b3 100644
--- a/docs/saml2/_modules/saml2/constants.html
+++ b/docs/saml2/_modules/saml2/constants.html
@@ -51,8 +51,8 @@ 

Navigation

Source code for saml2.constants

 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 
 
[docs]class OneLogin_Saml2_Constants: diff --git a/docs/saml2/_modules/saml2/errors.html b/docs/saml2/_modules/saml2/errors.html index e33ebb39..ef958f31 100644 --- a/docs/saml2/_modules/saml2/errors.html +++ b/docs/saml2/_modules/saml2/errors.html @@ -51,8 +51,8 @@

Navigation

Source code for saml2.errors

 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 
 
[docs]class OneLogin_Saml2_Error(Exception): diff --git a/docs/saml2/_modules/saml2/logout_request.html b/docs/saml2/_modules/saml2/logout_request.html index b6cddbc1..1b690dd4 100644 --- a/docs/saml2/_modules/saml2/logout_request.html +++ b/docs/saml2/_modules/saml2/logout_request.html @@ -51,8 +51,8 @@

Navigation

Source code for saml2.logout_request

 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from base64 import b64decode
 from datetime import datetime
diff --git a/docs/saml2/_modules/saml2/logout_response.html b/docs/saml2/_modules/saml2/logout_response.html
index dc6037d1..3ea3bbf7 100644
--- a/docs/saml2/_modules/saml2/logout_response.html
+++ b/docs/saml2/_modules/saml2/logout_response.html
@@ -51,8 +51,8 @@ 

Navigation

Source code for saml2.logout_response

 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from base64 import b64decode
 from datetime import datetime
diff --git a/docs/saml2/_modules/saml2/metadata.html b/docs/saml2/_modules/saml2/metadata.html
index 055db317..7b0a1be5 100644
--- a/docs/saml2/_modules/saml2/metadata.html
+++ b/docs/saml2/_modules/saml2/metadata.html
@@ -51,8 +51,8 @@ 

Navigation

Source code for saml2.metadata

 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from time import gmtime, strftime
 from datetime import datetime
diff --git a/docs/saml2/_modules/saml2/response.html b/docs/saml2/_modules/saml2/response.html
index 219be1b8..d3fee4e3 100644
--- a/docs/saml2/_modules/saml2/response.html
+++ b/docs/saml2/_modules/saml2/response.html
@@ -51,8 +51,8 @@ 

Navigation

Source code for saml2.response

 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from base64 import b64decode
 from copy import deepcopy
diff --git a/docs/saml2/_modules/saml2/settings.html b/docs/saml2/_modules/saml2/settings.html
index c18ebdf9..42800b51 100644
--- a/docs/saml2/_modules/saml2/settings.html
+++ b/docs/saml2/_modules/saml2/settings.html
@@ -51,8 +51,8 @@ 

Navigation

Source code for saml2.settings

 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from datetime import datetime
 import json
diff --git a/docs/saml2/_modules/saml2/utils.html b/docs/saml2/_modules/saml2/utils.html
index 726ba9dd..613d2a09 100644
--- a/docs/saml2/_modules/saml2/utils.html
+++ b/docs/saml2/_modules/saml2/utils.html
@@ -51,8 +51,8 @@ 

Navigation

Source code for saml2.utils

 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 import base64
 from datetime import datetime
diff --git a/setup.py b/setup.py
index 10d676b8..9b5cb63e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,8 +1,8 @@
 #! /usr/bin/env python
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from setuptools import setup
 
diff --git a/src/onelogin/__init__.py b/src/onelogin/__init__.py
index 110bc9df..ba664a65 100644
--- a/src/onelogin/__init__.py
+++ b/src/onelogin/__init__.py
@@ -1,8 +1,8 @@
 # -*- coding: utf-8 -*-
 
 """
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 Add SAML support to your Python softwares using this library.
 Forget those complicated libraries and use that open source
diff --git a/src/onelogin/saml2/__init__.py b/src/onelogin/saml2/__init__.py
index 110bc9df..ba664a65 100644
--- a/src/onelogin/saml2/__init__.py
+++ b/src/onelogin/saml2/__init__.py
@@ -1,8 +1,8 @@
 # -*- coding: utf-8 -*-
 
 """
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 Add SAML support to your Python softwares using this library.
 Forget those complicated libraries and use that open source
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index bb564e4d..8022ca24 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -2,8 +2,8 @@
 
 """ OneLogin_Saml2_Auth class
 
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 Main class of OneLogin's Python Toolkit.
 
diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py
index 636a6888..d0983703 100644
--- a/src/onelogin/saml2/authn_request.py
+++ b/src/onelogin/saml2/authn_request.py
@@ -2,8 +2,8 @@
 
 """ OneLogin_Saml2_Authn_Request class
 
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 AuthNRequest class of OneLogin's Python Toolkit.
 
diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py
index 2bf885ef..895c248a 100644
--- a/src/onelogin/saml2/constants.py
+++ b/src/onelogin/saml2/constants.py
@@ -2,8 +2,8 @@
 
 """ OneLogin_Saml2_Constants class
 
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 Constants class of OneLogin's Python Toolkit.
 
diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py
index 0675fb4a..ec879bfd 100644
--- a/src/onelogin/saml2/errors.py
+++ b/src/onelogin/saml2/errors.py
@@ -2,8 +2,8 @@
 
 """ OneLogin_Saml2_Error class
 
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 Error class of OneLogin's Python Toolkit.
 
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 51d446ff..3b5637a5 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -2,8 +2,8 @@
 
 """ OneLogin_Saml2_IdPMetadataParser class
 
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 Metadata class of OneLogin's Python Toolkit.
 
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 14c49e29..5945c652 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -2,8 +2,8 @@
 
 """ OneLogin_Saml2_Logout_Request class
 
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 Logout Request class of OneLogin's Python Toolkit.
 
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index 0cd1e2be..75ade5e8 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -2,8 +2,8 @@
 
 """ OneLogin_Saml2_Logout_Response class
 
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 Logout Response class of OneLogin's Python Toolkit.
 
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index c431be48..212c0e95 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -2,8 +2,8 @@
 
 """ OneLogin_Saml2_Metadata class
 
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 Metadata class of OneLogin's Python Toolkit.
 
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 56a8476b..b9eb3f8e 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -2,8 +2,8 @@
 
 """ OneLogin_Saml2_Response class
 
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 SAML Response class of OneLogin's Python Toolkit.
 
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index d5db3a05..f3c53be1 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -2,8 +2,8 @@
 
 """ OneLogin_Saml2_Settings class
 
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 Setting class of OneLogin's Python Toolkit.
 
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index dafeaa73..5b1d9b8b 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -2,8 +2,8 @@
 
 """ OneLogin_Saml2_Utils class
 
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
 
 Auxiliary class of OneLogin's Python Toolkit.
 
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index c50fc6e8..ee649448 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from base64 import b64decode, b64encode
 import json
diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py
index 6aee012d..be891cff 100644
--- a/tests/src/OneLogin/saml2_tests/authn_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from base64 import b64decode
 import json
diff --git a/tests/src/OneLogin/saml2_tests/error_test.py b/tests/src/OneLogin/saml2_tests/error_test.py
index 7699300a..cd997cea 100644
--- a/tests/src/OneLogin/saml2_tests/error_test.py
+++ b/tests/src/OneLogin/saml2_tests/error_test.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 import unittest
 from teamcity import is_running_under_teamcity
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index e441e0f0..4e0e79c2 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 
 from copy import deepcopy
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index 6d78d1bc..3ecf901a 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from base64 import b64encode
 import json
diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py
index f54690ed..27151a5a 100644
--- a/tests/src/OneLogin/saml2_tests/logout_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 import json
 from os.path import dirname, join, exists
diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py
index 02e551e7..70590497 100644
--- a/tests/src/OneLogin/saml2_tests/metadata_test.py
+++ b/tests/src/OneLogin/saml2_tests/metadata_test.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 
 import json
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index e6793b87..a67c8f62 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from base64 import b64decode, b64encode
 from datetime import datetime
diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
index bcdd7e1f..4b79715d 100644
--- a/tests/src/OneLogin/saml2_tests/settings_test.py
+++ b/tests/src/OneLogin/saml2_tests/settings_test.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 import json
 from os.path import dirname, join, exists, sep
diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py
index d630f00f..12db9143 100644
--- a/tests/src/OneLogin/saml2_tests/signed_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from base64 import b64encode
 import json
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index 0b9772f2..916c9c81 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
 
 from base64 import b64decode
 import json

From 5e08bb24b5dc5ec1d76023c8620ce8228c588da9 Mon Sep 17 00:00:00 2001
From: Sixto Martin 
Date: Mon, 23 Apr 2018 19:15:39 +0200
Subject: [PATCH 232/352] Update coveralls and coverage dependency version

---
 setup.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/setup.py b/setup.py
index 9b5cb63e..1442b9f2 100644
--- a/setup.py
+++ b/setup.py
@@ -39,12 +39,12 @@
     extras_require={
         'test': (
             'teamcity-messages==1.17',
-            'coverage==4.0.3',
+            'coverage>=3.6',
             'freezegun==0.3.5',
             'pylint==1.3.1',
             'pep8==1.5.7',
             'pyflakes==0.8.1',
-            'coveralls==0.4.4',
+            'coveralls==1.1',
         ),
     },
     keywords='saml saml2 xmlsec django flask',

From 562ad0dac3b2dc768b96105ba978e5f5eee60e5b Mon Sep 17 00:00:00 2001
From: Sixto Martin 
Date: Wed, 25 Apr 2018 15:48:25 +0200
Subject: [PATCH 233/352] Add ID to EntityDescriptor before sign it on add_sign
 method. Improve the way ds namespace is handled in add_sign method

---
 src/onelogin/saml2/utils.py                   | 33 ++++++++++---------
 .../src/OneLogin/saml2_tests/metadata_test.py |  5 +++
 2 files changed, 23 insertions(+), 15 deletions(-)

diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 5b1d9b8b..4ae37efb 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -869,7 +869,6 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
             error_callback_method = print_xmlsec_errors
         xmlsec.set_error_callback(error_callback_method)
 
-        # Sign the metadata with our private key.
         sign_algorithm_transform_map = {
             OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1,
             OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1,
@@ -879,18 +878,31 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
         }
         sign_algorithm_transform = sign_algorithm_transform_map.get(sign_algorithm, xmlsec.TransformRsaSha1)
 
-        signature = Signature(xmlsec.TransformExclC14N, sign_algorithm_transform)
+        signature = Signature(xmlsec.TransformExclC14N, sign_algorithm_transform, nsPrefix='ds')
 
         issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer')
         if len(issuer) > 0:
             issuer = issuer[0]
             issuer.addnext(signature)
+            elem_to_sign = issuer.getparent()
         else:
             entity_descriptor = OneLogin_Saml2_Utils.query(elem, '//md:EntityDescriptor')
             if len(entity_descriptor) > 0:
                 elem.insert(0, signature)
             else:
                 elem[0].insert(0, signature)
+            elem_to_sign = elem
+
+        elem_id = elem_to_sign.get('ID', None)
+        if elem_id is not None:
+            if elem_id:
+                elem_id = '#' + elem_id
+        else:
+            generated_id = generated_id = OneLogin_Saml2_Utils.generate_unique_id()
+            elem_id = '#' + generated_id
+            elem_to_sign.attrib['ID'] = generated_id
+
+        xmlsec.addIDs(elem_to_sign, ["ID"])
 
         digest_algorithm_transform_map = {
             OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1,
@@ -901,6 +913,9 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
         digest_algorithm_transform = digest_algorithm_transform_map.get(digest_algorithm, xmlsec.TransformSha1)
 
         ref = signature.addReference(digest_algorithm_transform)
+        if elem_id:
+            ref.attrib['URI'] = elem_id
+
         ref.addTransform(xmlsec.TransformEnveloped)
         ref.addTransform(xmlsec.TransformExclC14N)
 
@@ -917,20 +932,8 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
         dsig_ctx.signKey = sign_key
         dsig_ctx.sign(signature)
 
+        return tostring(elem, encoding='unicode').encode('utf-8')
         newdoc = parseString(tostring(elem, encoding='unicode').encode('utf-8'))
-
-        signature_nodes = newdoc.getElementsByTagName("Signature")
-
-        for signature in signature_nodes:
-            signature.removeAttribute('xmlns')
-            signature.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
-            if not signature.tagName.startswith('ds:'):
-                signature.tagName = 'ds:' + signature.tagName
-            nodes = signature.getElementsByTagName("*")
-            for node in nodes:
-                if not node.tagName.startswith('ds:'):
-                    node.tagName = 'ds:' + node.tagName
-
         return newdoc.saveXML(newdoc.firstChild)
 
     @staticmethod
diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py
index 70590497..f69f5c7e 100644
--- a/tests/src/OneLogin/saml2_tests/metadata_test.py
+++ b/tests/src/OneLogin/saml2_tests/metadata_test.py
@@ -14,6 +14,7 @@
 
 from onelogin.saml2.metadata import OneLogin_Saml2_Metadata
 from onelogin.saml2.settings import OneLogin_Saml2_Settings
+from onelogin.saml2.utils import OneLogin_Saml2_Utils
 from onelogin.saml2.constants import OneLogin_Saml2_Constants
 
 
@@ -208,9 +209,11 @@ def testSignMetadata(self):
         cert = self.file_contents(join(cert_path, 'sp.crt'))
 
         signed_metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key, cert)
+        self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(signed_metadata, cert))
 
         self.assertIn('
Date: Wed, 25 Apr 2018 15:48:38 +0200
Subject: [PATCH 234/352] Release 2.4.1

---
 changelog.md | 5 +++++
 setup.py     | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/changelog.md b/changelog.md
index 474a6549..d17975f1 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,9 @@
 # python-saml changelog
+### 2.4.1 (Apr 25, 2018)
+* Add ID to EntityDescriptor before sign it on add_sign method. Improve the way ds namespace is handled in add_sign method
+* Update defusedxml, coveralls and coverage dependencies
+* Update copyright and license reference
+
 ### 2.4.0 (Feb 27, 2018)
 * Fix vulnerability [CVE-2017-11427](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11427). Process text of nodes properly, ignoring comments
 * Improve how fingerprint is calcultated
diff --git a/setup.py b/setup.py
index 1442b9f2..e5598ec3 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
 
 setup(
     name='python-saml',
-    version='2.4.0',
+    version='2.4.1',
     description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
     classifiers=[
         'Development Status :: 5 - Production/Stable',

From 310fe65a8f4fe87c5be5d6357274502a2bd68487 Mon Sep 17 00:00:00 2001
From: afrobeard 
Date: Tue, 10 Jul 2018 12:41:23 -0400
Subject: [PATCH 235/352] Making this module compatible with the Current LTS
 version of Django

---
 demo-django/demo/settings.py | 17 ++++++++++++-----
 demo-django/demo/urls.py     | 17 +++++++++--------
 demo-django/demo/views.py    | 19 ++++++-------------
 demo-django/requirements.txt |  2 +-
 4 files changed, 28 insertions(+), 27 deletions(-)

diff --git a/demo-django/demo/settings.py b/demo-django/demo/settings.py
index 3428fdab..792ea82f 100644
--- a/demo-django/demo/settings.py
+++ b/demo-django/demo/settings.py
@@ -22,8 +22,6 @@
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = True
 
-TEMPLATE_DEBUG = True
-
 ALLOWED_HOSTS = []
 
 
@@ -85,6 +83,15 @@
 
 SESSION_ENGINE = 'django.contrib.sessions.backends.file'
 
-TEMPLATE_DIRS = (
-    os.path.join(BASE_DIR, 'templates'),
-)
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [os.path.join(BASE_DIR, 'templates')],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': {
+                'django.contrib.auth.context_processors.auth'
+            }
+        },
+    },
+]
diff --git a/demo-django/demo/urls.py b/demo-django/demo/urls.py
index 4f55a0b3..1f329074 100644
--- a/demo-django/demo/urls.py
+++ b/demo-django/demo/urls.py
@@ -1,11 +1,12 @@
-from django.conf.urls import patterns, url
-
+from django.conf.urls import url
 from django.contrib import admin
+from demo.views import index, attrs, metadata
+
 admin.autodiscover()
 
-urlpatterns = patterns(
-    '',
-    url(r'^$', 'demo.views.index', name='index'),
-    url(r'^attrs/$', 'demo.views.attrs', name='attrs'),
-    url(r'^metadata/$', 'demo.views.metadata', name='metadata'),
-)
+urlpatterns = [
+    url(r'^$', index, name='index'),
+    url(r'^attrs/$', attrs, name='attrs'),
+    url(r'^metadata/$', metadata, name='metadata')
+]
+
diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py
index 859ff9df..59ad2a05 100644
--- a/demo-django/demo/views.py
+++ b/demo-django/demo/views.py
@@ -2,8 +2,7 @@
 from django.core.urlresolvers import reverse
 from django.http import (HttpResponse, HttpResponseRedirect,
                          HttpResponseServerError)
-from django.shortcuts import render_to_response
-from django.template import RequestContext
+from django.shortcuts import render
 
 from onelogin.saml2.auth import OneLogin_Saml2_Auth
 from onelogin.saml2.settings import OneLogin_Saml2_Settings
@@ -97,13 +96,8 @@ def index(request):
         if len(request.session['samlUserdata']) > 0:
             attributes = request.session['samlUserdata'].items()
 
-    return render_to_response('index.html',
-                              {'errors': errors,
-                               'not_auth_warn': not_auth_warn,
-                               'success_slo': success_slo,
-                               'attributes': attributes,
-                               'paint_logout': paint_logout},
-                              context_instance=RequestContext(request))
+    return render(request, 'index.html', {'errors': errors, 'not_auth_warn': not_auth_warn, 'success_slo': success_slo,
+                                          'attributes': attributes, 'paint_logout': paint_logout})
 
 
 def attrs(request):
@@ -115,10 +109,9 @@ def attrs(request):
         if len(request.session['samlUserdata']) > 0:
             attributes = request.session['samlUserdata'].items()
 
-    return render_to_response('attrs.html',
-                              {'paint_logout': paint_logout,
-                               'attributes': attributes},
-                              context_instance=RequestContext(request))
+    return render(request, 'attrs.html',
+                  {'paint_logout': paint_logout,
+                   'attributes': attributes})
 
 
 def metadata(request):
diff --git a/demo-django/requirements.txt b/demo-django/requirements.txt
index 5a55855e..f305b2f2 100644
--- a/demo-django/requirements.txt
+++ b/demo-django/requirements.txt
@@ -1 +1 @@
-Django==1.6.5
+Django==1.11

From b463dd4abc4087bc891ce8c4e2598bd20db464fb Mon Sep 17 00:00:00 2001
From: Sixto Martin 
Date: Thu, 26 Jul 2018 14:06:18 +0200
Subject: [PATCH 236/352] Remove TeamCity CI depedency

---
 setup.py                                                   | 1 -
 tests/src/OneLogin/saml2_tests/auth_test.py                | 7 +------
 tests/src/OneLogin/saml2_tests/authn_request_test.py       | 7 +------
 tests/src/OneLogin/saml2_tests/error_test.py               | 7 +------
 tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py | 7 +------
 tests/src/OneLogin/saml2_tests/logout_request_test.py      | 7 +------
 tests/src/OneLogin/saml2_tests/logout_response_test.py     | 7 +------
 tests/src/OneLogin/saml2_tests/metadata_test.py            | 7 +------
 tests/src/OneLogin/saml2_tests/response_test.py            | 7 +------
 tests/src/OneLogin/saml2_tests/settings_test.py            | 7 +------
 tests/src/OneLogin/saml2_tests/signed_response_test.py     | 7 +------
 tests/src/OneLogin/saml2_tests/utils_test.py               | 7 +------
 12 files changed, 11 insertions(+), 67 deletions(-)

diff --git a/setup.py b/setup.py
index e5598ec3..1c23c817 100644
--- a/setup.py
+++ b/setup.py
@@ -38,7 +38,6 @@
     ],
     extras_require={
         'test': (
-            'teamcity-messages==1.17',
             'coverage>=3.6',
             'freezegun==0.3.5',
             'pylint==1.3.1',
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index ee649448..5bc13000 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -7,8 +7,6 @@
 import json
 from os.path import dirname, join, exists
 import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
 from urlparse import urlparse, parse_qs
 
 from onelogin.saml2.auth import OneLogin_Saml2_Auth
@@ -1166,8 +1164,5 @@ def testGetIdFromLogoutResponse(self):
         self.assertIn(auth.get_last_message_id(), '_f9ee61bd9dbf63606faa9ae3b10548d5b3656fb859')
 
 if __name__ == '__main__':
-    if is_running_under_teamcity():
-        runner = TeamcityTestRunner()
-    else:
-        runner = unittest.TextTestRunner()
+    runner = unittest.TextTestRunner()
     unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py
index be891cff..cd344890 100644
--- a/tests/src/OneLogin/saml2_tests/authn_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py
@@ -7,8 +7,6 @@
 import json
 from os.path import dirname, join, exists
 import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
 from urlparse import urlparse, parse_qs
 from zlib import decompress
 
@@ -341,8 +339,5 @@ def testAttributeConsumingService(self):
         self.assertRegexpMatches(inflated, 'AttributeConsumingServiceIndex="1"')
 
 if __name__ == '__main__':
-    if is_running_under_teamcity():
-        runner = TeamcityTestRunner()
-    else:
-        runner = unittest.TextTestRunner()
+    runner = unittest.TextTestRunner()
     unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/error_test.py b/tests/src/OneLogin/saml2_tests/error_test.py
index cd997cea..9cc861ad 100644
--- a/tests/src/OneLogin/saml2_tests/error_test.py
+++ b/tests/src/OneLogin/saml2_tests/error_test.py
@@ -4,8 +4,6 @@
 # MIT License
 
 import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
 from onelogin.saml2.errors import OneLogin_Saml2_Error
 
 
@@ -19,8 +17,5 @@ def runTest(self):
 
 
 if __name__ == '__main__':
-    if is_running_under_teamcity():
-        runner = TeamcityTestRunner()
-    else:
-        runner = unittest.TextTestRunner()
+    runner = unittest.TextTestRunner()
     unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index 4e0e79c2..6b11e74e 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -10,8 +10,6 @@
 from lxml.etree import XMLSyntaxError
 import unittest
 from urllib2 import URLError
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
 
 from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
 from onelogin.saml2.constants import OneLogin_Saml2_Constants
@@ -645,8 +643,5 @@ def test_merge_settings(self):
         self.assertEqual(expected_settings3, settings_result3)
 
 if __name__ == '__main__':
-    if is_running_under_teamcity():
-        runner = TeamcityTestRunner()
-    else:
-        runner = unittest.TextTestRunner()
+    runner = unittest.TextTestRunner()
     unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index 3ecf901a..cbeed68a 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -7,8 +7,6 @@
 import json
 from os.path import dirname, join, exists
 import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
 from urlparse import urlparse, parse_qs
 from xml.dom.minidom import parseString
 
@@ -545,8 +543,5 @@ def testGetXML(self):
 
 
 if __name__ == '__main__':
-    if is_running_under_teamcity():
-        runner = TeamcityTestRunner()
-    else:
-        runner = unittest.TextTestRunner()
+    runner = unittest.TextTestRunner()
     unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py
index 27151a5a..13b17a5c 100644
--- a/tests/src/OneLogin/saml2_tests/logout_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py
@@ -6,8 +6,6 @@
 import json
 from os.path import dirname, join, exists
 import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
 from urlparse import urlparse, parse_qs
 from xml.dom.minidom import parseString
 
@@ -412,8 +410,5 @@ def testGetXML(self):
 
 
 if __name__ == '__main__':
-    if is_running_under_teamcity():
-        runner = TeamcityTestRunner()
-    else:
-        runner = unittest.TextTestRunner()
+    runner = unittest.TextTestRunner()
     unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py
index f69f5c7e..a63a552e 100644
--- a/tests/src/OneLogin/saml2_tests/metadata_test.py
+++ b/tests/src/OneLogin/saml2_tests/metadata_test.py
@@ -9,8 +9,6 @@
 from time import strftime
 from datetime import datetime
 import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
 
 from onelogin.saml2.metadata import OneLogin_Saml2_Metadata
 from onelogin.saml2.settings import OneLogin_Saml2_Settings
@@ -290,8 +288,5 @@ def testAddX509KeyDescriptors(self):
 
 
 if __name__ == '__main__':
-    if is_running_under_teamcity():
-        runner = TeamcityTestRunner()
-    else:
-        runner = unittest.TextTestRunner()
+    runner = unittest.TextTestRunner()
     unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index a67c8f62..c51bfcaa 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -10,8 +10,6 @@
 import json
 from os.path import dirname, join, exists
 import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
 from xml.dom.minidom import parseString
 from lxml import etree
 from onelogin.saml2.response import OneLogin_Saml2_Response
@@ -1679,8 +1677,5 @@ def testGetAssertionNotOnOrAfter(self):
 
 
 if __name__ == '__main__':
-    if is_running_under_teamcity():
-        runner = TeamcityTestRunner()
-    else:
-        runner = unittest.TextTestRunner()
+    runner = unittest.TextTestRunner()
     unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
index 4b79715d..8d02d041 100644
--- a/tests/src/OneLogin/saml2_tests/settings_test.py
+++ b/tests/src/OneLogin/saml2_tests/settings_test.py
@@ -6,8 +6,6 @@
 import json
 from os.path import dirname, join, exists, sep
 import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
 
 from onelogin.saml2.errors import OneLogin_Saml2_Error
 from onelogin.saml2.settings import OneLogin_Saml2_Settings
@@ -750,8 +748,5 @@ def testIsDebugActive(self):
 
 
 if __name__ == '__main__':
-    if is_running_under_teamcity():
-        runner = TeamcityTestRunner()
-    else:
-        runner = unittest.TextTestRunner()
+    runner = unittest.TextTestRunner()
     unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py
index 12db9143..77e0d5c4 100644
--- a/tests/src/OneLogin/saml2_tests/signed_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py
@@ -7,8 +7,6 @@
 import json
 from os.path import dirname, join, exists
 import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
 
 from onelogin.saml2.response import OneLogin_Saml2_Response
 from onelogin.saml2.settings import OneLogin_Saml2_Settings
@@ -62,8 +60,5 @@ def testResponseAndAssertionSigned(self):
 
 
 if __name__ == '__main__':
-    if is_running_under_teamcity():
-        runner = TeamcityTestRunner()
-    else:
-        runner = unittest.TextTestRunner()
+    runner = unittest.TextTestRunner()
     unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index 916c9c81..d0c73e86 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -9,8 +9,6 @@
 from lxml import etree
 from os.path import dirname, join, exists
 import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
 from xml.dom.minidom import Document, parseString
 
 from onelogin.saml2.constants import OneLogin_Saml2_Constants
@@ -1029,8 +1027,5 @@ def testValidateSign(self):
 
 
 if __name__ == '__main__':
-    if is_running_under_teamcity():
-        runner = TeamcityTestRunner()
-    else:
-        runner = unittest.TextTestRunner()
+    runner = unittest.TextTestRunner()
     unittest.main(testRunner=runner)

From d8e0109e4c697f4a920ff4993c491eb5b0d38d55 Mon Sep 17 00:00:00 2001
From: Sixto Martin 
Date: Thu, 26 Jul 2018 14:09:54 +0200
Subject: [PATCH 237/352] Update pylint dependency to 1.9.1

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 1c23c817..14b1b7b1 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
         'test': (
             'coverage>=3.6',
             'freezegun==0.3.5',
-            'pylint==1.3.1',
+            'pylint==1.9.1',
             'pep8==1.5.7',
             'pyflakes==0.8.1',
             'coveralls==1.1',

From d1bd595a751027b779a0b78026aaa7aebbffc2e7 Mon Sep 17 00:00:00 2001
From: Sixto Martin 
Date: Sun, 5 Aug 2018 21:13:25 +0200
Subject: [PATCH 238/352] Discourage the use of fingerprint on production
 environments

---
 README.md | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 8d31d913..64d01d1d 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,8 @@ In production, the **strict** parameter MUST be set as **"true"**. Otherwise
 your environment is not secure and will be exposed to attacks.
 
 
+In production also we highly recommend to register on the settings the IdP certificate instead of using the fingerprint method. The fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass. Other SAML toolkits deprecated that mechanism, we maintain it for compatibility and also to be used on test environment.
+
 Getting started
 ---------------
 
@@ -326,7 +328,9 @@ This is the settings.json file:
         "x509cert": ""
         /*
          *  Instead of using the whole x509cert you can use a fingerprint in order to
-         *  validate a SAMLResponse, but you will need it to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding.
+         *  validate a SAMLResponse (but you still need the x509cert to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding).
+         *  But take in mind that the fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass,
+         *  that why we don't recommend it use for production environments.
          *
          *  (openssl x509 -noout -fingerprint -in "idp.crt" to generate it,
          *  or add for example the -sha256 , -sha384 or -sha512 parameter)
@@ -337,6 +341,7 @@ This is the settings.json file:
          *
          *  Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you
          *  will need to provide the whole x509cert.
+	 *
          */
         // 'certFingerprint': '',
         // 'certFingerprintAlgorithm': 'sha1',

From d3dbe3e9788c312c79d78d4292b0d2792605a3c4 Mon Sep 17 00:00:00 2001
From: Sixto Martin 
Date: Sun, 26 Aug 2018 13:00:17 +0200
Subject: [PATCH 239/352] Update dm.xmlsec.binding dependency to 1.3.7

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 14b1b7b1..cddaf740 100644
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,7 @@
     },
     test_suite='tests',
     install_requires=[
-        'dm.xmlsec.binding==1.3.3',
+        'dm.xmlsec.binding==1.3.7',
         'isodate>=0.5.0',
         'defusedxml>=0.4.1',
     ],

From 3814b0fe98d6ab78cf92b39c15e1785b1cab22bb Mon Sep 17 00:00:00 2001
From: Sixto Martin 
Date: Wed, 5 Sep 2018 20:02:21 +0200
Subject: [PATCH 240/352] Release 2.4.2

---
 changelog.md | 5 +++++
 setup.py     | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/changelog.md b/changelog.md
index d17975f1..996779c1 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,9 @@
 # python-saml changelog
+### 2.4.2 (Sep 05, 2018)
+* Update dm.xmlsec.binding dependency to 1.3.7
+* Update pylint dependency to 1.9.1
+* Update Django demo to use LTS version of Django
+
 ### 2.4.1 (Apr 25, 2018)
 * Add ID to EntityDescriptor before sign it on add_sign method. Improve the way ds namespace is handled in add_sign method
 * Update defusedxml, coveralls and coverage dependencies
diff --git a/setup.py b/setup.py
index cddaf740..cb53f643 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
 
 setup(
     name='python-saml',
-    version='2.4.1',
+    version='2.4.2',
     description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
     classifiers=[
         'Development Status :: 5 - Production/Stable',

From 4bafe94a70989b1a9cb49351e7615df11a72a9b7 Mon Sep 17 00:00:00 2001
From: Sixto Martin 
Date: Thu, 27 Sep 2018 16:48:26 +0200
Subject: [PATCH 241/352] Fix #238 DSA constant

---
 src/onelogin/saml2/constants.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py
index 895c248a..87cb0e6f 100644
--- a/src/onelogin/saml2/constants.py
+++ b/src/onelogin/saml2/constants.py
@@ -103,7 +103,7 @@ class OneLogin_Saml2_Constants(object):
     SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384'
     SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
 
-    DSA_SHA1 = 'http://www.w3.org/2000/09/xmld/sig#dsa-sha1'
+    DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1'
     RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
     RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
     RSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'

From c7e5cda0225ba3517a85b1fc726323ea0e1ef616 Mon Sep 17 00:00:00 2001
From: Sixto Martin 
Date: Wed, 17 Oct 2018 01:13:28 +0200
Subject: [PATCH 242/352] If debug enable, print reason for the SAMLResponse
 invalidation

---
 demo-django/demo/views.py        | 7 ++++++-
 demo-django/templates/index.html | 3 +++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py
index 59ad2a05..2ae0ebac 100644
--- a/demo-django/demo/views.py
+++ b/demo-django/demo/views.py
@@ -34,6 +34,7 @@ def index(request):
     req = prepare_django_request(request)
     auth = init_saml_auth(req)
     errors = []
+    error_reason = None
     not_auth_warn = False
     success_slo = False
     attributes = False
@@ -70,6 +71,7 @@ def index(request):
         auth.process_response(request_id=request_id)
         errors = auth.get_errors()
         not_auth_warn = not auth.is_authenticated()
+
         if not errors:
             if 'AuthNRequestID' in request.session:
                 del request.session['AuthNRequestID']
@@ -78,6 +80,9 @@ def index(request):
             request.session['samlSessionIndex'] = auth.get_session_index()
             if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']:
                 return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState']))
+        else:
+            if auth.get_settings().is_debug_active():
+                error_reason = auth.get_last_error_reason()
     elif 'sls' in req['get_data']:
         request_id = None
         if 'LogoutRequestID' in request.session:
@@ -96,7 +101,7 @@ def index(request):
         if len(request.session['samlUserdata']) > 0:
             attributes = request.session['samlUserdata'].items()
 
-    return render(request, 'index.html', {'errors': errors, 'not_auth_warn': not_auth_warn, 'success_slo': success_slo,
+    return render(request, 'index.html', {'errors': errors, 'error_reason': error_reason, not_auth_warn: not_auth_warn, 'success_slo': success_slo,
                                           'attributes': attributes, 'paint_logout': paint_logout})
 
 
diff --git a/demo-django/templates/index.html b/demo-django/templates/index.html
index f7d51101..87f2f08b 100644
--- a/demo-django/templates/index.html
+++ b/demo-django/templates/index.html
@@ -10,6 +10,9 @@
           
  • {{err}}
  • {% endfor %} + {% if error_reason %} +

    Reason: {{error_reason}}

    + {% endif %}
    {% endif %} From d0c319d7dd4e6ab87e6226c8f6a133470ad40ca0 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 2 Nov 2018 12:39:19 +0100 Subject: [PATCH 243/352] Don't require compression on LogoutResponse messages by relaxing the decode_base64_and_inflate method --- src/onelogin/saml2/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 4ae37efb..64b0bb6a 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -98,8 +98,14 @@ def decode_base64_and_inflate(value): :returns: the string after decoding and inflating :rtype: string """ + decoded = base64.b64decode(value) + # We try to inflate + try: + result = zlib.decompress(decoded, -15) + except Exception: + result = decoded - return zlib.decompress(base64.b64decode(value), -15).decode('utf-8') + return result.decode('utf-8') @staticmethod def deflate_and_base64_encode(value): From d7370622a4479b1b933d29a965e4720fa11391d3 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 2 Nov 2018 13:27:33 +0100 Subject: [PATCH 244/352] Add expected/received in WRONG_ISSUER error --- src/onelogin/saml2/logout_request.py | 6 +++++- src/onelogin/saml2/logout_response.py | 6 +++++- src/onelogin/saml2/response.py | 6 +++++- tests/src/OneLogin/saml2_tests/logout_response_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/response_test.py | 4 ++-- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 5945c652..a566f489 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -367,7 +367,11 @@ def is_valid(self, request_data, raise_exceptions=False): issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom) if issuer is not None and issuer != idp_entity_id: raise OneLogin_Saml2_ValidationError( - 'Invalid issuer in the Logout Request', + 'Invalid issuer in the Logout Request (expected %(idpEntityId)s, got %(issuer)s)' % + { + 'idpEntityId': idp_entity_id, + 'issuer': issuer + }, OneLogin_Saml2_ValidationError.WRONG_ISSUER ) diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 75ade5e8..7eaf4cba 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -114,7 +114,11 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): issuer = self.get_issuer() if issuer is not None and issuer != idp_entity_id: raise OneLogin_Saml2_ValidationError( - 'Invalid issuer in the Logout Request', + 'Invalid issuer in the Logout Response (expected %(idpEntityId)s, got %(issuer)s)' % + { + 'idpEntityId': idp_entity_id, + 'issuer': issuer + }, OneLogin_Saml2_ValidationError.WRONG_ISSUER ) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index b9eb3f8e..11a1ba47 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -224,7 +224,11 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): for issuer in issuers: if issuer is None or issuer != idp_entity_id: raise OneLogin_Saml2_ValidationError( - 'Invalid issuer in the Assertion/Response', + 'Invalid issuer in the Assertion/Response (expected %(idpEntityId)s, got %(issuer)s)' % + { + 'idpEntityId': idp_entity_id, + 'issuer': issuer + }, OneLogin_Saml2_ValidationError.WRONG_ISSUER ) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 13b17a5c..6493e534 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -186,7 +186,7 @@ def testIsInValidIssuer(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) self.assertFalse(response_2.is_valid(request_data)) - self.assertIn('Invalid issuer in the Logout Request', response_2.get_error()) + self.assertIn('Invalid issuer in the Logout Response', response_2.get_error()) def testIsInValidDestination(self): """ @@ -272,7 +272,7 @@ def testIsInValidSign(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertFalse(response_2.is_valid(request_data)) - self.assertIn('Invalid issuer in the Logout Request', response_2.get_error()) + self.assertIn('Invalid issuer in the Logout Response', response_2.get_error()) settings.set_strict(False) old_signature = request_data['get_data']['Signature'] diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index c51bfcaa..d6daaf97 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1012,11 +1012,11 @@ def testIsInValidIssuer(self): settings.set_strict(True) response_3 = OneLogin_Saml2_Response(settings, message) self.assertFalse(response_3.is_valid(request_data)) - self.assertEqual('Invalid issuer in the Assertion/Response', response_3.get_error()) + self.assertEqual('Invalid issuer in the Assertion/Response (expected http://idp.example.com/, got http://invalid.issuer.example.com/)', response_3.get_error()) response_4 = OneLogin_Saml2_Response(settings, message_2) self.assertFalse(response_4.is_valid(request_data)) - self.assertEqual('Invalid issuer in the Assertion/Response', response_4.get_error()) + self.assertEqual('Invalid issuer in the Assertion/Response (expected http://idp.example.com/, got http://invalid.isser.example.com/)', response_4.get_error()) def testIsInValidSessionIndex(self): """ From ec5ac009594c87fd3451e7fe0ba648d0fc9bcdd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Ku=C5=BAmi=C5=84ski?= Date: Fri, 9 Nov 2018 17:26:47 +0100 Subject: [PATCH 245/352] auth: fix docstring Fixed small docstring mismatch --- src/onelogin/saml2/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 8022ca24..eb61705e 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -42,8 +42,8 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): :param request_data: Request Data :type request_data: dict - :param settings: Optional. SAML Toolkit Settings - :type settings: dict + :param old_settings: Optional. SAML Toolkit Settings + :type old_settings: dict :param custom_base_path: Optional. Path where are stored the settings file and the cert folder :type custom_base_path: string From 5fc6294db4911d7ee67ba719ada0d661ba8ccd4a Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 14 Nov 2018 18:29:05 +0100 Subject: [PATCH 246/352] Fixed a ValidationError misspelling --- src/onelogin/saml2/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 11a1ba47..9e96288a 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -647,7 +647,7 @@ def process_signed_elements(self): 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 + OneLogin_Saml2_ValidationError.UNEXPECTED_SIGNED_ELEMENTS ) return signed_elements From 0f7652e550d3a17d65de8c3421b289830aad3c4c Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 15 Dec 2018 23:31:07 +0100 Subject: [PATCH 247/352] Update Shibboleth XML URL --- .../src/OneLogin/saml2_tests/idp_metadata_parser_test.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index 6b11e74e..33030cd3 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -48,7 +48,7 @@ def testGetMetadata(self): data = OneLogin_Saml2_IdPMetadataParser.get_metadata('http://google.es') try: - data = OneLogin_Saml2_IdPMetadataParser.get_metadata('https://www.testshib.org/metadata/testshib-providers.xml') + data = OneLogin_Saml2_IdPMetadataParser.get_metadata('https://idp.testshib.org/idp/shibboleth') except URLError: data = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml')) self.assertTrue(data is not None and data is not {}) @@ -61,7 +61,7 @@ def testParseRemote(self): data = OneLogin_Saml2_IdPMetadataParser.parse_remote('http://google.es') try: - data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://www.testshib.org/metadata/testshib-providers.xml') + data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://idp.testshib.org/idp/shibboleth') except URLError: xml = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml')) data = OneLogin_Saml2_IdPMetadataParser.parse(xml) @@ -83,7 +83,6 @@ def testParseRemote(self): } """ expected_settings = json.loads(expected_settings_json) - self.assertEqual(expected_settings, data) def testParse(self): @@ -144,7 +143,7 @@ def test_parse_testshib_required_binding_sso_redirect(self): """ try: xmldoc = OneLogin_Saml2_IdPMetadataParser.get_metadata( - 'https://www.testshib.org/metadata/testshib-providers.xml') + 'https://idp.testshib.org/idp/shibboleth') except Exception: xmldoc = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml')) @@ -181,7 +180,7 @@ def test_parse_testshib_required_binding_sso_post(self): """ try: xmldoc = OneLogin_Saml2_IdPMetadataParser.get_metadata( - 'https://www.testshib.org/metadata/testshib-providers.xml') + 'https://idp.testshib.org/idp/shibboleth') except URLError: xmldoc = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml')) From b81db0b69877ef958b89308618a65d9f7441f25c Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 18 Jan 2019 04:06:09 +0100 Subject: [PATCH 248/352] setup.py: flake8 is a superset of pep8 and pyflakes --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index cb53f643..c3f16089 100644 --- a/setup.py +++ b/setup.py @@ -41,8 +41,7 @@ 'coverage>=3.6', 'freezegun==0.3.5', 'pylint==1.9.1', - 'pep8==1.5.7', - 'pyflakes==0.8.1', + 'flake8==3.6.0', 'coveralls==1.1', ), }, From e24ddca007fae344048235d6adc20f958dcacbb0 Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 18 Jan 2019 04:15:57 +0100 Subject: [PATCH 249/352] pep8 --> pycodestyle https://pep8.readthedocs.io/en/release-1.7.x/ --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4f275e8d..7221c335 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ script: - 'coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test' - 'coverage report -m --rcfile=tests/coverage.rc' # - 'pylint src/onelogin/saml2 --rcfile=tests/pylint.rc' - - 'pep8 tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc' + - 'pycodestyle tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc' - 'pyflakes src/onelogin/saml2 demo-django demo-flask tests/src/OneLogin/saml2_tests' after_success: 'coveralls' From a3fe9b83686896808f1753d94fd1349b4479b4b6 Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 18 Jan 2019 04:23:20 +0100 Subject: [PATCH 250/352] Update pep8.rc --- tests/pep8.rc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pep8.rc b/tests/pep8.rc index 910e1eaa..1f1d592f 100644 --- a/tests/pep8.rc +++ b/tests/pep8.rc @@ -1,3 +1,3 @@ -[pep8] +[pycodestyle] ignore = E501, E731 max-line-length = 160 From 606e21a26e1bef3178a22df1d34b61eec76bafc1 Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 18 Jan 2019 04:36:11 +0100 Subject: [PATCH 251/352] Pycodestyle W605: Use r'strings' instead of 'strings' --- tests/src/OneLogin/saml2_tests/authn_request_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index cd344890..3dd3e9df 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -272,7 +272,7 @@ def testCreateDeflatedSAMLRequestURLParameter(self): 'SAMLRequest': authn_request.get_request() } auth_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SSOService.php', parameters, True) - self.assertRegexpMatches(auth_url, '^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=') + self.assertRegexpMatches(auth_url, r'^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=') exploded = urlparse(auth_url) exploded = parse_qs(exploded[4]) payload = exploded['SAMLRequest'][0] @@ -301,7 +301,7 @@ def testCreateEncSAMLRequest(self): 'SAMLRequest': authn_request.get_request() } auth_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SSOService.php', parameters, True) - self.assertRegexpMatches(auth_url, '^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=') + self.assertRegexpMatches(auth_url, r'^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=') exploded = urlparse(auth_url) exploded = parse_qs(exploded[4]) payload = exploded['SAMLRequest'][0] @@ -338,6 +338,7 @@ def testAttributeConsumingService(self): self.assertRegexpMatches(inflated, 'AttributeConsumingServiceIndex="1"') + if __name__ == '__main__': runner = unittest.TextTestRunner() unittest.main(testRunner=runner) From ff3b07c20cd579a08cedfc8a24b5964115a89b4a Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 18 Jan 2019 04:44:08 +0100 Subject: [PATCH 252/352] r'strings' --- tests/src/OneLogin/saml2_tests/logout_request_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index cbeed68a..eb50c785 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -46,7 +46,7 @@ def testConstructor(self): parameters = {'SAMLRequest': logout_request.get_request()} logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True) - self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=') + self.assertRegexpMatches(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=') url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) payload = exploded['SAMLRequest'][0] @@ -63,7 +63,7 @@ def testCreateDeflatedSAMLLogoutRequestURLParameter(self): parameters = {'SAMLRequest': logout_request.get_request()} logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True) - self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=') + self.assertRegexpMatches(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=') url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) payload = exploded['SAMLRequest'][0] @@ -120,7 +120,7 @@ def testConstructorEncryptIdUsingX509certMulti(self): parameters = {'SAMLRequest': logout_request.get_request()} logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True) - self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=') + self.assertRegexpMatches(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=') url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) payload = exploded['SAMLRequest'][0] From 98fa1ee5f2fa785db5671db9f94653f6f40f6cef Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 18 Jan 2019 04:45:13 +0100 Subject: [PATCH 253/352] r'strings' --- tests/src/OneLogin/saml2_tests/logout_response_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 6493e534..38d9fab6 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -56,7 +56,7 @@ def testCreateDeflatedSAMLLogoutResponseURLParameter(self): logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True) - self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLResponse=') + self.assertRegexpMatches(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLResponse=') url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(exploded['SAMLResponse'][0]) From 4948c7e1a38daabb4e29bb87c9cbf2c5f15a4e8e Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 18 Jan 2019 04:51:13 +0100 Subject: [PATCH 254/352] Do not use bare except --- src/onelogin/saml2/idp_metadata_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 3b5637a5..ad10661c 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -54,7 +54,7 @@ def get_metadata(url, validate_cert=True): idp_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:IDPSSODescriptor') if idp_descriptor_nodes: valid = True - except: + except Exception: pass if not valid: From 31129e8ecff8273428a54f6fc6d9dd8142869dcd Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 18 Jan 2019 04:53:27 +0100 Subject: [PATCH 255/352] ignore = W504 line break after binary operator --- tests/pep8.rc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pep8.rc b/tests/pep8.rc index 1f1d592f..ad7c254b 100644 --- a/tests/pep8.rc +++ b/tests/pep8.rc @@ -1,3 +1,3 @@ [pycodestyle] -ignore = E501, E731 +ignore = E501, E731, W504 max-line-length = 160 From 2a08d00b69d1c9820f1ec5b23adab4c071b0aa3c Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 18 Jan 2019 04:55:45 +0100 Subject: [PATCH 256/352] E305 expected 2 blank lines after class or function definition --- tests/src/OneLogin/saml2_tests/auth_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 5bc13000..2b946553 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -1163,6 +1163,7 @@ def testGetIdFromLogoutResponse(self): auth.process_slo() self.assertIn(auth.get_last_message_id(), '_f9ee61bd9dbf63606faa9ae3b10548d5b3656fb859') + if __name__ == '__main__': runner = unittest.TextTestRunner() unittest.main(testRunner=runner) From 581ef965a2795b7d23462f89276a5dcdf726ac9d Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 18 Jan 2019 04:58:12 +0100 Subject: [PATCH 257/352] r'strings' --- src/onelogin/saml2/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 64b0bb6a..565bb50c 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -434,7 +434,7 @@ def generate_unique_id(): @staticmethod def parse_time_to_SAML(time): - """ + r""" Converts a UNIX timestamp to SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(\.s+)?Z. @@ -449,7 +449,7 @@ def parse_time_to_SAML(time): @staticmethod def parse_SAML_to_time(timestr): - """ + r""" Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(\.s+)?Z to a UNIX timestamp. The sub-second part is ignored. From 941dd9d10ecc81ced4ce063a9ccd4fcb2e92ca0d Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 18 Jan 2019 05:03:15 +0100 Subject: [PATCH 258/352] expected 2 blank lines after class or function definition --- tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index 33030cd3..0359bb5a 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -641,6 +641,7 @@ def test_merge_settings(self): expected_settings3 = json.loads(expected_settings3_json) self.assertEqual(expected_settings3, settings_result3) + if __name__ == '__main__': runner = unittest.TextTestRunner() unittest.main(testRunner=runner) From c2e8b095e1af5fe665d5128dfb4ed53e21e92ad8 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 29 Jan 2019 13:11:29 +0100 Subject: [PATCH 259/352] Security improvements. Use of tagid to prevent XPath injection. Disable DTD on fromstring defusedxml method --- src/onelogin/saml2/idp_metadata_parser.py | 4 +- src/onelogin/saml2/logout_request.py | 10 ++--- src/onelogin/saml2/logout_response.py | 4 +- src/onelogin/saml2/metadata.py | 2 +- src/onelogin/saml2/response.py | 20 ++++++---- src/onelogin/saml2/utils.py | 46 +++++++++++++---------- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index ad10661c..55055921 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -50,7 +50,7 @@ def get_metadata(url, validate_cert=True): if xml: try: - dom = fromstring(xml) + dom = fromstring(xml, forbid_dtd=True) idp_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:IDPSSODescriptor') if idp_descriptor_nodes: valid = True @@ -124,7 +124,7 @@ def parse( """ data = {} - dom = fromstring(idp_metadata) + dom = fromstring(idp_metadata, forbid_dtd=True) entity_desc_path = '//md:EntityDescriptor' if entity_id: diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index a566f489..f201ff36 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -171,7 +171,7 @@ def get_id(request): else: if isinstance(request, Document): request = request.toxml() - elem = fromstring(request) + elem = fromstring(request, forbid_dtd=True) return elem.get('ID', None) @staticmethod @@ -190,7 +190,7 @@ def get_nameid_data(request, key=None): else: if isinstance(request, Document): request = request.toxml() - elem = fromstring(request) + elem = fromstring(request, forbid_dtd=True) name_id = None encrypted_entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID') @@ -271,7 +271,7 @@ def get_issuer(request): else: if isinstance(request, Document): request = request.toxml() - elem = fromstring(request) + elem = fromstring(request, forbid_dtd=True) issuer = None issuer_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:Issuer') @@ -293,7 +293,7 @@ def get_session_indexes(request): else: if isinstance(request, Document): request = request.toxml() - elem = fromstring(request) + elem = fromstring(request, forbid_dtd=True) session_indexes = [] session_index_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex') @@ -314,7 +314,7 @@ def is_valid(self, request_data, raise_exceptions=False): self.__error = None lowercase_urlencoding = False try: - dom = fromstring(self.__logout_request) + dom = fromstring(self.__logout_request, forbid_dtd=True) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 7eaf4cba..3e6be91f 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -44,7 +44,7 @@ def __init__(self, settings, response=None): if response is not None: self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response) - self.document = parseString(self.__logout_response) + self.document = parseString(self.__logout_response, forbid_dtd=True) self.id = self.document.documentElement.getAttribute('ID') def get_issuer(self): @@ -200,7 +200,7 @@ def __query(self, query): """ # Switch to lxml for querying xml = self.document.toxml() - return OneLogin_Saml2_Utils.query(fromstring(xml), query) + return OneLogin_Saml2_Utils.query(fromstring(xml, forbid_dtd=True), query) def build(self, in_response_to): """ diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 212c0e95..9a931c9e 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -247,7 +247,7 @@ def add_x509_key_descriptors(metadata, cert=None, add_encryption=True): if cert is None or cert == '': return metadata try: - xml = parseString(metadata.encode('utf-8')) + xml = parseString(metadata.encode('utf-8'), forbid_dtd=True) except Exception as e: raise Exception('Error parsing metadata. ' + e.message) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 9e96288a..da9c1d77 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -40,7 +40,7 @@ def __init__(self, settings, response): self.__settings = settings self.__error = None self.response = b64decode(response) - self.document = fromstring(self.response) + self.document = fromstring(self.response, forbid_dtd=True) self.decrypted_document = None self.encrypted = None self.valid_scd_not_on_or_after = None @@ -735,6 +735,7 @@ def __query_assertion(self, xpath_expr): signature_expr = '/ds:Signature/ds:SignedInfo/ds:Reference' signed_assertion_query = '/samlp:Response' + assertion_expr + signature_expr assertion_reference_nodes = self.__query(signed_assertion_query) + tagid = None if not assertion_reference_nodes: # Check if the message is signed @@ -742,23 +743,28 @@ def __query_assertion(self, xpath_expr): message_reference_nodes = self.__query(signed_message_query) if message_reference_nodes: message_id = message_reference_nodes[0].get('URI') - final_query = "/samlp:Response[@ID='%s']/" % message_id[1:] + final_query = "/samlp:Response[@ID=$tagid]/" + tagid = message_id[1:] else: final_query = "/samlp:Response" final_query += assertion_expr else: assertion_id = assertion_reference_nodes[0].get('URI') - final_query = '/samlp:Response' + assertion_expr + "[@ID='%s']" % assertion_id[1:] + final_query = '/samlp:Response' + assertion_expr + "[@ID=$tagid]" + tagid = assertion_id[1:] final_query += xpath_expr - return self.__query(final_query) + return self.__query(final_query, tagid) - def __query(self, query): + def __query(self, query, tagid=None): """ Extracts nodes that match the query from the Response :param query: Xpath Expresion :type query: String + :param tagid: Tag ID + :type query: String + :returns: The queried nodes :rtype: list """ @@ -766,7 +772,7 @@ def __query(self, query): document = self.decrypted_document else: document = self.document - return OneLogin_Saml2_Utils.query(document, query) + return OneLogin_Saml2_Utils.query(document, query, None, tagid) def __decrypt_assertion(self, dom): """ @@ -817,7 +823,7 @@ def __decrypt_assertion(self, dom): if not uri.startswith('#'): break uri = uri.split('#')[1] - encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id="' + uri + '"]') + encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id=$tagid]', None, uri) if encrypted_key: keyinfo.append(encrypted_key[0]) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 565bb50c..8d11447a 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -141,7 +141,7 @@ def validate_xml(xml, schema, debug=False): # Switch to lxml for schema validation try: - dom = fromstring(xml.encode('utf-8')) + dom = fromstring(xml.encode('utf-8'), forbid_dtd=True) except Exception: return 'unloaded_xml' @@ -160,7 +160,7 @@ def validate_xml(xml, schema, debug=False): return 'invalid_xml' - return parseString(tostring(dom, encoding='unicode').encode('utf-8')) + return parseString(tostring(dom, encoding='unicode').encode('utf-8'), forbid_dtd=True) @staticmethod def element_text(node): @@ -530,7 +530,7 @@ def get_expire_time(cache_duration=None, valid_until=None): return None @staticmethod - def query(dom, query, context=None): + def query(dom, query, context=None, tagid=None): """ Extracts nodes that match the query from the Element @@ -543,13 +543,21 @@ def query(dom, query, context=None): :param context: Context Node :type: DOMElement + :param tagid: Tag ID + :type: string + :returns: The queried nodes :rtype: list """ if context is None: - return dom.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) + source = dom + else: + source = context + + if tagid is None: + return source.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) else: - return context.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) + return source.xpath(query, tagid=tagid, namespaces=OneLogin_Saml2_Constants.NSMAP) @staticmethod def delete_local_session(callback=None): @@ -668,7 +676,7 @@ def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=No if cert is not None: xml = name_id_container.toxml() - elem = fromstring(xml) + elem = fromstring(xml, forbid_dtd=True) error_callback_method = None if debug: @@ -697,7 +705,7 @@ def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=No edata = enc_ctx.encryptXml(enc_data, elem[0]) - newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8')) + newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'), forbid_dtd=True) if newdoc.hasChildNodes(): child = newdoc.firstChild @@ -783,9 +791,9 @@ def decrypt_element(encrypted_data, key, debug=False, inplace=False): :rtype: lxml.etree.Element """ if isinstance(encrypted_data, Element): - encrypted_data = fromstring(str(encrypted_data.toxml())) + encrypted_data = fromstring(str(encrypted_data.toxml()), forbid_dtd=True) elif isinstance(encrypted_data, basestring): - encrypted_data = fromstring(str(encrypted_data)) + encrypted_data = fromstring(str(encrypted_data), forbid_dtd=True) elif not inplace and isinstance(encrypted_data, etree._Element): encrypted_data = deepcopy(encrypted_data) @@ -851,7 +859,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant elem = xml elif isinstance(xml, Document): xml = xml.toxml() - elem = fromstring(xml.encode('utf-8')) + elem = fromstring(xml.encode('utf-8'), forbid_dtd=True) elif isinstance(xml, Element): xml.setAttributeNS( unicode(OneLogin_Saml2_Constants.NS_SAMLP), @@ -864,9 +872,9 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant unicode(OneLogin_Saml2_Constants.NS_SAML) ) xml = xml.toxml() - elem = fromstring(xml.encode('utf-8')) + elem = fromstring(xml.encode('utf-8'), forbid_dtd=True) elif isinstance(xml, basestring): - elem = fromstring(xml.encode('utf-8')) + elem = fromstring(xml.encode('utf-8'), forbid_dtd=True) else: raise Exception('Error parsing xml string') @@ -939,8 +947,6 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant dsig_ctx.sign(signature) return tostring(elem, encoding='unicode').encode('utf-8') - newdoc = parseString(tostring(elem, encoding='unicode').encode('utf-8')) - return newdoc.saveXML(newdoc.firstChild) @staticmethod @return_false_on_exception @@ -981,7 +987,7 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid elem = xml elif isinstance(xml, Document): xml = xml.toxml() - elem = fromstring(str(xml)) + elem = fromstring(str(xml), forbid_dtd=True) elif isinstance(xml, Element): xml.setAttributeNS( unicode(OneLogin_Saml2_Constants.NS_SAMLP), @@ -994,9 +1000,9 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid unicode(OneLogin_Saml2_Constants.NS_SAML) ) xml = xml.toxml() - elem = fromstring(str(xml)) + elem = fromstring(str(xml), forbid_dtd=True) elif isinstance(xml, basestring): - elem = fromstring(str(xml)) + elem = fromstring(str(xml), forbid_dtd=True) else: raise Exception('Error parsing xml string') @@ -1065,7 +1071,7 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha elem = xml elif isinstance(xml, Document): xml = xml.toxml() - elem = fromstring(str(xml)) + elem = fromstring(str(xml), forbid_dtd=True) elif isinstance(xml, Element): xml.setAttributeNS( unicode(OneLogin_Saml2_Constants.NS_MD), @@ -1073,9 +1079,9 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha unicode(OneLogin_Saml2_Constants.NS_MD) ) xml = xml.toxml() - elem = fromstring(str(xml)) + elem = fromstring(str(xml), forbid_dtd=True) elif isinstance(xml, basestring): - elem = fromstring(str(xml)) + elem = fromstring(str(xml), forbid_dtd=True) else: raise Exception('Error parsing xml string') From c999587f8b0cafd43a16ef5fa16dede6b9dfee93 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 29 Jan 2019 13:31:10 +0100 Subject: [PATCH 260/352] flake8 fixes --- src/onelogin/saml2/logout_request.py | 2 +- src/onelogin/saml2/logout_response.py | 2 +- src/onelogin/saml2/response.py | 2 +- src/onelogin/saml2/utils.py | 2 +- tests/src/OneLogin/saml2_tests/utils_test.py | 6 ++++-- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index f201ff36..e419f585 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -425,7 +425,7 @@ def is_valid(self, request_data, raise_exceptions=False): self.__error = err.__str__() debug = self.__settings.is_debug_active() if debug: - print err.__str__() + print(err.__str__()) if raise_exceptions: raise err return False diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 3e6be91f..4af91a4f 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -185,7 +185,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): self.__error = err.__str__() debug = self.__settings.is_debug_active() if debug: - print err.__str__() + print(err.__str__()) if raise_exceptions: raise err return False diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index da9c1d77..798f51bf 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -328,7 +328,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): self.__error = err.__str__() debug = self.__settings.is_debug_active() if debug: - print err.__str__() + print(err.__str__()) if raise_exceptions: raise err return False diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 8d11447a..8fea81d1 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -75,7 +75,7 @@ def print_xmlsec_errors(filename, line, func, error_object, error_subject, reaso if reason != 1: info.append("errno=%d" % reason) if info: - print "%s:%d(%s)" % (filename, line, func), " ".join(info) + print("%s:%d(%s)" % (filename, line, func), " ".join(info)) class OneLogin_Saml2_Utils(object): diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index d0c73e86..9fa38952 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -655,6 +655,9 @@ def testCalculateX509Fingerprint(self): self.assertEqual('3db29251b97559c67988ea0754cb0573fc409b6f75d89282d57cfb75089539b0bbdb2dcd9ec6e032549ecbc466439d5992e18db2cf5494ca2fe1b2e16f348dff', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert, 'sha512')) + def dscb(self): + return self.session_clear() + def testDeleteLocalSession(self): """ Tests the delete_local_session method of the OneLogin_Saml2_Utils @@ -665,8 +668,7 @@ def testDeleteLocalSession(self): OneLogin_Saml2_Utils.delete_local_session() self.assertEqual(1, local_session_test) - dscb = lambda: self.session_clear() - OneLogin_Saml2_Utils.delete_local_session(dscb) + OneLogin_Saml2_Utils.delete_local_session(self.dscb) self.assertEqual(0, local_session_test) def session_clear(self): From 44a92d35a8027b7e396d4f3f5257beeefeffce0e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 29 Jan 2019 17:36:41 +0100 Subject: [PATCH 261/352] Fix #239 .Check that the response has all of the AuthnContexts that we provided --- src/onelogin/saml2/auth.py | 2 + src/onelogin/saml2/errors.py | 1 + src/onelogin/saml2/response.py | 22 +++++++++++ src/onelogin/saml2/settings.py | 1 + .../src/OneLogin/saml2_tests/response_test.py | 38 +++++++++++++++++++ 5 files changed, 64 insertions(+) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index eb61705e..fa5cb243 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -62,6 +62,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): self.__last_message_id = None self.__last_assertion_id = None self.__last_assertion_not_on_or_after = None + self.__last_authn_contexts = [] self.__last_request = None self.__last_response = None @@ -107,6 +108,7 @@ def process_response(self, request_id=None): self.__session_expiration = response.get_session_not_on_or_after() self.__last_message_id = response.get_id() self.__last_assertion_id = response.get_assertion_id() + self.__last_authn_contexts = response.get_authn_contexts() self.__last_assertion_not_on_or_after = response.get_assertion_not_on_or_after() self.__authenticated = True diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index ec879bfd..560ec8e6 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -112,6 +112,7 @@ class OneLogin_Saml2_ValidationError(Exception): INVALID_SIGNATURE = 42 WRONG_NUMBER_OF_SIGNATURES = 43 RESPONSE_EXPIRED = 44 + AUTHN_CONTEXT_MISMATCH = 45 def __init__(self, message, code=0, errors=None): """ diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 798f51bf..0ce801ea 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -178,6 +178,19 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_AUTHSTATEMENTS ) + # Checks that the response has all of the AuthnContexts that we provided in the request. + # Only check if failOnAuthnContextMismatch is true and requestedAuthnContext is set to a list. + requested_authn_contexts = security.get('requestedAuthnContext', True) + + if security.get('failOnAuthnContextMismatch', False) and requested_authn_contexts and requested_authn_contexts is not True: + authn_contexts = self.get_authn_contexts() + unmatched_contexts = set(requested_authn_contexts).difference(authn_contexts) + if unmatched_contexts: + raise OneLogin_Saml2_ValidationError( + 'The AuthnContext "%s" didn\'t include requested context "%s"' % (', '.join(authn_contexts), ', '.join(unmatched_contexts)), + OneLogin_Saml2_ValidationError.AUTHN_CONTEXT_MISMATCH + ) + # 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: @@ -383,6 +396,15 @@ def get_audiences(self): audience_nodes = self.__query_assertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience') return [OneLogin_Saml2_Utils.element_text(node) for node in audience_nodes if OneLogin_Saml2_Utils.element_text(node) is not None] + def get_authn_contexts(self): + """ + Gets the authentication contexts + :returns: The authentication classes for the SAML Response + :rtype: list + """ + authn_context_nodes = self.__query_assertion('/saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef') + return [OneLogin_Saml2_Utils.element_text(node) for node in authn_context_nodes] + def get_issuers(self): """ Gets the issuers (from message and from assertion) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index f3c53be1..c210c5b3 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -304,6 +304,7 @@ def __add_default_values(self): self.__sp.setdefault('privateKey', '') self.__security.setdefault('requestedAuthnContext', True) + self.__security.setdefault('failOnAuthnContextMismatch', False) def check_settings(self, settings): """ diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index d6daaf97..bc172546 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -980,6 +980,44 @@ def testIsInValidAudience(self): self.assertFalse(response_2.is_valid(request_data)) self.assertIn('is not a valid audience for this Response', response_2.get_error()) + def testIsInValidAuthenticationContext(self): + """ + Tests that requestedAuthnContext, when set, is compared against the + response AuthnContext, which is what you use for two-factor + authentication. Without this check you can get back a valid response + that didn't complete the two-factor step. + """ + request_data = self.get_request_data() + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + two_factor_context = 'urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken' + password_context = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password' + settings_dict = self.loadSettingsJSON() + settings_dict['security']['requestedAuthnContext'] = [two_factor_context] + settings_dict['security']['failOnAuthnContextMismatch'] = True + settings_dict['strict'] = True + settings = OneLogin_Saml2_Settings(settings_dict) + + # check that we catch when the contexts don't match + response = OneLogin_Saml2_Response(settings, message) + self.assertFalse(response.is_valid(request_data)) + self.assertIn('The AuthnContext "%s" didn\'t include requested context "%s"' % (password_context, two_factor_context), response.get_error()) + + # now drop in the expected AuthnContextClassRef and see that it passes + original_message = b64decode(message) + two_factor_message = original_message.replace(password_context, two_factor_context) + two_factor_message = b64encode(two_factor_message) + response = OneLogin_Saml2_Response(settings, two_factor_message) + response.is_valid(request_data) + # check that we got as far as destination validation, which comes later + self.assertIn('The response was received at', response.get_error()) + + # with the default setting, check that we succeed with our original context + settings_dict['security']['requestedAuthnContext'] = True + settings = OneLogin_Saml2_Settings(settings_dict) + response = OneLogin_Saml2_Response(settings, message) + response.is_valid(request_data) + self.assertIn('The response was received at', response.get_error()) + def testIsInValidIssuer(self): """ Tests the is_valid method of the OneLogin_Saml2_Response class From f7d7034b92bb57cef4bbcd7858c7c78b13dbc862 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 29 Jan 2019 17:37:45 +0100 Subject: [PATCH 262/352] Release 2.5.0 --- README.md | 5 +++++ changelog.md | 10 ++++++++++ setup.py | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 64d01d1d..971eeea1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml). #### Warning #### +Update python-saml to 2.5.0, this version includes security improvements for preventing XEE and Xpath Injections. + Update python-saml to 2.4.0, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability. This version also changes how the calculate fingerprint method works, and will expect as input a formatted x509 certificate @@ -436,6 +438,9 @@ In addition to the required settings data (idp, sp), extra settings can be defin // Allows the authn comparison parameter to be set, defaults to 'exact' if the setting is not present. "requestedAuthnContextComparison": "exact", + // Set to true to check that the AuthnContext received matches the one requested. + "failOnAuthnContextMismatch": false, + // In some environment you will need to set how long the published metadata of the Service Provider gonna be valid. // is possible to not set the 2 following parameters (or set to null) and default values will be set (2 days, 1 week) // Provide the desired Timestamp, for example 2015-06-26T20:00:00Z diff --git a/changelog.md b/changelog.md index 996779c1..91de981e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,14 @@ # python-saml changelog +### 2.5.0 (Jan 29, 2019) +* Security improvements. Use of tagid to prevent XPath injection. Disable DTD on fromstring defusedxml method +* [#239](https://github.com/onelogin/python-saml/issues/239) Check that the response has all of the AuthnContexts that we provided +* Fixed a ValidationError misspelling +* Don't require compression on LogoutResponse messages by relaxing the decode_base64_and_inflate method +* Add expected/received in WRONG_ISSUER error +* If debug enable, print reason for the SAMLResponse invalidation +* [#238](https://github.com/onelogin/python-saml/issues/238) Fix DSA constant +* Start using flake8 for code quality + ### 2.4.2 (Sep 05, 2018) * Update dm.xmlsec.binding dependency to 1.3.7 * Update pylint dependency to 1.9.1 diff --git a/setup.py b/setup.py index c3f16089..be6c5d69 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.4.2', + version='2.5.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', From 4f7add5b2c1491038048eed529b949159d1db7f4 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 30 Jan 2019 16:58:34 +0100 Subject: [PATCH 263/352] Fix bug on friendlyName/nameFormat parameters on RequestedAttribute elements. Wrong variable name caused FriendlyName to overwrite NameFormat --- src/onelogin/saml2/metadata.py | 2 +- tests/src/OneLogin/saml2_tests/metadata_test.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 9a931c9e..2260bb8b 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -91,7 +91,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N if 'nameFormat' in req_attribs.keys() and req_attribs['nameFormat']: req_attr_nameformat_str = " NameFormat=\"%s\"" % req_attribs['nameFormat'] if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']: - req_attr_nameformat_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName'] + req_attr_friendlyname_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName'] if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']: req_attr_isrequired_str = " isRequired=\"%s\"" % 'true' if req_attribs['isRequired'] else 'false' diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index a63a552e..60453026 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -158,11 +158,11 @@ def testBuilderAttributeConsumingService(self): self.assertIn(""" Test Service Test Service - - - - - + + + + + """, metadata) def testBuilderAttributeConsumingServiceWithMultipleAttributeValue(self): @@ -184,7 +184,7 @@ def testBuilderAttributeConsumingServiceWithMultipleAttributeValue(self): userType admin - + """, metadata) def testSignMetadata(self): From cd97e1f40bb7a04abebb2e846fff0cc53d144461 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 31 Jan 2019 12:27:33 +0100 Subject: [PATCH 264/352] Add get_last_authn_contexts method --- README.md | 1 + src/onelogin/saml2/auth.py | 7 +++++++ tests/src/OneLogin/saml2_tests/auth_test.py | 13 +++++++++++++ 3 files changed, 21 insertions(+) diff --git a/README.md b/README.md index 971eeea1..ae669920 100644 --- a/README.md +++ b/README.md @@ -940,6 +940,7 @@ Main class of OneLogin Python Toolkit * ***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). +* ***get_last_authn_contexts*** Returns the list of authentication contexts sent in the last SAML Response. * ***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 fa5cb243..722c1c22 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -202,6 +202,13 @@ def redirect_to(self, url=None, parameters={}): url = self.__request_data['get_data']['RelayState'] return OneLogin_Saml2_Utils.redirect(url, parameters, request_data=self.__request_data) + def get_last_authn_contexts(self): + """ + :returns: The list of authentication contexts sent in the last SAML Response. + :rtype: list + """ + return self.__last_authn_contexts + def is_authenticated(self): """ Checks if the user is authenticated or not. diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 2b946553..3c36d5a4 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -1053,6 +1053,19 @@ def testGetLastAuthnRequest(self): ) self.assertIn(expectedFragment, auth.get_last_request_xml()) + def testGetLastAuthnContexts(self): + settings = self.loadSettingsJSON() + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + del request_data['get_data'] + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + + auth.process_response() + self.assertEqual(auth.get_last_authn_contexts(), ['urn:oasis:names:tc:SAML:2.0:ac:classes:Password']) + def testGetLastLogoutRequest(self): settings = self.loadSettingsJSON() auth = OneLogin_Saml2_Auth({'http_host': 'localhost', 'script_name': 'thing'}, old_settings=settings) From c789f1547647ce3fc289b1291e342b54104d6982 Mon Sep 17 00:00:00 2001 From: Victor Mireyev Date: Tue, 26 Feb 2019 23:33:07 +0300 Subject: [PATCH 265/352] Fix typo in docstring. --- src/onelogin/saml2/authn_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index d0983703..009c7233 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -27,7 +27,7 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol Constructs the AuthnRequest object. :param settings: OSetting data - :type return_to: OneLogin_Saml2_Settings + :type settings: OneLogin_Saml2_Settings :param force_authn: Optional argument. When true the AuthNRequest will set the ForceAuthn='true'. :type force_authn: bool From 8a98992dd016d256a99aae0eee9e608dbc2d0835 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 2 Apr 2019 13:24:39 +0200 Subject: [PATCH 266/352] Add support for Subjects on AuthNRequests by the new name_id_value_req parameeter.Fix testshib test. Improve README: Added inline markup to important references --- README.md | 299 +++++++++--------- src/onelogin/saml2/auth.py | 7 +- src/onelogin/saml2/authn_request.py | 16 +- tests/data/metadata/testshib-providers.xml | 39 +-- tests/src/OneLogin/saml2_tests/auth_test.py | 47 ++- .../saml2_tests/authn_request_test.py | 32 ++ 6 files changed, 262 insertions(+), 178 deletions(-) diff --git a/README.md b/README.md index ae669920..3f70a5b1 100644 --- a/README.md +++ b/README.md @@ -14,17 +14,17 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml). #### Warning #### -Update python-saml to 2.5.0, this version includes security improvements for preventing XEE and Xpath Injections. +Update ``python-saml`` to ``2.5.0``, this version includes security improvements for preventing XEE and Xpath Injections. -Update python-saml to 2.4.0, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability. +Update ``python-saml`` to ``2.4.0``, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability. -This version also changes how the calculate fingerprint method works, and will expect as input a formatted x509 certificate +This version also changes how the calculate fingerprint method works, and will expect as input a formatted X.509 certificate -Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recently, by the sanitized call provided by defusedxml +Update ``python-saml`` to ``2.2.3``, this version replaces some etree.tostring calls, that were introduced recently, by the sanitized call provided by ``defusedxml`` -Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json) +Update ``python-saml`` to ``2.2.0``, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json) -python-saml < v2.2.0 is vulnerable and allows signature wrapping! +``python-saml`` < ``v2.2.0`` is vulnerable and allows signature wrapping! #### Security Guidelines #### @@ -53,23 +53,23 @@ since 2002, but lately it is becoming popular due its advantages: * **Opportunity** - B2B cloud vendor should support SAML to facilitate the integration of their product. -General description +General Description ------------------- OneLogin's SAML Python toolkit lets you turn your Python application into a SP (Service Provider) that can be connected to an IdP (Identity Provider). -Supports: +**Supports:** * SSO and SLO (SP-Initiated and IdP-Initiated). * Assertion and nameId encryption. * Assertion signatures. - * Message signatures: AuthNRequest, LogoutRequest, LogoutResponses. + * Message signatures: ``AuthNRequest``, ``LogoutRequest``, ``LogoutResponses``. * Enable an Assertion Consumer Service endpoint. * Enable a Single Logout Service endpoint. * Publish the SP metadata (which can be signed). -Key features: +**Key features:** * **saml2int** - Implements the SAML 2.0 Web Browser SSO Profile. * **Session-less** - Forget those common conflicts between the SP and @@ -77,7 +77,7 @@ Key features: * **Easy to use** - Programmer will be allowed to code high-level and low-level programming, 2 easy to use APIs are available. * **Tested** - Thoroughly tested. - * **Popular** - OneLogin's customers use it. Add easy support to your django/flask/bottle web projects. + * **Popular** - OneLogin's customers use it. Add easy support to your Django/Flask/Bottle/Pyramid web projects. Installation @@ -106,18 +106,18 @@ $ brew install libxmlsec1 ### Code ### -#### Option 1. Download from github #### +#### Option 1. Download from Github #### -The toolkit is hosted on github. You can download it from: +The toolkit is hosted on Github. You can download it from: * Lastest release: https://github.com/onelogin/python-saml/releases/latest * Master repo: https://github.com/onelogin/python-saml/tree/master -Copy the core of the library (src/onelogin/saml2 folder) and merge the setup.py inside the python application. (each application has its structure so take your time to locate the Python SAML toolkit in the best place). +Copy the core of the library ``(src/onelogin/saml2 folder)`` and merge the setup.py inside the Python application. (Each application has its structure so take your time to locate the Python SAML toolkit in the best place). #### Option 2. Download from pypi #### -The toolkit is hosted in pypi, you can find the python-saml package at https://pypi.python.org/pypi/python-saml +The toolkit is hosted in pypi, you can find the ``python-saml`` package at https://pypi.python.org/pypi/python-saml You can install it executing: ``` @@ -127,7 +127,7 @@ $ pip install python-saml If you want to know how a project can handle python packages review this [guide](https://packaging.python.org/en/latest/tutorial.html) and review this [sampleproject](https://github.com/pypa/sampleproject) -Security warning +Security Warning ---------------- In production, the **strict** parameter MUST be set as **"true"**. Otherwise @@ -136,12 +136,12 @@ your environment is not secure and will be exposed to attacks. In production also we highly recommend to register on the settings the IdP certificate instead of using the fingerprint method. The fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass. Other SAML toolkits deprecated that mechanism, we maintain it for compatibility and also to be used on test environment. -Getting started +Getting Started --------------- ### Knowing the toolkit ### -The new OneLogin SAML Toolkit contains different folders (certs, lib, demo-django, demo-flask, demo-bottle and tests) and some files. +The new OneLogin SAML Toolkit contains different folders (``cert``, ``lib``, ``demo-django``, ``demo-flask``, ``demo-bottle`` and ``tests``) and some files. Let's start describing them: @@ -152,23 +152,23 @@ the classes and methods that are described in a later section. #### demo-django #### -This folder contains a Django project that will be used as demo to show how to add SAML support to the Django Framework. 'demo' is the main folder of the django project (with its settings.py, views.py, urls.py), 'templates' is the django templates of the project and 'saml' is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). +This folder contains a Django project that will be used as demo to show how to add SAML support to the Django Framework. **demo** is the main folder of the Django project (with its ``settings.py``, ``views.py``, ``urls.py``), **templates** is the Django templates of the project and **saml** is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the SAML toolkit settings (``settings.json`` and ``advanced_settings.json``). *** Notice about certs *** -SAML requires a x.509 cert to sign and encrypt elements like NameID, Message, Assertion, Metadata. +SAML requires a x.509 cert to sign and encrypt elements like ``NameID``, ``Message``, ``Assertion``, ``Metadata``. -If our environment requires sign or encrypt support, the certs folder may contain the x509 cert and the private key that the SP will use: +If our environment requires sign or encrypt support, the certs folder may contain the X.509 cert and the private key that the SP will use: * sp.crt The public cert of the SP * sp.key The private key of the SP -Or also we can provide those data in the setting file at the 'x509cert' and the privateKey' json parameters of the 'sp' element. +Or also we can provide those data in the setting file at the 'x509cert' and the privateKey' JSON parameters of the ``sp`` element. -Sometimes we could need a signature on the metadata published by the SP, in this case we could use the x.509 cert previously mentioned or use a new x.509 cert: metadata.crt and metadata.key. +Sometimes we could need a signature on the metadata published by the SP, in this case we could use the x.509 cert previously mentioned or use a new x.509 cert: ``metadata.crt`` and ``metadata.key``. -Use `sp_new.crt` if you are in a key rollover process and you want to -publish that x509certificate on Service Provider metadata. +Use ``sp_new.crt`` if you are in a key rollover process and you want to +publish that X.509 certificate on Service Provider metadata. If you want to create self-signed certs, you can do it at the https://www.samltool.com/self_signed_certs.php service, or using the command: @@ -178,17 +178,17 @@ openssl req -new -x509 -days 3652 -nodes -out sp.crt -keyout saml.key #### demo-bottle #### -This folder contains a Bottle project that will be used as demo to show how to add SAML support to the Bottle Framework. index.py contains all the logic of the demo project, 'templates' is the Bottle templates of the project and 'saml' is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). +This folder contains a Bottle project that will be used as demo to show how to add SAML support to the Bottle Framework. ``index.py`` contains all the logic of the demo project, **templates** is the Bottle templates of the project and **saml** is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the SAML toolkit settings (``settings.json`` and ``advanced_settings.json``). #### demo-flask #### -This folder contains a Flask project that will be used as demo to show how to add SAML support to the Flask Framework. 'index.py' is the main flask file that has all the code, this file uses the templates stored at the 'templates' folder. In the 'saml' folder we found the 'certs' folder to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). +This folder contains a Flask project that will be used as demo to show how to add SAML support to the Flask Framework. ``index.py`` is the main Flask file that has all the code, this file uses the templates stored at the 'templates' folder. In the 'saml' folder we found the 'certs' folder to store the X.509 public and private key, and the SAML toolkit settings (``settings.json`` and ``advanced_settings.json``). #### demo_pyramid #### -This folder contains a Pyramid project that will be used as demo to show how to add SAML support to the [Pyramid Web Framework](http://docs.pylonsproject.org/projects/pyramid/en/latest/). '\_\_init__.py' is the main file that configures the app and its routes, 'views.py' is where all the logic and SAML handling takes place, and the templates are stored in the 'templates' folder. The 'saml' folder is the same as in the other two demos. +This folder contains a Pyramid project that will be used as demo to show how to add SAML support to the [Pyramid Web Framework](http://docs.pylonsproject.org/projects/pyramid/en/latest/). ``\_\_init__.py`` is the main file that configures the app and its routes, ``views.py`` is where all the logic and SAML handling takes place, and the templates are stored in the **templates** folder. The **saml** folder is the same as in the other two demos. #### setup.py #### @@ -200,7 +200,7 @@ Read more at https://pythonhosted.org/an_example_pypi_project/setuptools.html Contains the unit test of the toolkit. -In order to execute the test you need to load the virtualenv with the toolkit installed on it and execute: +In order to execute the test you need to load the ``virtualenv`` with the toolkit installed on it and execute: ``` pip install -e ".[test]" ``` @@ -215,9 +215,9 @@ The previous line will run the tests for the whole toolkit. You can also run the python setup.py test --test-suite tests.src.OneLogin.saml2_tests.auth_test.OneLogin_Saml2_Auth_Test ``` -With the --test-suite parameter you can specify the module to test. You'll find all the module available and their class names at tests/src/OneLogin/saml2_tests/ +With the ``--test-suite`` parameter you can specify the module to test. You'll find all the module available and their class names at ``tests/src/OneLogin/saml2_tests/`` -### How it works ### +### How it Works ### #### Settings #### @@ -225,13 +225,13 @@ First of all we need to configure the toolkit. The SP's info, the IdP's info, an There are two ways to provide the settings information: -* Use a settings.json file that we should locate in any folder, but indicates its path with the 'custom_base_path' parameter. +* Use a ``settings.json`` file that we should locate in any folder, but indicates its path with the ``custom_base_path`` parameter. -* Use a json object with the setting data and provide it directly to the constructor of the class (if your toolkit integation requires certs, remember to provide the 'custom_base_path' as part of the settings or as a parameter in the constructor. +* Use a JSON object with the setting data and provide it directly to the constructor of the class (if your toolkit integation requires certs, remember to provide the ``custom_base_path`` as part of the settings or as a parameter in the constructor. -In the demo-django, demo-flask and demo-bottle folders you will find a 'saml' folder, inside there is a 'certs' folder and a settings.json and a advanced_settings.json files. Those files contain the settings for the saml toolkit. Copy them in your project and set the correct values. +In the ``demo-django``, ``demo-flask``, ``demo-pyramid`` and ``demo-bottle`` folders you will find a ``saml`` folder, inside there is a ``certs`` folder and a ``settings.json`` and a ``advanced_settings.json`` files. Those files contain the settings for the SAML toolkit. Copy them in your project and set the correct values. -This is the settings.json file: +This is the ``settings.json`` file: ```javascript { @@ -288,15 +288,15 @@ This is the settings.json file: // represent the requested subject. // Take a look on src/onelogin/saml2/constants.py to see the NameIdFormat that are supported. "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", - // Usually x509cert and privateKey of the SP are provided by files placed at + // Usually X.509 cert and privateKey of the SP are provided by files placed at // the certs folder. But we can also provide them with the following parameters "x509cert": "", "privateKey": "" /* * Key rollover - * If you plan to update the SP x509cert and privateKey - * you can define here the new x509cert and it will be + * If you plan to update the SP X.509 cert and privateKey + * you can define here the new X.509 cert and it will be * published on the SP metadata so Identity Providers can * read them and get ready for rollover. */ @@ -326,11 +326,11 @@ This is the settings.json file: // only for this endpoint. "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - // Public x509 certificate of the IdP + // Public X.509 certificate of the IdP "x509cert": "" /* - * Instead of using the whole x509cert you can use a fingerprint in order to - * validate a SAMLResponse (but you still need the x509cert to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding). + * Instead of using the whole X.509 cert you can use a fingerprint in order to + * validate a SAMLResponse (but you still need the X.509 cert to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding). * But take in mind that the fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass, * that why we don't recommend it use for production environments. * @@ -342,7 +342,7 @@ This is the settings.json file: * 'sha1' is the default value. * * Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you - * will need to provide the whole x509cert. + * will need to provide the whole X.509 cert. * */ // 'certFingerprint': '', @@ -367,7 +367,7 @@ This is the settings.json file: } ``` -In addition to the required settings data (idp, sp), extra settings can be defined in `advanced_settings.json`: +In addition to the required settings data (idp, sp), extra settings can be defined in ``advanced_settings.json``: ```javascript { @@ -489,11 +489,11 @@ In addition to the required settings data (idp, sp), extra settings can be defin } ``` -In the security section, you can set the way that the SP will handle the messages and assertions. Contact the admin of the IdP and ask them what the IdP expects, and decide what validations will handle the SP and what requirements the SP will have and communicate them to the IdP's admin too. +In the ``security`` section, you can set the way that the SP will handle the messages and assertions. Contact the admin of the IdP and ask them what the IdP expects, and decide what validations will handle the SP and what requirements the SP will have and communicate them to the IdP's admin too. Once we know what kind of data could be configured, let's talk about the way settings are handled within the toolkit. -The settings files described (settings.json and advanced_settings.json) are loaded by the toolkit if not other dict with settings info is provided in the constructors of the toolkit. Let's see some examples. +The settings files described (``settings.json`` and ``advanced_settings.json``) are loaded by the toolkit if not other dict with settings info is provided in the constructors of the toolkit. Let's see some examples. ```python # Initializes toolkit with settings.json & advanced_settings.json files. @@ -513,7 +513,7 @@ auth = OneLogin_Saml2_Auth(req, settings_data) settings = OneLogin_Saml2_Settings(settings_data) ``` -You can declare the settings_data in the file that constains the constructor execution or locate them in any file and load the file in order to get the dict available as we see in the following example: +You can declare the ``settings_data`` in the file that constains the constructor execution or locate them in any file and load the file in order to get the dict available as we see in the following example: ```python filename = "/var/www/django-project/custom_settings.json" # The custom_settings.json contains a @@ -536,9 +536,10 @@ Using ````parse_remote```` IdP metadata can be obtained and added to the setting idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://example.com/auth/saml2/idp/metadata') `` -If the Metadata contains several entities, the relevant EntityDescriptor can be specified when retrieving the settings from the IdpMetadataParser by its Entity Id value: - +If the Metadata contains several entities, the relevant ``EntityDescriptor`` can be specified when retrieving the settings from the ``IdpMetadataParser`` by its ``EntityId`` value: +``` idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(https://example.com/metadatas, entity_id='idp_entity_id') +``` #### How load the library #### @@ -554,7 +555,7 @@ from onelogin.saml2.utils import OneLogin_Saml2_Utils #### The Request #### -Building an OneLogin_Saml2_Auth object requires a 'request' parameter. +Building an ``OneLogin_Saml2_Auth object`` requires a ``request`` parameter. ```python auth = OneLogin_Saml2_Auth(req) @@ -573,7 +574,7 @@ req = { } ``` -Each python framework built its own request object, you may map its data to match what the saml toolkit expects. +Each Python framework built its own ``request`` object, you may map its data to match what the SAML toolkit expects. Let`s see some examples: ```python @@ -596,7 +597,7 @@ def prepare_from_flask_request(request): 'post_data': request.form.copy() } ``` -The https dictionary entry should be set to on for https requests and off for http +The ``https`` dictionary entry should be set to ``on`` for https requests and ``off`` for http #### Initiate SSO #### @@ -612,21 +613,22 @@ auth = OneLogin_Saml2_Auth(req) # Constructor of the SP, loads settings.json auth.login() # Method that builds and sends the AuthNRequest ``` -The AuthNRequest will be sent signed or unsigned based on the security info of the advanced_settings.json ('authnRequestsSigned'). +The ``AuthNRequest`` will be sent signed or unsigned based on the security info of the ``advanced_settings.json`` (``authnRequestsSigned``). -The IdP will then return the SAML Response to the user's client. The client is then forwarded to the Attribute Consumer Service of the SP with this information. +The IdP will then return the SAML Response to the user's client. The client is then forwarded to the **Attribute Consumer Service (ACS)** of the SP with this information. -We can set a 'return_to' url parameter to the login function and that will be converted as a 'RelayState' parameter: +We can set a ``return_to`` url parameter to the login function and that will be converted as a ``RelayState`` parameter: ```python target_url = 'https://example.com' auth.login(return_to=target_url) ``` -The login method can recieve 3 more optional parameters: +The login method can recieve 4 more optional parameters: -* force_authn When true the AuthNReuqest will set the ForceAuthn='true' -* is_passive When true the AuthNReuqest will set the Ispassive='true' -* set_nameid_policy When true the AuthNReuqest will set a nameIdPolicy element. +* ``force_authn`` When ``true`` the ``AuthNReuqest`` will set the ``ForceAuthn='true'`` +* ``is_passive`` When ``true`` the ``AuthNReuqest`` will set the ``Ispassive='true'`` +* ``set_nameid_policy`` When ``true`` the ``AuthNReuqest`` will set a ``nameIdPolicy`` element. +* ``name_id_value_req`` Indicates to the IdP the ``Subject`` that should be authenticated 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 @@ -655,17 +657,17 @@ else: print "Error found on Metadata: %s" % (', '.join(errors)) ``` -The get_sp_metadata will return the metadata signed or not based on the security info of the advanced_settings.json ('signMetadata'). +The ``get_sp_metadata`` will return the metadata signed or not based on the security info of the ``advanced_settings.json`` (``signMetadata``). Before the XML metadata is exposed, a check takes place to ensure that the info to be provided is valid. -Instead of using the Auth object, you can directly use +Instead of using the ``Auth`` object, you can directly use ``` saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp_validation_only=True) ``` -to get the settings object and with the sp_validation_only=True parameter we will avoid the IdP Settings validation. +to get the settings object and with the ``sp_validation_only=True`` parameter we will avoid the IdP settings validation. -*** Attribute Consumer Service(ACS) *** +*** Attribute Consumer Service (ACS) *** This code handles the SAML response that the IdP forwards to the SP through the user's client. @@ -693,10 +695,10 @@ The SAML response is processed and then checked that there are no errors. It als At that point there are 2 possible alternatives: -* If no RelayState is provided, we could show the user data in this view or however we wanted. -* If RelayState is provided, a rediretion take place. +* If no ``RelayState`` is provided, we could show the user data in this view or however we wanted. +* If ``RelayState`` is provided, a rediretion take place. -Notice that we saved the user data in the session before the redirection to have the user data available at the RelayState view. +Notice that we saved the user data in the session before the redirection to have the user data available at the ``RelayState`` view. In order to retrieve attributes we use: @@ -728,7 +730,7 @@ print attributes['cn'] print auth.get_attribute('cn') ``` -Before trying to get an attribute, check that the user is authenticated. If the user isn't authenticated, an empty dict will be returned. For example, if we call to get_attributes before a auth.process_response, the get_attributes() will return an empty dict. +Before trying to get an attribute, check that the user is authenticated. If the user isn't authenticated, an empty dict will be returned. For example, if we call to ``auth.get_attributes`` before a ``auth.process_response``, the ``auth.get_attributes`` will return an empty dict. *** Single Logout Service (SLS) *** @@ -789,7 +791,7 @@ else: return self.redirect_to(self.get_slo_url(), parameters) ``` -If we don't want that process_slo to destroy the session, pass a true parameter to the process_slo method +If we don't want that ``process_slo`` to destroy the session, pass a ``true`` parameter to the ``process_slo`` method ```python keepLocalSession = true @@ -809,11 +811,11 @@ auth = OneLogin_Saml2_Auth(req) # Constructor of the SP, loads settings.json auth.logout() # Method that builds and sends the LogoutRequest ``` -The Logout Request will be sent signed or unsigned based on the security info of the advanced_settings.json ('logoutRequestSigned'). +The Logout Request will be sent signed or unsigned based on the security info of the ``advanced_settings.json`` (``logoutRequestSigned``). -The IdP will return the Logout Response through the user's client to the Single Logout Service of the SP. +The IdP will return the Logout Response through the user's client to the Single Logout Service (SLS) of the SP. -We can set a 'return_to' url parameter to the logout function and that will be converted as a 'RelayState' parameter: +We can set a ``return_to`` url parameter to the logout function and that will be converted as a ``RelayState`` parameter: ```python target_url = 'https://example.com' @@ -822,17 +824,17 @@ auth.logout(return_to=target_url) Also there are 4 optional parameters that can be set: -* name_id. That will be used to build the LogoutRequest. If not name_id parameter is set and the auth object processed a +* ``name_id``. That will be used to build the LogoutRequest. If not ``name_id`` parameter is set and the auth object processed a SAML Response with a NameId, then this NameId will be used. -* session_index. SessionIndex that identifies the session of the user. -* nq. IDP Name Qualifier -* name_id_format. The NameID Format that will be set in the LogoutRequest +* ``session_index``. SessionIndex that identifies the session of the user. +* ``nq``. IDP Name Qualifier +* ``name_id_format``. The NameID Format that will be set in the LogoutRequest If no name_id is provided, the LogoutRequest will contain a NameID with the entity Format. If name_id is provided and no name_id_format is provided, the NameIDFormat of the settings will be used. If nq is provided, the SPNameQualifier will be also attached to the NameId. -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 +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: ```python auth.get_last_request_id() @@ -890,7 +892,7 @@ else: ### SP Key rollover ### -If you plan to update the SP x509cert and privateKey you can define the new x509cert as settings['sp']['x509certNew'] and it will be +If you plan to update the SP X.509 cert and privateKey you can define the new X.509 cert as ``settings['sp']['x509certNew']`` and it will be published on the SP metadata so Identity Providers can read them and get ready for rollover. @@ -899,20 +901,20 @@ published on the SP metadata so Identity Providers can read them and get ready f In some scenarios the IdP uses different certificates for signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata. -In order to handle that the toolkit offers the settings['idp']['x509certMulti'] parameter. +In order to handle that the toolkit offers the ``settings['idp']['x509certMulti']`` parameter. -When that parameter is used, 'x509cert' and 'certFingerprint' values will be ignored by the toolkit. +When that parameter is used, ``x509cert`` and ``certFingerprint`` values will be ignored by the toolkit. -The 'x509certMulti' is an array with 2 keys: -- 'signing'. An array of certs that will be used to validate IdP signature -- 'encryption' An array with one unique cert that will be used to encrypt data to be sent to the IdP +The ``x509certMulti`` is an array with 2 keys: +- ``signing``. An array of certs that will be used to validate IdP signature +- ``encryption`` An array with one unique cert that will be used to encrypt data to be sent to the IdP ### Replay attacks ### In order to avoid reply attacks, you can store the ID of the SAML messages already processed, to avoid processing them twice. Since the Messages expires and will be invalidated due that fact, you don't need to store those IDs longer than the time frame that you currently accepting. - Get the ID of the last processed message/assertion with the get_last_message_id/get_last_assertion_id method of the Auth object. + Get the ID of the last processed message/assertion with the ``get_last_message_id``/``get_last_assertion_id method`` of the ``Auth`` object. ### Main classes and methods ### @@ -932,30 +934,30 @@ Main class of OneLogin Python Toolkit * ***is_authenticated*** Checks if the user is authenticated or not. * ***get_attributes*** Returns the set of SAML attributes. * ***get_attribute*** Returns the requested SAML attribute. -* ***get_nameid*** Returns the nameID. -* ***get_session_index*** Gets the SessionIndex from the AuthnStatement. -* ***get_session_expiration*** Gets the SessionNotOnOrAfter from the AuthnStatement. +* ***get_nameid*** Returns the ``nameID``. +* ***get_session_index*** Gets the ``SessionIndex`` from the ``AuthnStatement``. +* ***get_session_expiration*** Gets the ``SessionNotOnOrAfter`` from the ``AuthnStatement``. * ***get_errors*** Returns a list with code errors if something went wrong. * ***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). +* ***get_last_request_id*** The ``ID`` of the last Request SAML message generated (``AuthNRequest``, ``LogoutRequest``). * ***get_last_authn_contexts*** Returns the list of authentication contexts sent in the last SAML Response. * ***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. * ***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 had an encrypted assertion, decrypts it. -* ***get_last_message_id*** The ID of the last Response SAML message processed. -* ***get_last_assertion_id*** The ID of the last assertion processed. -* ***get_last_assertion_not_on_or_after*** The NotOnOrAfter value of the valid SubjectConfirmationData node (if any) of the last assertion processed (is only calculated with strict = true) +* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (``SAMLResponse``, ``LogoutResponse``). If the SAMLResponse had an encrypted assertion, decrypts it. +* ***get_last_message_id*** The ``ID`` of the last Response SAML message processed. +* ***get_last_assertion_id*** The ``ID`` of the last assertion processed. +* ***get_last_assertion_not_on_or_after*** The ``NotOnOrAfter`` value of the valid SubjectConfirmationData node (if any) of the last assertion processed (is only calculated with strict = true) #### OneLogin_Saml2_Auth - authn_request.py #### SAML 2 Authentication Request class -* `__init__` This class handles an AuthNRequest. It builds an AuthNRequest object. +* `__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. @@ -969,30 +971,30 @@ SAML 2 Authentication Response class * ***check_status*** Check if the status of the response is success or not * ***get_audiences*** Gets the audiences * ***get_issuers*** Gets the issuers (from message and from assertion) -* ***get_nameid_data*** Gets the NameID Data provided by the SAML Response from the IdP (returns a dict) +* ***get_nameid_data*** Gets the ``NameID`` Data provided by the SAML Response from the IdP (returns a dict) * ***get_nameid*** Gets the NameID provided by the SAML Response from the IdP (returns a string) * ***get_session_not_on_or_after*** Gets the SessionNotOnOrAfter from the AuthnStatement -* ***get_session_index*** Gets the SessionIndex from the AuthnStatement -* ***get_attributes*** Gets the Attributes from the AttributeStatement element. +* ***get_session_index*** Gets the ``SessionIndex`` from the ``AuthnStatement`` +* ***get_attributes*** Gets the ``Attributes`` from the ``AttributeStatement`` element. * ***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 +* ***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*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it). * ***get_id*** the ID of the response -* ***get_assertion_id*** the ID of the assertion in the response -* ***get_assertion_not_on_or_after*** the NotOnOrAfter value of the valid SubjectConfirmationData if any +* ***get_assertion_id*** the ``ID`` of the assertion in the response +* ***get_assertion_not_on_or_after*** the ``NotOnOrAfter`` value of the valid SubjectConfirmationData if any #### OneLogin_Saml2_LogoutRequest - logout_request.py #### SAML 2 Logout Request class * `__init__` Constructs the Logout Request object. -* ***get_request*** Returns the Logout Request defated, base64encoded. +* ***get_request*** Returns the Logout Request defated, base64-encoded. * ***get_id*** Returns the ID of the Logout Request. (If you have the object you can access to the id attribute) * ***get_nameid_data*** Gets the NameID Data of the the Logout Request (returns a dict). * ***get_nameid*** Gets the NameID of the Logout Request Message (returns a string). * ***get_issuer*** Gets the Issuer of the Logout Request Message. -* ***get_session_indexes*** Gets the SessionIndexes from the Logout Request. +* ***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 @@ -1027,11 +1029,11 @@ Configuration of the OneLogin Python Toolkit * ***get_lib_path*** Returns lib path. * ***get_ext_lib_path*** Returns external lib path. * ***get_schemas_path*** Returns schema path. -* ***check_sp_certs*** Checks if the x509 certs of the SP exists and are valid. -* ***get_sp_key*** Returns the x509 private key of the SP. -* ***get_sp_cert*** Returns the x509 public cert of the SP. -* ***get_sp_cert_new*** Returns the future x509 public cert of the SP. -* ***get_idp_cert*** Returns the x509 public cert of the IdP. +* ***check_sp_certs*** Checks if the X.509 certs of the SP exists and are valid. +* ***get_sp_key*** Returns the X.509 private key of the SP. +* ***get_sp_cert*** Returns the X.509 public cert of the SP. +* ***get_sp_cert_new*** Returns the future X.509 public cert of the SP. +* ***get_idp_cert*** Returns the X.509 public cert of the IdP. * ***get_sp_data*** Gets the SP data. * ***get_idp_data*** Gets the IdP data. * ***get_security_data*** Gets security data. @@ -1043,7 +1045,7 @@ Configuration of the OneLogin Python Toolkit * ***format_sp_cert_new*** Formats the SP cert new. * ***format_sp_key*** Formats the private key. * ***set_strict*** Activates or deactivates the strict mode. -* ***is_strict*** Returns if the 'strict' mode is active. +* ***is_strict*** Returns if the ``strict`` mode is active. * ***is_debug_active*** Returns if the debug is active. #### OneLogin_Saml2_Metadata - metadata.py #### @@ -1052,7 +1054,7 @@ A class that contains functionality related to the metadata of the SP * ***builder*** Generates the metadata of the SP based on the settings. * ***sign_metadata*** Signs the metadata with the key/cert provided. -* ***add_x509_key_descriptors*** Adds the x509 descriptors (sign/encriptation) to the metadata +* ***add_x509_key_descriptors*** Adds the X.509 descriptors (sign/encriptation) to the metadata #### OneLogin_Saml2_Utils - utils.py #### @@ -1061,7 +1063,7 @@ Auxiliary class that contains several methods * ***decode_base64_and_inflate*** Base64 decodes and then inflates according to RFC1951. * ***deflate_and_base64_encode*** Deflates and the base64 encodes a string. * ***validate_xml*** Validates a xml against a schema. -* ***format_cert*** Returns a x509 cert (adding header & footer if required). +* ***format_cert*** Returns a X.509 cert (adding header & footer if required). * ***format_private_key*** Returns a private key (adding header & footer if required). * ***redirect*** Executes a redirection to the provided url (or return the target url). * ***get_self_url_host*** Returns the protocol + the current host + the port (if different than common ports). @@ -1078,7 +1080,7 @@ Auxiliary class that contains several methods * ***get_expire_time*** Compares 2 dates and returns the earliest. * ***query*** Extracts nodes that match the query from the Element. * ***delete_local_session*** Deletes the local session. -* ***calculate_x509_fingerprint*** Calculates the fingerprint of a x509cert. +* ***calculate_x509_fingerprint*** Calculates the fingerprint of a X.509 cert. * ***format_finger_print*** Formates a fingerprint. * ***generate_name_id*** Generates a nameID. * ***get_status*** Gets Status from a Response. @@ -1099,7 +1101,7 @@ A class that contains methods to obtain and parse metadata from IdP * ***parse*** Parse the Identity Provider metadata and returns a dict with extracted data * ***merge_settings*** Will update the settings with the provided new settings data extracted from the IdP metadata -For more info, look at the source code; each method is documented and details about what does and how to use it are provided. Make sure to also check the doc folder where HTML documentation about the classes and methods is provided. +For more info, look at the source code. Each method is documented and details about what does and how to use it are provided. Make sure to also check the doc folder where HTML documentation about the classes and methods is provided. @@ -1107,7 +1109,7 @@ For more info, look at the source code; each method is documented and details ab Demos included in the toolkit ----------------------------- -The toolkit includes 2 demos to teach how use the toolkit (A django and a flask project), take a look on it. +The toolkit includes 4 demos to teach how use the toolkit (Django, Flask, Pyramid and Bootle projects), take a look on them. Demos require that SP and IdP are well configured before test it, so edit the settings files. Notice that each python framework has it own way to handle routes/urls and process request, so focus on @@ -1115,8 +1117,7 @@ how it deployed. New demos using other python frameworks are welcome as a contri ### Getting Started ### -We said that this toolkit includes a django application demo and a flask applicacion demo, -lets see how fast is deploy them. +We said that this toolkit includes a demos, lets see how fast is deploy some of them. *** Virtualenv *** @@ -1161,7 +1162,7 @@ Now, with the virtualenv loaded, you can run the demo like this: python index.py ``` -You'll have the demo running at http://localhost:8000 +You'll have the demo running at ``http://localhost:8000`` #### Content #### @@ -1170,42 +1171,42 @@ The flask project contains: * ***index.py*** Is the main flask file, where or the SAML handle take place. -* ***templates***. Is the folder where flask stores the templates of the project. It was implemented a base.html template that is extended by index.html and attrs.html, the templates of our simple demo that shows messages, user attributes when available and login and logout links. +* ***templates***. Is the folder where flask stores the templates of the project. It was implemented a base.html template that is extended by ``index.html`` and ``attrs.html``, the templates of our simple demo that shows messages, user attributes when available and login and logout links. -* ***saml*** Is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). +* ***saml*** Is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the saml toolkit settings (``settings.json`` and ``advanced_settings.json``). #### SP setup #### -The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-flask it used the first method. +The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the ``demo-flask``, it uses the first method. -In the index.py file we define the app.config['SAML_PATH'], that will target to the 'saml' folder. We require it in order to load the settings files. +In the index.py file we define the ``app.config['SAML_PATH']``, that will target to the ``saml`` folder. We require it in order to load the settings files. -First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. +First we need to edit the ``saml/settings.json``, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. #### IdP setup #### -Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP. +Once the SP is configured, the metadata of the SP is published at the ``/metadata`` url. Based on that info, configure the IdP. #### How it works #### -1. First time you access to the main view 'http://localhost:8000', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view). +1. First time you access to the main view ``http://localhost:8000``, you can select to login and return to the same view or login and be redirected to ``/?attrs`` (attrs view). 2. When you click: - 2.1 in the first link, we access to /?sso (index view). An AuthNRequest is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view. + 2.1 in the first link, we access to ``/?sso`` (index view). An ``AuthNRequest`` is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view. - 2.2 in the second link we access to /?attrs (attrs view), we will expetience have the same process described at 2.1 with the diference that as RelayState is set the attrs url. + 2.2 in the second link we access to ``/?attrs`` (attrs view), we will expetience have the same process described at 2.1 with the diference that as ``RelayState`` is set the attrs url. - 3. The SAML Response is processed in the ACS /?acs, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the RelayState view. a) / or b) /?attrs + 3. The ``SAMLResponse`` is processed in the ACS ``/?acs``, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the ``RelayState`` view. a) / or b) ``/?attrs`` 4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality. The single log out funcionality could be tested by 2 ways. - 5.1 SLO Initiated by SP. Click on the "logout" link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint /?sls of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP. + 5.1 SLO Initiated by SP. Click on the ``logout`` link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint ``/?sls`` of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP. - 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, /?sls). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP. + 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, ``/?sls``). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP. Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET paramters are used to know the action that must be done. @@ -1226,7 +1227,7 @@ Later, with the virtualenv loaded, you can run the demo like this: python manage.py runserver 0.0.0.0:8000 ``` -You'll have the demo running at http://localhost:8000. +You'll have the demo running at ``http://localhost:8000``. Note that many of the configuration files expect HTTPS. This is not required by the demo, as replacing these SP URLs with HTTP will work just fine. HTTPS is however highly encouraged, and left as an exercise for the reader for their specific needs. @@ -1236,33 +1237,33 @@ If you want to integrate a production django application, take a look on this SA The django project contains: -* ***manage.py***. A file that is automatically created in each Django project. Is a thin wrapper around django-admin.py that takes care of putting the project’s package on sys.path and sets the DJANGO_SETTINGS_MODULE environment variable. +* ***manage.py***. A file that is automatically created in each Django project. Is a thin wrapper around ``django-admin.py`` that takes care of putting the project’s package on ``sys.path`` and sets the ``DJANGO_SETTINGS_MODULE`` environment variable. -* ***saml*** Is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). +* ***saml*** Is a folder that contains the ``certs`` folder that could be used to store the X.509 public and private key, and the saml toolkit settings (``settings.json`` and ``advanced_settings.json``). * ***demo*** Is the main folder of the django project, that contains the typical files: * ***settings.py*** Contains the default parameters of a django project except the SAML_FOLDER parameter, that may contain the path where is located the 'saml' folder. - * ***urls.py*** A file that define url routes. In the demo we defined '/' that is related to the index view, '/attrs' that is related with the attrs view and '/metadata', related to th metadata view. + * ***urls.py*** A file that define url routes. In the demo we defined ``/`` that is related to the index view, ``/attrs`` that is related with the attrs view and ``/metadata``, related to th metadata view. * ***views.py*** This file contains the views of the django project and some aux methods. * ***wsgi.py*** A file that let as deploy django using WSGI, the Python standard for web servers and applications. -* ***templates***. Is the folder where django stores the templates of the project. It was implemented a base.html template that is extended by index.html and attrs.html, the templates of our simple demo that shows messages, user attributes when available and login and logout links. +* ***templates***. Is the folder where django stores the templates of the project. It was implemented a base.html template that is extended by ``index.html`` and ``attrs.html``, the templates of our simple demo that shows messages, user attributes when available and login and logout links. #### SP setup #### -The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-django it used the first method. +The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the ``demo-django``, it uses the first method. -After set the SAML_FOLDER in the demo/settings.py, the settings of the python toolkit will be loaded on the django web. +After set the ``SAML_FOLDER`` in the ``demo/settings.py``, the settings of the python toolkit will be loaded on the django web. -First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. +First we need to edit the ``saml/settings.json``, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. #### IdP setup #### -Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP. +Once the SP is configured, the metadata of the SP is published at the ``/metadata`` url. Based on that info, configure the IdP. #### How it works #### -This demo works very similar to the flask-demo (We did it intentionally). +This demo works very similar to the ``flask-demo`` (We did it intentionally). ### Demo Pyramid ### @@ -1279,7 +1280,7 @@ To run the demo you need to install Pyramid, the requirements, etc.: env/bin/pip install -e ".[testing]" ``` -Next, edit the settings in `demo_pyramid/saml/settings.json`. (Pyramid runs on +Next, edit the settings in ``demo_pyramid/saml/settings.json``. (Pyramid runs on port 6543 by default.) Now you can run the demo like this: @@ -1287,7 +1288,7 @@ Now you can run the demo like this: env/bin/pserve development.ini ``` -If that worked, the demo is now running at http://localhost:6543. +If that worked, the demo is now running at ``http://localhost:6543``. #### Content #### @@ -1298,41 +1299,41 @@ The Pyramid project contains: * ***views.py*** is where all the SAML handling takes place. -* ***templates*** is the folder where Pyramid stores the templates of the project. It was implemented a layout.jinja2 template that is extended by index.jinja2 and attrs.jinja2, the templates of our simple demo that shows messages, user attributes when available and login and logout links. +* ***templates*** is the folder where Pyramid stores the templates of the project. It was implemented a ``layout.jinja2`` template that is extended by ``index.jinja2`` and ``attrs.jinja2``, the templates of our simple demo that shows messages, user attributes when available and login and logout links. -* ***saml*** is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). +* ***saml*** is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the saml toolkit settings (``settings.json`` and ``advanced_settings.json``). #### SP setup #### -The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In demo_pyramid the first method is used. +The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In ``demo_pyramid`` the first method is used. -In the views.py file we define the SAML_PATH, which will target the 'saml' folder. We require it in order to load the settings files. +In the views.py file we define the ``SAML_PATH``, which will target the ``saml`` folder. We require it in order to load the settings files. -First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. +First we need to edit the ``saml/settings.json``, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the ``saml/advanced_settings.json`` files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. #### IdP setup #### -Once the SP is configured, the metadata of the SP is published at the /metadata/ url. Based on that info, configure the IdP. +Once the SP is configured, the metadata of the SP is published at the ``/metadata/`` url. Based on that info, configure the IdP. #### How it works #### -1. First time you access to the main view 'http://localhost:6543', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view). +1. First time you access to the main view ``http://localhost:6543``, you can select to login and return to the same view or login and be redirected to ``/?attrs`` (attrs view). 2. When you click: - 2.1 in the first link, we access to /?sso (index view). An AuthNRequest is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view. + 2.1 in the first link, we access to ``/?sso`` (index view). An ``AuthNRequest`` is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: ``/?acs``. Notice that a RelayState parameter is set to the url that initiated the process, the index view. - 2.2 in the second link we access to /?attrs (attrs view), we will expetience have the same process described at 2.1 with the diference that as RelayState is set the attrs url. + 2.2 in the second link we access to ``/?attrs`` (attrs view), we will expetience have the same process described at 2.1 with the diference that as ``RelayState`` is set the attrs url. - 3. The SAML Response is processed in the ACS /?acs, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the RelayState view. a) / or b) /?attrs + 3. The SAML Response is processed in the ACS ``/?acs``, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the ``RelayState`` view. a) / or b) ``/?attrs`` 4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality. The single log out funcionality could be tested by 2 ways. - 5.1 SLO Initiated by SP. Click on the "logout" link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint /?sls of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP. + 5.1 SLO Initiated by SP. Click on the ``logout`` link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint ``/?sls`` of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP. - 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, /?sls). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP. + 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, ``/?sls``). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP. Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET parameters are used to know the action that must be done. diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 722c1c22..d9b42a32 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -323,7 +323,7 @@ def get_last_assertion_id(self): """ return self.__last_assertion_id - def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True): + def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True, name_id_value_req=None): """ Initiates the SSO process. @@ -339,10 +339,13 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_ :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element. :type set_nameid_policy: bool + :param name_id_value_req: Optional argument. Indicates to the IdP the subject that should be authenticated + :type name_id_value_req: string + :returns: Redirection URL :rtype: string """ - authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy) + authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy, name_id_value_req) self.__last_request = authn_request.get_xml() self.__last_request_id = authn_request.get_id() saml_request = authn_request.get_request() diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 009c7233..d7be43fe 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -22,7 +22,7 @@ class OneLogin_Saml2_Authn_Request(object): """ - def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_policy=True): + def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_policy=True, name_id_value_req=None): """ Constructs the AuthnRequest object. @@ -37,6 +37,9 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element. :type set_nameid_policy: bool + + :param name_id_value_req: Optional argument. Indicates to the IdP the subject that should be authenticated + :type name_id_value_req: string """ self.__settings = settings @@ -69,6 +72,14 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol if is_passive is True: is_passive_str = "\n" + ' IsPassive="true"' + subject_str = '' + if name_id_value_req: + subject_str = """ + + %s + + """ % (sp_data['NameIDFormat'], name_id_value_req) + nameid_policy_str = '' if set_nameid_policy: name_id_policy_format = sp_data['NameIDFormat'] @@ -110,7 +121,7 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol 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 + %(entity_id)s%(subject_str)s%(nameid_policy_str)s%(requested_authn_context_str)s """ % \ { 'id': uid, @@ -121,6 +132,7 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol 'destination': destination, 'assertion_url': sp_data['assertionConsumerService']['url'], 'entity_id': sp_data['entityId'], + 'subject_str': subject_str, 'nameid_policy_str': nameid_policy_str, 'requested_authn_context_str': requested_authn_context_str, 'attr_consuming_service_str': attr_consuming_service_str diff --git a/tests/data/metadata/testshib-providers.xml b/tests/data/metadata/testshib-providers.xml index c00a1b77..47c2a873 100644 --- a/tests/data/metadata/testshib-providers.xml +++ b/tests/data/metadata/testshib-providers.xml @@ -37,28 +37,23 @@ - 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 + MLRKhDgdmA== diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 3c36d5a4..12e66465 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -615,7 +615,7 @@ def testLoginSigned(self): def testLoginForceAuthN(self): """ Tests the login method of the OneLogin_Saml2_Auth class - Case Login with no parameters. A AuthN Request is built with ForceAuthn and redirect executed + Case AuthN Request is built with ForceAuthn and redirect executed """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' @@ -649,7 +649,7 @@ def testLoginForceAuthN(self): def testLoginIsPassive(self): """ Tests the login method of the OneLogin_Saml2_Auth class - Case Login with no parameters. A AuthN Request is built with IsPassive and redirect executed + Case AuthN Request is built with IsPassive and redirect executed """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' @@ -683,7 +683,7 @@ def testLoginIsPassive(self): def testLoginSetNameIDPolicy(self): """ Tests the login method of the OneLogin_Saml2_Auth class - Case Logout with no parameters. A AuthN Request is built with and without NameIDPolicy + Case AuthN Request is built with and without NameIDPolicy """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' @@ -714,6 +714,47 @@ def testLoginSetNameIDPolicy(self): request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0]) self.assertNotIn('', request) + self.assertNotIn('', request_2) + self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">testuser@example.com', request_2) + self.assertIn('', request_2) + + settings_info['sp']['NameIDFormat'] = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' + auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url_3 = auth_3.login(return_to, name_id_value_req='testuser@example.com') + parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) + self.assertIn(sso_url, target_url_3) + self.assertIn('SAMLRequest', parsed_query_3) + request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0]) + self.assertIn('', request_3) + self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">testuser@example.com', request_3) + self.assertIn('', request_3) + + def testLogout(self): """ Tests the logout method of the OneLogin_Saml2_Auth class diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 3dd3e9df..5190d84a 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -262,6 +262,38 @@ def testCreateRequestSetNameIDPolicy(self): self.assertRegexpMatches(inflated_3, '^', inflated) + + authn_request_2 = OneLogin_Saml2_Authn_Request(settings, name_id_value_req='testuser@example.com') + authn_request_encoded_2 = authn_request_2.get_request() + decoded_2 = b64decode(authn_request_encoded_2) + inflated_2 = decompress(decoded_2, -15) + self.assertRegexpMatches(inflated_2, '^testuser@example.com', inflated_2) + self.assertIn('', inflated_2) + + saml_settings['sp']['NameIDFormat'] = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' + settings = OneLogin_Saml2_Settings(saml_settings) + authn_request_3 = OneLogin_Saml2_Authn_Request(settings, name_id_value_req='testuser@example.com') + authn_request_encoded_3 = authn_request_3.get_request() + decoded_3 = b64decode(authn_request_encoded_3) + inflated_3 = decompress(decoded_3, -15) + self.assertRegexpMatches(inflated_3, '^testuser@example.com', inflated_3) + self.assertIn('', inflated_3) + def testCreateDeflatedSAMLRequestURLParameter(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor. From d61af25f924759c4d3fd71413b7e563acbf7775d Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 2 Apr 2019 14:17:19 +0200 Subject: [PATCH 267/352] Fix pycodestyle --- tests/src/OneLogin/saml2_tests/auth_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 12e66465..070a4887 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -754,7 +754,6 @@ def testLoginWithSubject(self): self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">testuser@example.com', request_3) self.assertIn('', request_3) - def testLogout(self): """ Tests the logout method of the OneLogin_Saml2_Auth class From 9fe7a72da5b4caa1529c1640b52d2649447ce49b Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 26 Apr 2019 11:36:45 +0200 Subject: [PATCH 268/352] Update defusedxml --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index be6c5d69..74f81d58 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ install_requires=[ 'dm.xmlsec.binding==1.3.7', 'isodate>=0.5.0', - 'defusedxml>=0.4.1', + 'defusedxml>=0.6.0', ], extras_require={ 'test': ( From b6741fdabfb89377d63a93dc9e0c08efa8b71f85 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 24 Jun 2019 20:30:27 +0200 Subject: [PATCH 269/352] fix path in flask demo --- demo-flask/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-flask/index.py b/demo-flask/index.py index ac2c1320..0f70b777 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -11,7 +11,7 @@ app = Flask(__name__) app.config['SECRET_KEY'] = 'onelogindemopytoolkit' -app.config['SAML_PATH'] = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'saml') +app.config['SAML_PATH'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'saml') def init_saml_auth(req): From bd86f1e780fa4c9dfe37fce944951a0268ab373d Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 27 Jun 2019 16:33:33 +0200 Subject: [PATCH 270/352] Adjusted acs endpoint to extract NameQualifier and SPNameQualifier from SAMLResponse. Adjusted single logout service to provide NameQualifier and SPNameQualifier to logout method. Add getNameIdNameQualifier to Auth and SamlResponse. Extend logout method from Auth and LogoutRequest constructor to support SPNameQualifier parameter. Align LogoutRequest constructor with SAML specs --- README.md | 16 ++++- demo-django/demo/settings.py | 3 +- demo-django/demo/views.py | 14 +++- demo-django/saml/settings.json | 20 +++--- demo-flask/index.py | 38 +++++++++-- src/onelogin/saml2/auth.py | 30 +++++++- src/onelogin/saml2/logout_request.py | 24 ++++--- src/onelogin/saml2/response.py | 26 +++++++ tests/src/OneLogin/saml2_tests/auth_test.py | 67 ++++++++++++++++++ .../saml2_tests/logout_request_test.py | 1 - .../src/OneLogin/saml2_tests/response_test.py | 68 +++++++++++++++++++ 11 files changed, 271 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 3f70a5b1..5f6865a5 100644 --- a/README.md +++ b/README.md @@ -822,17 +822,17 @@ target_url = 'https://example.com' auth.logout(return_to=target_url) ``` -Also there are 4 optional parameters that can be set: +Also there are another 5 optional parameters that can be set: * ``name_id``. That will be used to build the LogoutRequest. If not ``name_id`` parameter is set and the auth object processed a SAML Response with a NameId, then this NameId will be used. * ``session_index``. SessionIndex that identifies the session of the user. * ``nq``. IDP Name Qualifier * ``name_id_format``. The NameID Format that will be set in the LogoutRequest +* ``spnq``: The ``NameID SP NameQualifier`` will be set in the ``LogoutRequest``. If no name_id is provided, the LogoutRequest will contain a NameID with the entity Format. If name_id is provided and no name_id_format is provided, the NameIDFormat of the settings will be used. -If nq is provided, the SPNameQualifier will be also attached to the NameId. 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: @@ -858,7 +858,12 @@ elif 'sso2' in request.args: # Another SSO init action return_to = '%sattrs/' % request.host_url # but set a custom RelayState URL return redirect(auth.login(return_to)) elif 'slo' in request.args: # SLO action. Will sent a Logout Request to IdP - return redirect(auth.logout()) + nameid = request.session['samlNameId'] + nameid_format = request.session['samlNameIdFormat'] + nameid_nq = request.session['samlNameIdNameQualifier'] + nameid_spnq = request.session['samlNameIdSPNameQualifier'] + session_index = request.session['samlSessionIndex'] + return redirect(auth.logout(None, nameid, session_index, nameid_nq, nameid_format, nameid_spnq)) elif 'acs' in request.args: # Assertion Consumer Service auth.process_response() # Process the Response of the IdP errors = auth.get_errors() # This method receives an array with the errors @@ -867,6 +872,11 @@ elif 'acs' in request.args: # Assertion Consumer Service msg = "Not authenticated" # data retrieved or not (user authenticated) else: request.session['samlUserdata'] = auth.get_attributes() # Retrieves user data + request.session['samlNameId'] = auth.get_nameid() + request.session['samlNameIdFormat'] = auth.get_nameid_format() + request.session['samlNameIdNameQualifier'] = auth.get_nameid_nq() + request.session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() + request.session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.form and self_url != request.form['RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) # Redirect if there is a relayState diff --git a/demo-django/demo/settings.py b/demo-django/demo/settings.py index 792ea82f..5305f382 100644 --- a/demo-django/demo/settings.py +++ b/demo-django/demo/settings.py @@ -22,8 +22,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] - +ALLOWED_HOSTS = ['pitbulk.no-ip.org'] # Application definition diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index 2ae0ebac..40ebdd66 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -50,14 +50,19 @@ def index(request): return_to = OneLogin_Saml2_Utils.get_self_url(req) + reverse('attrs') return HttpResponseRedirect(auth.login(return_to)) elif 'slo' in req['get_data']: - name_id = None - session_index = None + name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None if 'samlNameId' in request.session: name_id = request.session['samlNameId'] if 'samlSessionIndex' in request.session: session_index = request.session['samlSessionIndex'] + if 'samlNameIdFormat' in request.session: + name_id_format = request.session['samlNameIdFormat'] + if 'samlNameIdNameQualifier' in request.session: + name_id_nq = request.session['samlNameIdNameQualifier'] + if 'samlNameIdSPNameQualifier' in request.session: + name_id_spnq = request.session['samlNameIdSPNameQualifier'] - return HttpResponseRedirect(auth.logout(name_id=name_id, session_index=session_index)) + return HttpResponseRedirect(auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq, name_id_format=name_id_format, spnq=name_id_spnq)) # If LogoutRequest ID need to be stored in order to later validate it, do instead # slo_built_url = auth.logout(name_id=name_id, session_index=session_index) @@ -77,6 +82,9 @@ def index(request): del request.session['AuthNRequestID'] request.session['samlUserdata'] = auth.get_attributes() request.session['samlNameId'] = auth.get_nameid() + request.session['samlNameIdFormat'] = auth.get_nameid_format() + request.session['samlNameIdNameQualifier'] = auth.get_nameid_nq() + request.session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() request.session['samlSessionIndex'] = auth.get_session_index() if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState'])) diff --git a/demo-django/saml/settings.json b/demo-django/saml/settings.json index 391b91c1..3758746c 100644 --- a/demo-django/saml/settings.json +++ b/demo-django/saml/settings.json @@ -2,29 +2,29 @@ "strict": true, "debug": true, "sp": { - "entityId": "https:///metadata/", + "entityId": "http://pitbulk.no-ip.org:8000/metadata/", "assertionConsumerService": { - "url": "https:///?acs", + "url": "http://pitbulk.no-ip.org:8000/?acs", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, "singleLogoutService": { - "url": "https:///?sls", + "url": "http://pitbulk.no-ip.org:8000/?sls", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", - "x509cert": "", - "privateKey": "" + "x509cert": "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m", + "privateKey": "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAECgYEA0wDXZPS9hKqMTNh+nnfONioXBjhA6fQ7GVtWKDxa3ofMoPyt7ejGL/Hnvcv13Vn02UAsFx1bKrCstDqVtYwrWrnmywXyH+o9paJnTmd+cRIjWU8mRvCrxzH5I/Bcvbp1qZoASuqZEaGwNjM6JpW2o3QTmHGMALcLUPfEvhApssECQQDy2e65E86HcFhi/Ta8TQ0odDCNbiWA0bI1Iu8B7z+NAy1D1+WnCd7w2u9U6CF/k2nFHCsvxEoeANM0z7h5T/XvAkEA8e4JqKmDrfdiakQT7nf9svU2jXZtxSbPiIRMafNikDvzZ1vJCZkvdmaWYL70GlDZIwc9ad67rHZ/n/fqX1d0MQJAbRpRsJ5gY+KqItbFt3UaWzlP8sowWR5cZJjsLb9RmsV5mYguKYw6t5R0f33GRu1wUFimYlBaR/5w5MIJi57LywJATO1a5uWX+G5MPewNxmsjIY91XEAHIYR4wzkGLz5z3dciS4BVCZdLD0QJlxPA/MkuckPwFET9uhYn+M7VGKHvUQJBANSDwsY+BdCGpi/WRV37HUfwLl07damaFbW3h08PQx8G8SuF7DpN+FPBcI6VhzrIWNRBxWprkgeGioKNfFWzSaM=" }, "idp": { - "entityId": "https://app.onelogin.com/saml/metadata/", + "entityId": "https://app.onelogin.com/saml/metadata/3dbd155e-be64-4a4d-8fab-e44788bce74f", "singleSignOnService": { - "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", + "url": "https://sgarcia-us-preprod.onelogin.com/trust/saml2/http-redirect/sso/850162", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, "singleLogoutService": { - "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + "url": "https://sgarcia-us-preprod.onelogin.com/trust/saml2/http-redirect/slo/850162", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "x509cert": "" + "x509cert": "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==" } -} \ No newline at end of file +} diff --git a/demo-flask/index.py b/demo-flask/index.py index 0f70b777..1f5404dc 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -47,32 +47,56 @@ def index(): if 'sso' in request.args: return redirect(auth.login()) + # If AuthNRequest ID need to be stored in order to later validate it, do instead + # sso_built_url = auth.login() + # request.session['AuthNRequestID'] = auth.get_last_request_id() + # return redirect(sso_built_url) elif 'sso2' in request.args: return_to = '%sattrs/' % request.host_url return redirect(auth.login(return_to)) elif 'slo' in request.args: - name_id = None - session_index = None + name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None if 'samlNameId' in session: name_id = session['samlNameId'] if 'samlSessionIndex' in session: session_index = session['samlSessionIndex'] - - return redirect(auth.logout(name_id=name_id, session_index=session_index)) + if 'samlNameIdFormat' in session: + name_id_format = session['samlNameIdFormat'] + if 'samlNameIdNameQualifier' in session: + name_id_nq = session['samlNameIdNameQualifier'] + if 'samlNameIdSPNameQualifier' in session: + name_id_spnq = session['samlNameIdSPNameQualifier'] + + return redirect(auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq, name_id_format=name_id_format, spnq=name_id_spnq)) + # If LogoutRequest ID need to be stored in order to later validate it, do instead + # slo_built_url = auth.logout(name_id=name_id, session_index=session_index) + # session['LogoutRequestID'] = auth.get_last_request_id() + #return redirect(slo_built_url) elif 'acs' in request.args: - auth.process_response() + request_id = None + if 'AuthNRequestID' in session: + request_id = session['AuthNRequestID'] + + auth.process_response(request_id=request_id) errors = auth.get_errors() not_auth_warn = not auth.is_authenticated() if len(errors) == 0: + if 'AuthNRequestID' in session: + del session['AuthNRequestID'] session['samlUserdata'] = auth.get_attributes() - session['samlNameId'] = auth.get_nameid() + session['samlNameIdFormat'] = auth.get_nameid_format() + session['samlNameIdNameQualifier'] = auth.get_nameid_nq() + session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.form and self_url != request.form['RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) elif 'sls' in request.args: + request_id = None + if 'LogoutRequestID' in session: + request_id = session['LogoutRequestID'] dscb = lambda: session.clear() - url = auth.process_slo(delete_session_cb=dscb) + url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) errors = auth.get_errors() if len(errors) == 0: if url is not None: diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index d9b42a32..0b12bbe7 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -53,6 +53,8 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): self.__attributes = [] self.__nameid = None self.__nameid_format = None + self.__nameid_nq = None + self.__nameid_spnq = None self.__session_index = None self.__session_expiration = None self.__authenticated = False @@ -104,6 +106,8 @@ def process_response(self, request_id=None): self.__attributes = response.get_attributes() self.__nameid = response.get_nameid() self.__nameid_format = response.get_nameid_format() + self.__nameid_nq = response.get_nameid_nq() + self.__nameid_spnq = response.get_nameid_spnq() self.__session_index = response.get_session_index() self.__session_expiration = response.get_session_not_on_or_after() self.__last_message_id = response.get_id() @@ -245,6 +249,24 @@ def get_nameid_format(self): """ return self.__nameid_format + def get_nameid_nq(self): + """ + Returns the nameID NameQualifier of the Assertion. + + :returns: NameID NameQualifier + :rtype: string|None + """ + return self.__nameid_nq + + def get_nameid_spnq(self): + """ + Returns the nameID SP NameQualifier of the Assertion. + + :returns: NameID SP NameQualifier + :rtype: string|None + """ + return self.__nameid_spnq + def get_session_index(self): """ Returns the SessionIndex from the AuthnStatement. @@ -362,7 +384,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_ parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) return self.redirect_to(self.get_sso_url(), parameters) - def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None): + def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None): """ Initiates the SLO process. @@ -381,6 +403,9 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name :param name_id_format: The NameID Format that will be set in the LogoutRequest. :type: string + :param spnq: SP Name Qualifier + :type: string + :returns: Redirection url """ slo_url = self.get_slo_url() @@ -400,7 +425,8 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name name_id=name_id, session_index=session_index, nq=nq, - name_id_format=name_id_format + name_id_format=name_id_format, + spnq=spnq ) self.__last_request = logout_request.get_xml() self.__last_request_id = logout_request.id diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index e419f585..1cb75efd 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -29,7 +29,7 @@ class OneLogin_Saml2_Logout_Request(object): """ - def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None): + def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None): """ Constructs the Logout Request object. @@ -50,6 +50,10 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= :param name_id_format: The NameID Format that will be set in the LogoutRequest. :type: string + + :param spnq: SP Name Qualifier + :type: string + """ self.__settings = settings self.__error = None @@ -79,19 +83,23 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= if not name_id_format and sp_data['NameIDFormat'] != OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED: name_id_format = sp_data['NameIDFormat'] else: + name_id = idp_data['entityId'] name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY - spNameQualifier = None - if name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY: - name_id = idp_data['entityId'] + # From saml-core-2.0-os 8.3.6, when the entity Format is used: + # "The NameQualifier, SPNameQualifier, and SPProvidedID attributes + # MUST be omitted. + if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY: nq = None - elif nq is not None: - # We only gonna include SPNameQualifier if NameQualifier is provided - spNameQualifier = sp_data['entityId'] + spnq = None + + # NameID Format UNSPECIFIED omitted + if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED: + name_id_format = None name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, - spNameQualifier, + spnq, name_id_format, cert, False, diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 0ce801ea..cc5e8936 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -512,6 +512,32 @@ def get_nameid_format(self): nameid_format = nameid_data['Format'] return nameid_format + def get_nameid_nq(self): + """ + Gets the NameID NameQualifier provided by the SAML Response from the IdP + + :returns: NameID NameQualifier + :rtype: string|None + """ + nameid_nq = None + nameid_data = self.get_nameid_data() + if nameid_data and 'NameQualifier' in nameid_data.keys(): + nameid_nq = nameid_data['NameQualifier'] + return nameid_nq + + def get_nameid_spnq(self): + """ + Gets the NameID SP NameQualifier provided by the SAML response from the IdP. + + :returns: NameID SP NameQualifier + :rtype: string|None + """ + nameid_spnq = None + nameid_data = self.get_nameid_data() + if nameid_data and 'SPNameQualifier' in nameid_data.keys(): + nameid_spnq = nameid_data['SPNameQualifier'] + return nameid_spnq + def get_session_not_on_or_after(self): """ Gets the SessionNotOnOrAfter from the AuthnStatement diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 070a4887..3a57c438 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -220,6 +220,9 @@ def testProcessResponseValid(self): self.assertEqual(auth.get_attribute('mail'), attributes['mail']) session_index = auth.get_session_index() self.assertEqual('_6273d77b8cde0c333ec79d22a9fa0003b9fe2d75cb', session_index) + self.assertEqual("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", auth.get_nameid_format()) + self.assertIsNone(auth.get_nameid_nq()) + self.assertEqual("http://stuff.com/endpoints/metadata.php", auth.get_nameid_spnq()) def testRedirectTo(self): """ @@ -1002,6 +1005,70 @@ def testGetNameIdFormat(self): self.assertTrue(auth.is_authenticated()) self.assertEqual("urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", auth.get_nameid_format()) + def testGetNameIdNameQualifier(self): + """ + Tests the get_nameid_nq method of the OneLogin_Saml2_Auth + """ + settings = self.loadSettingsJSON() + message = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_namequalifier.xml.base64')) + request_data = self.get_request() + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + self.assertIsNone(auth.get_nameid_nq()) + auth.process_response() + self.assertTrue(auth.is_authenticated()) + self.assertEqual("https://test.example.com/saml/metadata", auth.get_nameid_nq()) + + def testGetNameIdNameQualifier2(self): + """ + Tests the get_nameid_nq method of the OneLogin_Saml2_Auth + """ + settings = self.loadSettingsJSON() + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + request_data = self.get_request() + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + self.assertIsNone(auth.get_nameid_nq()) + auth.process_response() + self.assertTrue(auth.is_authenticated()) + self.assertIsNone(auth.get_nameid_nq()) + + def testGetNameIdSPNameQualifier(self): + """ + Tests the get_nameid_spnq method of the OneLogin_Saml2_Auth + """ + settings = self.loadSettingsJSON() + message = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_namequalifier.xml.base64')) + request_data = self.get_request() + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + self.assertIsNone(auth.get_nameid_spnq()) + auth.process_response() + self.assertTrue(auth.is_authenticated()) + self.assertIsNone(auth.get_nameid_spnq()) + + def testGetNameIdSPNameQualifier2(self): + """ + Tests the get_nameid_spnq method of the OneLogin_Saml2_Auth + """ + settings = self.loadSettingsJSON() + message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + request_data = self.get_request() + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + self.assertIsNone(auth.get_nameid_spnq()) + auth.process_response() + self.assertTrue(auth.is_authenticated()) + self.assertEqual("http://stuff.com/endpoints/metadata.php", auth.get_nameid_spnq()) + def testBuildRequestSignature(self): """ Tests the build_request_signature method of the OneLogin_Saml2_Auth diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index eb50c785..6fd75d3a 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -193,7 +193,6 @@ def testGetNameIdData(self): expected_name_id_data = { 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress', 'NameQualifier': idp_data['entityId'], - 'SPNameQualifier': sp_data['entityId'], 'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69' } diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index bc172546..4794017b 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -285,6 +285,74 @@ def testReturnNameIdFormat(self): with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): response_17.get_nameid_format() + def testReturnNameIdNameQualifier(self): + """ + Tests the get_nameid_nq method of the OneLogin_Saml2_Response + """ + json_settings = self.loadSettingsJSON() + json_settings['strict'] = False + settings = OneLogin_Saml2_Settings(json_settings) + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertIsNone(response.get_nameid_nq()) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertIsNone(response_2.get_nameid_nq()) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) + response_3 = OneLogin_Saml2_Response(settings, xml_3) + self.assertIsNone(response_3.get_nameid_nq()) + + xml_4 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + response_4 = OneLogin_Saml2_Response(settings, xml_4) + self.assertIsNone(response_4.get_nameid_nq()) + + xml_5 = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_namequalifier.xml.base64')) + response_5 = OneLogin_Saml2_Response(settings, xml_5) + self.assertEqual('https://test.example.com/saml/metadata', response_5.get_nameid_nq()) + + json_settings['strict'] = True + settings = OneLogin_Saml2_Settings(json_settings) + xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64')) + response_6 = OneLogin_Saml2_Response(settings, xml_6) + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): + response_6.get_nameid_nq() + + def testReturnNameIdNameSPQualifier(self): + """ + Tests the get_nameid_spnq method of the OneLogin_Saml2_Response + """ + json_settings = self.loadSettingsJSON() + json_settings['strict'] = False + settings = OneLogin_Saml2_Settings(json_settings) + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertIsNone(response.get_nameid_spnq()) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertEqual("http://stuff.com/endpoints/metadata.php", response_2.get_nameid_spnq()) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) + response_3 = OneLogin_Saml2_Response(settings, xml_3) + self.assertEqual("http://stuff.com/endpoints/metadata.php", response_3.get_nameid_spnq()) + + xml_4 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + response_4 = OneLogin_Saml2_Response(settings, xml_4) + self.assertEqual("http://stuff.com/endpoints/metadata.php", response_4.get_nameid_spnq()) + + xml_5 = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_namequalifier.xml.base64')) + response_5 = OneLogin_Saml2_Response(settings, xml_5) + self.assertIsNone(response_5.get_nameid_spnq()) + + json_settings['strict'] = True + settings = OneLogin_Saml2_Settings(json_settings) + xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64')) + response_6 = OneLogin_Saml2_Response(settings, xml_6) + with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'): + response_6.get_nameid_spnq() + def testGetNameIdData(self): """ Tests the get_nameid_data method of the OneLogin_Saml2_Response From fd06de0eb966c01f791522317ea5e22fc4dddf1b Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 2 Jul 2019 20:19:27 +0200 Subject: [PATCH 271/352] Added get_in_response_to method to Response and LogoutResponse classes --- src/onelogin/saml2/logout_response.py | 21 ++++++++++++------- src/onelogin/saml2/response.py | 10 ++++++++- ...lid_response_with_namequalifier.xml.base64 | 1 + .../src/OneLogin/saml2_tests/response_test.py | 18 +++++++++++++++- 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 tests/data/responses/valid_response_with_namequalifier.xml.base64 diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 4af91a4f..6d326caf 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -101,14 +101,13 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): security = self.__settings.get_security_data() + in_response_to = self.get_in_response_to() # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided - if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'): - in_response_to = self.document.documentElement.getAttribute('InResponseTo') - if request_id != in_response_to: - 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 - ) + if request_id is not None and in_response_to and 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() @@ -237,6 +236,14 @@ def build(self, in_response_to): self.__logout_response = logout_response + def get_in_response_to(self): + """ + Gets the ID of the LogoutRequest which this response is in response to + :returns: ID of LogoutRequest this LogoutResponse is in response to or None if it is not present + :rtype: str + """ + return self.document.documentElement.getAttribute('InResponseTo') + def get_response(self, deflate=True): """ Returns the Logout Response defated, base64encoded diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index cc5e8936..73c24f9a 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -133,7 +133,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) - in_response_to = self.document.get('InResponseTo', None) + in_response_to = self.get_in_response_to() if request_id is None and in_response_to is not None and security.get('rejectUnsolicitedResponsesWithInResponseTo', False): raise OneLogin_Saml2_ValidationError( 'The Response has an InResponseTo attribute: %s while no InResponseTo was expected' % in_response_to, @@ -405,6 +405,14 @@ def get_authn_contexts(self): authn_context_nodes = self.__query_assertion('/saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef') return [OneLogin_Saml2_Utils.element_text(node) for node in authn_context_nodes] + def get_in_response_to(self): + """ + Gets the ID of the request which this response is in response to + :returns: ID of AuthNRequest this Response is in response to or None if it is not present + :rtype: str + """ + return self.document.get('InResponseTo') + def get_issuers(self): """ Gets the issuers (from message and from assertion) diff --git a/tests/data/responses/valid_response_with_namequalifier.xml.base64 b/tests/data/responses/valid_response_with_namequalifier.xml.base64 new file mode 100644 index 00000000..a98de3ff --- /dev/null +++ b/tests/data/responses/valid_response_with_namequalifier.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDhmZWI5YWNkLTFlODYtYWMxMi05MDIzLTEzYjg0NDc5YjI1YiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDhmZWI5YWNkLTFlODYtYWMxMi05MDIzLTEzYjg0NDc5YjI1YiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+NVRWZURYbGQ3YzhURmtybVlDeFpuL2ZHRTRzPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5hZlFaVUE2REpHa0hLNjVMMENBaTJBSDJkOWNwbExuekNPTHBCYm9hUmVmaWdtVC92L0tJZGcyYXpWRzY2Ykk1aFA1NTBNR0c2ZVVzaWJ1N2N3ZytFbG9tejVBalE3dzlGZG8waHdWWWhib3JaSkN2TUxLUzBEWkFzc01XZnZ3RGNUNmhra3UreXFlS2RhZ1BBOTYwQ25YcUMxeHpjMk43WS82dlBCU081bVU9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDQxN2ZiOTc2LTk0NGEtNDNiZi05ZTUyLWZiOWM1OTYxNzYxZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng0MTdmYjk3Ni05NDRhLTQzYmYtOWU1Mi1mYjljNTk2MTc2MWYiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmxSbTJ3UW13ZGhmZVZuMDFaS1Ewb05CN1JqQT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+aktwQUNmMWkxR0FMSWQ5Y0liQlFsTkJQMVhpZDhhYXFKOUxyTkFIZ1lpR2VIc0NscldVUkZJREprOGI0T3RmdHdXTGZKeXBXbXgwWm15M2hpTTJyVHBIbDBLMGVqSFNsOS9Ed0pabkNEQW1CS1lhZ0ZFR0xxWXYwaXI0Y2lYaForTkdXSDY1czhBRlVibjU2SytaS3lpMFkwMWc4TmVqaS92OTNlZFZ6ZTZnPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vdGVzdC5leGFtcGxlLmNvbS9zYW1sL21ldGFkYXRhIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 4794017b..aa180c05 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -467,7 +467,7 @@ def testGetNameIdData(self): settings = OneLogin_Saml2_Settings(json_settings) response_13 = OneLogin_Saml2_Response(settings, xml_6) nameid_data_13 = response_13.get_nameid_data() - nameid_data_13 = self.assertEqual(expected_nameid_data_5, nameid_data_13) + self.assertEqual(expected_nameid_data_5, nameid_data_13) json_settings['strict'] = False json_settings['security']['wantNameId'] = False @@ -745,6 +745,22 @@ def testGetSessionNotOnOrAfter(self): response_3 = OneLogin_Saml2_Response(settings, xml_3) self.assertEqual(2696012228, response_3.get_session_not_on_or_after()) + def testGetInResponseTo(self): + """ + Tests the retrieval of the InResponseTo attribute + """ + + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + # Response without an InResponseTo element should return None + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertIsNone(response.get_in_response_to()) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) + response_3 = OneLogin_Saml2_Response(settings, xml_3) + self.assertEqual('ONELOGIN_be60b8caf8e9d19b7a3551b244f116c947ff247d', response_3.get_in_response_to()) + def testIsInvalidXML(self): """ Tests the is_valid method of the OneLogin_Saml2_Response From 484f381a7266bdbd5e96371cffcd22a8f4cec180 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 2 Jul 2019 22:17:35 +0200 Subject: [PATCH 272/352] Release 2.6.0 --- changelog.md | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 91de981e..97fbc946 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,13 @@ # python-saml changelog +### 2.6.0 (Jul 02, 2019) +* Adjusted acs endpoint to extract NameQualifier and SPNameQualifier from SAMLResponse. Adjusted single logout service to provide NameQualifier and SPNameQualifier to logout method. Add getNameIdNameQualifier to Auth and SamlResponse. Extend logout method from Auth and LogoutRequest constructor to support SPNameQualifier parameter. Align LogoutRequest constructor with SAML specs +* Added get_in_response_to method to Response and LogoutResponse classes +* Add get_last_authn_contexts method +* Fix bug on friendlyName/nameFormat parameters on RequestedAttribute elements. Wrong variable name caused FriendlyName to overwrite NameFormat +* Add support for Subjects on AuthNRequests by the new name_id_value_req parameeter.Fix testshib test. Improve README: Added inline markup to important references +* Update defusedxml +* Fix path in flask demo + ### 2.5.0 (Jan 29, 2019) * Security improvements. Use of tagid to prevent XPath injection. Disable DTD on fromstring defusedxml method * [#239](https://github.com/onelogin/python-saml/issues/239) Check that the response has all of the AuthnContexts that we provided diff --git a/setup.py b/setup.py index 74f81d58..462bf96f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.5.0', + version='2.6.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', From e35150b87680289fd9e5b5b839d06f701d7f79bf Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 4 Sep 2019 00:06:09 +0200 Subject: [PATCH 273/352] Fix docstring of get_attribute method --- src/onelogin/saml2/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 0b12bbe7..188e289e 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -315,7 +315,7 @@ def get_attribute(self, name): :param name: Name of the attribute :type name: string - :returns: Attribute value if exists or [] + :returns: Attribute value if exists or None :rtype: string """ assert isinstance(name, basestring) From a63ba08b83cd39d2a196dd01c229b95a90666649 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 11 Sep 2019 16:25:49 +0200 Subject: [PATCH 274/352] Fix CI --- .travis.yml | 3 +-- demo-flask/index.py | 8 ++++---- tests/src/OneLogin/saml2_tests/logout_request_test.py | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7221c335..2d923162 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - - '2.7.6' -# - '2.7.12' + - '2.7' install: - sudo apt-get update -qq diff --git a/demo-flask/index.py b/demo-flask/index.py index 1f5404dc..e4c11c6f 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -68,10 +68,10 @@ def index(): name_id_spnq = session['samlNameIdSPNameQualifier'] return redirect(auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq, name_id_format=name_id_format, spnq=name_id_spnq)) - # If LogoutRequest ID need to be stored in order to later validate it, do instead - # slo_built_url = auth.logout(name_id=name_id, session_index=session_index) - # session['LogoutRequestID'] = auth.get_last_request_id() - #return redirect(slo_built_url) + # If LogoutRequest ID need to be stored in order to later validate it, do instead + # slo_built_url = auth.logout(name_id=name_id, session_index=session_index) + # session['LogoutRequestID'] = auth.get_last_request_id() + # return redirect(slo_built_url) elif 'acs' in request.args: request_id = None if 'AuthNRequestID' in session: diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 6fd75d3a..de782f75 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -189,7 +189,6 @@ def testGetNameIdData(self): OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key) idp_data = settings.get_idp_data() - sp_data = settings.get_sp_data() expected_name_id_data = { 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress', 'NameQualifier': idp_data['entityId'], From 8bb4901efa89435466d0087f66efbb5c3f053f44 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 11 Sep 2019 16:38:49 +0200 Subject: [PATCH 275/352] Set true as the default value for strict setting --- src/onelogin/saml2/settings.py | 4 ++-- tests/src/OneLogin/saml2_tests/settings_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index c210c5b3..75498ebe 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -72,7 +72,7 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals """ self.__sp_validation_only = sp_validation_only self.__paths = {} - self.__strict = False + self.__strict = True self.__debug = False self.__sp = {} self.__idp = {} @@ -205,7 +205,7 @@ def __load_settings_from_dict(self, settings): self.__sp = settings['sp'] self.__idp = settings.get('idp', {}) - self.__strict = settings.get('strict', False) + self.__strict = settings.get('strict', True) self.__debug = settings.get('debug', False) self.__security = settings.get('security', {}) self.__contacts = settings.get('contactPerson', {}) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 8d02d041..bf2b547d 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -718,7 +718,7 @@ def testIsStrict(self): del settings_info['strict'] settings = OneLogin_Saml2_Settings(settings_info) - self.assertFalse(settings.is_strict()) + self.assertTrue(settings.is_strict()) settings_info['strict'] = False settings_2 = OneLogin_Saml2_Settings(settings_info) From 55f1fcbded4faffc1796c1f79ee3bfb0a20f2635 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 11 Sep 2019 16:39:43 +0200 Subject: [PATCH 276/352] Release 2.7.0 --- README.md | 2 ++ changelog.md | 3 +++ setup.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f6865a5..0f988a1c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml). #### Warning #### +Version 2.7.0 sets strict mode active by default + Update ``python-saml`` to ``2.5.0``, this version includes security improvements for preventing XEE and Xpath Injections. Update ``python-saml`` to ``2.4.0``, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability. diff --git a/changelog.md b/changelog.md index 97fbc946..8ce586f9 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,7 @@ # python-saml changelog +### 2.7.0 (Sep 11, 2019) +* Set true as the default value for strict setting + ### 2.6.0 (Jul 02, 2019) * Adjusted acs endpoint to extract NameQualifier and SPNameQualifier from SAMLResponse. Adjusted single logout service to provide NameQualifier and SPNameQualifier to logout method. Add getNameIdNameQualifier to Auth and SamlResponse. Extend logout method from Auth and LogoutRequest constructor to support SPNameQualifier parameter. Align LogoutRequest constructor with SAML specs * Added get_in_response_to method to Response and LogoutResponse classes diff --git a/setup.py b/setup.py index 462bf96f..7d545fb0 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.6.0', + version='2.7.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', From 2ffc45b253a4c63a04127af31a7d103cf6ee53c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2019 09:43:00 +0000 Subject: [PATCH 277/352] Bump django from 1.11 to 1.11.23 in /demo-django Bumps [django](https://github.com/django/django) from 1.11 to 1.11.23. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/1.11...1.11.23) Signed-off-by: dependabot[bot] --- demo-django/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-django/requirements.txt b/demo-django/requirements.txt index f305b2f2..d0d5a268 100644 --- a/demo-django/requirements.txt +++ b/demo-django/requirements.txt @@ -1 +1 @@ -Django==1.11 +Django==1.11.23 From fb2a89a9fd3b068d6ea38988411db88bc1e73b6c Mon Sep 17 00:00:00 2001 From: Sergio Pulgarin Date: Wed, 13 Nov 2019 16:17:15 -0500 Subject: [PATCH 278/352] Fixes minor typos with word "enough" (was "enought) Addressing https://github.com/onelogin/python-saml/issues/261 --- docs/saml2/_modules/saml2/settings.html | 6 +++--- src/onelogin/saml2/settings.py | 4 ++-- tests/src/OneLogin/saml2_tests/settings_test.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/saml2/_modules/saml2/settings.html b/docs/saml2/_modules/saml2/settings.html index 42800b51..5f8f5c2c 100644 --- a/docs/saml2/_modules/saml2/settings.html +++ b/docs/saml2/_modules/saml2/settings.html @@ -406,7 +406,7 @@

    Source code for saml2.settings

                     contact = settings['contactPerson'][t]
                     if (('givenName' not in contact or len(contact['givenName']) == 0) or
                             ('emailAddress' not in contact or len(contact['emailAddress']) == 0)):
    -                    errors.append('contact_not_enought_data')
    +                    errors.append('contact_not_enough_data')
                         break
     
             if 'organization' in settings:
    @@ -415,7 +415,7 @@ 

    Source code for saml2.settings

                     if (('name' not in organization or len(organization['name']) == 0) or
                         ('displayname' not in organization or len(organization['displayname']) == 0) or
                             ('url' not in organization or len(organization['url']) == 0)):
    -                    errors.append('organization_not_enought_data')
    +                    errors.append('organization_not_enough_data')
                         break
     
             return errors
    @@ -692,4 +692,4 @@ 

    Navigation

    Created using Sphinx 1.1.3.
    - \ No newline at end of file + diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 75498ebe..09071844 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -477,7 +477,7 @@ def check_sp_settings(self, settings): contact = settings['contactPerson'][c_type] if ('givenName' not in contact or len(contact['givenName']) == 0) or \ ('emailAddress' not in contact or len(contact['emailAddress']) == 0): - errors.append('contact_not_enought_data') + errors.append('contact_not_enough_data') break if 'organization' in settings: @@ -486,7 +486,7 @@ def check_sp_settings(self, settings): if ('name' not in organization or len(organization['name']) == 0) or \ ('displayname' not in organization or len(organization['displayname']) == 0) or \ ('url' not in organization or len(organization['url']) == 0): - errors.append('organization_not_enought_data') + errors.append('organization_not_enough_data') break # Restores the value that had the self.__sp if 'old_sp' in locals(): diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index bf2b547d..c4a9d7bc 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -331,7 +331,7 @@ def testCheckSettings(self): 'emailAddress': 'auxiliar@example.com' } } - with self.assertRaisesRegexp(Exception, 'Invalid dict settings: sp_signMetadata_invalid,contact_type_invalid,contact_not_enought_data,organization_not_enought_data'): + with self.assertRaisesRegexp(Exception, 'Invalid dict settings: sp_signMetadata_invalid,contact_type_invalid,contact_not_enough_data,organization_not_enough_data'): OneLogin_Saml2_Settings(settings_info) def testGetSPMetadata(self): From 3527fdffdcbea2ebb16bc210098c44aa50ce924b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2019 16:17:43 +0000 Subject: [PATCH 279/352] Bump flask from 0.10.1 to 1.0 in /demo-flask Bumps [flask](https://github.com/pallets/flask) from 0.10.1 to 1.0. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/master/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/0.10.1...1.0) Signed-off-by: dependabot[bot] --- demo-flask/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-flask/requirements.txt b/demo-flask/requirements.txt index 335836f7..d9340937 100644 --- a/demo-flask/requirements.txt +++ b/demo-flask/requirements.txt @@ -1 +1 @@ -flask==0.10.1 +flask==1.0 From 102e3683c2955f00177b7bc582016a79e92f9ebe Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 19 Nov 2019 19:37:25 +0100 Subject: [PATCH 280/352] Fix #258. Fix failOnAuthnContextMismatch code --- README.md | 2 +- src/onelogin/saml2/authn_request.py | 4 +--- src/onelogin/saml2/response.py | 4 ++-- src/onelogin/saml2/settings.py | 1 + tests/src/OneLogin/saml2_tests/response_test.py | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0f988a1c..bb86afb3 100644 --- a/README.md +++ b/README.md @@ -440,7 +440,7 @@ In addition to the required settings data (idp, sp), extra settings can be defin // Allows the authn comparison parameter to be set, defaults to 'exact' if the setting is not present. "requestedAuthnContextComparison": "exact", - // Set to true to check that the AuthnContext received matches the one requested. + // Set to true to check that the AuthnContext(s) received match(es) the requested. "failOnAuthnContextMismatch": false, // In some environment you will need to set how long the published metadata of the Service Provider gonna be valid. diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index d7be43fe..e99f494f 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -93,9 +93,7 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol requested_authn_context_str = '' if 'requestedAuthnContext' in security.keys() and security['requestedAuthnContext'] is not False: - authn_comparison = 'exact' - if 'requestedAuthnContextComparison' in security.keys(): - authn_comparison = security['requestedAuthnContextComparison'] + authn_comparison = security['requestedAuthnContextComparison'] if security['requestedAuthnContext'] is True: requested_authn_context_str = "\n" + """ diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 73c24f9a..90f24db5 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -184,10 +184,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if security.get('failOnAuthnContextMismatch', False) and requested_authn_contexts and requested_authn_contexts is not True: authn_contexts = self.get_authn_contexts() - unmatched_contexts = set(requested_authn_contexts).difference(authn_contexts) + unmatched_contexts = set(authn_contexts).difference(requested_authn_contexts) if unmatched_contexts: raise OneLogin_Saml2_ValidationError( - 'The AuthnContext "%s" didn\'t include requested context "%s"' % (', '.join(authn_contexts), ', '.join(unmatched_contexts)), + 'The AuthnContext "%s" was not a requested context "%s"' % (', '.join(unmatched_contexts), ', '.join(requested_authn_contexts)), OneLogin_Saml2_ValidationError.AUTHN_CONTEXT_MISMATCH ) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 09071844..a9e863fb 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -304,6 +304,7 @@ def __add_default_values(self): self.__sp.setdefault('privateKey', '') self.__security.setdefault('requestedAuthnContext', True) + self.__security.setdefault('requestedAuthnContextComparison', 'exact') self.__security.setdefault('failOnAuthnContextMismatch', False) def check_settings(self, settings): diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index aa180c05..3fee66ab 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1084,7 +1084,7 @@ def testIsInValidAuthenticationContext(self): # check that we catch when the contexts don't match response = OneLogin_Saml2_Response(settings, message) self.assertFalse(response.is_valid(request_data)) - self.assertIn('The AuthnContext "%s" didn\'t include requested context "%s"' % (password_context, two_factor_context), response.get_error()) + self.assertIn('The AuthnContext "%s" was not a requested context "%s"' % (password_context, two_factor_context), response.get_error()) # now drop in the expected AuthnContextClassRef and see that it passes original_message = b64decode(message) From 8b87e5f5eaeb58a78c7e85294a61aae9a45cad32 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 20 Nov 2019 09:58:33 +0100 Subject: [PATCH 281/352] Fix #250. Allow any number of decimal places for seconds on SAML datetimes --- src/onelogin/saml2/utils.py | 18 +++++++++++++++--- tests/src/OneLogin/saml2_tests/utils_test.py | 8 ++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 8fea81d1..b1605dbf 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -89,6 +89,11 @@ class OneLogin_Saml2_Utils(object): RESPONSE_SIGNATURE_XPATH = '/samlp:Response/ds:Signature' ASSERTION_SIGNATURE_XPATH = '/samlp:Response/saml:Assertion/ds:Signature' + TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" + TIME_FORMAT_2 = "%Y-%m-%dT%H:%M:%S.%fZ" + TIME_FORMAT_WITH_FRAGMENT = re.compile( + "^(\d{4,4}-\d{2,2}-\d{2,2}T\d{2,2}:\d{2,2}:\d{2,2})(\.\d*)?Z?$") + @staticmethod def decode_base64_and_inflate(value): """ @@ -445,7 +450,7 @@ def parse_time_to_SAML(time): :rtype: string """ data = datetime.utcfromtimestamp(float(time)) - return data.strftime('%Y-%m-%dT%H:%M:%SZ') + return data.strftime(OneLogin_Saml2_Utils.TIME_FORMAT) @staticmethod def parse_SAML_to_time(timestr): @@ -460,9 +465,16 @@ def parse_SAML_to_time(timestr): :rtype: int """ try: - data = datetime.strptime(timestr, '%Y-%m-%dT%H:%M:%SZ') + data = datetime.strptime(timestr, OneLogin_Saml2_Utils.TIME_FORMAT) except ValueError: - data = datetime.strptime(timestr, '%Y-%m-%dT%H:%M:%S.%fZ') + try: + data = datetime.strptime(timestr, OneLogin_Saml2_Utils.TIME_FORMAT_2) + except ValueError: + elem = OneLogin_Saml2_Utils.TIME_FORMAT_WITH_FRAGMENT.match(timestr) + if not elem: + raise Exception("time data %s does not match format %s" % (timestr, "yyyy-mm-ddThh:mm:ss(\.s+)?Z")) + data = datetime.strptime(elem.groups()[0] + "Z", OneLogin_Saml2_Utils.TIME_FORMAT) + return calendar.timegm(data.utctimetuple()) @staticmethod diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 9fa38952..90b9c91e 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -476,6 +476,14 @@ def testParseSAML2Time(self): saml_time2 = '2013-12-10T04:39:31.120Z' self.assertEqual(time, OneLogin_Saml2_Utils.parse_SAML_to_time(saml_time2)) + # Now test if toolkit supports microseconds + saml_time3 = '2013-12-10T04:39:31.120240Z' + self.assertEqual(time, OneLogin_Saml2_Utils.parse_SAML_to_time(saml_time3)) + + # Now test if toolkit supports nanoseconds + saml_time4 = '2013-12-10T04:39:31.120240360Z' + self.assertEqual(time, OneLogin_Saml2_Utils.parse_SAML_to_time(saml_time4)) + def testParseTime2SAML(self): """ Tests the parse_time_to_SAML method of the OneLogin_Saml2_Utils From b17374aa0db09a98af63ded8ba1ef6f7cc7c6bee Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 20 Nov 2019 10:20:04 +0100 Subject: [PATCH 282/352] Fix pycodestyle --- src/onelogin/saml2/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index b1605dbf..7ce29b16 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -91,8 +91,7 @@ class OneLogin_Saml2_Utils(object): TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" TIME_FORMAT_2 = "%Y-%m-%dT%H:%M:%S.%fZ" - TIME_FORMAT_WITH_FRAGMENT = re.compile( - "^(\d{4,4}-\d{2,2}-\d{2,2}T\d{2,2}:\d{2,2}:\d{2,2})(\.\d*)?Z?$") + TIME_FORMAT_WITH_FRAGMENT = re.compile(r'^(\d{4,4}-\d{2,2}-\d{2,2}T\d{2,2}:\d{2,2}:\d{2,2})(\.\d*)?Z?$') @staticmethod def decode_base64_and_inflate(value): @@ -472,7 +471,7 @@ def parse_SAML_to_time(timestr): except ValueError: elem = OneLogin_Saml2_Utils.TIME_FORMAT_WITH_FRAGMENT.match(timestr) if not elem: - raise Exception("time data %s does not match format %s" % (timestr, "yyyy-mm-ddThh:mm:ss(\.s+)?Z")) + raise Exception("time data %s does not match format %s" % (timestr, r'yyyy-mm-ddThh:mm:ss(\.s+)?Z')) data = datetime.strptime(elem.groups()[0] + "Z", OneLogin_Saml2_Utils.TIME_FORMAT) return calendar.timegm(data.utctimetuple()) From 4b8f4015b4c181136660cabe1225c0187f565732 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 20 Nov 2019 10:29:18 +0100 Subject: [PATCH 283/352] Fix typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bb86afb3..8e0558b1 100644 --- a/README.md +++ b/README.md @@ -617,7 +617,7 @@ auth.login() # Method that builds and sends the AuthNRequest The ``AuthNRequest`` will be sent signed or unsigned based on the security info of the ``advanced_settings.json`` (``authnRequestsSigned``). -The IdP will then return the SAML Response to the user's client. The client is then forwarded to the **Attribute Consumer Service (ACS)** of the SP with this information. +The IdP will then return the SAML Response to the user's client. The client is then forwarded to the **Assertion Consumer Service (ACS)** of the SP with this information. We can set a ``return_to`` url parameter to the login function and that will be converted as a ``RelayState`` parameter: @@ -669,7 +669,7 @@ saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp ``` to get the settings object and with the ``sp_validation_only=True`` parameter we will avoid the IdP settings validation. -*** Attribute Consumer Service (ACS) *** +*** Assertion Consumer Service (ACS) *** This code handles the SAML response that the IdP forwards to the SP through the user's client. From 52871773e7af8ae6a7bfab541ad3f364ba157871 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 20 Nov 2019 10:31:12 +0100 Subject: [PATCH 284/352] Fix parameter type annotations in merge_settings --- src/onelogin/saml2/idp_metadata_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 55055921..33312b00 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -221,10 +221,10 @@ def merge_settings(settings, new_metadata_settings): Will update the settings with the provided new settings data extracted from the IdP metadata :param settings: Current settings dict data - :type settings: string + :type settings: dict :param new_metadata_settings: Settings to be merged (extracted from IdP metadata after parsing) - :type new_metadata_settings: string + :type new_metadata_settings: dict :returns: merged settings :rtype: dict From 2dc6f314b3be61fff6d50fe55b8a9597f0144641 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 20 Nov 2019 13:09:03 +0100 Subject: [PATCH 285/352] Update demos --- demo-django/demo/views.py | 5 +++-- demo-django/requirements.txt | 2 +- demo-flask/index.py | 6 ++++++ demo-flask/templates/index.html | 3 +++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index 40ebdd66..b6bf0cfd 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -88,8 +88,7 @@ def index(request): request.session['samlSessionIndex'] = auth.get_session_index() if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState'])) - else: - if auth.get_settings().is_debug_active(): + elif auth.get_settings().is_debug_active(): error_reason = auth.get_last_error_reason() elif 'sls' in req['get_data']: request_id = None @@ -103,6 +102,8 @@ def index(request): return HttpResponseRedirect(url) else: success_slo = True + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() if 'samlUserdata' in request.session: paint_logout = True diff --git a/demo-django/requirements.txt b/demo-django/requirements.txt index d0d5a268..62648b8c 100644 --- a/demo-django/requirements.txt +++ b/demo-django/requirements.txt @@ -1 +1 @@ -Django==1.11.23 +Django==1.11.26 diff --git a/demo-flask/index.py b/demo-flask/index.py index e4c11c6f..2b748182 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -40,6 +40,7 @@ def index(): req = prepare_flask_request(request) auth = init_saml_auth(req) errors = [] + error_reason = None not_auth_warn = False success_slo = False attributes = False @@ -91,6 +92,8 @@ def index(): self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.form and self_url != request.form['RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() elif 'sls' in request.args: request_id = None if 'LogoutRequestID' in session: @@ -103,6 +106,8 @@ def index(): return redirect(url) else: success_slo = True + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() if 'samlUserdata' in session: paint_logout = True @@ -112,6 +117,7 @@ def index(): return render_template( 'index.html', errors=errors, + error_reason=error_reason, not_auth_warn=not_auth_warn, success_slo=success_slo, attributes=attributes, diff --git a/demo-flask/templates/index.html b/demo-flask/templates/index.html index ad42cf5c..c7d5137c 100644 --- a/demo-flask/templates/index.html +++ b/demo-flask/templates/index.html @@ -10,6 +10,9 @@
  • {{err}}
  • {% endfor %} + {% if error_reason %} + {{error_reason}} + {% endif %}
    {% endif %} From e4d3ba43865214a4999b48f82b1e32740698670c Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 20 Nov 2019 16:46:49 +0100 Subject: [PATCH 286/352] Update signature validation test when no reference uri --- ...nse_without_assertion_reference_uri.xml.base64 | 1 + .../response_without_reference_uri.xml.base64 | 2 +- tests/src/OneLogin/saml2_tests/response_test.py | 15 ++++++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 tests/data/responses/response_without_assertion_reference_uri.xml.base64 diff --git a/tests/data/responses/response_without_assertion_reference_uri.xml.base64 b/tests/data/responses/response_without_assertion_reference_uri.xml.base64 new file mode 100644 index 00000000..8c4dab5d --- /dev/null +++ b/tests/data/responses/response_without_assertion_reference_uri.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0icGZ4ZDU5NDM0N2QtNDk1Zi1iOGQxLTBlZTItNDFjZmRhMTRkZDM1IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNS0wMS0wMlQyMjo0ODo0OFoiIERlc3RpbmF0aW9uPSJodHRwOi8vbG9jYWxob3N0OjkwMDEvdjEvdXNlcnMvYXV0aG9yaXplL3NhbWwiIENvbnNlbnQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjb25zZW50OnVuc3BlY2lmaWVkIiBJblJlc3BvbnNlVG89Il9lZDkxNWE0MC03NGZiLTAxMzItNWIxNi00OGUwZWIxNGExYzciPgogIDxJc3N1ZXIgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHA6Ly9leGFtcGxlLmNvbTwvSXNzdWVyPgogIDxzYW1scDpTdGF0dXM+CiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+CiAgPC9zYW1scDpTdGF0dXM+CgogIDxBc3NlcnRpb24geG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNzAwYWMzMjAtNzRmZi0wMTMyLTViMTQtNDhlMGViMTRhMWM3IiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBWZXJzaW9uPSIyLjAiPgogICAgPElzc3Vlcj5odHRwOi8vZXhhbXBsZS5jb208L0lzc3Vlcj4KICAgIDxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPgogICAgPGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICAgIDxkczpSZWZlcmVuY2UgVVJJPSIiPgogICAgICA8ZHM6VHJhbnNmb3Jtcz4KICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgIDwvZHM6VHJhbnNmb3Jtcz4KICAgICAgPGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+CiAgICAgIDxkczpEaWdlc3RWYWx1ZT5qQ2dlWENQREZsd2pUZ3FnUHAwbVUyVHF3OWc9PC9kczpEaWdlc3RWYWx1ZT4KICAgIDwvZHM6UmVmZXJlbmNlPgogIDwvZHM6U2lnbmVkSW5mbz4KICA8ZHM6U2lnbmF0dXJlVmFsdWU+bG9SN21DRmlNSURIUHBLeVgzRUd2dzJYeTZycEtFZWZVMDhYS1lWRXJ6MXB3a1BUUFFlYU5iK2RGMHZLai9rNQoyUmJ2Z3ZFUFN2ZGI3RDJOMTY5QjJMTGVmbXpaWTBDY0RKcThkK3lNbnZSNER3YitSUFl6bWJoS29XQ1ZyY3VPCnNvbEUxQTg3WFZjenNpd2JYRWllM2p4RHdDSk5vWi9GRFJRZy80RHRQVmc9PC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+CiAgPGRzOlg1MDlEYXRhPgogICAgPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDVnpDQ0FjQUNDUURJVkhhTlNCWUw2VEFOQmdrcWhraUc5dzBCQVFzRkFEQndNUXN3Q1FZRFZRUUdFd0pHVWpFT01Bd0dBMVVFQ0F3RlVHRnlhWE14RGpBTUJnTlZCQWNNQlZCaGNtbHpNUll3RkFZRFZRUUtEQTFPYjNaaGNHOXpkQ0JVUlZOVU1Ta3dKd1lKS29aSWh2Y05BUWtCRmhwbWJHOXlaVzUwTG5CcFoyOTFkRUJ1YjNaaGNHOXpkQzVtY2pBZUZ3MHhOREF5TVRNeE16VXpOREJhRncweE5UQXlNVE14TXpVek5EQmFNSEF4Q3pBSkJnTlZCQVlUQWtaU01RNHdEQVlEVlFRSURBVlFZWEpwY3pFT01Bd0dBMVVFQnd3RlVHRnlhWE14RmpBVUJnTlZCQW9NRFU1dmRtRndiM04wSUZSRlUxUXhLVEFuQmdrcWhraUc5dzBCQ1FFV0dtWnNiM0psYm5RdWNHbG5iM1YwUUc1dmRtRndiM04wTG1aeU1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ2hMRkhuM0xuTjRKUS83V0NkWXVweGtVZ2NOT1FuUEYreWxsKy9EUHB1eDlucGZZMDU5UElVYXRCOFg3a0NuNWk4dFJ3SXkvaWtISlI2TXI4K01QdmM2Vk9aRHhQTmRadk1vLzhsaHhyYk4zSmRydzN3aFptVS9LUFI5RjNCZEZkdStTTHpyTWwxVERVWmxQdFk5WHpVRlhjcU44SVhjeThUSnpDQmVOZXkzUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJDd1VBQTRHQkFDdEo4ZmVHemUxTkhCNVZ3MThqTVVQdkhvN0gzR3dtajZaREFYUWxhaUFYTXVOQnhOWFZXVndpZmw2VituVzN3OVFhN0Zlby9uWi9PNFRVT0gxbnorYWRrbGNDRDRRcFphRUlibUFicmlQV0pLZ2I0TFdHaHFRcnV3WVI3SXRUUjFNTlg5Z0xiUDB6MHp2REVRbm50L1ZVV0ZFQkxTSnE0WjROcmU4TEZtUzI8L2RzOlg1MDlDZXJ0aWZpY2F0ZT4KICA8L2RzOlg1MDlEYXRhPgo8L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PFN1YmplY3Q+CiAgICAgIDxOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnNhbWxAdXNlci5jb208L05hbWVJRD4KICAgICAgPFN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4KICAgICAgICA8U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgSW5SZXNwb25zZVRvPSJfZWQ5MTVhNDAtNzRmYi0wMTMyLTViMTYtNDhlMGViMTRhMWM3IiBOb3RPbk9yQWZ0ZXI9IjIwMzgtMDEtMDJUMjI6NTE6NDhaIiBSZWNpcGllbnQ9Imh0dHA6Ly9sb2NhbGhvc3Q6OTAwMS92MS91c2Vycy9hdXRob3JpemUvc2FtbCIvPgogICAgICA8L1N1YmplY3RDb25maXJtYXRpb24+CiAgICA8L1N1YmplY3Q+CiAgICA8Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTUtMDEtMDJUMjI6NDg6NDNaIiBOb3RPbk9yQWZ0ZXI9IjIwMzgtMDEtMDJUMjM6NDg6NDhaIj4KICAgICAgPEF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICAgICAgPEF1ZGllbmNlPmh0dHA6Ly9sb2NhbGhvc3Q6OTAwMS88L0F1ZGllbmNlPgogICAgICAgIDxBdWRpZW5jZT5mbGF0X3dvcmxkPC9BdWRpZW5jZT4KICAgICAgPC9BdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgPC9Db25kaXRpb25zPgogICAgPEF0dHJpYnV0ZVN0YXRlbWVudD4KICAgICAgPEF0dHJpYnV0ZSBOYW1lPSJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiPgogICAgICAgIDxBdHRyaWJ1dGVWYWx1ZT5zYW1sQHVzZXIuY29tPC9BdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9BdHRyaWJ1dGU+CiAgICA8L0F0dHJpYnV0ZVN0YXRlbWVudD4KICAgIDxBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBTZXNzaW9uSW5kZXg9Il83MDBhYzMyMC03NGZmLTAxMzItNWIxNC00OGUwZWIxNGExYzciPgogICAgICA8QXV0aG5Db250ZXh0PgogICAgICAgIDxBdXRobkNvbnRleHRDbGFzc1JlZj51cm46ZmVkZXJhdGlvbjphdXRoZW50aWNhdGlvbjp3aW5kb3dzPC9BdXRobkNvbnRleHRDbGFzc1JlZj4KICAgICAgPC9BdXRobkNvbnRleHQ+CiAgICA8L0F1dGhuU3RhdGVtZW50PgogIDwvQXNzZXJ0aW9uPgo8L3NhbWxwOlJlc3BvbnNlPgo= diff --git a/tests/data/responses/response_without_reference_uri.xml.base64 b/tests/data/responses/response_without_reference_uri.xml.base64 index dd5f7b50..d830db01 100644 --- a/tests/data/responses/response_without_reference_uri.xml.base64 +++ b/tests/data/responses/response_without_reference_uri.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgSUQ9InBmeGQ1OTQzNDdkLTQ5NWYtYjhkMS0wZWUyLTQxY2ZkYTE0ZGQzNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo5MDAxL3YxL3VzZXJzL2F1dGhvcml6ZS9zYW1sIiBDb25zZW50PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y29uc2VudDp1bnNwZWNpZmllZCIgSW5SZXNwb25zZVRvPSJfZWQ5MTVhNDAtNzRmYi0wMTMyLTViMTYtNDhlMGViMTRhMWM3Ij4NCiAgPElzc3VlciB4bWxucz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+aHR0cDovL2V4YW1wbGUuY29tPC9Jc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT5qQ2dlWENQREZsd2pUZ3FnUHAwbVUyVHF3OWc9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPkRmdXByMTh3UityRGFndENQRWZRbFNHSHp3NE5kZlBIWjRIc3pGZTFKUENKWGpmYnlFTTFmZytqemdHYk1NdDZYemdDWGNLSk03RS9DUFNURGt2TWUzRFVKbEh1NERodURPQXovRHN5b0J3V3VWK1JmM1dpTmNGNFhDYzl3QlF6dm4vYXREN3pXNnh3TzdOL2hrQVpKcWZ2SmRkbnBNTUhLR1hxRy9aSFpBdz08L2RzOlNpZ25hdHVyZVZhbHVlPg0KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ3FEQ0NBaEdnQXdJQkFnSUJBREFOQmdrcWhraUc5dzBCQVEwRkFEQnhNUXN3Q1FZRFZRUUdFd0oxY3pFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRWlNQ0FHQTFVRUNnd1pSbXhoZENCWGIzSnNaQ0JMYm05M2JHVmtaMlVzSUVsdVl6RWNNQm9HQTFVRUF3d1RiR1ZoY200dVpteGhkSGR2Y214a0xtTnZiVEVMTUFrR0ExVUVCd3dDUkVNd0hoY05NVFV3TnpBNE1EazFPVEF6V2hjTk1qVXdOekExTURrMU9UQXpXakJ4TVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tWMkZ6YUdsdVozUnZiakVpTUNBR0ExVUVDZ3daUm14aGRDQlhiM0pzWkNCTGJtOTNiR1ZrWjJVc0lFbHVZekVjTUJvR0ExVUVBd3dUYkdWaGNtNHVabXhoZEhkdmNteGtMbU52YlRFTE1Ba0dBMVVFQnd3Q1JFTXdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JBTVBEd3NsNW82eDJRb3VOaTEvRTdJVXFSWWoyWW9jSlJGc3VFR1RldnlVKzJhRkNhQk5WL3R0NnNBYk05V1N1dEx1cWpFL2hmYm5sRWNaMDMrZ24wQ29MbDZZbXdiS0tlUnBrSXplVmhveUoxWVlNUUVBVmhMcmR5OFBvd3U4VUNaMFBiQXorbjlka2lSek01cENDTzc3K2d5Y0ZUQkZLSEFBOXFJcFVaWmtQQWdNQkFBR2pVREJPTUIwR0ExVWREZ1FXQkJRSFU1OGl1R3hGbFp1ckJVSndvbGFsSnIrRlJ6QWZCZ05WSFNNRUdEQVdnQlFIVTU4aXVHeEZsWnVyQlVKd29sYWxKcitGUnpBTUJnTlZIUk1FQlRBREFRSC9NQTBHQ1NxR1NJYjNEUUVCRFFVQUE0R0JBQzZpSGZNbWQraE1TUnpma29zaTNDK3d2cUhDTEVVc2czSEZwa1ptNWp4bVREbEY1cU8rQnQwbjB4bWZvcVdCekJNbE5DOFRzR3JhZmhKM3p1OEdORjBMZW8xMXJmYzFHTUdCdnI1SG9aM1dBQXltbkJFREFBb3N4TjZXWlJtajF4YWdhMTMrNnBXZkdCKysyblB3Y1pXUC84ZGtQY1JvZ2V2VjB4MHA1Njg2PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCg0KICA8QXNzZXJ0aW9uIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iXzcwMGFjMzIwLTc0ZmYtMDEzMi01YjE0LTQ4ZTBlYjE0YTFjNyIgSXNzdWVJbnN0YW50PSIyMDE1LTAxLTAyVDIyOjQ4OjQ4WiIgVmVyc2lvbj0iMi4wIj4NCiAgICA8SXNzdWVyPmh0dHA6Ly9leGFtcGxlLmNvbTwvSXNzdWVyPg0KICAgIDxTdWJqZWN0Pg0KICAgICAgPE5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c2FtbEB1c2VyLmNvbTwvTmFtZUlEPg0KICAgICAgPFN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPFN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iX2VkOTE1YTQwLTc0ZmItMDEzMi01YjE2LTQ4ZTBlYjE0YTFjNyIgTm90T25PckFmdGVyPSIyMDM4LTAxLTAyVDIyOjUxOjQ4WiIgUmVjaXBpZW50PSJodHRwOi8vbG9jYWxob3N0OjkwMDEvdjEvdXNlcnMvYXV0aG9yaXplL3NhbWwiLz4NCiAgICAgIDwvU3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L1N1YmplY3Q+DQogICAgPENvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE1LTAxLTAyVDIyOjQ4OjQzWiIgTm90T25PckFmdGVyPSIyMDM4LTAxLTAyVDIzOjQ4OjQ4WiI+DQogICAgICA8QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPEF1ZGllbmNlPmh0dHA6Ly9sb2NhbGhvc3Q6OTAwMS88L0F1ZGllbmNlPg0KICAgICAgICA8QXVkaWVuY2U+ZmxhdF93b3JsZDwvQXVkaWVuY2U+DQogICAgICA8L0F1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9Db25kaXRpb25zPg0KICAgIDxBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8QXR0cmlidXRlIE5hbWU9Imh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL2VtYWlsYWRkcmVzcyI+DQogICAgICAgIDxBdHRyaWJ1dGVWYWx1ZT5zYW1sQHVzZXIuY29tPC9BdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvQXR0cmlidXRlPg0KICAgIDwvQXR0cmlidXRlU3RhdGVtZW50Pg0KICAgIDxBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBTZXNzaW9uSW5kZXg9Il83MDBhYzMyMC03NGZmLTAxMzItNWIxNC00OGUwZWIxNGExYzciPg0KICAgICAgPEF1dGhuQ29udGV4dD4NCiAgICAgICAgPEF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpmZWRlcmF0aW9uOmF1dGhlbnRpY2F0aW9uOndpbmRvd3M8L0F1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9BdXRobkNvbnRleHQ+DQogICAgPC9BdXRoblN0YXRlbWVudD4NCiAgPC9Bc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== +PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0icGZ4ZDU5NDM0N2QtNDk1Zi1iOGQxLTBlZTItNDFjZmRhMTRkZDM1IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNS0wMS0wMlQyMjo0ODo0OFoiIERlc3RpbmF0aW9uPSJodHRwOi8vbG9jYWxob3N0OjkwMDEvdjEvdXNlcnMvYXV0aG9yaXplL3NhbWwiIENvbnNlbnQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjb25zZW50OnVuc3BlY2lmaWVkIiBJblJlc3BvbnNlVG89Il9lZDkxNWE0MC03NGZiLTAxMzItNWIxNi00OGUwZWIxNGExYzciPgogIDxJc3N1ZXIgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHA6Ly9leGFtcGxlLmNvbTwvSXNzdWVyPgogIDxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPgogICAgPGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICAgIDxkczpSZWZlcmVuY2UgVVJJPSIiPgogICAgICA8ZHM6VHJhbnNmb3Jtcz4KICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgIDwvZHM6VHJhbnNmb3Jtcz4KICAgICAgPGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+CiAgICAgIDxkczpEaWdlc3RWYWx1ZT5qQ2dlWENQREZsd2pUZ3FnUHAwbVUyVHF3OWc9PC9kczpEaWdlc3RWYWx1ZT4KICAgIDwvZHM6UmVmZXJlbmNlPgogIDwvZHM6U2lnbmVkSW5mbz4KICA8ZHM6U2lnbmF0dXJlVmFsdWU+bG9SN21DRmlNSURIUHBLeVgzRUd2dzJYeTZycEtFZWZVMDhYS1lWRXJ6MXB3a1BUUFFlYU5iK2RGMHZLai9rNQoyUmJ2Z3ZFUFN2ZGI3RDJOMTY5QjJMTGVmbXpaWTBDY0RKcThkK3lNbnZSNER3YitSUFl6bWJoS29XQ1ZyY3VPCnNvbEUxQTg3WFZjenNpd2JYRWllM2p4RHdDSk5vWi9GRFJRZy80RHRQVmc9PC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+CiAgPGRzOlg1MDlEYXRhPgogICAgPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDVnpDQ0FjQUNDUURJVkhhTlNCWUw2VEFOQmdrcWhraUc5dzBCQVFzRkFEQndNUXN3Q1FZRFZRUUdFd0pHVWpFT01Bd0dBMVVFQ0F3RlVHRnlhWE14RGpBTUJnTlZCQWNNQlZCaGNtbHpNUll3RkFZRFZRUUtEQTFPYjNaaGNHOXpkQ0JVUlZOVU1Ta3dKd1lKS29aSWh2Y05BUWtCRmhwbWJHOXlaVzUwTG5CcFoyOTFkRUJ1YjNaaGNHOXpkQzVtY2pBZUZ3MHhOREF5TVRNeE16VXpOREJhRncweE5UQXlNVE14TXpVek5EQmFNSEF4Q3pBSkJnTlZCQVlUQWtaU01RNHdEQVlEVlFRSURBVlFZWEpwY3pFT01Bd0dBMVVFQnd3RlVHRnlhWE14RmpBVUJnTlZCQW9NRFU1dmRtRndiM04wSUZSRlUxUXhLVEFuQmdrcWhraUc5dzBCQ1FFV0dtWnNiM0psYm5RdWNHbG5iM1YwUUc1dmRtRndiM04wTG1aeU1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ2hMRkhuM0xuTjRKUS83V0NkWXVweGtVZ2NOT1FuUEYreWxsKy9EUHB1eDlucGZZMDU5UElVYXRCOFg3a0NuNWk4dFJ3SXkvaWtISlI2TXI4K01QdmM2Vk9aRHhQTmRadk1vLzhsaHhyYk4zSmRydzN3aFptVS9LUFI5RjNCZEZkdStTTHpyTWwxVERVWmxQdFk5WHpVRlhjcU44SVhjeThUSnpDQmVOZXkzUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJDd1VBQTRHQkFDdEo4ZmVHemUxTkhCNVZ3MThqTVVQdkhvN0gzR3dtajZaREFYUWxhaUFYTXVOQnhOWFZXVndpZmw2VituVzN3OVFhN0Zlby9uWi9PNFRVT0gxbnorYWRrbGNDRDRRcFphRUlibUFicmlQV0pLZ2I0TFdHaHFRcnV3WVI3SXRUUjFNTlg5Z0xiUDB6MHp2REVRbm50L1ZVV0ZFQkxTSnE0WjROcmU4TEZtUzI8L2RzOlg1MDlDZXJ0aWZpY2F0ZT4KICA8L2RzOlg1MDlEYXRhPgo8L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz4KICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz4KICA8L3NhbWxwOlN0YXR1cz4KCiAgPEFzc2VydGlvbiB4bWxucz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9Il83MDBhYzMyMC03NGZmLTAxMzItNWIxNC00OGUwZWIxNGExYzciIElzc3VlSW5zdGFudD0iMjAxNS0wMS0wMlQyMjo0ODo0OFoiIFZlcnNpb249IjIuMCI+CiAgICA8SXNzdWVyPmh0dHA6Ly9leGFtcGxlLmNvbTwvSXNzdWVyPgogICAgPFN1YmplY3Q+CiAgICAgIDxOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnNhbWxAdXNlci5jb208L05hbWVJRD4KICAgICAgPFN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4KICAgICAgICA8U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgSW5SZXNwb25zZVRvPSJfZWQ5MTVhNDAtNzRmYi0wMTMyLTViMTYtNDhlMGViMTRhMWM3IiBOb3RPbk9yQWZ0ZXI9IjIwMzgtMDEtMDJUMjI6NTE6NDhaIiBSZWNpcGllbnQ9Imh0dHA6Ly9sb2NhbGhvc3Q6OTAwMS92MS91c2Vycy9hdXRob3JpemUvc2FtbCIvPgogICAgICA8L1N1YmplY3RDb25maXJtYXRpb24+CiAgICA8L1N1YmplY3Q+CiAgICA8Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTUtMDEtMDJUMjI6NDg6NDNaIiBOb3RPbk9yQWZ0ZXI9IjIwMzgtMDEtMDJUMjM6NDg6NDhaIj4KICAgICAgPEF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICAgICAgPEF1ZGllbmNlPmh0dHA6Ly9sb2NhbGhvc3Q6OTAwMS88L0F1ZGllbmNlPgogICAgICAgIDxBdWRpZW5jZT5mbGF0X3dvcmxkPC9BdWRpZW5jZT4KICAgICAgPC9BdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgPC9Db25kaXRpb25zPgogICAgPEF0dHJpYnV0ZVN0YXRlbWVudD4KICAgICAgPEF0dHJpYnV0ZSBOYW1lPSJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiPgogICAgICAgIDxBdHRyaWJ1dGVWYWx1ZT5zYW1sQHVzZXIuY29tPC9BdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9BdHRyaWJ1dGU+CiAgICA8L0F0dHJpYnV0ZVN0YXRlbWVudD4KICAgIDxBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBTZXNzaW9uSW5kZXg9Il83MDBhYzMyMC03NGZmLTAxMzItNWIxNC00OGUwZWIxNGExYzciPgogICAgICA8QXV0aG5Db250ZXh0PgogICAgICAgIDxBdXRobkNvbnRleHRDbGFzc1JlZj51cm46ZmVkZXJhdGlvbjphdXRoZW50aWNhdGlvbjp3aW5kb3dzPC9BdXRobkNvbnRleHRDbGFzc1JlZj4KICAgICAgPC9BdXRobkNvbnRleHQ+CiAgICA8L0F1dGhuU3RhdGVtZW50PgogIDwvQXNzZXJ0aW9uPgo8L3NhbWxwOlJlc3BvbnNlPgo= diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 3fee66ab..a496c578 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1702,14 +1702,23 @@ def testIsValidSignUsingX509certMulti(self): response = OneLogin_Saml2_Response(settings, xml) self.assertTrue(response.is_valid(self.get_request_data())) - def testIsValidSignWithEmptyReferenceURI(self): + def testMessageSignedIsValidSignWithEmptyReferenceURI(self): settings_info = self.loadSettingsJSON() del settings_info['idp']['x509cert'] - settings_info['idp']['certFingerprint'] = "194d97e4d8c9c8cfa4b721e5ee497fd9660e5213" + settings_info['idp']['certFingerprint'] = "657302a5e11a4794a1e50a705988d66c9377575d" settings = OneLogin_Saml2_Settings(settings_info) xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - self.assertFalse(response.is_valid(self.get_request_data())) + self.assertTrue(response.is_valid(self.get_request_data())) + + def testAssertionSignedIsValidSignWithEmptyReferenceURI(self): + settings_info = self.loadSettingsJSON() + del settings_info['idp']['x509cert'] + settings_info['idp']['certFingerprint'] = "657302a5e11a4794a1e50a705988d66c9377575d" + settings = OneLogin_Saml2_Settings(settings_info) + xml = self.file_contents(join(self.data_path, 'responses', 'response_without_assertion_reference_uri.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertTrue(response.is_valid(self.get_request_data())) def testIsValidWithoutInResponseTo(self): """ From 2e799ab3b1a50758bd2064fc04a8d19a1931717d Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 20 Nov 2019 18:00:41 +0100 Subject: [PATCH 287/352] Release 2.8.0 --- changelog.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 8ce586f9..eb866956 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,10 @@ # python-saml changelog +### 2.8.0 (NOv 20, 2019) +* [#258](https://github.com/onelogin/python-saml/issues/258) Fix failOnAuthnContextMismatch feature +* [#250](https://github.com/onelogin/python-saml/issues/250) Allow any number of decimal places for seconds on SAML datetimes +* Update demo versions. Improve them and add Tornado demo. + + ### 2.7.0 (Sep 11, 2019) * Set true as the default value for strict setting diff --git a/setup.py b/setup.py index 7d545fb0..0c0cd8ca 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.7.0', + version='2.8.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', From 15564c6c9a231586b1987bee575bf0db0ccc5412 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 21 Feb 2020 10:43:11 +0100 Subject: [PATCH 288/352] Fix #269 Add sha256 instead sha1 algorithm for sign/digest as recommended value on documentation and settings --- README.md | 6 +++--- demo-bottle/saml/advanced_settings.json | 4 ++-- demo-django/saml/advanced_settings.json | 4 ++-- demo-flask/saml/advanced_settings.json | 4 ++-- demo_pyramid/demo_pyramid/saml/advanced_settings.json | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8e0558b1..21b49f0d 100644 --- a/README.md +++ b/README.md @@ -456,14 +456,14 @@ In addition to the required settings data (idp, sp), extra settings can be defin // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", // Algorithm that the toolkit will use on digest process. Options: // 'http://www.w3.org/2000/09/xmldsig#sha1' // 'http://www.w3.org/2001/04/xmlenc#sha256' // 'http://www.w3.org/2001/04/xmldsig-more#sha384' // 'http://www.w3.org/2001/04/xmlenc#sha512' - "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" }, // Contact information template, it is recommended to supply @@ -787,7 +787,7 @@ else: security = self.__settings.get_security_data() if 'logoutResponseSigned' in security and security['logoutResponseSigned']: - parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 + parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA256 parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None)) return self.redirect_to(self.get_slo_url(), parameters) diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json index 47ec8c28..022f70ef 100644 --- a/demo-bottle/saml/advanced_settings.json +++ b/demo-bottle/saml/advanced_settings.json @@ -10,8 +10,8 @@ "wantNameId" : true, "wantNameIdEncrypted": false, "wantAssertionsEncrypted": false, - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", - "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" }, "contactPerson": { "technical": { diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json index 7efb5d1b..022f70ef 100644 --- a/demo-django/saml/advanced_settings.json +++ b/demo-django/saml/advanced_settings.json @@ -10,8 +10,8 @@ "wantNameId" : true, "wantNameIdEncrypted": false, "wantAssertionsEncrypted": false, - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", - "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" }, "contactPerson": { "technical": { diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json index 7efb5d1b..022f70ef 100644 --- a/demo-flask/saml/advanced_settings.json +++ b/demo-flask/saml/advanced_settings.json @@ -10,8 +10,8 @@ "wantNameId" : true, "wantNameIdEncrypted": false, "wantAssertionsEncrypted": false, - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", - "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" }, "contactPerson": { "technical": { diff --git a/demo_pyramid/demo_pyramid/saml/advanced_settings.json b/demo_pyramid/demo_pyramid/saml/advanced_settings.json index 3115e17e..1307b0ae 100644 --- a/demo_pyramid/demo_pyramid/saml/advanced_settings.json +++ b/demo_pyramid/demo_pyramid/saml/advanced_settings.json @@ -10,8 +10,8 @@ "wantNameId" : true, "wantNameIdEncrypted": false, "wantAssertionsEncrypted": false, - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", - "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" }, "contactPerson": { "technical": { From d660880578aa8ea72d23ef9f420a85e2ff6354c5 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 12 Mar 2020 18:16:42 +0100 Subject: [PATCH 289/352] Add python2 deprecation info --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 21b49f0d..c481c2b8 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,12 @@ [![PyPi Version](https://img.shields.io/pypi/v/python-saml.svg)](https://pypi.python.org/pypi/python-saml) ![Python versions](https://img.shields.io/pypi/pyversions/python-saml.svg) +``` +Python 2 was deprecated on January 1, 2020. We recommend to migrate your project +to Python 3 and use python3-saml +``` + + Add SAML support to your Python software using this library. Forget those complicated libraries and use the open source library provided and supported by OneLogin Inc. From 2223d4c60207d1998af8d3c45bc7be7fd878072b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2020 16:48:30 +0000 Subject: [PATCH 290/352] Bump django from 1.11.26 to 1.11.29 in /demo-django Bumps [django](https://github.com/django/django) from 1.11.26 to 1.11.29. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/1.11.26...1.11.29) Signed-off-by: dependabot[bot] --- demo-django/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-django/requirements.txt b/demo-django/requirements.txt index 62648b8c..30eb6217 100644 --- a/demo-django/requirements.txt +++ b/demo-django/requirements.txt @@ -1 +1 @@ -Django==1.11.26 +Django==1.11.29 From d2337ca6f90cb377c97f748fa58188d52ed2dc05 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 3 Jan 2021 13:40:23 +0100 Subject: [PATCH 291/352] Fix tests --- tests/data/metadata/metadata_settings1.xml | 2 +- .../invalids/invalid_subjectconfirmation_nb.xml.base64 | 2 +- tests/data/responses/unsigned_response.xml.base64 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/data/metadata/metadata_settings1.xml b/tests/data/metadata/metadata_settings1.xml index c9529f00..e470c778 100644 --- a/tests/data/metadata/metadata_settings1.xml +++ b/tests/data/metadata/metadata_settings1.xml @@ -1,6 +1,6 @@ diff --git a/tests/data/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 b/tests/data/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 index ff6d371c..c88c4edb 100644 --- a/tests/data/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +++ b/tests/data/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdEJlZm9yZT0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdEJlZm9yZT0iMjA5OS0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/unsigned_response.xml.base64 b/tests/data/responses/unsigned_response.xml.base64 index 66892b53..64815b02 100644 --- a/tests/data/responses/unsigned_response.xml.base64 +++ b/tests/data/responses/unsigned_response.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file From 60c8cec1451c27744628cfeca49885ee3a6efec5 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 7 Jan 2021 21:48:29 +0100 Subject: [PATCH 292/352] Add Wrapping attack test from pysaml2 --- .../invalids/signature_wrapping_attack2.xml.base64 | 1 + tests/src/OneLogin/saml2_tests/response_test.py | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 tests/data/responses/invalids/signature_wrapping_attack2.xml.base64 diff --git a/tests/data/responses/invalids/signature_wrapping_attack2.xml.base64 b/tests/data/responses/invalids/signature_wrapping_attack2.xml.base64 new file mode 100644 index 00000000..408b2f77 --- /dev/null +++ b/tests/data/responses/invalids/signature_wrapping_attack2.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxuczA6UmVzcG9uc2UgeG1sbnM6bnMwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOm5zMT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6bnMyPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xpbmdvbi5jYXRhbG9naXguc2U6ODA4Ny8iIElEPSJpZC12cU9RNzJKQ3BwWGFCV25CRSIgSW5SZXNwb25zZVRvPSJpZDEyIiBJc3N1ZUluc3RhbnQ9IjIwMTktMTItMjBUMTI6MTU6MTZaIiBWZXJzaW9uPSIyLjAiPjxuczE6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij51cm46bWFjZTpleGFtcGxlLmNvbTpzYW1sOnJvbGFuZDppZHA8L25zMTpJc3N1ZXI+PG5zMDpTdGF0dXM+PG5zMDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L25zMDpTdGF0dXM+PG5zMTpBc3NlcnRpb24gSUQ9ImlkLVNQT09GRURfQVNTRVJUSU9OIiBJc3N1ZUluc3RhbnQ9IjIwMTktMTItMjBUMTI6MTU6MTZaIiBWZXJzaW9uPSIyLjAiPjxuczE6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij51cm46bWFjZTpleGFtcGxlLmNvbTpzYW1sOnJvbGFuZDppZHA8L25zMTpJc3N1ZXI+PG5zMjpTaWduYXR1cmUgSWQ9IlNpZ25hdHVyZTIiPjxuczI6U2lnbmVkSW5mbz48bnMyOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48bnMyOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPjxuczI6UmVmZXJlbmNlIFVSST0iI2lkLUFhOUlXZkR4SlZJWDZHUXllIj48bnMyOlRyYW5zZm9ybXM+PG5zMjpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxuczI6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9uczI6VHJhbnNmb3Jtcz48bnMyOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PG5zMjpEaWdlc3RWYWx1ZT5FV0J2UVVscndRYnRyQWp1VVhrU0JBVnNaNTA9PC9uczI6RGlnZXN0VmFsdWU+PC9uczI6UmVmZXJlbmNlPjwvbnMyOlNpZ25lZEluZm8+PG5zMjpTaWduYXR1cmVWYWx1ZT5tNHpSZ1RXbGVNY3gxZEZib2VpWWxiaURpZ0hXQVZoSFZhK0dMTisrRUxOTUZEdXR1ekJ4YzN0dTZva3lhTlFHVzNsZXUzMnd6YmZkcGI1KzNSbHBHb0tqMndQWDU3MC9FTUpqNHV3OTFYZlhzWmZwTlArNUdsZ05UOHcvZWxEbUJYaEcvS3dtU080NzdJbWswc3pLb3ZUQk1WSG1vM1FPZCtiYS8vZFZzSkU9PC9uczI6U2lnbmF0dXJlVmFsdWU+PG5zMjpLZXlJbmZvPjxuczI6WDUwOURhdGE+PG5zMjpYNTA5Q2VydGlmaWNhdGU+TUlJQ3NEQ0NBaG1nQXdJQkFnSUpBSnJ6cVNTd21EWTlNQTBHQ1NxR1NJYjNEUUVCQlFVQU1FVXhDekFKQmdOVkJBWVRBa0ZWTVJNd0VRWURWUVFJRXdwVGIyMWxMVk4wWVhSbE1TRXdId1lEVlFRS0V4aEpiblJsY201bGRDQlhhV1JuYVhSeklGQjBlU0JNZEdRd0hoY05NRGt4TURBMk1UazBPVFF4V2hjTk1Ea3hNVEExTVRrME9UUXhXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNCTUtVMjl0WlMxVGRHRjBaVEVoTUI4R0ExVUVDaE1ZU1c1MFpYSnVaWFFnVjJsa1oybDBjeUJRZEhrZ1RIUmtNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURKZzJjbXM3TXFqbmlUOEZpL1hrTkhaTlBiTlZReU1VTVhFOXRYT2Rxd1lDQTFjYzh2UWR6a2loc2NRTVh5M2lQdzJjTWdnQnU2Z2pNVE9TT3hFQ2t1dlg1WkNjbEtyOHBYQUpNNWNZNmdWT2FWTzJQZFRaY3ZEQktHYmlhTmVmaUV3NWhub1pvbXFaR3A4d0hOTEFVa3d0SDl2anFxdnh5Uy92Y2xjNmsyZXdJREFRQUJvNEduTUlHa01CMEdBMVVkRGdRV0JCUmVQc0tIS1lKc2lvakU3OFpXWGNjSzlLNGFKVEIxQmdOVkhTTUViakJzZ0JSZVBzS0hLWUpzaW9qRTc4WldYY2NLOUs0YUphRkpwRWN3UlRFTE1Ba0dBMVVFQmhNQ1FWVXhFekFSQmdOVkJBZ1RDbE52YldVdFUzUmhkR1V4SVRBZkJnTlZCQW9UR0VsdWRHVnlibVYwSUZkcFpHZHBkSE1nVUhSNUlFeDBaSUlKQUpyenFTU3dtRFk5TUF3R0ExVWRFd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVGQlFBRGdZRUFKU3JLT0V6SE83VEw1Y3k2aDNxaCszK0pBazhIYkdCVytjYlg2S0JDQXcvbXpVOGZsSzI1dm5Xd1hTM2R2MkZGM0FvZDAvUzdBV05mS2liNVUvU0E5bkphei9tV2VGOVMwZmFyejlBUUZjOC9OU3pBemFWcTdZYk00RjZmNk4yRlJsN0dpa2RYUkNlZDQ1ajZtclB6R3prM0VDYnVwRm5xeVJFSDMrWlBTZGs9PC9uczI6WDUwOUNlcnRpZmljYXRlPjwvbnMyOlg1MDlEYXRhPjwvbnMyOktleUluZm8+PC9uczI6U2lnbmF0dXJlPjxuczE6U3ViamVjdD48bnMxOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCIgTmFtZVF1YWxpZmllcj0iIiBTUE5hbWVRdWFsaWZpZXI9ImlkMTIiPkFOT1RIRVJfSUQ8L25zMTpOYW1lSUQ+PG5zMTpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PG5zMTpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBJblJlc3BvbnNlVG89ImlkMTIiIE5vdE9uT3JBZnRlcj0iMjAxOS0xMi0yMFQxMjoyMDoxNloiIFJlY2lwaWVudD0iaHR0cDovL2xpbmdvbi5jYXRhbG9naXguc2U6ODA4Ny8iLz48L25zMTpTdWJqZWN0Q29uZmlybWF0aW9uPjwvbnMxOlN1YmplY3Q+PG5zMTpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxOS0xMi0yMFQxMjoxNToxNloiIE5vdE9uT3JBZnRlcj0iMjAxOS0xMi0yMFQxMjoyMDoxNloiPjxuczE6QXVkaWVuY2VSZXN0cmljdGlvbj48bnMxOkF1ZGllbmNlPnVybjptYWNlOmV4YW1wbGUuY29tOnNhbWw6cm9sYW5kOnNwPC9uczE6QXVkaWVuY2U+PC9uczE6QXVkaWVuY2VSZXN0cmljdGlvbj48L25zMTpDb25kaXRpb25zPjxuczE6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE5LTEyLTIwVDEyOjE1OjE2WiIgU2Vzc2lvbkluZGV4PSJpZC1lRWhOQ2M1QlNpZXNWT2w4QiI+PG5zMTpBdXRobkNvbnRleHQ+PG5zMTpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpJbnRlcm5ldFByb3RvY29sUGFzc3dvcmQ8L25zMTpBdXRobkNvbnRleHRDbGFzc1JlZj48bnMxOkF1dGhlbnRpY2F0aW5nQXV0aG9yaXR5Pmh0dHA6Ly93d3cuZXhhbXBsZS5jb20vbG9naW48L25zMTpBdXRoZW50aWNhdGluZ0F1dGhvcml0eT48L25zMTpBdXRobkNvbnRleHQ+PC9uczE6QXV0aG5TdGF0ZW1lbnQ+PG5zMTpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PG5zMTpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZT0idXJuOm9pZDoxLjMuNi4xLjQuMS41OTIzLjEuMS4xLjEiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48bnMxOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeHNpOnR5cGU9InhzOnN0cmluZyI+c3RhZmY8L25zMTpBdHRyaWJ1dGVWYWx1ZT48bnMxOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeHNpOnR5cGU9InhzOnN0cmluZyI+QURNSU48L25zMTpBdHRyaWJ1dGVWYWx1ZT48L25zMTpBdHRyaWJ1dGU+PG5zMTpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJtYWlsIiBOYW1lPSJ1cm46b2lkOjAuOS4yMzQyLjE5MjAwMzAwLjEwMC4xLjMiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48bnMxOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeHNpOnR5cGU9InhzOnN0cmluZyI+SEFDS0VSQGdtYWlsLmNvbTwvbnMxOkF0dHJpYnV0ZVZhbHVlPjwvbnMxOkF0dHJpYnV0ZT48bnMxOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9ImdpdmVuTmFtZSIgTmFtZT0idXJuOm9pZDoyLjUuNC40MiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxuczE6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5EZXJlazwvbnMxOkF0dHJpYnV0ZVZhbHVlPjwvbnMxOkF0dHJpYnV0ZT48bnMxOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9InN1ck5hbWUiIE5hbWU9InVybjpvaWQ6Mi41LjQuNCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxuczE6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5KZXRlcjwvbnMxOkF0dHJpYnV0ZVZhbHVlPjwvbnMxOkF0dHJpYnV0ZT48bnMxOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9InRpdGxlIiBOYW1lPSJ1cm46b2lkOjIuNS40LjEyIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PG5zMTpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNob3J0c3RvcDwvbnMxOkF0dHJpYnV0ZVZhbHVlPjwvbnMxOkF0dHJpYnV0ZT48L25zMTpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9uczE6QXNzZXJ0aW9uPg0KPFhTV19BVFRBQ0s+DQo8bnMxOkFzc2VydGlvbiBJRD0iaWQtQWE5SVdmRHhKVklYNkdReWUiIElzc3VlSW5zdGFudD0iMjAxOS0xMi0yMFQxMjoxNToxNloiIFZlcnNpb249IjIuMCI+PG5zMTpJc3N1ZXIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPnVybjptYWNlOmV4YW1wbGUuY29tOnNhbWw6cm9sYW5kOmlkcDwvbnMxOklzc3Vlcj48bnMxOlN1YmplY3Q+PG5zMTpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiIE5hbWVRdWFsaWZpZXI9IiIgU1BOYW1lUXVhbGlmaWVyPSJpZDEyIj5hYzViMjJiYjhlYWM0YTI2ZWQwN2E1NTQzMmEwZmUwZGEyNDNmNmU5MTFhYTYxNGNmZjQwMmM0NGQ3Y2RlYzM2PC9uczE6TmFtZUlEPjxuczE6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxuczE6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgSW5SZXNwb25zZVRvPSJpZDEyIiBOb3RPbk9yQWZ0ZXI9IjIwMTktMTItMjBUMTI6MjA6MTZaIiBSZWNpcGllbnQ9Imh0dHA6Ly9saW5nb24uY2F0YWxvZ2l4LnNlOjgwODcvIi8+PC9uczE6U3ViamVjdENvbmZpcm1hdGlvbj48L25zMTpTdWJqZWN0PjxuczE6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTktMTItMjBUMTI6MTU6MTZaIiBOb3RPbk9yQWZ0ZXI9IjIwMTktMTItMjBUMTI6MjA6MTZaIj48bnMxOkF1ZGllbmNlUmVzdHJpY3Rpb24+PG5zMTpBdWRpZW5jZT51cm46bWFjZTpleGFtcGxlLmNvbTpzYW1sOnJvbGFuZDpzcDwvbnMxOkF1ZGllbmNlPjwvbnMxOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9uczE6Q29uZGl0aW9ucz48bnMxOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxOS0xMi0yMFQxMjoxNToxNloiIFNlc3Npb25JbmRleD0iaWQtZUVoTkNjNUJTaWVzVk9sOEIiPjxuczE6QXV0aG5Db250ZXh0PjxuczE6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6SW50ZXJuZXRQcm90b2NvbFBhc3N3b3JkPC9uczE6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PG5zMTpBdXRoZW50aWNhdGluZ0F1dGhvcml0eT5odHRwOi8vd3d3LmV4YW1wbGUuY29tL2xvZ2luPC9uczE6QXV0aGVudGljYXRpbmdBdXRob3JpdHk+PC9uczE6QXV0aG5Db250ZXh0PjwvbnMxOkF1dGhuU3RhdGVtZW50PjxuczE6QXR0cmlidXRlU3RhdGVtZW50PjxuczE6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWU9InVybjpvaWQ6MS4zLjYuMS40LjEuNTkyMy4xLjEuMS4xIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PG5zMTpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnN0YWZmPC9uczE6QXR0cmlidXRlVmFsdWU+PG5zMTpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhzaTp0eXBlPSJ4czpzdHJpbmciPm1lbWJlcjwvbnMxOkF0dHJpYnV0ZVZhbHVlPjwvbnMxOkF0dHJpYnV0ZT48bnMxOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9Im1haWwiIE5hbWU9InVybjpvaWQ6MC45LjIzNDIuMTkyMDAzMDAuMTAwLjEuMyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxuczE6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5mb29AZ21haWwuY29tPC9uczE6QXR0cmlidXRlVmFsdWU+PC9uczE6QXR0cmlidXRlPjxuczE6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0iZ2l2ZW5OYW1lIiBOYW1lPSJ1cm46b2lkOjIuNS40LjQyIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PG5zMTpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhzaTp0eXBlPSJ4czpzdHJpbmciPkRlcmVrPC9uczE6QXR0cmlidXRlVmFsdWU+PC9uczE6QXR0cmlidXRlPjxuczE6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0ic3VyTmFtZSIgTmFtZT0idXJuOm9pZDoyLjUuNC40IiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PG5zMTpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhzaTp0eXBlPSJ4czpzdHJpbmciPkpldGVyPC9uczE6QXR0cmlidXRlVmFsdWU+PC9uczE6QXR0cmlidXRlPjxuczE6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0idGl0bGUiIE5hbWU9InVybjpvaWQ6Mi41LjQuMTIiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48bnMxOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeHNpOnR5cGU9InhzOnN0cmluZyI+c2hvcnRzdG9wPC9uczE6QXR0cmlidXRlVmFsdWU+PC9uczE6QXR0cmlidXRlPjwvbnMxOkF0dHJpYnV0ZVN0YXRlbWVudD48L25zMTpBc3NlcnRpb24+DQo8L1hTV19BVFRBQ0s+DQo8L25zMDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index a496c578..e57fe35c 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -714,6 +714,17 @@ def testDoesNotAllowSignatureWrappingAttack(self): self.assertFalse(response.is_valid(self.get_request_data())) self.assertEqual('test@onelogin.com', response.get_nameid()) + + def testDoesNotAllowSignatureWrappingAttack2(self): + # Signature Wraping attack 2 + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings.set_strict(False) + xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'signature_wrapping_attack2.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertEquals("SAML Response must contain 1 assertion", response.get_error()) + + def testNodeTextAttack(self): """ Tests the get_nameid and get_attributes methods of the OneLogin_Saml2_Response From 64fbe77fac8ff565de69adbec742cc4f26dbac56 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 7 Jan 2021 22:02:53 +0100 Subject: [PATCH 293/352] See #221 and #267. Custom lxml parser based on the one defined at xmldefused. Parser will ignore comments and processing instructions and by default have deactivated huge_tree, DTD and access to external documents --- setup.py | 1 + src/onelogin/saml2/auth.py | 12 +- src/onelogin/saml2/idp_metadata_parser.py | 2 +- src/onelogin/saml2/logout_request.py | 5 +- src/onelogin/saml2/logout_response.py | 5 +- src/onelogin/saml2/metadata.py | 2 +- src/onelogin/saml2/response.py | 4 +- src/onelogin/saml2/utils.py | 9 +- src/onelogin/saml2/xmlparser.py | 145 +++++++++++++++++++ tests/src/OneLogin/saml2_tests/utils_test.py | 5 +- 10 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 src/onelogin/saml2/xmlparser.py diff --git a/setup.py b/setup.py index 0c0cd8ca..3ebb6cc6 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ }, test_suite='tests', install_requires=[ + 'lxml>=3.3.5', 'dm.xmlsec.binding==1.3.7', 'isodate>=0.5.0', 'defusedxml>=0.6.0', diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 188e289e..1e5e248f 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -13,16 +13,16 @@ from base64 import b64encode from urllib import quote_plus -from defusedxml.lxml import tostring -from onelogin.saml2.settings import OneLogin_Saml2_Settings -from onelogin.saml2.response import OneLogin_Saml2_Response +from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request +from onelogin.saml2.constants import OneLogin_Saml2_Constants 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, xmlsec from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request -from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request +from onelogin.saml2.response import OneLogin_Saml2_Response +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.utils import OneLogin_Saml2_Utils, xmlsec +from onelogin.saml2.xmlparser import tostring class OneLogin_Saml2_Auth(object): diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 33312b00..e66acf7a 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -13,10 +13,10 @@ import ssl from copy import deepcopy -from defusedxml.lxml import fromstring from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xmlparser import fromstring class OneLogin_Saml2_IdPMetadataParser(object): diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 1cb75efd..1d0c715a 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -12,13 +12,12 @@ from zlib import decompress from base64 import b64encode, b64decode from lxml import etree -from defusedxml.lxml import fromstring from xml.dom.minidom import Document from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError - +from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xmlparser import fromstring class OneLogin_Saml2_Logout_Request(object): """ diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 6d326caf..0c6b6b49 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -10,14 +10,13 @@ """ from base64 import b64encode, b64decode -from defusedxml.lxml import fromstring - from xml.dom.minidom import Document from defusedxml.minidom import parseString from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError +from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xmlparser import fromstring class OneLogin_Saml2_Logout_Response(object): diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 2260bb8b..4716a49e 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -247,7 +247,7 @@ def add_x509_key_descriptors(metadata, cert=None, add_encryption=True): if cert is None or cert == '': return metadata try: - xml = parseString(metadata.encode('utf-8'), forbid_dtd=True) + xml = parseString(metadata.encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True) except Exception as e: raise Exception('Error parsing metadata. ' + e.message) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 90f24db5..96ee81d4 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -11,12 +11,12 @@ from base64 import b64decode from copy import deepcopy -from defusedxml.lxml import tostring, fromstring from xml.dom.minidom import Document from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError +from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception +from onelogin.saml2.xmlparser import tostring, fromstring class OneLogin_Saml2_Response(object): diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 7ce29b16..ba5a8331 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -16,7 +16,6 @@ from hashlib import sha1, sha256, sha384, sha512 from isodate import parse_duration as duration_parser from lxml import etree -from defusedxml.lxml import tostring, fromstring from os.path import basename, dirname, join import re from sys import stderr @@ -35,6 +34,7 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError +from onelogin.saml2.xmlparser import tostring, fromstring if not globals().get('xmlsec_setup', False): @@ -164,11 +164,12 @@ def validate_xml(xml, schema, debug=False): return 'invalid_xml' - return parseString(tostring(dom, encoding='unicode').encode('utf-8'), forbid_dtd=True) + return parseString(tostring(dom, encoding='unicode').encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True) @staticmethod def element_text(node): - etree.strip_tags(node, etree.Comment) + # Double check, the LXML Parser already removes comments + #etree.strip_tags(node, etree.Comment) return node.text @staticmethod @@ -716,7 +717,7 @@ def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=No edata = enc_ctx.encryptXml(enc_data, elem[0]) - newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'), forbid_dtd=True) + newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True) if newdoc.hasChildNodes(): child = newdoc.firstChild diff --git a/src/onelogin/saml2/xmlparser.py b/src/onelogin/saml2/xmlparser.py new file mode 100644 index 00000000..ce51cd04 --- /dev/null +++ b/src/onelogin/saml2/xmlparser.py @@ -0,0 +1,145 @@ +# Based on the lxml example from defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""lxml.etree protection""" + +from __future__ import print_function, absolute_import + +import threading + +from lxml import etree as _etree + +from defusedxml.lxml import DTDForbidden, EntitiesForbidden, NotSupportedError + +LXML3 = _etree.LXML_VERSION[0] >= 3 + +__origin__ = "lxml.etree" + +tostring = _etree.tostring + + +class RestrictedElement(_etree.ElementBase): + """A restricted Element class that filters out instances of some classes + """ + + __slots__ = () + blacklist = (_etree._Entity, _etree._ProcessingInstruction, _etree._Comment) + + def _filter(self, iterator): + blacklist = self.blacklist + for child in iterator: + if isinstance(child, blacklist): + continue + yield child + + def __iter__(self): + iterator = super(RestrictedElement, self).__iter__() + return self._filter(iterator) + + def iterchildren(self, tag=None, reversed=False): + iterator = super(RestrictedElement, self).iterchildren(tag=tag, reversed=reversed) + return self._filter(iterator) + + def iter(self, tag=None, *tags): + iterator = super(RestrictedElement, self).iter(tag=tag, *tags) + return self._filter(iterator) + + def iterdescendants(self, tag=None, *tags): + iterator = super(RestrictedElement, self).iterdescendants(tag=tag, *tags) + return self._filter(iterator) + + def itersiblings(self, tag=None, preceding=False): + iterator = super(RestrictedElement, self).itersiblings(tag=tag, preceding=preceding) + return self._filter(iterator) + + def getchildren(self): + iterator = super(RestrictedElement, self).__iter__() + return list(self._filter(iterator)) + + def getiterator(self, tag=None): + iterator = super(RestrictedElement, self).getiterator(tag) + return self._filter(iterator) + + +class GlobalParserTLS(threading.local): + """Thread local context for custom parser instances + """ + + parser_config = { + "resolve_entities": False, + 'remove_comments': True, + 'no_network': True, + 'remove_pis': True, + 'huge_tree': False + } + + element_class = RestrictedElement + + def createDefaultParser(self): + parser = _etree.XMLParser(**self.parser_config) + element_class = self.element_class + if self.element_class is not None: + lookup = _etree.ElementDefaultClassLookup(element=element_class) + parser.set_element_class_lookup(lookup) + return parser + + def setDefaultParser(self, parser): + self._default_parser = parser + + def getDefaultParser(self): + parser = getattr(self, "_default_parser", None) + if parser is None: + parser = self.createDefaultParser() + self.setDefaultParser(parser) + return parser + + +_parser_tls = GlobalParserTLS() +getDefaultParser = _parser_tls.getDefaultParser + + +def check_docinfo(elementtree, forbid_dtd=False, forbid_entities=True): + """Check docinfo of an element tree for DTD and entity declarations + The check for entity declarations needs lxml 3 or newer. lxml 2.x does + not support dtd.iterentities(). + """ + docinfo = elementtree.docinfo + if docinfo.doctype: + if forbid_dtd: + raise DTDForbidden(docinfo.doctype, docinfo.system_url, docinfo.public_id) + if forbid_entities and not LXML3: + # lxml < 3 has no iterentities() + raise NotSupportedError("Unable to check for entity declarations " "in lxml 2.x") + + if forbid_entities: + for dtd in docinfo.internalDTD, docinfo.externalDTD: + if dtd is None: + continue + for entity in dtd.iterentities(): + raise EntitiesForbidden(entity.name, entity.content, None, None, None, None) + + +def parse(source, parser=None, base_url=None, forbid_dtd=True, forbid_entities=True): + if parser is None: + parser = getDefaultParser() + elementtree = _etree.parse(source, parser, base_url=base_url) + check_docinfo(elementtree, forbid_dtd, forbid_entities) + return elementtree + + +def fromstring(text, parser=None, base_url=None, forbid_dtd=True, forbid_entities=True): + if parser is None: + parser = getDefaultParser() + rootelement = _etree.fromstring(text, parser, base_url=base_url) + elementtree = rootelement.getroottree() + check_docinfo(elementtree, forbid_dtd, forbid_entities) + return rootelement + + +XML = fromstring + + +def iterparse(*args, **kwargs): + raise NotSupportedError("iterparse not available") diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 90b9c91e..468478b7 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -5,16 +5,16 @@ from base64 import b64decode import json -from defusedxml.lxml import fromstring from lxml import etree from os.path import dirname, join, exists import unittest from xml.dom.minidom import Document, parseString from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils -from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError +from onelogin.saml2.xmlparser import fromstring class OneLogin_Saml2_Utils_Test(unittest.TestCase): @@ -1035,7 +1035,6 @@ def testValidateSign(self): with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."): OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert, raise_exceptions=True) - if __name__ == '__main__': runner = unittest.TextTestRunner() unittest.main(testRunner=runner) From fec59a0142b8998f5ab19e0aaa09940f5d1034af Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 9 Jan 2021 00:46:51 +0100 Subject: [PATCH 294/352] Adding get_idp_sso_url, get_idp_slo_url and get_idp_slo_response_url methods to the Settings class and use it in the toolkit --- README.md | 13 ++++--- src/onelogin/saml2/auth.py | 22 +++++++----- src/onelogin/saml2/logout_request.py | 2 +- src/onelogin/saml2/logout_response.py | 3 +- src/onelogin/saml2/settings.py | 34 ++++++++++++++++++ tests/src/OneLogin/saml2_tests/auth_test.py | 15 ++++++++ .../src/OneLogin/saml2_tests/settings_test.py | 35 +++++++++++++++++++ 7 files changed, 108 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index c481c2b8..6d411fd1 100644 --- a/README.md +++ b/README.md @@ -282,11 +282,13 @@ This is the ``settings.json`` file: } ] }, - // Specifies info about where and how the message MUST be - // returned to the requester, in this case our SP. + // Specifies info about where and how the message MUST be sent. "singleLogoutService": { - // URL Location where the from the IdP will be returned + // URL Location where the from the IdP will be sent (IdP-initiated logout) "url": "https:///?sls", + // URL Location where the from the IdP will sent (SP-initiated logout, reply) + // OPTIONAL: only specify if different from url parameter + //"responseUrl": "https:///?sls", // SAML protocol binding to be used when returning the // message. OneLogin Toolkit supports the HTTP-Redirect binding // only for this endpoint. @@ -327,8 +329,11 @@ This is the ``settings.json`` file: }, // SLO endpoint info of the IdP. "singleLogoutService": { - // URL Location of the IdP where SLO Request will be sent. + // URL Location where the from the IdP will be sent (IdP-initiated logout) "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + // URL Location where the from the IdP will sent (SP-initiated logout, reply) + // OPTIONAL: only specify if different from url parameter + "responseUrl": "https://app.onelogin.com/trust/saml2/http-redirect/slo_return/", // SAML protocol binding to be used when returning the // message. OneLogin Toolkit supports the HTTP-Redirect binding // only for this endpoint. diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 188e289e..707c16a4 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -446,26 +446,30 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name def get_sso_url(self): """ - Gets the SSO URL. + Gets the IdP SSO URL. :returns: An URL, the SSO endpoint of the IdP :rtype: string """ - idp_data = self.__settings.get_idp_data() - return idp_data['singleSignOnService']['url'] + return self.__settings.get_idp_sso_url() def get_slo_url(self): """ - Gets the SLO URL. + Gets the IdP SLO URL. :returns: An URL, the SLO endpoint of the IdP :rtype: string """ - url = None - idp_data = self.__settings.get_idp_data() - if 'singleLogoutService' in idp_data.keys() and 'url' in idp_data['singleLogoutService']: - url = idp_data['singleLogoutService']['url'] - return url + return self.__settings.get_idp_slo_url() + + def get_slo_response_url(self): + """ + Gets the SLO return URL for IdP-initiated logout. + + :returns: an URL, the SLO return endpoint of the IdP + :rtype: string + """ + return self.__settings.get_idp_slo_response_url() def build_request_signature(self, saml_request, relay_state, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 1cb75efd..826d96fe 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -125,7 +125,7 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= { 'id': uid, 'issue_instant': issue_instant, - 'single_logout_url': idp_data['singleLogoutService']['url'], + 'single_logout_url': self.__settings.get_idp_slo_url(), 'entity_id': sp_data['entityId'], 'name_id': name_id_obj, 'session_index': session_index_str, diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 6d326caf..0c1cd178 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -208,7 +208,6 @@ def build(self, in_response_to): :type in_response_to: string """ sp_data = self.__settings.get_sp_data() - idp_data = self.__settings.get_idp_data() uid = OneLogin_Saml2_Utils.generate_unique_id() issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) @@ -229,7 +228,7 @@ def build(self, in_response_to): { 'id': uid, 'issue_instant': issue_instant, - 'destination': idp_data['singleLogoutService']['url'], + 'destination': self.__settings.get_idp_slo_response_url(), 'in_response_to': in_response_to, 'entity_id': sp_data['entityId'], } diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index a9e863fb..357058be 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -259,6 +259,8 @@ def __add_default_values(self): self.__sp.setdefault('singleLogoutService', {}) self.__sp['singleLogoutService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT) + self.__idp.setdefault('singleLogoutService', {}) + # Related to nameID self.__sp.setdefault('NameIDFormat', OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED) self.__security.setdefault('nameIdEncrypted', False) @@ -506,6 +508,38 @@ def check_sp_certs(self): cert = self.get_sp_cert() return key is not None and cert is not None + def get_idp_sso_url(self): + """ + Gets the IdP SSO URL. + + :returns: An URL, the SSO endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + return idp_data['singleSignOnService']['url'] + + def get_idp_slo_url(self): + """ + Gets the IdP SLO URL. + + :returns: An URL, the SLO endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + if 'url' in idp_data['singleLogoutService']: + return idp_data['singleLogoutService']['url'] + + def get_idp_slo_response_url(self): + """ + Gets the IdP SLO return URL for IdP-initiated logout. + + :returns: an URL, the SLO return endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + if 'url' in idp_data['singleLogoutService']: + return idp_data['singleLogoutService'].get('responseUrl', self.get_idp_slo_url()) + def get_sp_key(self): """ Returns the x509 private key of the SP. diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 3a57c438..6051c72e 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -78,6 +78,21 @@ def testGetSLOurl(self): slo_url = settings_info['idp']['singleLogoutService']['url'] self.assertEqual(auth.get_slo_url(), slo_url) + def testGetSLOresponseUrl(self): + """ + Tests the get_slo_response_url method of the OneLogin_Saml2_Auth class + """ + settings_info = self.loadSettingsJSON() + settings_info['idp']['singleLogoutService']['responseUrl'] = "http://idp.example.com/SingleLogoutReturn.php" + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + slo_url = settings_info['idp']['singleLogoutService']['responseUrl'] + self.assertEqual(auth.get_slo_response_url(), slo_url) + # test that the function falls back to the url setting if responseUrl is not set + settings_info['idp']['singleLogoutService'].pop('responseUrl') + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertEqual(auth.get_slo_response_url(), slo_url) + def testGetSessionIndex(self): """ Tests the get_session_index method of the OneLogin_Saml2_Auth class diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index c4a9d7bc..9027c425 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -142,6 +142,41 @@ def testGetSchemasPath(self): base = settings.get_base_path() self.assertEqual(join(base, 'lib', 'schemas') + sep, settings.get_schemas_path()) + def testGetIdPSSOurl(self): + """ + Tests the get_idp_sso_url method of the OneLogin_Saml2_Settings class + """ + settings_info = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(settings_info) + + sso_url = settings_info['idp']['singleSignOnService']['url'] + self.assertEqual(settings.get_idp_sso_url(), sso_url) + + def testGetIdPSLOurl(self): + """ + Tests the get_idp_slo_url method of the OneLogin_Saml2_Settings class + """ + settings_info = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(settings_info) + + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertEqual(settings.get_idp_slo_url(), slo_url) + + def testGetIdPSLOresponseUrl(self): + """ + Tests the get_idp_slo_response_url method of the OneLogin_Saml2_Settings class + """ + settings_info = self.loadSettingsJSON() + settings_info['idp']['singleLogoutService']['responseUrl'] = "http://idp.example.com/SingleLogoutReturn.php" + settings = OneLogin_Saml2_Settings(settings_info) + slo_url = settings_info['idp']['singleLogoutService']['responseUrl'] + self.assertEqual(settings.get_idp_slo_response_url(), slo_url) + # test that the function falls back to the url setting if responseUrl is not set + settings_info['idp']['singleLogoutService'].pop('responseUrl') + settings = OneLogin_Saml2_Settings(settings_info) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertEqual(settings.get_idp_slo_response_url(), slo_url) + def testGetSPCert(self): """ Tests the get_sp_cert method of the OneLogin_Saml2_Settings From e0ffaf85aeabbb5ac230a75bd8ffd5b086fd52a3 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 9 Jan 2021 00:55:15 +0100 Subject: [PATCH 295/352] Fix doc of get_attribute method --- src/onelogin/saml2/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 707c16a4..2232b521 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -315,8 +315,8 @@ def get_attribute(self, name): :param name: Name of the attribute :type name: string - :returns: Attribute value if exists or None - :rtype: string + :returns: Attribute value(s) if exists or None + :rtype: list """ assert isinstance(name, basestring) value = None From f904996d255d6512abf18bba53b5e425b590cae4 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 9 Jan 2021 02:21:20 +0100 Subject: [PATCH 296/352] Support single-label-domains as valid. New security parameter allowSingleLabelDomains --- README.md | 4 +++ demo-bottle/saml/advanced_settings.json | 1 + demo-django/saml/advanced_settings.json | 1 + demo-flask/saml/advanced_settings.json | 1 + .../demo_pyramid/saml/advanced_settings.json | 1 + src/onelogin/saml2/settings.py | 35 +++++++++++++++---- .../src/OneLogin/saml2_tests/response_test.py | 2 -- .../src/OneLogin/saml2_tests/settings_test.py | 7 ++++ 8 files changed, 43 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6d411fd1..eee24c17 100644 --- a/README.md +++ b/README.md @@ -461,6 +461,10 @@ In addition to the required settings data (idp, sp), extra settings can be defin // Provide the desired duration, for example PT518400S (6 days) "metadataCacheDuration": null, + // If enabled, URLs with single-label-domains will + // be allowed and not rejected by the settings validator (Enable it under Docker/Kubernetes/testing env, not recommended on production) + "allowSingleLabelDomains": false, + // Algorithm that the toolkit will use on signing process. Options: // 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' // 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json index 022f70ef..c5b37c11 100644 --- a/demo-bottle/saml/advanced_settings.json +++ b/demo-bottle/saml/advanced_settings.json @@ -10,6 +10,7 @@ "wantNameId" : true, "wantNameIdEncrypted": false, "wantAssertionsEncrypted": false, + "allowSingleLabelDomains": false, "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" }, diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json index 022f70ef..c5b37c11 100644 --- a/demo-django/saml/advanced_settings.json +++ b/demo-django/saml/advanced_settings.json @@ -10,6 +10,7 @@ "wantNameId" : true, "wantNameIdEncrypted": false, "wantAssertionsEncrypted": false, + "allowSingleLabelDomains": false, "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" }, diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json index 022f70ef..c5b37c11 100644 --- a/demo-flask/saml/advanced_settings.json +++ b/demo-flask/saml/advanced_settings.json @@ -10,6 +10,7 @@ "wantNameId" : true, "wantNameIdEncrypted": false, "wantAssertionsEncrypted": false, + "allowSingleLabelDomains": false, "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" }, diff --git a/demo_pyramid/demo_pyramid/saml/advanced_settings.json b/demo_pyramid/demo_pyramid/saml/advanced_settings.json index 1307b0ae..fef16fe9 100644 --- a/demo_pyramid/demo_pyramid/saml/advanced_settings.json +++ b/demo_pyramid/demo_pyramid/saml/advanced_settings.json @@ -10,6 +10,7 @@ "wantNameId" : true, "wantNameIdEncrypted": false, "wantAssertionsEncrypted": false, + "allowSingleLabelDomains": false, "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" }, diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 357058be..86c727df 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -31,14 +31,25 @@ r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) +url_regex_single_label_domain = re.compile( + r'^(?:[a-z0-9\.\-]*)://' # scheme is validated separately + r'(?:(?:[A-Z0-9_](?:[A-Z0-9-_]{0,61}[A-Z0-9_])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... + r'(?:[A-Z0-9_](?:[A-Z0-9-_]{0,61}[A-Z0-9_]))|' # single-label-domain + r'localhost|' # localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 + r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 + r'(?::\d+)?' # optional port + r'(?:/?|[/?]\S+)$', re.IGNORECASE) url_schemes = ['http', 'https', 'ftp', 'ftps'] -def validate_url(url): +def validate_url(url, allow_single_label_domain=False): """ Auxiliary method to validate an urllib :param url: An url to be validated :type url: string + :param allow_single_label_domain: In order to allow or not single label domain + :type url: bool :returns: True if the url is valid :rtype: bool """ @@ -46,8 +57,12 @@ def validate_url(url): scheme = url.split('://')[0].lower() if scheme not in url_schemes: return False - if not bool(url_regex.search(url)): - return False + if allow_single_label_domain: + if not bool(url_regex_single_label_domain.search(url)): + return False + else: + if not bool(url_regex.search(url)): + return False return True @@ -351,17 +366,18 @@ def check_idp_settings(self, settings): if not settings.get('idp'): errors.append('idp_not_found') else: + allow_single_domain_urls = self._get_allow_single_label_domain(settings) idp = settings['idp'] if not idp.get('entityId'): errors.append('idp_entityId_not_found') if not idp.get('singleSignOnService', {}).get('url'): errors.append('idp_sso_not_found') - elif not validate_url(idp['singleSignOnService']['url']): + elif not validate_url(idp['singleSignOnService']['url'], allow_single_domain_urls): errors.append('idp_sso_url_invalid') slo_url = idp.get('singleLogoutService', {}).get('url') - if slo_url and not validate_url(slo_url): + if slo_url and not validate_url(slo_url, allow_single_domain_urls): errors.append('idp_slo_url_invalid') if 'security' in settings: @@ -408,6 +424,7 @@ def check_sp_settings(self, settings): if not settings.get('sp'): errors.append('sp_not_found') else: + allow_single_domain_urls = self._get_allow_single_label_domain(settings) # check_sp_certs uses self.__sp so I add it old_sp = self.__sp self.__sp = settings['sp'] @@ -420,7 +437,7 @@ def check_sp_settings(self, settings): if not sp.get('assertionConsumerService', {}).get('url'): errors.append('sp_acs_not_found') - elif not validate_url(sp['assertionConsumerService']['url']): + elif not validate_url(sp['assertionConsumerService']['url'], allow_single_domain_urls): errors.append('sp_acs_url_invalid') if sp.get('attributeConsumingService'): @@ -449,7 +466,7 @@ def check_sp_settings(self, settings): errors.append('sp_attributeConsumingService_serviceDescription_type_invalid') slo_url = sp.get('singleLogoutService', {}).get('url') - if slo_url and not validate_url(slo_url): + if slo_url and not validate_url(slo_url, allow_single_domain_urls): errors.append('sp_sls_url_invalid') if 'signMetadata' in security and isinstance(security['signMetadata'], dict): @@ -840,3 +857,7 @@ def is_debug_active(self): :rtype: boolean """ return self.__debug + + def _get_allow_single_label_domain(self, settings): + security = settings.get('security', {}) + return 'allowSingleLabelDomains' in security.keys() and security['allowSingleLabelDomains'] diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index e57fe35c..078700cc 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -714,7 +714,6 @@ def testDoesNotAllowSignatureWrappingAttack(self): self.assertFalse(response.is_valid(self.get_request_data())) self.assertEqual('test@onelogin.com', response.get_nameid()) - def testDoesNotAllowSignatureWrappingAttack2(self): # Signature Wraping attack 2 settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) @@ -724,7 +723,6 @@ def testDoesNotAllowSignatureWrappingAttack2(self): self.assertFalse(response.is_valid(self.get_request_data())) self.assertEquals("SAML Response must contain 1 assertion", response.get_error()) - def testNodeTextAttack(self): """ Tests the get_nameid and get_attributes methods of the OneLogin_Saml2_Response diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 9027c425..3d11effa 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -64,6 +64,13 @@ def testLoadSettingsFromDict(self): with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_sso_url_invalid'): OneLogin_Saml2_Settings(settings_info) + settings_info['idp']['singleSignOnService']['url'] = 'http://single-label-domain' + settings_info['security'] = {} + settings_info['security']['allowSingleLabelDomains'] = True + settings = OneLogin_Saml2_Settings(settings_info) + self.assertEqual(len(settings.get_errors()), 0) + + del settings_info['security'] del settings_info['sp'] del settings_info['idp'] with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_not_found,sp_not_found'): From 7c01457fb3e80521d95564dd385cd92efe5aedd9 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 9 Jan 2021 03:33:14 +0100 Subject: [PATCH 297/352] Remove external lib method get_ext_lib_path. Add set_cert_path in order to allow set the cert path in a different folder than the toolkit --- src/onelogin/saml2/settings.py | 18 +++---- .../src/OneLogin/saml2_tests/settings_test.py | 48 +++++++++++++++---- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 86c727df..b5ea7e54 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -143,8 +143,7 @@ def __load_paths(self, base_path=None): self.__paths = { 'base': base_path, 'cert': base_path + 'certs' + sep, - 'lib': base_path + 'lib' + sep, - 'extlib': base_path + 'extlib' + sep, + 'lib': dirname(__file__) + sep } def __update_paths(self, settings): @@ -168,6 +167,12 @@ def get_base_path(self): """ return self.__paths['base'] + def set_cert_path(self, path): + """ + Set a new cert path + """ + self.__paths['cert'] = path + def get_cert_path(self): """ Returns cert path @@ -186,15 +191,6 @@ def get_lib_path(self): """ return self.__paths['lib'] - def get_ext_lib_path(self): - """ - Returns external lib path - - :return: The external library folder path - :rtype: string - """ - return self.__paths['extlib'] - def get_schemas_path(self): """ Returns schema path diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 3d11effa..9fe5c218 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -125,29 +125,57 @@ def testGetCertPath(self): settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) self.assertEqual(self.settings_path + sep + 'certs' + sep, settings.get_cert_path()) - def testGetLibPath(self): + def testSetCertPath(self): """ - Tests getLibPath method of the OneLogin_Saml2_Settings + Tests setCertPath method of the OneLogin_Saml2_Settings """ settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) - base = settings.get_base_path() - self.assertEqual(join(base, 'lib') + sep, settings.get_lib_path()) + self.assertEqual(self.settings_path + sep + 'certs' + sep, settings.get_cert_path()) - def testGetExtLibPath(self): + settings.set_cert_path('/tmp') + self.assertEqual('/tmp', settings.get_cert_path()) + + def testGetLibPath(self): """ - Tests getExtLibPath method of the OneLogin_Saml2_Settings + Tests getLibPath method of the OneLogin_Saml2_Settings """ + settingsInfo = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(settingsInfo) + path = settings.get_base_path() + self.assertEqual(settings.get_lib_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/../../../tests/data/customPath/')) + + del settingsInfo['custom_base_path'] + settings = OneLogin_Saml2_Settings(settingsInfo) + path = settings.get_base_path() + self.assertEqual(settings.get_lib_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/')) + settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) - base = settings.get_base_path() - self.assertEqual(join(base, 'extlib') + sep, settings.get_ext_lib_path()) + path = settings.get_base_path() + self.assertEqual(settings.get_lib_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(__file__)))), 'settings/')) def testGetSchemasPath(self): """ Tests getSchemasPath method of the OneLogin_Saml2_Settings """ + settingsInfo = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(settingsInfo) + path = settings.get_base_path() + self.assertEqual(settings.get_schemas_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/schemas/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/../../../tests/data/customPath/')) + + del settingsInfo['custom_base_path'] + settings = OneLogin_Saml2_Settings(settingsInfo) + path = settings.get_base_path() + self.assertEqual(settings.get_schemas_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/schemas/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/')) + settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) - base = settings.get_base_path() - self.assertEqual(join(base, 'lib', 'schemas') + sep, settings.get_schemas_path()) + path = settings.get_base_path() + self.assertEqual(settings.get_schemas_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/schemas/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(__file__)))), 'settings/')) def testGetIdPSSOurl(self): """ From 54404b0f3b40d28e3a6892e33ae378af2de092f7 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 11 Jan 2021 17:37:56 +0100 Subject: [PATCH 298/352] Add get_friendlyname_attributes support --- src/onelogin/saml2/auth.py | 29 +++++++++++++- src/onelogin/saml2/response.py | 39 +++++++++++++++++++ .../response1_with_friendlyname.xml.base64 | 1 + tests/src/OneLogin/saml2_tests/auth_test.py | 2 + .../src/OneLogin/saml2_tests/response_test.py | 28 ++++++++++++- 5 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 tests/data/responses/response1_with_friendlyname.xml.base64 diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 2232b521..d23f8bac 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -51,6 +51,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): self.__request_data = request_data self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path) self.__attributes = [] + self.__friendlyname_attributes = [] self.__nameid = None self.__nameid_format = None self.__nameid_nq = None @@ -104,6 +105,7 @@ def process_response(self, request_id=None): self.__last_response = response.get_xml_document() if response.is_valid(self.__request_data, request_id): self.__attributes = response.get_attributes() + self.__friendlyname_attributes = response.get_friendlyname_attributes() self.__nameid = response.get_nameid() self.__nameid_format = response.get_nameid_format() self.__nameid_nq = response.get_nameid_nq() @@ -115,11 +117,9 @@ def process_response(self, request_id=None): self.__last_authn_contexts = response.get_authn_contexts() self.__last_assertion_not_on_or_after = response.get_assertion_not_on_or_after() self.__authenticated = True - else: self.__errors.append('invalid_response') self.__error_reason = response.get_error() - else: self.__errors.append('invalid_binding') raise OneLogin_Saml2_Error( @@ -231,6 +231,15 @@ def get_attributes(self): """ return self.__attributes + def get_friendlyname_attributes(self): + """ + Returns the set of SAML attributes indexed by FiendlyName. + + :returns: SAML attributes + :rtype: dict + """ + return self.__friendlyname_attributes + def get_nameid(self): """ Returns the nameID. @@ -324,6 +333,22 @@ def get_attribute(self, name): value = self.__attributes[name] return value + def get_friendlyname_attribute(self, friendlyname): + """ + Returns the requested SAML attribute searched by FriendlyName. + + :param friendlyname: FriendlyName of the attribute + :type friendlyname: string + + :returns: Attribute value(s) if exists or None + :rtype: list + """ + assert isinstance(friendlyname, basestring) + value = None + if self.__friendlyname_attributes and friendlyname in self.__friendlyname_attributes.keys(): + value = self.__friendlyname_attributes[friendlyname] + return value + def get_last_request_id(self): """ :returns: The ID of the last Request SAML message generated. diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 90f24db5..7a5f2cef 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -620,6 +620,45 @@ def get_attributes(self): attributes[attr_name] = values return attributes + def get_friendlyname_attributes(self): + """ + Gets the Attributes from the AttributeStatement element indexed by FiendlyName. + EncryptedAttributes are not supported + """ + attributes = {} + attribute_nodes = self.__query_assertion('/saml:AttributeStatement/saml:Attribute') + for attribute_node in attribute_nodes: + attr_friendlyname = attribute_node.get('FriendlyName') + if attr_friendlyname: + if attr_friendlyname in attributes.keys(): + raise OneLogin_Saml2_ValidationError( + 'Found an Attribute element with duplicated FriendlyName', + OneLogin_Saml2_ValidationError.DUPLICATED_ATTRIBUTE_NAME_FOUND + ) + + values = [] + for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]): + # Remove any whitespace (which may be present where attributes are + # nested inside NameID children). + attr_text = OneLogin_Saml2_Utils.element_text(attr) + if attr_text: + attr_text = attr_text.strip() + if attr_text: + values.append(attr_text) + + # Parse any nested NameID children + for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]): + values.append({ + 'NameID': { + 'Format': nameid.get('Format'), + 'NameQualifier': nameid.get('NameQualifier'), + 'value': OneLogin_Saml2_Utils.element_text(nameid) + } + }) + + attributes[attr_friendlyname] = values + return attributes + def validate_num_assertions(self): """ Verifies that the document only contains a single Assertion (encrypted or not) diff --git a/tests/data/responses/response1_with_friendlyname.xml.base64 b/tests/data/responses/response1_with_friendlyname.xml.base64 new file mode 100644 index 00000000..a17c3941 --- /dev/null +++ b/tests/data/responses/response1_with_friendlyname.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJHT1NBTUxSMTI5MDExNzQ1NzE3OTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgRGVzdGluYXRpb249IntyZWNpcGllbnR9Ij4NCiAgPHNhbWxwOlN0YXR1cz4NCiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBmeDhmZmIzOTgzLWNiZjYtOTJhMS1mMmM0LTYxOWFlMWJlMWM4NiIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzEzNTkwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4NCiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4OGZmYjM5ODMtY2JmNi05MmExLWYyYzQtNjE5YWUxYmUxYzg2Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT5oZ3VRYkNIYW5pYmJEQzdxM1p6eHpIY1Bvbkk9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPkdhbmNEOXZSb2g5TWJUMDAyRHk3OXQxbTZJNllmaFVLUGZibGttcDJ1ZG9sWHVqdjZlMU1XdnNWbXhOenRzSUdseEFhMHFLRGlTTXpDTkRac2szanN5c1VsMW5BS25BZzE4NWpmWGpzemhzZG1SK005MWR4azZrZmNMVW9zT29sb3ZhZFdMUFdxbjdQM2o4LzV4enA5THBSQTNndkI0MTgyUlNpcldDQlhQUT08L2RzOlNpZ25hdHVyZVZhbHVlPg0KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ2dUQ0NBZW9DQ1FDYk9scldEZFg3RlRBTkJna3Foa2lHOXcwQkFRVUZBRENCaERFTE1Ba0dBMVVFQmhNQ1RrOHhHREFXQmdOVkJBZ1REMEZ1WkhKbFlYTWdVMjlzWW1WeVp6RU1NQW9HQTFVRUJ4TURSbTl2TVJBd0RnWURWUVFLRXdkVlRrbE9SVlJVTVJnd0ZnWURWUVFERXc5bVpXbGtaUzVsY214aGJtY3VibTh4SVRBZkJna3Foa2lHOXcwQkNRRVdFbUZ1WkhKbFlYTkFkVzVwYm1WMGRDNXViekFlRncwd056QTJNVFV4TWpBeE16VmFGdzB3TnpBNE1UUXhNakF4TXpWYU1JR0VNUXN3Q1FZRFZRUUdFd0pPVHpFWU1CWUdBMVVFQ0JNUFFXNWtjbVZoY3lCVGIyeGlaWEpuTVF3d0NnWURWUVFIRXdOR2IyOHhFREFPQmdOVkJBb1RCMVZPU1U1RlZGUXhHREFXQmdOVkJBTVREMlpsYVdSbExtVnliR0Z1Wnk1dWJ6RWhNQjhHQ1NxR1NJYjNEUUVKQVJZU1lXNWtjbVZoYzBCMWJtbHVaWFIwTG01dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRGl2YmhSN1A1MTZ4L1MzQnFLeHVwUWUwTE9Ob2xpdXBpQk9lc0NPM1NIYkRybDMrcTlJYmZuZm1FMDRyTnVNY1BzSXhCMTYxVGREcEllc0xDbjdjOGFQSElTS090UGxBZVRaU25iOFFBdTdhUmpacTMrUGJyUDV1VzNUY2ZDR1B0S1R5dEhPZ2UvT2xKYm8wNzhkVmhYUTE0ZDFFRHdYSlcxclJYdVV0NEM4UUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDRFZmcDg2SE9icVkrZThCVW9XUTkrVk1ReDFBU0RvaEJqd09zZzJXeWtVcVJYRitkTGZjVUg5ZFdSNjNDdFpJS0ZEYlN0Tm9tUG5RejduYksrb255Z3dCc3BWRWJuSHVVaWhacTNaVWRtdW1RcUN3NFV2cy8xVXZxM29yT28vV0pWaFR5dkxnRlZLMlFhclE0LzY3T1pmSGQ3UitQT0JYaG9waFNNdjFaT288L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT4NCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zdXBwb3J0QG9uZWxvZ2luLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiIFJlY2lwaWVudD0ie3JlY2lwaWVudH0iLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0xMS0xOFQyMTo1MjozN1oiIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+e2F1ZGllbmNlfTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMC0xMS0xOFQyMTo1NzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTAtMTEtMTlUMjE6NTc6MzdaIiBTZXNzaW9uSW5kZXg9Il81MzFjMzJkMjgzYmRmZjdlMDRlNDg3YmNkYmM0ZGQ4ZCI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgRnJpZW5kbHlOYW1lPSJ1c2VybmFtZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+ZGVtbzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iYW5vdGhlcl92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+dmFsdWU8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 6051c72e..4d7fdb71 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -233,6 +233,8 @@ def testProcessResponseValid(self): attributes = auth.get_attributes() self.assertNotEqual(len(attributes), 0) self.assertEqual(auth.get_attribute('mail'), attributes['mail']) + friendlyname_attributes = auth.get_friendlyname_attributes() + self.assertEqual(len(friendlyname_attributes), 0) session_index = auth.get_session_index() self.assertEqual('_6273d77b8cde0c333ec79d22a9fa0003b9fe2d75cb', session_index) self.assertEqual("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", auth.get_nameid_format()) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 078700cc..bf9be8a0 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -648,7 +648,7 @@ def testGetSessionIndex(self): def testGetAttributes(self): """ - Tests the getAttributes method of the OneLogin_Saml2_Response + Tests the get_attributes method of the OneLogin_Saml2_Response """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) @@ -670,9 +670,33 @@ def testGetAttributes(self): response_3 = OneLogin_Saml2_Response(settings, xml_3) self.assertEqual({}, response_3.get_attributes()) + def testGetFriendlyAttributes(self): + """ + Tests the get_friendlyname_attributes method of the OneLogin_Saml2_Response + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertEqual({}, response.get_friendlyname_attributes()) + + expected_attributes = { + 'username': ['demo'] + } + xml_2 = self.file_contents(join(self.data_path, 'responses', 'response1_with_friendlyname.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertEqual(expected_attributes, response_2.get_friendlyname_attributes()) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'response2.xml.base64')) + response_3 = OneLogin_Saml2_Response(settings, xml_3) + self.assertEqual({}, response_3.get_friendlyname_attributes()) + + xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_attrs.xml.base64')) + response_4 = OneLogin_Saml2_Response(settings, xml_4) + self.assertEqual({}, response_4.get_friendlyname_attributes()) + def testGetNestedNameIDAttributes(self): """ - Tests the getAttributes method of the OneLogin_Saml2_Response with nested + Tests the get_attributes method of the OneLogin_Saml2_Response with nested nameID data """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) From 78ab3ca90ee2781685100137df22d324a531dd25 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 11 Jan 2021 21:38:59 +0100 Subject: [PATCH 299/352] Destination URL Comparison is now case-insensitive for netloc --- src/onelogin/saml2/logout_request.py | 25 ++++---- src/onelogin/saml2/logout_response.py | 4 +- src/onelogin/saml2/response.py | 2 +- src/onelogin/saml2/utils.py | 22 +++++++ .../saml2_tests/logout_request_test.py | 64 +++++++++++++++++++ .../saml2_tests/logout_response_test.py | 56 ++++++++++++++++ .../src/OneLogin/saml2_tests/response_test.py | 58 +++++++++++++++++ 7 files changed, 215 insertions(+), 16 deletions(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 826d96fe..623d705c 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -357,19 +357,18 @@ def is_valid(self, request_data, raise_exceptions=False): ) # Check destination - if dom.get('Destination', None): - destination = dom.get('Destination') - if destination != '': - if current_url not in destination: - raise Exception( - 'The LogoutRequest was received at ' - '%(currentURL)s instead of %(destination)s' % - { - 'currentURL': current_url, - 'destination': destination, - }, - OneLogin_Saml2_ValidationError.WRONG_DESTINATION - ) + destination = dom.get('Destination') + if destination: + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=current_url)): + raise Exception( + '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(dom) diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 0c1cd178..5fd97471 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -126,8 +126,8 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): # Check destination if self.document.documentElement.hasAttribute('Destination'): destination = self.document.documentElement.getAttribute('Destination') - if destination != '': - if current_url not in destination: + if destination: + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=current_url)): raise OneLogin_Saml2_ValidationError( 'The LogoutResponse was received at %s instead of %s' % (current_url, destination), OneLogin_Saml2_ValidationError.WRONG_DESTINATION diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 7a5f2cef..6b1ed449 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -209,7 +209,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): # Checks destination destination = self.document.get('Destination', None) if destination: - if not destination.startswith(current_url): + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=current_url)): # TODO: Review if following lines are required, since we can control the # request_data # current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 7ce29b16..e00582b8 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -23,6 +23,7 @@ from tempfile import NamedTemporaryFile from textwrap import wrap from urllib import quote_plus +from urlparse import urlsplit, urlunsplit from uuid import uuid4 from xml.dom.minidom import Document, Element from defusedxml.minidom import parseString @@ -1280,3 +1281,24 @@ def extract_raw_query_parameter(query_string, parameter, default=''): def case_sensitive_urlencode(to_encode, lowercase=False): encoded = quote_plus(to_encode) return re.sub(r"%[A-F0-9]{2}", lambda m: m.group(0).lower(), encoded) if lowercase else encoded + + @staticmethod + def normalize_url(url): + """ + Returns normalized URL for comparison. + This method converts the netloc to lowercase, as it should be case-insensitive (per RFC 4343, RFC 7617) + If standardization fails, the original URL is returned + Python documentation indicates that URL split also normalizes query strings if empty query fields are present + + :param url: URL + :type url: String + + :returns: A normalized URL, or the given URL string if parsing fails + :rtype: String + """ + try: + scheme, netloc, path, query, fragment = urlsplit(url) + normalized_url = urlunsplit((scheme.lower(), netloc.lower(), path, query, fragment)) + return normalized_url + except Exception: + return url diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index de782f75..ba73cf53 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -343,6 +343,70 @@ def testIsInvalidDestination(self): logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) self.assertTrue(logout_request4.is_valid(request_data)) + def testIsValidWithCapitalization(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + """ + request_data = { + 'http_host': 'exaMPLe.com', + 'script_name': 'index.html' + } + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertTrue(logout_request.is_valid(request_data)) + + settings.set_strict(True) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertFalse(logout_request2.is_valid(request_data)) + + settings.set_strict(False) + dom = parseString(request) + logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertTrue(logout_request3.is_valid(request_data)) + + settings.set_strict(True) + logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertFalse(logout_request4.is_valid(request_data)) + + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url.lower()) + logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertTrue(logout_request5.is_valid(request_data)) + + def testIsInValidWithCapitalization(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'INdex.html' + } + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertTrue(logout_request.is_valid(request_data)) + + settings.set_strict(True) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertFalse(logout_request2.is_valid(request_data)) + + settings.set_strict(False) + dom = parseString(request) + logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertTrue(logout_request3.is_valid(request_data)) + + settings.set_strict(True) + logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertFalse(logout_request4.is_valid(request_data)) + + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url.lower()) + logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertFalse(logout_request5.is_valid(request_data)) + def testIsInvalidNotOnOrAfter(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 38d9fab6..30ce793c 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -225,6 +225,62 @@ def testIsInValidDestination(self): response_4 = OneLogin_Saml2_Logout_Response(settings, message_4) self.assertTrue(response_4.is_valid(request_data)) + def testIsValidWithCapitalization(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + """ + request_data = { + 'http_host': 'exaMPLe.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + + response = OneLogin_Saml2_Logout_Response(settings, message) + self.assertTrue(response.is_valid(request_data)) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Logout_Response(settings, message) + with self.assertRaisesRegexp(Exception, 'The LogoutResponse was received at'): + response_2.is_valid(request_data, raise_exceptions=True) + + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data).lower() + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message_3 = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + + response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) + self.assertTrue(response_3.is_valid(request_data)) + + def testIsInValidWithCapitalization(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'INdex.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + + response = OneLogin_Saml2_Logout_Response(settings, message) + self.assertTrue(response.is_valid(request_data)) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Logout_Response(settings, message) + with self.assertRaisesRegexp(Exception, 'The LogoutResponse was received at'): + response_2.is_valid(request_data, raise_exceptions=True) + + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data).lower() + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message_3 = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + + response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) + self.assertFalse(response_3.is_valid(request_data)) + def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): message = OneLogin_Saml2_Utils.deflate_and_base64_encode('invalid') request_data = { diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index bf9be8a0..a9252b3c 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -42,6 +42,24 @@ def get_request_data(self): 'script_name': 'index.html' } + def get_request_data_domain_capitalized(self): + return { + 'http_host': 'StuFF.Com', + 'script_name': 'endpoints/endpoints/acs.php' + } + + def get_request_data_path_capitalized(self): + return { + 'http_host': 'stuff.com', + 'script_name': 'Endpoints/endPoints/acs.php' + } + + def get_request_data_both_capitalized(self): + return { + 'http_host': 'StuFF.Com', + 'script_name': 'Endpoints/endPoints/aCs.php' + } + def testConstruct(self): """ Tests the OneLogin_Saml2_Response Constructor. @@ -1075,6 +1093,46 @@ def testIsInValidDestination(self): self.assertFalse(response_5.is_valid(self.get_request_data())) self.assertIn('A valid SubjectConfirmation was not found on this Response', response_5.get_error()) + settings.set_strict(True) + response_2 = OneLogin_Saml2_Response(settings, message) + self.assertFalse(response_2.is_valid(self.get_request_data())) + self.assertIn('The response was received at', response_2.get_error()) + + def testIsInValidDestinationCapitalizationOfElements(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response class + Case Invalid Response due to differences in capitalization of path + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) + + # Test path capitalized + settings.set_strict(True) + response = OneLogin_Saml2_Response(settings, message) + self.assertFalse(response.is_valid(self.get_request_data_path_capitalized())) + self.assertIn('The response was received at', response.get_error()) + + # Test both domain and path capitalized + response_2 = OneLogin_Saml2_Response(settings, message) + self.assertFalse(response_2.is_valid(self.get_request_data_both_capitalized())) + self.assertIn('The response was received at', response_2.get_error()) + + def testIsValidDestinationCapitalizationOfHost(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response class + Case Valid Response, even if host is differently capitalized (per RFC) + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) + # Test domain capitalized + settings.set_strict(True) + response = OneLogin_Saml2_Response(settings, message) + self.assertFalse(response.is_valid(self.get_request_data_domain_capitalized())) + self.assertNotIn('The response was received at', response.get_error()) + + # Assert we got past the destination check, which appears later + self.assertIn('A valid SubjectConfirmation was not found', response.get_error()) + def testIsInValidAudience(self): """ Tests the is_valid method of the OneLogin_Saml2_Response class From 910a2888e2c7f47f6ffd639138d3e6373cfb07f7 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 11 Jan 2021 22:04:03 +0100 Subject: [PATCH 300/352] Fix get_session_expiration signature --- src/onelogin/saml2/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index d23f8bac..78e7f476 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -288,7 +288,7 @@ def get_session_expiration(self): """ Returns the SessionNotOnOrAfter from the AuthnStatement. :returns: The SessionNotOnOrAfter of the assertion - :rtype: DateTime|None + :rtype: unix/posix timestamp|None """ return self.__session_expiration From 97fc56feaf8aa3ff1385f9383cc6fb7a0c114167 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 12 Jan 2021 13:20:11 +0100 Subject: [PATCH 301/352] Fix pep8 --- src/onelogin/saml2/logout_request.py | 1 + src/onelogin/saml2/utils.py | 2 +- tests/src/OneLogin/saml2_tests/utils_test.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 8bfe164c..2a2d881f 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -19,6 +19,7 @@ from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.xmlparser import fromstring + class OneLogin_Saml2_Logout_Request(object): """ diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index c0a4fc5d..16fcfcb6 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -170,7 +170,7 @@ def validate_xml(xml, schema, debug=False): @staticmethod def element_text(node): # Double check, the LXML Parser already removes comments - #etree.strip_tags(node, etree.Comment) + etree.strip_tags(node, etree.Comment) return node.text @staticmethod diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 468478b7..9ca2f3ea 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -1035,6 +1035,7 @@ def testValidateSign(self): with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."): OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert, raise_exceptions=True) + if __name__ == '__main__': runner = unittest.TextTestRunner() unittest.main(testRunner=runner) From 4589079c310e3c4515abb8a5cd52c962dac27fe6 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 13 Jan 2021 13:16:15 +0100 Subject: [PATCH 302/352] Fix Travis CI --- .travis.yml | 1 - setup.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2d923162..ea58d44a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ install: script: - 'coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test' - 'coverage report -m --rcfile=tests/coverage.rc' -# - 'pylint src/onelogin/saml2 --rcfile=tests/pylint.rc' - 'pycodestyle tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc' - 'pyflakes src/onelogin/saml2 demo-django demo-flask tests/src/OneLogin/saml2_tests' diff --git a/setup.py b/setup.py index 3ebb6cc6..e2507545 100644 --- a/setup.py +++ b/setup.py @@ -39,9 +39,8 @@ ], extras_require={ 'test': ( - 'coverage>=3.6', + 'coverage>=3.6, <5.0', 'freezegun==0.3.5', - 'pylint==1.9.1', 'flake8==3.6.0', 'coveralls==1.1', ), From fcea290783f514688ee07d48c67ee84f13f12402 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 14 Jan 2021 09:54:56 +0100 Subject: [PATCH 303/352] Release 2.9.0 --- changelog.md | 10 ++++++++++ setup.py | 4 ++-- src/onelogin/__init__.py | 2 +- src/onelogin/saml2/__init__.py | 2 +- src/onelogin/saml2/auth.py | 2 +- src/onelogin/saml2/authn_request.py | 2 +- src/onelogin/saml2/constants.py | 2 +- src/onelogin/saml2/errors.py | 2 +- src/onelogin/saml2/idp_metadata_parser.py | 2 +- src/onelogin/saml2/logout_request.py | 2 +- src/onelogin/saml2/logout_response.py | 2 +- src/onelogin/saml2/metadata.py | 2 +- src/onelogin/saml2/response.py | 2 +- src/onelogin/saml2/settings.py | 2 +- src/onelogin/saml2/utils.py | 2 +- tests/src/OneLogin/saml2_tests/auth_test.py | 2 +- tests/src/OneLogin/saml2_tests/authn_request_test.py | 2 +- tests/src/OneLogin/saml2_tests/error_test.py | 2 +- .../OneLogin/saml2_tests/idp_metadata_parser_test.py | 2 +- tests/src/OneLogin/saml2_tests/logout_request_test.py | 2 +- tests/src/OneLogin/saml2_tests/logout_response_test.py | 2 +- tests/src/OneLogin/saml2_tests/metadata_test.py | 2 +- tests/src/OneLogin/saml2_tests/response_test.py | 2 +- tests/src/OneLogin/saml2_tests/settings_test.py | 2 +- tests/src/OneLogin/saml2_tests/signed_response_test.py | 2 +- tests/src/OneLogin/saml2_tests/utils_test.py | 2 +- 26 files changed, 36 insertions(+), 26 deletions(-) diff --git a/changelog.md b/changelog.md index eb866956..940f9be4 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,14 @@ # python-saml changelog +### 2.9.0 (Jan 13, 2021) +* Destination URL Comparison is now case-insensitive for netloc +* Support single-label-domains as valid. New security parameter allowSingleLabelDomains +* Added get_idp_sso_url, get_idp_slo_url and get_idp_slo_response_url methods to the Settings class and use it in the toolkit +* [#267](https://github.com/onelogin/python-saml/issues/267) Custom lxml parser based on the one defined at xmldefused. Parser will ignore comments and processing instructions and by default have deactivated huge_tree, DTD and access to external documents +* Add get_friendlyname_attributes support +* Remove external lib method get_ext_lib_path. Add set_cert_path in order to allow set the cert path in a different folder than the toolkit +* Add python2 deprecation info +* [#269](https://github.com/onelogin/python-saml/issues/269) Add sha256 instead sha1 algorithm for sign/digest as recommended value on documentation and settings + ### 2.8.0 (NOv 20, 2019) * [#258](https://github.com/onelogin/python-saml/issues/258) Fix failOnAuthnContextMismatch feature * [#250](https://github.com/onelogin/python-saml/issues/250) Allow any number of decimal places for seconds on SAML datetimes diff --git a/setup.py b/setup.py index e2507545..5966f6c9 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from setuptools import setup @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.8.0', + version='2.9.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', diff --git a/src/onelogin/__init__.py b/src/onelogin/__init__.py index ba664a65..52ea1212 100644 --- a/src/onelogin/__init__.py +++ b/src/onelogin/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License Add SAML support to your Python softwares using this library. diff --git a/src/onelogin/saml2/__init__.py b/src/onelogin/saml2/__init__.py index ba664a65..52ea1212 100644 --- a/src/onelogin/saml2/__init__.py +++ b/src/onelogin/saml2/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License Add SAML support to your Python softwares using this library. diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index ec4c123b..4911da6f 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -2,7 +2,7 @@ """ OneLogin_Saml2_Auth class -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License Main class of OneLogin's Python Toolkit. diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index e99f494f..21bb746c 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -2,7 +2,7 @@ """ OneLogin_Saml2_Authn_Request class -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License AuthNRequest class of OneLogin's Python Toolkit. diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py index 87cb0e6f..3cb78dd4 100644 --- a/src/onelogin/saml2/constants.py +++ b/src/onelogin/saml2/constants.py @@ -2,7 +2,7 @@ """ OneLogin_Saml2_Constants class -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License Constants class of OneLogin's Python Toolkit. diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 560ec8e6..081d57d5 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -2,7 +2,7 @@ """ OneLogin_Saml2_Error class -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License Error class of OneLogin's Python Toolkit. diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index e66acf7a..cdcbce0f 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -2,7 +2,7 @@ """ OneLogin_Saml2_IdPMetadataParser class -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License Metadata class of OneLogin's Python Toolkit. diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 2a2d881f..4aed6d6d 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -2,7 +2,7 @@ """ OneLogin_Saml2_Logout_Request class -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License Logout Request class of OneLogin's Python Toolkit. diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 4bb834bb..96900aea 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -2,7 +2,7 @@ """ OneLogin_Saml2_Logout_Response class -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License Logout Response class of OneLogin's Python Toolkit. diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 4716a49e..a1ef514f 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -2,7 +2,7 @@ """ OneLogin_Saml2_Metadata class -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License Metadata class of OneLogin's Python Toolkit. diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index fe4136ae..bb186f86 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -2,7 +2,7 @@ """ OneLogin_Saml2_Response class -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License SAML Response class of OneLogin's Python Toolkit. diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index b5ea7e54..660da4e5 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -2,7 +2,7 @@ """ OneLogin_Saml2_Settings class -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License Setting class of OneLogin's Python Toolkit. diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 16fcfcb6..aef6b9fb 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -2,7 +2,7 @@ """ OneLogin_Saml2_Utils class -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2021 OneLogin, Inc. MIT License Auxiliary class of OneLogin's Python Toolkit. diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 4d7fdb71..30e48766 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64decode, b64encode diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 5190d84a..3e10e71e 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64decode diff --git a/tests/src/OneLogin/saml2_tests/error_test.py b/tests/src/OneLogin/saml2_tests/error_test.py index 9cc861ad..2c18354d 100644 --- a/tests/src/OneLogin/saml2_tests/error_test.py +++ b/tests/src/OneLogin/saml2_tests/error_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License import unittest diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index 0359bb5a..545f0d12 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index ba73cf53..33c1548d 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64encode diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 30ce793c..56797425 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License import json diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 60453026..c6071453 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index a9252b3c..3b3499c4 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64decode, b64encode diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 9fe5c218..8341beb6 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License import json diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py index 77e0d5c4..ea011457 100644 --- a/tests/src/OneLogin/saml2_tests/signed_response_test.py +++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64encode diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 9ca2f3ea..3fbb34db 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2018 OneLogin, Inc. +# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64decode From 3dccb5d562874a661e7ead9b24f4ea9e27d78a86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Apr 2021 21:29:28 +0000 Subject: [PATCH 304/352] Bump bottle from 0.12.8 to 0.12.19 in /demo-bottle Bumps [bottle](https://github.com/bottlepy/bottle) from 0.12.8 to 0.12.19. - [Release notes](https://github.com/bottlepy/bottle/releases) - [Changelog](https://github.com/bottlepy/bottle/blob/master/docs/changelog.rst) - [Commits](https://github.com/bottlepy/bottle/compare/0.12.8...0.12.19) Signed-off-by: dependabot[bot] --- demo-bottle/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-bottle/requirements.txt b/demo-bottle/requirements.txt index b97d5eeb..639ee202 100644 --- a/demo-bottle/requirements.txt +++ b/demo-bottle/requirements.txt @@ -1,4 +1,4 @@ -bottle==0.12.8 +bottle==0.12.19 beaker==1.6.4 paste==1.7.5.1 jinja2==2.7.3 From 11cd9600c00f9f9ab26d1999f4069de8d799b5a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Apr 2021 10:34:03 +0000 Subject: [PATCH 305/352] Bump jinja2 from 2.7.3 to 2.11.3 in /demo-bottle Bumps [jinja2](https://github.com/pallets/jinja) from 2.7.3 to 2.11.3. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/master/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/2.7.3...2.11.3) Signed-off-by: dependabot[bot] --- demo-bottle/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-bottle/requirements.txt b/demo-bottle/requirements.txt index 639ee202..497e3896 100644 --- a/demo-bottle/requirements.txt +++ b/demo-bottle/requirements.txt @@ -1,4 +1,4 @@ bottle==0.12.19 beaker==1.6.4 paste==1.7.5.1 -jinja2==2.7.3 +jinja2==2.11.3 From 611f8bee3404071bde1151859d27b872711359a1 Mon Sep 17 00:00:00 2001 From: William Czifro Date: Thu, 13 May 2021 14:48:27 -0700 Subject: [PATCH 306/352] Create python-package.yml --- .github/workflows/python-package.yml | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/python-package.yml diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 00000000..174aef29 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,31 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python package + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 2.7 + uses: actions/setup-python@v2 + with: + python-version: 2.7 + + - name: Install dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -qq swig python-dev libxml2-dev libxmlsec1-dev + pip install . + pip install -e ".[test]" + + - name: Test + run: | + coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test + coverage report -m --rcfile=tests/coverage.rc + pycodestyle tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc + pyflakes src/onelogin/saml2 demo-django demo-flask tests/src/OneLogin/saml2_tests From 62a96394344c3188edc8aee540706f3255a59c06 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 26 May 2021 16:32:02 +0200 Subject: [PATCH 307/352] Add warning about the use of OneLogin_Saml2_IdPMetadataParser class --- README.md | 7 +++++++ src/onelogin/saml2/idp_metadata_parser.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index eee24c17..be8f3b3e 100644 --- a/README.md +++ b/README.md @@ -549,6 +549,13 @@ There's an easier method -- use a metadata exchange. Metadata is just an XML fi Using ````parse_remote```` IdP metadata can be obtained and added to the settings withouth further ado. +But take in mind that the OneLogin_Saml2_IdPMetadataParser class does not validate in any way the URL that is introduced in order to be parsed. + +Usually the same administrator that handles the Service Provider also sets the URL to the IdP, which should be a trusted resource. + +But there are other scenarios, like a SAAS app where the administrator of the app delegates this functionality to other users. In this case, extra precaution should be taken in order to validate such URL inputs and avoid attacks like SSRF. + + `` idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://example.com/auth/saml2/idp/metadata') `` diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index cdcbce0f..fafe3a2e 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -22,6 +22,9 @@ class OneLogin_Saml2_IdPMetadataParser(object): """ A class that contain methods related to obtaining and parsing metadata from IdP + + This class does not validate in any way the URL that is introduced, + make sure to validate it properly before use it in a get_metadata method. """ @staticmethod From edb8d5e4525c72aa3753e3af30f29429e91c7a10 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 6 Jul 2021 16:49:28 +0200 Subject: [PATCH 308/352] Tests: Update expired dates from responses --- tests/data/responses/hmac1_response.xml.base64 | 1 + tests/data/responses/invalids/encrypted_attrs.xml.base64 | 2 +- tests/data/responses/invalids/invalid_audience.xml.base64 | 2 +- .../data/responses/invalids/invalid_issuer_assertion.xml.base64 | 2 +- tests/data/responses/invalids/invalid_issuer_message.xml.base64 | 2 +- tests/data/responses/invalids/invalid_sessionindex.xml.base64 | 2 +- .../invalids/invalid_subjectconfirmation_inresponse.xml.base64 | 2 +- .../invalids/invalid_subjectconfirmation_nb.xml.base64 | 2 +- .../invalids/invalid_subjectconfirmation_noa.xml.base64 | 2 +- .../invalids/invalid_subjectconfirmation_recipient.xml.base64 | 2 +- tests/data/responses/invalids/no_nameid.xml.base64 | 2 +- .../responses/invalids/no_subjectconfirmation_data.xml.base64 | 2 +- .../responses/invalids/no_subjectconfirmation_method.xml.base64 | 2 +- .../data/responses/invalids/response_encrypted_attrs.xml.base64 | 2 +- tests/data/responses/no_audience.xml.base64 | 2 +- .../data/responses/unsigned_response_with_miliseconds.xm.base64 | 2 +- 16 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 tests/data/responses/hmac1_response.xml.base64 diff --git a/tests/data/responses/hmac1_response.xml.base64 b/tests/data/responses/hmac1_response.xml.base64 new file mode 100644 index 00000000..7be67630 --- /dev/null +++ b/tests/data/responses/hmac1_response.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQyYmU0MGJmLTM5YzMtNzdmMC1jNmFlLThiZjJlMjNhMWEyZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PFNpZ25hdHVyZSB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxTaWduZWRJbmZvPg0KICAgIDxDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPFNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNobWFjLXNoYTEiLz4NCiAgICA8UmVmZXJlbmNlIFVSST0iIj4NCiAgICAgIDxUcmFuc2Zvcm1zPg0KICAgICAgICA8VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4NCiAgICAgICAgPFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgPC9UcmFuc2Zvcm1zPg0KICAgICAgPERpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+DQogICAgICA8RGlnZXN0VmFsdWU+eGFSZDN2WWxLaVB5TlBJTVhWYlVQVEhjQ1JzPTwvRGlnZXN0VmFsdWU+DQogICAgPC9SZWZlcmVuY2U+DQogIDwvU2lnbmVkSW5mbz4NCiAgPFNpZ25hdHVyZVZhbHVlPlhrM2lCRHoxN0QzM05MMS9qUm9lR2M5enhtcz08L1NpZ25hdHVyZVZhbHVlPg0KPEtleUluZm8+PFg1MDlEYXRhPjxYNTA5Q2VydGlmaWNhdGU+TUlJQ2dUQ0NBZW9DQ1FDYk9scldEZFg3RlRBTkJna3Foa2lHOXcwQkFRVUZBRENCaERFTE1Ba0dBMVVFQmhNQ1RrOHhHREFXQmdOVkJBZ1REMEZ1WkhKbFlYTWdVMjlzWW1WeVp6RU1NQW9HQTFVRUJ4TURSbTl2TVJBd0RnWURWUVFLRXdkVlRrbE9SVlJVTVJnd0ZnWURWUVFERXc5bVpXbGtaUzVsY214aGJtY3VibTh4SVRBZkJna3Foa2lHOXcwQkNRRVdFbUZ1WkhKbFlYTkFkVzVwYm1WMGRDNXViekFlRncwd056QTJNVFV4TWpBeE16VmFGdzB3TnpBNE1UUXhNakF4TXpWYU1JR0VNUXN3Q1FZRFZRUUdFd0pPVHpFWU1CWUdBMVVFQ0JNUFFXNWtjbVZoY3lCVGIyeGlaWEpuTVF3d0NnWURWUVFIRXdOR2IyOHhFREFPQmdOVkJBb1RCMVZPU1U1RlZGUXhHREFXQmdOVkJBTVREMlpsYVdSbExtVnliR0Z1Wnk1dWJ6RWhNQjhHQ1NxR1NJYjNEUUVKQVJZU1lXNWtjbVZoYzBCMWJtbHVaWFIwTG01dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRGl2YmhSN1A1MTZ4L1MzQnFLeHVwUWUwTE9Ob2xpdXBpQk9lc0NPM1NIYkRybDMrcTlJYmZuZm1FMDRyTnVNY1BzSXhCMTYxVGREcEllc0xDbjdjOGFQSElTS090UGxBZVRaU25iOFFBdTdhUmpacTMrUGJyUDV1VzNUY2ZDR1B0S1R5dEhPZ2UvT2xKYm8wNzhkVmhYUTE0ZDFFRHdYSlcxclJYdVV0NEM4UUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDRFZmcDg2SE9icVkrZThCVW9XUTkrVk1ReDFBU0RvaEJqd09zZzJXeWtVcVJYRitkTGZjVUg5ZFdSNjNDdFpJS0ZEYlN0Tm9tUG5RejduYksrb255Z3dCc3BWRWJuSHVVaWhacTNaVWRtdW1RcUN3NFV2cy8xVXZxM29yT28vV0pWaFR5dkxnRlZLMlFhclE0LzY3T1pmSGQ3UitQT0JYaG9waFNNdjFaT288L1g1MDlDZXJ0aWZpY2F0ZT48L1g1MDlEYXRhPjwvS2V5SW5mbz48L1NpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiBJRD0icGZ4NTdkZmRhNjAtYjIxMS00Y2RhLTBmNjMtNmQ1ZGViNjllNWJiIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5hdHRhY2tlckBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hdHRhY2tlckBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+QXR0YWNrZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= diff --git a/tests/data/responses/invalids/encrypted_attrs.xml.base64 b/tests/data/responses/invalids/encrypted_attrs.xml.base64 index f452075c..a170970a 100644 --- a/tests/data/responses/invalids/encrypted_attrs.xml.base64 +++ b/tests/data/responses/invalids/encrypted_attrs.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaGh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjAyMS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KIDxzYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZSB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj4NCiAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCIgSWQ9Il9GMzk2MjVBRjY4QjRGQzA3OENDNzU4MkQyOEQwNUQ5QyI+DQogICAgICAgICAgICA8eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMjU2LWNiYyIvPg0KICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICAgICAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICAgICAgPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIi8+DQogICAgICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogICAgICAgICAgICAgICAgICA8ZHM6S2V5TmFtZT42MjM1NWZiZDFmNjI0NTAzYzVjOTY3NzQwMmVjY2EwMGVmMWY2Mjc3PC9kczpLZXlOYW1lPg0KICAgICAgICAgICAgICAgIDwvZHM6S2V5SW5mbz4NCiAgICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICAgICAgPHhlbmM6Q2lwaGVyVmFsdWU+SzBtQkx4Zkx6aUtWVUtFQU9ZZTdENnVWU0NQeTh2eVdWaDNSZWNuUEVTKzhRa0FoT3VSU3VFL0xRcEZyMGh1SS9pQ0V5OXBkZTFRZ2pZREx0akhjdWpLaTJ4R3FXNmprWFcvRXVLb21xV1BQQTJ4WXMxZnBCMXN1NGFYVU9RQjZPSjcwL29EY09zeTgzNGdoRmFCV2lsRThmcXlEQlVCdlcrMkl2YU1VWmFid04vczltVmtXek0zcjMwdGxraExLN2lPcmJHQWxkSUh3RlU1ejdQUFI2Uk8zWTNmSXhqSFU0ME9uTHNKYzN4SXFkTEgzZlhwQzBrZ2k1VXNwTGRxMTRlNU9vWGpMb1BHM0JPM3p3T0FJSjhYTkJXWTV1UW9mNktyS2JjdnRaU1kwZk12UFloWWZOanRSRnk4eTQ5b3ZMOWZ3akNSVERsVDUrYUhxc0NUQnJ3PT08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICAgICAgPC94ZW5jOkNpcGhlckRhdGE+DQogICAgICAgICAgICAgIDwveGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICA8L2RzOktleUluZm8+DQogICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5aekN1NmF4R2dBWVpIVmY3N05YOGFwWktCL0dKRGV1VjZiRkJ5QlMwQUlnaVhrdkRVQW1MQ3BhYlRBV0JNK3l6MTlvbEE2cnJ5dU9mcjgyZXYyYnpQTlVSdm00U1l4YWh2dUw0UGlibjV3Smt5MEJsNTRWcW1jVStBcWowZEF2T2dxRzF5M1g0d085bjliUnNUdjY5MjFtMGVxUkFGcGg4a0s4TDloaXJLMUJ4WUJZajJSeUZDb0ZEUHhWWjV3eXJhM3E0cW1FNC9FTFFwRlA2bWZVOExYYjB1b1dKVWpHVWVsUzJBYTdiWmlzOHpFcHdvdjRDd3RsTmpsdFFpaDRtdjd0dENBZllxY1FJRnpCVEIrREFhMCtYZ2d4Q0xjZEIzK21RaVJjRUNCZndISEo3Z1JtbnVCRWdlV1QzQ0dLYTNOYjdHTVhPZnV4RktGNXBJZWhXZ28za2ROUUxhbG9yOFJWVzZJOFAvSThmUTMzRmUrTnNIVm5KM3p3U0EvL2E8L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICA8L3hlbmM6Q2lwaGVyRGF0YT4NCiAgICAgICAgICA8L3hlbmM6RW5jcnlwdGVkRGF0YT4NCiAgICAgICAgPC9zYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaGh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KIDxzYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZSB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj4NCiAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCIgSWQ9Il9GMzk2MjVBRjY4QjRGQzA3OENDNzU4MkQyOEQwNUQ5QyI+DQogICAgICAgICAgICA8eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMjU2LWNiYyIvPg0KICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICAgICAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICAgICAgPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIi8+DQogICAgICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogICAgICAgICAgICAgICAgICA8ZHM6S2V5TmFtZT42MjM1NWZiZDFmNjI0NTAzYzVjOTY3NzQwMmVjY2EwMGVmMWY2Mjc3PC9kczpLZXlOYW1lPg0KICAgICAgICAgICAgICAgIDwvZHM6S2V5SW5mbz4NCiAgICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICAgICAgPHhlbmM6Q2lwaGVyVmFsdWU+SzBtQkx4Zkx6aUtWVUtFQU9ZZTdENnVWU0NQeTh2eVdWaDNSZWNuUEVTKzhRa0FoT3VSU3VFL0xRcEZyMGh1SS9pQ0V5OXBkZTFRZ2pZREx0akhjdWpLaTJ4R3FXNmprWFcvRXVLb21xV1BQQTJ4WXMxZnBCMXN1NGFYVU9RQjZPSjcwL29EY09zeTgzNGdoRmFCV2lsRThmcXlEQlVCdlcrMkl2YU1VWmFid04vczltVmtXek0zcjMwdGxraExLN2lPcmJHQWxkSUh3RlU1ejdQUFI2Uk8zWTNmSXhqSFU0ME9uTHNKYzN4SXFkTEgzZlhwQzBrZ2k1VXNwTGRxMTRlNU9vWGpMb1BHM0JPM3p3T0FJSjhYTkJXWTV1UW9mNktyS2JjdnRaU1kwZk12UFloWWZOanRSRnk4eTQ5b3ZMOWZ3akNSVERsVDUrYUhxc0NUQnJ3PT08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICAgICAgPC94ZW5jOkNpcGhlckRhdGE+DQogICAgICAgICAgICAgIDwveGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICA8L2RzOktleUluZm8+DQogICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5aekN1NmF4R2dBWVpIVmY3N05YOGFwWktCL0dKRGV1VjZiRkJ5QlMwQUlnaVhrdkRVQW1MQ3BhYlRBV0JNK3l6MTlvbEE2cnJ5dU9mcjgyZXYyYnpQTlVSdm00U1l4YWh2dUw0UGlibjV3Smt5MEJsNTRWcW1jVStBcWowZEF2T2dxRzF5M1g0d085bjliUnNUdjY5MjFtMGVxUkFGcGg4a0s4TDloaXJLMUJ4WUJZajJSeUZDb0ZEUHhWWjV3eXJhM3E0cW1FNC9FTFFwRlA2bWZVOExYYjB1b1dKVWpHVWVsUzJBYTdiWmlzOHpFcHdvdjRDd3RsTmpsdFFpaDRtdjd0dENBZllxY1FJRnpCVEIrREFhMCtYZ2d4Q0xjZEIzK21RaVJjRUNCZndISEo3Z1JtbnVCRWdlV1QzQ0dLYTNOYjdHTVhPZnV4RktGNXBJZWhXZ28za2ROUUxhbG9yOFJWVzZJOFAvSThmUTMzRmUrTnNIVm5KM3p3U0EvL2E8L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICA8L3hlbmM6Q2lwaGVyRGF0YT4NCiAgICAgICAgICA8L3hlbmM6RW5jcnlwdGVkRGF0YT4NCiAgICAgICAgPC9zYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_audience.xml.base64 b/tests/data/responses/invalids/invalid_audience.xml.base64 index a2575836..787c8a79 100644 --- a/tests/data/responses/invalids/invalid_audience.xml.base64 +++ b/tests/data/responses/invalids/invalid_audience.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9pbnZhbGlkLmF1ZGllbmNlLmNvbTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9pbnZhbGlkLmF1ZGllbmNlLmNvbTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_issuer_assertion.xml.base64 b/tests/data/responses/invalids/invalid_issuer_assertion.xml.base64 index 07748ece..426ea5c2 100644 --- a/tests/data/responses/invalids/invalid_issuer_assertion.xml.base64 +++ b/tests/data/responses/invalids/invalid_issuer_assertion.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2ludmFsaWQuaXNzdWVyLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+ICAgIA0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJoZWxsby5jb20iIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOk5hbWVJRD4NCiAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIwLTA2LTE3VDE0OjU5OjE0WiIgUmVjaXBpZW50PSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvYWNzLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjAyMS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyMS0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2ludmFsaWQuaXNzdWVyLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+ICAgIA0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJoZWxsby5jb20iIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOk5hbWVJRD4NCiAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIwLTA2LTE3VDE0OjU5OjE0WiIgUmVjaXBpZW50PSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvYWNzLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_issuer_message.xml.base64 b/tests/data/responses/invalids/invalid_issuer_message.xml.base64 index f1f49fe5..0c06a25e 100644 --- a/tests/data/responses/invalids/invalid_issuer_message.xml.base64 +++ b/tests/data/responses/invalids/invalid_issuer_message.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaW52YWxpZC5pc3Nlci5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICA8c2FtbHA6U3RhdHVzPg0KICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz4NCiAgPC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZng3ODQxOTkxYy1jNzNmLTQwMzUtZTJlZS1jMTcwYzBlMWQzZTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjE0WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vPC9zYW1sOklzc3Vlcj4gICAgDQogICAgPHNhbWw6U3ViamVjdD4NCiAgICAgIDxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9ImhlbGxvLmNvbSIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6TmFtZUlEPg0KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPg0KICAgICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjAtMDYtMTdUMTQ6NTk6MTRaIiBSZWNpcGllbnQ9Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiLz4NCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDIxLTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDIxLTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg0KICA= +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaW52YWxpZC5pc3Nlci5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICA8c2FtbHA6U3RhdHVzPg0KICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz4NCiAgPC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZng3ODQxOTkxYy1jNzNmLTQwMzUtZTJlZS1jMTcwYzBlMWQzZTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjE0WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vPC9zYW1sOklzc3Vlcj4gICAgDQogICAgPHNhbWw6U3ViamVjdD4NCiAgICAgIDxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9ImhlbGxvLmNvbSIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6TmFtZUlEPg0KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPg0KICAgICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjAtMDYtMTdUMTQ6NTk6MTRaIiBSZWNpcGllbnQ9Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiLz4NCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg0KICA= \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_sessionindex.xml.base64 b/tests/data/responses/invalids/invalid_sessionindex.xml.base64 index cc5a581c..f2e4c4c6 100644 --- a/tests/data/responses/invalids/invalid_sessionindex.xml.base64 +++ b/tests/data/responses/invalids/invalid_sessionindex.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTMtMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTMtMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 b/tests/data/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 index 2ce545f8..b6a4e2eb 100644 --- a/tests/data/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +++ b/tests/data/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iaW52YWxpZF9pbnJlc3BvbnNlIi8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjAyMS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyMS0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iaW52YWxpZF9pbnJlc3BvbnNlIi8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 b/tests/data/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 index c88c4edb..5d1f8bc1 100644 --- a/tests/data/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +++ b/tests/data/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdEJlZm9yZT0iMjA5OS0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdEJlZm9yZT0iMjk5OS0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjI5OTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjI5OTktMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 b/tests/data/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 index 6d4b70aa..4f922132 100644 --- a/tests/data/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +++ b/tests/data/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 b/tests/data/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 index 1bf1d25a..30c55eb2 100644 --- a/tests/data/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +++ b/tests/data/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL2ludmFsaWQucmVjaXBlbnQuZXhhbXBsZS5jb20iIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL2ludmFsaWQucmVjaXBlbnQuZXhhbXBsZS5jb20iIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/invalids/no_nameid.xml.base64 b/tests/data/responses/invalids/no_nameid.xml.base64 index 3c2b6d9b..db8879b2 100644 --- a/tests/data/responses/invalids/no_nameid.xml.base64 +++ b/tests/data/responses/invalids/no_nameid.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPg0KICAgICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjAtMDYtMTdUMTQ6NTk6MTRaIiBSZWNpcGllbnQ9Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiLz4NCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDIxLTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDIxLTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPg0KICAgICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjAtMDYtMTdUMTQ6NTk6MTRaIiBSZWNpcGllbnQ9Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiLz4NCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/invalids/no_subjectconfirmation_data.xml.base64 b/tests/data/responses/invalids/no_subjectconfirmation_data.xml.base64 index 3c92f561..bc28d907 100644 --- a/tests/data/responses/invalids/no_subjectconfirmation_data.xml.base64 +++ b/tests/data/responses/invalids/no_subjectconfirmation_data.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+ICAgICAgICANCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDIxLTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDIxLTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+ICAgICAgICANCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/invalids/no_subjectconfirmation_method.xml.base64 b/tests/data/responses/invalids/no_subjectconfirmation_method.xml.base64 index 07ce153a..37b69370 100644 --- a/tests/data/responses/invalids/no_subjectconfirmation_method.xml.base64 +++ b/tests/data/responses/invalids/no_subjectconfirmation_method.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmhvbGRlci1vZi1rZXkiPg0KICAgICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjAtMDYtMTdUMTQ6NTk6MTRaIiBSZWNpcGllbnQ9Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiLz4NCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDIxLTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDIxLTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmhvbGRlci1vZi1rZXkiPg0KICAgICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjAtMDYtMTdUMTQ6NTk6MTRaIiBSZWNpcGllbnQ9Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiLz4NCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/invalids/response_encrypted_attrs.xml.base64 b/tests/data/responses/invalids/response_encrypted_attrs.xml.base64 index 32449899..ad6392c5 100644 --- a/tests/data/responses/invalids/response_encrypted_attrs.xml.base64 +++ b/tests/data/responses/invalids/response_encrypted_attrs.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaGh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjAyMS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyMS0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KIDxzYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZSB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj4NCiAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCIgSWQ9Il9GMzk2MjVBRjY4QjRGQzA3OENDNzU4MkQyOEQwNUQ5QyI+DQogICAgICAgICAgICA8eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMjU2LWNiYyIvPg0KICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICAgICAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICAgICAgPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIi8+DQogICAgICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogICAgICAgICAgICAgICAgICA8ZHM6S2V5TmFtZT42MjM1NWZiZDFmNjI0NTAzYzVjOTY3NzQwMmVjY2EwMGVmMWY2Mjc3PC9kczpLZXlOYW1lPg0KICAgICAgICAgICAgICAgIDwvZHM6S2V5SW5mbz4NCiAgICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICAgICAgPHhlbmM6Q2lwaGVyVmFsdWU+SzBtQkx4Zkx6aUtWVUtFQU9ZZTdENnVWU0NQeTh2eVdWaDNSZWNuUEVTKzhRa0FoT3VSU3VFL0xRcEZyMGh1SS9pQ0V5OXBkZTFRZ2pZREx0akhjdWpLaTJ4R3FXNmprWFcvRXVLb21xV1BQQTJ4WXMxZnBCMXN1NGFYVU9RQjZPSjcwL29EY09zeTgzNGdoRmFCV2lsRThmcXlEQlVCdlcrMkl2YU1VWmFid04vczltVmtXek0zcjMwdGxraExLN2lPcmJHQWxkSUh3RlU1ejdQUFI2Uk8zWTNmSXhqSFU0ME9uTHNKYzN4SXFkTEgzZlhwQzBrZ2k1VXNwTGRxMTRlNU9vWGpMb1BHM0JPM3p3T0FJSjhYTkJXWTV1UW9mNktyS2JjdnRaU1kwZk12UFloWWZOanRSRnk4eTQ5b3ZMOWZ3akNSVERsVDUrYUhxc0NUQnJ3PT08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICAgICAgPC94ZW5jOkNpcGhlckRhdGE+DQogICAgICAgICAgICAgIDwveGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICA8L2RzOktleUluZm8+DQogICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5aekN1NmF4R2dBWVpIVmY3N05YOGFwWktCL0dKRGV1VjZiRkJ5QlMwQUlnaVhrdkRVQW1MQ3BhYlRBV0JNK3l6MTlvbEE2cnJ5dU9mcjgyZXYyYnpQTlVSdm00U1l4YWh2dUw0UGlibjV3Smt5MEJsNTRWcW1jVStBcWowZEF2T2dxRzF5M1g0d085bjliUnNUdjY5MjFtMGVxUkFGcGg4a0s4TDloaXJLMUJ4WUJZajJSeUZDb0ZEUHhWWjV3eXJhM3E0cW1FNC9FTFFwRlA2bWZVOExYYjB1b1dKVWpHVWVsUzJBYTdiWmlzOHpFcHdvdjRDd3RsTmpsdFFpaDRtdjd0dENBZllxY1FJRnpCVEIrREFhMCtYZ2d4Q0xjZEIzK21RaVJjRUNCZndISEo3Z1JtbnVCRWdlV1QzQ0dLYTNOYjdHTVhPZnV4RktGNXBJZWhXZ28za2ROUUxhbG9yOFJWVzZJOFAvSThmUTMzRmUrTnNIVm5KM3p3U0EvL2E8L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICA8L3hlbmM6Q2lwaGVyRGF0YT4NCiAgICAgICAgICA8L3hlbmM6RW5jcnlwdGVkRGF0YT4NCiAgICAgICAgPC9zYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaGh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KIDxzYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZSB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj4NCiAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCIgSWQ9Il9GMzk2MjVBRjY4QjRGQzA3OENDNzU4MkQyOEQwNUQ5QyI+DQogICAgICAgICAgICA8eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMjU2LWNiYyIvPg0KICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICAgICAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICAgICAgPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIi8+DQogICAgICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogICAgICAgICAgICAgICAgICA8ZHM6S2V5TmFtZT42MjM1NWZiZDFmNjI0NTAzYzVjOTY3NzQwMmVjY2EwMGVmMWY2Mjc3PC9kczpLZXlOYW1lPg0KICAgICAgICAgICAgICAgIDwvZHM6S2V5SW5mbz4NCiAgICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICAgICAgPHhlbmM6Q2lwaGVyVmFsdWU+SzBtQkx4Zkx6aUtWVUtFQU9ZZTdENnVWU0NQeTh2eVdWaDNSZWNuUEVTKzhRa0FoT3VSU3VFL0xRcEZyMGh1SS9pQ0V5OXBkZTFRZ2pZREx0akhjdWpLaTJ4R3FXNmprWFcvRXVLb21xV1BQQTJ4WXMxZnBCMXN1NGFYVU9RQjZPSjcwL29EY09zeTgzNGdoRmFCV2lsRThmcXlEQlVCdlcrMkl2YU1VWmFid04vczltVmtXek0zcjMwdGxraExLN2lPcmJHQWxkSUh3RlU1ejdQUFI2Uk8zWTNmSXhqSFU0ME9uTHNKYzN4SXFkTEgzZlhwQzBrZ2k1VXNwTGRxMTRlNU9vWGpMb1BHM0JPM3p3T0FJSjhYTkJXWTV1UW9mNktyS2JjdnRaU1kwZk12UFloWWZOanRSRnk4eTQ5b3ZMOWZ3akNSVERsVDUrYUhxc0NUQnJ3PT08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICAgICAgPC94ZW5jOkNpcGhlckRhdGE+DQogICAgICAgICAgICAgIDwveGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICA8L2RzOktleUluZm8+DQogICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5aekN1NmF4R2dBWVpIVmY3N05YOGFwWktCL0dKRGV1VjZiRkJ5QlMwQUlnaVhrdkRVQW1MQ3BhYlRBV0JNK3l6MTlvbEE2cnJ5dU9mcjgyZXYyYnpQTlVSdm00U1l4YWh2dUw0UGlibjV3Smt5MEJsNTRWcW1jVStBcWowZEF2T2dxRzF5M1g0d085bjliUnNUdjY5MjFtMGVxUkFGcGg4a0s4TDloaXJLMUJ4WUJZajJSeUZDb0ZEUHhWWjV3eXJhM3E0cW1FNC9FTFFwRlA2bWZVOExYYjB1b1dKVWpHVWVsUzJBYTdiWmlzOHpFcHdvdjRDd3RsTmpsdFFpaDRtdjd0dENBZllxY1FJRnpCVEIrREFhMCtYZ2d4Q0xjZEIzK21RaVJjRUNCZndISEo3Z1JtbnVCRWdlV1QzQ0dLYTNOYjdHTVhPZnV4RktGNXBJZWhXZ28za2ROUUxhbG9yOFJWVzZJOFAvSThmUTMzRmUrTnNIVm5KM3p3U0EvL2E8L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICA8L3hlbmM6Q2lwaGVyRGF0YT4NCiAgICAgICAgICA8L3hlbmM6RW5jcnlwdGVkRGF0YT4NCiAgICAgICAgPC9zYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/data/responses/no_audience.xml.base64 b/tests/data/responses/no_audience.xml.base64 index 6ac34337..25550e3b 100644 --- a/tests/data/responses/no_audience.xml.base64 +++ b/tests/data/responses/no_audience.xml.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDIxLTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/unsigned_response_with_miliseconds.xm.base64 b/tests/data/responses/unsigned_response_with_miliseconds.xm.base64 index 76522b7e..1722cbf9 100644 --- a/tests/data/responses/unsigned_response_with_miliseconds.xm.base64 +++ b/tests/data/responses/unsigned_response_with_miliseconds.xm.base64 @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTQuMTIwWiIgRGVzdGluYXRpb249Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiPg0KICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICA8c2FtbHA6U3RhdHVzPg0KICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz4NCiAgPC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZng3ODQxOTkxYy1jNzNmLTQwMzUtZTJlZS1jMTcwYzBlMWQzZTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjE0LjEyMFoiPg0KICAgIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+ICAgIA0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJoZWxsby5jb20iIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOk5hbWVJRD4NCiAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIwLTA2LTE3VDE0OjU5OjE0WiIgUmVjaXBpZW50PSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvYWNzLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NC4xNzNaIiBOb3RPbk9yQWZ0ZXI9IjIwMjEtMDYtMTdUMTQ6NTk6MTQuMjM1WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDcuMTIwWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyMS0wNi0xN1QyMjo1NDoxNC4xMjBaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTQuMTIwWiIgRGVzdGluYXRpb249Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiPg0KICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICA8c2FtbHA6U3RhdHVzPg0KICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz4NCiAgPC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZng3ODQxOTkxYy1jNzNmLTQwMzUtZTJlZS1jMTcwYzBlMWQzZTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjE0LjEyMFoiPg0KICAgIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+ICAgIA0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJoZWxsby5jb20iIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOk5hbWVJRD4NCiAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIwLTA2LTE3VDE0OjU5OjE0WiIgUmVjaXBpZW50PSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvYWNzLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NC4xNzNaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTQuMjM1WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDcuMTIwWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QyMjo1NDoxNC4xMjBaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file From e2e1bc462005f6fc529adc51e269427877dbf351 Mon Sep 17 00:00:00 2001 From: william chu Date: Sat, 10 Jul 2021 12:28:06 +1000 Subject: [PATCH 309/352] fix: Removed CC-BY-SA 3.0 non compliant implementation of dict_deep_merge --- src/onelogin/saml2/idp_metadata_parser.py | 28 ++++++++--------------- 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index fafe3a2e..ba389eca 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -252,22 +252,14 @@ def merge_settings(settings, new_metadata_settings): return result_settings -def dict_deep_merge(a, b, path=None): - """Deep-merge dictionary `b` into dictionary `a`. - Kudos to http://stackoverflow.com/a/7205107/145400 - """ - if path is None: - path = [] - for key in b: - if key in a: - if isinstance(a[key], dict) and isinstance(b[key], dict): - dict_deep_merge(a[key], b[key], path + [str(key)]) - elif a[key] == b[key]: - # Key conflict, but equal value. - pass - else: - # Key/value conflict. Prioritize b over a. - a[key] = b[key] +def dict_deep_merge(lhs, rhs): + """Deep-merge dictionary `rhs` into dictionary `lhs`.""" + updated_rhs = {} + for key in rhs: + if key in lhs and isinstance(lhs[key], dict) and isinstance(rhs[key], dict): + updated_rhs[key] = dict_deep_merge(lhs[key], rhs[key]) else: - a[key] = b[key] - return a + updated_rhs[key] = rhs[key] + lhs.update(updated_rhs) + return lhs + From ff9abdaf633e09b3328493e8f1c4453cf6e3315e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 23 Jul 2021 02:09:20 +0200 Subject: [PATCH 310/352] Fix pycodestyle --- src/onelogin/saml2/idp_metadata_parser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index ba389eca..7197ef5e 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -22,7 +22,7 @@ class OneLogin_Saml2_IdPMetadataParser(object): """ A class that contain methods related to obtaining and parsing metadata from IdP - + This class does not validate in any way the URL that is introduced, make sure to validate it properly before use it in a get_metadata method. """ @@ -262,4 +262,3 @@ def dict_deep_merge(lhs, rhs): updated_rhs[key] = rhs[key] lhs.update(updated_rhs) return lhs - From 00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 23 Jul 2021 02:14:29 +0200 Subject: [PATCH 311/352] Release 2.10.0 --- changelog.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 940f9be4..13473bed 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,10 @@ # python-saml changelog +### 2.10.0 (Jul 23, 2021) +* Removed CC-BY-SA 3.0 non compliant implementation of dict_deep_merge +* Update expired dates from test responses +* Add warning about the use of OneLogin_Saml2_IdPMetadataParser class about SSRF attacks +* Migrate from Travis to Github Actions + ### 2.9.0 (Jan 13, 2021) * Destination URL Comparison is now case-insensitive for netloc * Support single-label-domains as valid. New security parameter allowSingleLabelDomains diff --git a/setup.py b/setup.py index 5966f6c9..12c1ae2b 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.9.0', + version='2.10.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', From d08836293a04429e080d826dd18630fcb75c649f Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 18 Oct 2021 21:20:15 +0200 Subject: [PATCH 312/352] Warn about Open Redirect and Reply attacks --- README.md | 29 +++++++++++++++++++++++++++++ demo-bottle/index.py | 4 ++++ demo-django/demo/views.py | 4 ++++ demo-flask/index.py | 4 ++++ demo_pyramid/demo_pyramid/views.py | 4 ++++ 5 files changed, 45 insertions(+) diff --git a/README.md b/README.md index be8f3b3e..f3d64518 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,35 @@ your environment is not secure and will be exposed to attacks. In production also we highly recommend to register on the settings the IdP certificate instead of using the fingerprint method. The fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass. Other SAML toolkits deprecated that mechanism, we maintain it for compatibility and also to be used on test environment. +### Avoiding Open Redirect attacks ### + +Some implementations uses the RelayState parameter as a way to control the flow when SSO and SLO succeeded. So basically the +user is redirected to the value of the RelayState. + +If you are using Signature Validation on the HTTP-Redirect binding, you will have the RelayState value integrity covered, otherwise, and +on HTTP-POST binding, you can't trust the RelayState so before +executing the validation, you need to verify that its value belong +a trusted and expected URL. + +Read more about Open Redirect [CWE-601](https://cwe.mitre.org/data/definitions/601.html). + +### Avoiding Reply attacks ### + +A reply attack is basically try to reuse an intercepted valid SAML Message in order to impersonate a SAML action (SSO or SLO). + +SAML Messages have a limited timelife (NotBefore, NotOnOrAfter) that +make harder this kind of attacks, but they are still possible. + +In order to avoid them, the SP can keep a list of SAML Messages or Assertion IDs alredy valdidated and processed. Those values only need +to be stored the amount of time of the SAML Message life time, so +we don't need to store all processed message/assertion Ids, but the most recent ones. + +The OneLogin_Saml2_Auth class contains the [get_last_request_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L352), [get_last_message_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L359) and [get_last_assertion_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L366) methods to retrieve the IDs + +Checking that the ID of the current Message/Assertion does not exists in the lis of the ones already processed will prevent reply +attacks. + + Getting Started --------------- diff --git a/demo-bottle/index.py b/demo-bottle/index.py index f6b5d78a..40dd0b7c 100644 --- a/demo-bottle/index.py +++ b/demo-bottle/index.py @@ -61,6 +61,8 @@ def index(): session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.forms and self_url != request.forms['RelayState']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the request.forms['RelayState'] is a trusted URL. return redirect(request.forms['RelayState']) if 'samlUserdata' in session: @@ -110,6 +112,8 @@ def index(): errors = auth.get_errors() if len(errors) == 0: if url is not None: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the url is a trusted URL. return redirect(url) else: success_slo = True diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index b6bf0cfd..231061d2 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -87,6 +87,8 @@ def index(request): request.session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() request.session['samlSessionIndex'] = auth.get_session_index() if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the req['post_data']['RelayState'] is a trusted URL. return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState'])) elif auth.get_settings().is_debug_active(): error_reason = auth.get_last_error_reason() @@ -99,6 +101,8 @@ def index(request): errors = auth.get_errors() if len(errors) == 0: if url is not None: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the url is a trusted URL. return HttpResponseRedirect(url) else: success_slo = True diff --git a/demo-flask/index.py b/demo-flask/index.py index 2b748182..42d539cd 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -91,6 +91,8 @@ def index(): session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.form and self_url != request.form['RelayState']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the request.form['RelayState'] is a trusted URL. return redirect(auth.redirect_to(request.form['RelayState'])) elif auth.get_settings().is_debug_active(): error_reason = auth.get_last_error_reason() @@ -103,6 +105,8 @@ def index(): errors = auth.get_errors() if len(errors) == 0: if url is not None: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the request.form['RelayState'] is a trusted URL. return redirect(url) else: success_slo = True diff --git a/demo_pyramid/demo_pyramid/views.py b/demo_pyramid/demo_pyramid/views.py index 86814a99..761d6e00 100644 --- a/demo_pyramid/demo_pyramid/views.py +++ b/demo_pyramid/demo_pyramid/views.py @@ -65,6 +65,8 @@ def index(request): session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.POST and self_url != request.POST['RelayState']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the request.POST['RelayState'] is a trusted URL. return HTTPFound(auth.redirect_to(request.POST['RelayState'])) else: error_reason = auth.get_last_error_reason() @@ -74,6 +76,8 @@ def index(request): errors = auth.get_errors() if len(errors) == 0: if url is not None: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the url is a trusted URL. return HTTPFound(url) else: success_slo = True From b315dc5b94fb3b2d9ddecddfeb558dad5fb96b30 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 18 Oct 2021 21:29:30 +0200 Subject: [PATCH 313/352] Modify examples of README as well --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f3d64518..c6cd24da 100644 --- a/README.md +++ b/README.md @@ -734,6 +734,8 @@ if not errors: request.session['samlUserdata'] = auth.get_attributes() if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the req['post_data']['RelayState'] is a trusted URL. auth.redirect_to(req['post_data']['RelayState']) else: for attr_name in request.session['samlUserdata'].keys(): @@ -796,6 +798,8 @@ url = auth.process_slo(delete_session_cb=delete_session_callback) errors = auth.get_errors() if len(errors) == 0: if url is not None: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the url is a trusted URL. return redirect(url) else: print "Sucessfully Logged out" @@ -932,7 +936,9 @@ elif 'acs' in request.args: # Assertion Consumer Service request.session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.form and self_url != request.form['RelayState']: - return redirect(auth.redirect_to(request.form['RelayState'])) # Redirect if there is a relayState + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the request.form['RelayState'] is a trusted URL. + return redirect(request.form['RelayState']) # Redirect if there is a relayState else: # If there is user data we save that to print it later. msg = '' for attr_name in request.session['samlUserdata'].keys(): @@ -943,6 +949,8 @@ elif 'sls' in request.args: # Single errors = auth.get_errors() # Retrieves possible validation errors if len(errors) == 0: if url is not None: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the url is a trusted URL. return redirect(url) else: msg = "Sucessfully logged out" From 8f76c85756c252d9c845eb268f2111d9d7a53b97 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 26 Jan 2022 12:31:48 +0100 Subject: [PATCH 314/352] Set sha256 and rsa-sha256 as default algorithms --- src/onelogin/saml2/logout_request.py | 2 +- src/onelogin/saml2/logout_response.py | 2 +- src/onelogin/saml2/metadata.py | 2 +- src/onelogin/saml2/settings.py | 4 ++-- src/onelogin/saml2/utils.py | 4 ++-- tests/src/OneLogin/saml2_tests/auth_test.py | 6 +++--- tests/src/OneLogin/saml2_tests/metadata_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/settings_test.py | 2 +- tests/src/OneLogin/saml2_tests/utils_test.py | 4 ++-- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 4aed6d6d..3c57d2f6 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -398,7 +398,7 @@ def is_valid(self, request_data, raise_exceptions=False): signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding) if 'RelayState' in get_data: signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) - signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', sign_alg, lowercase_urlencoding=lowercase_urlencoding)) exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert'] exists_multix509sign = 'x509certMulti' in idp_data and \ diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 96900aea..a2733019 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -148,7 +148,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse', lowercase_urlencoding=lowercase_urlencoding) if 'RelayState' in get_data: signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) - signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', sign_alg, lowercase_urlencoding=lowercase_urlencoding)) exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert'] exists_multix509sign = 'x509certMulti' in idp_data and \ diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index a1ef514f..84493428 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -202,7 +202,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N return metadata @staticmethod - def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1, digest_algorithm=OneLogin_Saml2_Constants.SHA1): + def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA256, digest_algorithm=OneLogin_Saml2_Constants.SHA256): """ Signs the metadata with the key/cert provided diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 660da4e5..d5d0a8c7 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -301,10 +301,10 @@ def __add_default_values(self): self.__security.setdefault('wantNameIdEncrypted', False) # Signature Algorithm - self.__security.setdefault('signatureAlgorithm', OneLogin_Saml2_Constants.RSA_SHA1) + self.__security.setdefault('signatureAlgorithm', OneLogin_Saml2_Constants.RSA_SHA256) # Digest Algorithm - self.__security.setdefault('digestAlgorithm', OneLogin_Saml2_Constants.SHA1) + self.__security.setdefault('digestAlgorithm', OneLogin_Saml2_Constants.SHA256) # AttributeStatement required by default self.__security.setdefault('wantAttributeStatement', True) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index aef6b9fb..72eb71fe 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -840,7 +840,7 @@ def write_temp_file(content): return f_temp @staticmethod - def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1, digest_algorithm=OneLogin_Saml2_Constants.SHA1): + def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA256, digest_algorithm=OneLogin_Saml2_Constants.SHA256): """ Adds signature key and senders certificate to an element (Message or Assertion). @@ -1208,7 +1208,7 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger @staticmethod @return_false_on_exception - def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False): + def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA256, debug=False): """ Validates signed binary data (Used to validate GET Signature). diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 30e48766..90e78d75 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -559,7 +559,7 @@ def testProcessSLORequestSignedResponse(self): self.assertIn('SigAlg', parsed_query) self.assertIn('Signature', parsed_query) self.assertIn('http://relaystate.com', parsed_query['RelayState']) - self.assertIn(OneLogin_Saml2_Constants.RSA_SHA1, parsed_query['SigAlg']) + self.assertIn(OneLogin_Saml2_Constants.RSA_SHA256, parsed_query['SigAlg']) def testLogin(self): """ @@ -630,7 +630,7 @@ def testLoginSigned(self): self.assertIn('SigAlg', parsed_query) self.assertIn('Signature', parsed_query) self.assertIn(return_to, parsed_query['RelayState']) - self.assertIn(OneLogin_Saml2_Constants.RSA_SHA1, parsed_query['SigAlg']) + self.assertIn(OneLogin_Saml2_Constants.RSA_SHA256, parsed_query['SigAlg']) def testLoginForceAuthN(self): """ @@ -831,7 +831,7 @@ def testLogoutSigned(self): self.assertIn('SigAlg', parsed_query) self.assertIn('Signature', parsed_query) self.assertIn(return_to, parsed_query['RelayState']) - self.assertIn(OneLogin_Saml2_Constants.RSA_SHA1, parsed_query['SigAlg']) + self.assertIn(OneLogin_Saml2_Constants.RSA_SHA256, parsed_query['SigAlg']) def testLogoutNoSLO(self): """ diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index c6071453..91b9047a 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -223,8 +223,8 @@ def testSignMetadata(self): self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', signed_metadata) self.assertIn('', signed_metadata) - self.assertIn('', signed_metadata) - self.assertIn('', signed_metadata) + self.assertIn('', signed_metadata) + self.assertIn('', signed_metadata) self.assertIn('\n', signed_metadata) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 8341beb6..fa308684 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -529,7 +529,7 @@ def generateAndCheckMetadata(self, settings): self.assertIn('', metadata) self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata) self.assertIn('', metadata) - self.assertIn('', metadata) + self.assertIn('', metadata) self.assertIn('', metadata) return metadata diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 3fbb34db..6a4dd94c 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -883,8 +883,8 @@ def testAddSignCheckAlg(self): xml_authn_signed = OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert) self.assertIn('', xml_authn_signed) self.assertIn('', xml_authn_signed) - self.assertIn('', xml_authn_signed) - self.assertIn('', xml_authn_signed) + self.assertIn('', xml_authn_signed) + self.assertIn('', xml_authn_signed) xml_authn_signed_2 = OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert, False, OneLogin_Saml2_Constants.RSA_SHA256, OneLogin_Saml2_Constants.SHA384) self.assertIn('', xml_authn_signed_2) From 51e3dde1ef320046bb4e6726f1cb326777902457 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 26 Jan 2022 12:34:49 +0100 Subject: [PATCH 315/352] Remove settings data that shouldnt be there --- demo-django/demo/settings.py | 2 +- demo-django/saml/settings.json | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/demo-django/demo/settings.py b/demo-django/demo/settings.py index 5305f382..f0c1726b 100644 --- a/demo-django/demo/settings.py +++ b/demo-django/demo/settings.py @@ -22,7 +22,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['pitbulk.no-ip.org'] +ALLOWED_HOSTS = ['localhost'] # Application definition diff --git a/demo-django/saml/settings.json b/demo-django/saml/settings.json index 3758746c..ec40b674 100644 --- a/demo-django/saml/settings.json +++ b/demo-django/saml/settings.json @@ -2,29 +2,29 @@ "strict": true, "debug": true, "sp": { - "entityId": "http://pitbulk.no-ip.org:8000/metadata/", + "entityId": "https:///metadata/", "assertionConsumerService": { - "url": "http://pitbulk.no-ip.org:8000/?acs", + "url": "https:///?acs", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, "singleLogoutService": { - "url": "http://pitbulk.no-ip.org:8000/?sls", + "url": "https:///?sls", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", - "x509cert": "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m", - "privateKey": "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAECgYEA0wDXZPS9hKqMTNh+nnfONioXBjhA6fQ7GVtWKDxa3ofMoPyt7ejGL/Hnvcv13Vn02UAsFx1bKrCstDqVtYwrWrnmywXyH+o9paJnTmd+cRIjWU8mRvCrxzH5I/Bcvbp1qZoASuqZEaGwNjM6JpW2o3QTmHGMALcLUPfEvhApssECQQDy2e65E86HcFhi/Ta8TQ0odDCNbiWA0bI1Iu8B7z+NAy1D1+WnCd7w2u9U6CF/k2nFHCsvxEoeANM0z7h5T/XvAkEA8e4JqKmDrfdiakQT7nf9svU2jXZtxSbPiIRMafNikDvzZ1vJCZkvdmaWYL70GlDZIwc9ad67rHZ/n/fqX1d0MQJAbRpRsJ5gY+KqItbFt3UaWzlP8sowWR5cZJjsLb9RmsV5mYguKYw6t5R0f33GRu1wUFimYlBaR/5w5MIJi57LywJATO1a5uWX+G5MPewNxmsjIY91XEAHIYR4wzkGLz5z3dciS4BVCZdLD0QJlxPA/MkuckPwFET9uhYn+M7VGKHvUQJBANSDwsY+BdCGpi/WRV37HUfwLl07damaFbW3h08PQx8G8SuF7DpN+FPBcI6VhzrIWNRBxWprkgeGioKNfFWzSaM=" + "x509cert": "", + "privateKey": "" }, "idp": { - "entityId": "https://app.onelogin.com/saml/metadata/3dbd155e-be64-4a4d-8fab-e44788bce74f", + "entityId": "https://app.onelogin.com/saml/metadata/", "singleSignOnService": { - "url": "https://sgarcia-us-preprod.onelogin.com/trust/saml2/http-redirect/sso/850162", + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, "singleLogoutService": { - "url": "https://sgarcia-us-preprod.onelogin.com/trust/saml2/http-redirect/slo/850162", + "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "x509cert": "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==" + "x509cert": "" } } From 99d606051e81147f374242a42d1e193c4daf3490 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 28 Jan 2022 00:56:23 +0100 Subject: [PATCH 316/352] Upgrade dm.xmlsec.binding to 2.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 12c1ae2b..d9b6263b 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ test_suite='tests', install_requires=[ 'lxml>=3.3.5', - 'dm.xmlsec.binding==1.3.7', + 'dm.xmlsec.binding==2.1', 'isodate>=0.5.0', 'defusedxml>=0.6.0', ], From 55ea2995a4733d211165435b0db8a0e6c228ff51 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 28 Jan 2022 01:00:35 +0100 Subject: [PATCH 317/352] Add rejectDeprecatedAlgorithm settings. Define DEPRECATED_ALGORITHMS list on Constants. If flag enabled, reject signatures on response, logout_request and logout_response with deprecated algorithm --- README.md | 7 ++++- demo-bottle/saml/advanced_settings.json | 3 +- demo-django/saml/advanced_settings.json | 3 +- demo-flask/saml/advanced_settings.json | 3 +- .../demo_pyramid/saml/advanced_settings.json | 3 +- src/onelogin/saml2/constants.py | 3 ++ src/onelogin/saml2/errors.py | 2 ++ src/onelogin/saml2/logout_request.py | 11 +++++-- src/onelogin/saml2/logout_response.py | 11 +++++-- src/onelogin/saml2/response.py | 29 +++++++++++++++++-- src/onelogin/saml2/settings.py | 3 ++ src/onelogin/saml2/utils.py | 3 +- .../saml2_tests/logout_request_test.py | 21 ++++++++++++++ .../saml2_tests/logout_response_test.py | 24 +++++++++++++++ .../src/OneLogin/saml2_tests/response_test.py | 13 +++++++++ 15 files changed, 125 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c6cd24da..71c2da68 100644 --- a/README.md +++ b/README.md @@ -507,7 +507,12 @@ In addition to the required settings data (idp, sp), extra settings can be defin // 'http://www.w3.org/2001/04/xmlenc#sha256' // 'http://www.w3.org/2001/04/xmldsig-more#sha384' // 'http://www.w3.org/2001/04/xmlenc#sha512' - "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + + // If the toolkit receive a message signed with a + // deprecated algoritm (defined at the constant class) + // will raise an error and reject the message + "rejectDeprecatedAlgorithm": true }, // Contact information template, it is recommended to supply diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json index c5b37c11..ed284e05 100644 --- a/demo-bottle/saml/advanced_settings.json +++ b/demo-bottle/saml/advanced_settings.json @@ -12,7 +12,8 @@ "wantAssertionsEncrypted": false, "allowSingleLabelDomains": false, "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + "rejectDeprecatedAlgorithm": true }, "contactPerson": { "technical": { diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json index c5b37c11..ed284e05 100644 --- a/demo-django/saml/advanced_settings.json +++ b/demo-django/saml/advanced_settings.json @@ -12,7 +12,8 @@ "wantAssertionsEncrypted": false, "allowSingleLabelDomains": false, "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + "rejectDeprecatedAlgorithm": true }, "contactPerson": { "technical": { diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json index c5b37c11..ed284e05 100644 --- a/demo-flask/saml/advanced_settings.json +++ b/demo-flask/saml/advanced_settings.json @@ -12,7 +12,8 @@ "wantAssertionsEncrypted": false, "allowSingleLabelDomains": false, "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + "rejectDeprecatedAlgorithm": true }, "contactPerson": { "technical": { diff --git a/demo_pyramid/demo_pyramid/saml/advanced_settings.json b/demo_pyramid/demo_pyramid/saml/advanced_settings.json index fef16fe9..3960911a 100644 --- a/demo_pyramid/demo_pyramid/saml/advanced_settings.json +++ b/demo_pyramid/demo_pyramid/saml/advanced_settings.json @@ -12,7 +12,8 @@ "wantAssertionsEncrypted": false, "allowSingleLabelDomains": false, "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + "rejectDeprecatedAlgorithm": true }, "contactPerson": { "technical": { diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py index 3cb78dd4..5bff8d4d 100644 --- a/src/onelogin/saml2/constants.py +++ b/src/onelogin/saml2/constants.py @@ -116,3 +116,6 @@ class OneLogin_Saml2_Constants(object): AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' + + # Define here the deprecated algorithms + DEPRECATED_ALGORITHMS = [DSA_SHA1, RSA_SHA1, SHA1] diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 081d57d5..f5dca8f2 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -113,6 +113,8 @@ class OneLogin_Saml2_ValidationError(Exception): WRONG_NUMBER_OF_SIGNATURES = 43 RESPONSE_EXPIRED = 44 AUTHN_CONTEXT_MISMATCH = 45 + DEPRECATED_SIGNATURE_METHOD = 46 + DEPRECATED_DIGEST_METHOD = 47 def __init__(self, message, code=0, errors=None): """ diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 3c57d2f6..fa034a1b 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -335,6 +335,7 @@ def is_valid(self, request_data, raise_exceptions=False): if 'lowercase_urlencoding' in request_data.keys(): lowercase_urlencoding = request_data['lowercase_urlencoding'] + security = self.__settings.get_security_data() if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): @@ -343,8 +344,6 @@ def is_valid(self, request_data, raise_exceptions=False): OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT ) - security = self.__settings.get_security_data() - current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check NotOnOrAfter @@ -395,6 +394,14 @@ def is_valid(self, request_data, raise_exceptions=False): else: sign_alg = get_data['SigAlg'] + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + if reject_deprecated_alg: + if sign_alg in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sign_alg, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding) if 'RelayState' in get_data: signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index a2733019..50e07f1e 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -90,6 +90,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if 'lowercase_urlencoding' in request_data.keys(): lowercase_urlencoding = request_data['lowercase_urlencoding'] + security = self.__settings.get_security_data() if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): @@ -98,8 +99,6 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT ) - security = self.__settings.get_security_data() - in_response_to = self.get_in_response_to() # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided if request_id is not None and in_response_to and in_response_to != request_id: @@ -145,6 +144,14 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): else: sign_alg = get_data['SigAlg'] + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + if reject_deprecated_alg: + if sign_alg in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sign_alg, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse', lowercase_urlencoding=lowercase_urlencoding) if 'RelayState' in get_data: signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index bb186f86..2d780237 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -104,6 +104,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): has_signed_response = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements has_signed_assertion = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML in signed_elements + security = self.__settings.get_security_data() if self.__settings.is_strict(): no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' res = OneLogin_Saml2_Utils.validate_xml( @@ -130,7 +131,6 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT ) - security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) in_response_to = self.get_in_response_to() @@ -323,14 +323,14 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): multicerts = idp_data['x509certMulti']['signing'] # 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, multicerts=multicerts, raise_exceptions=False): + if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False): 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, multicerts=multicerts, raise_exceptions=False): + if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. SAML Response rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE @@ -688,6 +688,9 @@ def process_signed_elements(self): """ sign_nodes = self.__query('//ds:Signature') + security = self.__settings.get_security_data() + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + signed_elements = [] verified_seis = [] verified_ids = [] @@ -736,6 +739,26 @@ def process_signed_elements(self): ) verified_seis.append(sei) + # Check the signature and digest algorithm + if reject_deprecated_alg: + sig_method_node = OneLogin_Saml2_Utils.query(sign_node, './/ds:SignatureMethod') + if sig_method_node: + sig_method = sig_method_node[0].get("Algorithm") + if sig_method in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sig_method, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + + dig_method_node = OneLogin_Saml2_Utils.query(sign_node, './/ds:DigestMethod') + if dig_method_node: + dig_method = dig_method_node[0].get("Algorithm") + if dig_method in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated digest algorithm found: %s' % dig_method, + OneLogin_Saml2_ValidationError.DEPRECATED_DIGEST_METHOD + ) + signed_elements.append(signed_element) if signed_elements: diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index d5d0a8c7..054612de 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -306,6 +306,9 @@ def __add_default_values(self): # Digest Algorithm self.__security.setdefault('digestAlgorithm', OneLogin_Saml2_Constants.SHA256) + # Reject Deprecated Algorithms + self.__security.setdefault('rejectDeprecatedAlgorithm', False) + # AttributeStatement required by default self.__security.setdefault('wantAttributeStatement', True) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 72eb71fe..d97edf15 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1208,14 +1208,13 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger @staticmethod @return_false_on_exception - def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA256, debug=False): + def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False): """ Validates signed binary data (Used to validate GET Signature). :param signed_query: The element we should validate :type: string - :param signature: The signature that will be validate :type: string diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 33c1548d..2b37f0a1 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -582,6 +582,27 @@ def testIsValidSignUsingX509certMulti(self): logout_request = OneLogin_Saml2_Logout_Request(settings, request_data['get_data']['SAMLRequest']) self.assertTrue(logout_request.is_valid(request_data)) + def testIsInValidRejectingDeprecatedSignatureAlgorithm(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': { + 'SAMLRequest': 'fZJNa+MwEIb/itHdiTz6sC0SQyEsBPoB27KHXoIsj7cGW3IlGfLzV7G7kN1DL2KYmeedmRcdgp7GWT26326JP/FzwRCz6zTaoNbKkSzeKqfDEJTVEwYVjXp9eHpUsKNq9i4640Zyh3xP6BDQx8FZkp1PR3KpqexAl72QmpUCS8SW01IiZz2TVVGD4X1VQYlAsl/oQyKPJAklPIQFzzZEbWNK0YLnlOVA3wqpQCoB7yQ7pWsGq+NKfcQ4q/0+xKXvd8ZNe7Td7AYbw10UxrCbP2aSPbv4Yl/8Qx/R3+SB5bTOoXiDQvFNvjnc7lXrIr75kh+6eYdXPc0jrkMO+/umjXhOtpxP2Q/nJx2/9+uWGbq8X1tV9NqGAW0kzaVvoe1AAJeCSWqYaUVRM2SilKKuqDTpFSlszdcK29RthVm9YriZebYdXpsLdhVAB7VJzif3haYMqqTVcl0JMBR4y+s2zak3sf/4v8l/vlHzBw==', + 'RelayState': '_1037fbc88ec82ce8e770b2bed1119747bb812a07e6', + 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', + 'Signature': 'Ouxo9BV6zmq4yrgamT9EbSKy/UmvSxGS8z26lIMgKOEP4LFR/N23RftdANmo4HafrzSfA0YTXwhKDqbOByS0j+Ql8OdQOes7vGioSjo5qq/Bi+5i6jXwQfphnfcHAQiJL4gYVIifkhhHRWpvYeiysF1Y9J02me0izwazFmoRXr4=' + } + } + settings_info = self.loadSettingsJSON('settings8.json') + settings_info['security']['rejectDeprecatedAlgorithm'] = True + settings = OneLogin_Saml2_Settings(settings_info) + logout_request = OneLogin_Saml2_Logout_Request(settings, request_data['get_data']['SAMLRequest']) + self.assertFalse(logout_request.is_valid(request_data)) + self.assertEqual('Deprecated signature algorithm found: http://www.w3.org/2000/09/xmldsig#rsa-sha1', logout_request.get_error()) + def testGetXML(self): """ Tests that we can get the logout request XML directly without diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 56797425..f92111f7 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -439,6 +439,30 @@ def testIsValidSignUsingX509certMulti(self): logout_response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) self.assertTrue(logout_response.is_valid(request_data)) + def testIsInValidRejectingDeprecatedSignatureAlgorithm(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + """ + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': { + 'SAMLResponse': 'fZHbasJAEIZfJey9ZrNZc1gSodRSBKtQxYveyGQz1kCyu2Q24OM3jS21UHo3p++f4Z+CoGud2th3O/hXJGcNYXDtWkNqapVs6I2yQA0pAx2S8lrtH142Ssy5cr31VtuW3SH/E0CEvW+sYcF6VbLTIktFLMWZgxQR8DSP85wDB4GJGMOqShYVaoBUsOCIPY1kyUahEScacG3Ig/FjiUdyxuOZ4IcoUVGq4vSNBSsk3xjwE3Xx3qkwJD+cz3NtuxBN7WxjPN1F1NLcXdwob77tONiS7bZPm93zenvCqopxgVJmuU50jREsZF4noKWAOuNZJbNznnBky+LTDDVd2S+/dje1m+MVOtfidEER3g8Vt2fsPfiBfmePtsbgCO2A/9tL07TaD1ojEQuXtw0/ouFfD19+AA==', + 'RelayState': 'http://stuff.com/endpoints/endpoints/index.php', + 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', + 'Signature': 'OV9c4R0COSjN69fAKCpV7Uj/yx6/KFxvbluVCzdK3UuortpNMpgHFF2wYNlMSG9GcYGk6p3I8nB7Z+1TQchMWZOlO/StjAqgtZhtpiwPcWryNuq8vm/6hnJ3zMDhHTS7F8KG4qkCXmJ9sQD3Y31UNcuygBwIbNakvhDT5Qo9Nsw=' + } + } + settings_info = self.loadSettingsJSON('settings8.json') + settings_info['security']['rejectDeprecatedAlgorithm'] = True + settings = OneLogin_Saml2_Settings(settings_info) + logout_response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) + self.assertFalse(logout_response.is_valid(request_data)) + self.assertEqual('Deprecated signature algorithm found: http://www.w3.org/2000/09/xmldsig#rsa-sha1', logout_response.get_error()) + def testGetXML(self): """ Tests that we can get the logout response XML directly without diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 3b3499c4..acd13d34 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1017,6 +1017,19 @@ def testIsInValidNoKey(self): self.assertFalse(response.is_valid(self.get_request_data())) self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error()) + def testIsInValidDeprecatedAlgorithm(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case Deprecated algorithm used + """ + settings_dict = self.loadSettingsJSON() + settings_dict['security']['rejectDeprecatedAlgorithm'] = True + settings = OneLogin_Saml2_Settings(settings_dict) + xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertEqual('Deprecated signature algorithm found: http://www.w3.org/2000/09/xmldsig#rsa-sha1', response.get_error()) + def testIsInValidMultipleAssertions(self): """ Tests the is_valid method of the OneLogin_Saml2_Response From edfd11c4e70afda6868bd9339f32a59795a12bcc Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 28 Jan 2022 01:20:49 +0100 Subject: [PATCH 318/352] pep8 --- 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 2d780237..a8aa5327 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -323,14 +323,14 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): multicerts = idp_data['x509certMulti']['signing'] # 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, multicerts=multicerts, raise_exceptions=False): + if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False): 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, multicerts=multicerts, raise_exceptions=False): + if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. SAML Response rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE From c80bd8343fac992406321536ca35a83275226dc6 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 28 Jan 2022 12:04:51 +0100 Subject: [PATCH 319/352] Prepare release 1.12.0. Upgrade lxml, isodate and defusedxml dependencies --- changelog.md | 6 ++++++ setup.py | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 13473bed..e8e3442b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,10 @@ # python-saml changelog +### 2.11.0 (Jan 28, 2022) +- [#292](https://github.com/onelogin/python-saml/pull/292) Add rejectDeprecatedAlgorithm settings in order to be able reject messages signed with deprecated algorithms. +- Upgrade dm.xmlsec.binding to 2.1 +- Set sha256 and rsa-sha256 as default algorithms +- Added warning about Open Redirect and Reply attacks + ### 2.10.0 (Jul 23, 2021) * Removed CC-BY-SA 3.0 non compliant implementation of dict_deep_merge * Update expired dates from test responses diff --git a/setup.py b/setup.py index d9b6263b..eaca5b88 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.10.0', + version='2.11.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -32,10 +32,10 @@ }, test_suite='tests', install_requires=[ - 'lxml>=3.3.5', + 'lxml>=4.7.1', 'dm.xmlsec.binding==2.1', - 'isodate>=0.5.0', - 'defusedxml>=0.6.0', + 'isodate>=0.6.1', + 'defusedxml>=0.7.1', ], extras_require={ 'test': ( From 9106605416cd5afa1a0834e6fdaa90794dea2f7f Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 28 Jan 2022 17:43:51 +0100 Subject: [PATCH 320/352] Fixing lxml to 4.7.0, it seems 4.7.1 had conflicts when the signature inside a encrypted element is validated. See https://github.com/onelogin/python3-saml/issues/292 Also reverted dm.xmlsec.binding to 1.3.7 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index eaca5b88..e1924257 100644 --- a/setup.py +++ b/setup.py @@ -32,8 +32,8 @@ }, test_suite='tests', install_requires=[ - 'lxml>=4.7.1', - 'dm.xmlsec.binding==2.1', + 'lxml<4.7.1', + 'dm.xmlsec.binding==1.3.7', 'isodate>=0.6.1', 'defusedxml>=0.7.1', ], From 37132ebb791a21e6e0c274b32e7be808fd6fde6a Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 28 Jan 2022 17:47:46 +0100 Subject: [PATCH 321/352] Prepare release 2.11.1 --- changelog.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index e8e3442b..6ffd3f12 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,7 @@ # python-saml changelog +### 2.11.1 (Jan 28, 2022) +- lxml fixed to be lower than 4.7.1 since it seems to have issues validating the signature of encrypted elements See https://github.com/onelogin/python3-saml/issues/292 + ### 2.11.0 (Jan 28, 2022) - [#292](https://github.com/onelogin/python-saml/pull/292) Add rejectDeprecatedAlgorithm settings in order to be able reject messages signed with deprecated algorithms. - Upgrade dm.xmlsec.binding to 2.1 diff --git a/setup.py b/setup.py index e1924257..abb93f1e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python-saml', - version='2.11.0', + version='2.11.1', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', From dc555823e5cdec33192393cc6d6c67b464de7a49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jun 2022 22:43:46 +0000 Subject: [PATCH 322/352] Bump bottle from 0.12.19 to 0.12.20 in /demo-bottle Bumps [bottle](https://github.com/bottlepy/bottle) from 0.12.19 to 0.12.20. - [Release notes](https://github.com/bottlepy/bottle/releases) - [Changelog](https://github.com/bottlepy/bottle/blob/master/docs/changelog.rst) - [Commits](https://github.com/bottlepy/bottle/compare/0.12.19...0.12.20) --- updated-dependencies: - dependency-name: bottle dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- demo-bottle/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-bottle/requirements.txt b/demo-bottle/requirements.txt index 639ee202..895f476a 100644 --- a/demo-bottle/requirements.txt +++ b/demo-bottle/requirements.txt @@ -1,4 +1,4 @@ -bottle==0.12.19 +bottle==0.12.20 beaker==1.6.4 paste==1.7.5.1 jinja2==2.7.3 From c1138acf24d36bb1d7b540105de6b14e58c3f57c Mon Sep 17 00:00:00 2001 From: Noam <69756316+noamsan@users.noreply.github.com> Date: Tue, 5 Jul 2022 15:08:52 +0300 Subject: [PATCH 323/352] Typo fix: reply -> replay --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 71c2da68..915d1c6c 100644 --- a/README.md +++ b/README.md @@ -156,9 +156,9 @@ a trusted and expected URL. Read more about Open Redirect [CWE-601](https://cwe.mitre.org/data/definitions/601.html). -### Avoiding Reply attacks ### +### Avoiding Replay attacks ### -A reply attack is basically try to reuse an intercepted valid SAML Message in order to impersonate a SAML action (SSO or SLO). +A replay attack is basically try to reuse an intercepted valid SAML Message in order to impersonate a SAML action (SSO or SLO). SAML Messages have a limited timelife (NotBefore, NotOnOrAfter) that make harder this kind of attacks, but they are still possible. @@ -169,7 +169,7 @@ we don't need to store all processed message/assertion Ids, but the most recent The OneLogin_Saml2_Auth class contains the [get_last_request_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L352), [get_last_message_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L359) and [get_last_assertion_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L366) methods to retrieve the IDs -Checking that the ID of the current Message/Assertion does not exists in the lis of the ones already processed will prevent reply +Checking that the ID of the current Message/Assertion does not exists in the lis of the ones already processed will prevent replay attacks. @@ -988,7 +988,7 @@ The ``x509certMulti`` is an array with 2 keys: ### Replay attacks ### - In order to avoid reply attacks, you can store the ID of the SAML messages already processed, to avoid processing them twice. Since the Messages expires and will be invalidated due that fact, you don't need to store those IDs longer than the time frame that you currently accepting. + In order to avoid replay attacks, you can store the ID of the SAML messages already processed, to avoid processing them twice. Since the Messages expires and will be invalidated due that fact, you don't need to store those IDs longer than the time frame that you currently accepting. Get the ID of the last processed message/assertion with the ``get_last_message_id``/``get_last_assertion_id method`` of the ``Auth`` object. From a704e45c6ba6797b741869205eda2b498dec7634 Mon Sep 17 00:00:00 2001 From: Noam <69756316+noamsan@users.noreply.github.com> Date: Tue, 5 Jul 2022 15:10:26 +0300 Subject: [PATCH 324/352] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 915d1c6c..d157b5b5 100644 --- a/README.md +++ b/README.md @@ -169,8 +169,7 @@ we don't need to store all processed message/assertion Ids, but the most recent The OneLogin_Saml2_Auth class contains the [get_last_request_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L352), [get_last_message_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L359) and [get_last_assertion_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L366) methods to retrieve the IDs -Checking that the ID of the current Message/Assertion does not exists in the lis of the ones already processed will prevent replay -attacks. +Checking that the ID of the current Message/Assertion does not exists in the lis of the ones already processed will prevent replay attacks. Getting Started From a2ccfc5bd0f08dd5633271a992c7e44893e86558 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 28 Dec 2022 00:14:55 +0100 Subject: [PATCH 325/352] Update dependencies. Add flake8. Fix code syntax --- .travis.yml | 7 ++++--- demo-bottle/index.py | 28 ++++++++++++++-------------- demo-django/demo/urls.py | 1 - demo-django/demo/views.py | 6 +++--- demo-django/demo/wsgi.py | 6 +++--- setup.py | 21 +++++++++++---------- src/onelogin/saml2/response.py | 4 ++-- 7 files changed, 37 insertions(+), 36 deletions(-) diff --git a/.travis.yml b/.travis.yml index ea58d44a..93ffbcae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,10 +8,11 @@ install: - 'travis_retry pip install .' - 'travis_retry pip install -e ".[test]"' -script: +script: - 'coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test' - 'coverage report -m --rcfile=tests/coverage.rc' - - 'pycodestyle tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc' - - 'pyflakes src/onelogin/saml2 demo-django demo-flask tests/src/OneLogin/saml2_tests' + #- 'pycodestyle tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc' + #- 'pyflakes src/onelogin/saml2 demo-django demo-flask tests/src/OneLogin/saml2_tests' + - flake8 --ignore E226,E302,E41,E731,E501,C901,W504 after_success: 'coveralls' diff --git a/demo-bottle/index.py b/demo-bottle/index.py index 40dd0b7c..45172421 100644 --- a/demo-bottle/index.py +++ b/demo-bottle/index.py @@ -44,7 +44,7 @@ def prepare_bottle_request(req): @app.route('/acs/', method='POST') @jinja2_view('index.html', template_lookup=['templates']) -def index(): +def acs(): req = prepare_bottle_request(request) auth = init_saml_auth(req) paint_logout = False @@ -62,7 +62,7 @@ def index(): self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.forms and self_url != request.forms['RelayState']: # To avoid 'Open Redirect' attacks, before execute the redirection confirm - # the value of the request.forms['RelayState'] is a trusted URL. + # the value of the request.forms['RelayState'] is a trusted URL. return redirect(request.forms['RelayState']) if 'samlUserdata' in session: @@ -71,10 +71,10 @@ def index(): attributes = session['samlUserdata'].items() return { - 'errors':errors, - 'not_auth_warn':not_auth_warn, - 'attributes':attributes, - 'paint_logout':paint_logout + 'errors': errors, + 'not_auth_warn': not_auth_warn, + 'attributes': attributes, + 'paint_logout': paint_logout } @@ -124,11 +124,11 @@ def index(): attributes = session['samlUserdata'].items() return { - 'errors':errors, - 'not_auth_warn':not_auth_warn, - 'success_slo':success_slo, - 'attributes':attributes, - 'paint_logout':paint_logout + 'errors': errors, + 'not_auth_warn': not_auth_warn, + 'success_slo': success_slo, + 'attributes': attributes, + 'paint_logout': paint_logout } @@ -144,8 +144,8 @@ def attrs(): if len(session['samlUserdata']) > 0: attributes = session['samlUserdata'].items() - return {'paint_logout':paint_logout, - 'attributes':attributes} + return {'paint_logout': paint_logout, + 'attributes': attributes} @app.route('/metadata/') @@ -178,7 +178,7 @@ def run(self, handler): if __name__ == "__main__": # To run HTTPS - #run(SessionMiddleware(app, config=session_opts), host='0.0.0.0', port=8000, debug=True, reloader=True, server=SSLPasteServer) + # run(SessionMiddleware(app, config=session_opts), host='0.0.0.0', port=8000, debug=True, reloader=True, server=SSLPasteServer) # To run HTTP run(SessionMiddleware(app, config=session_opts), host='0.0.0.0', port=8000, debug=True, reloader=True, server='paste') diff --git a/demo-django/demo/urls.py b/demo-django/demo/urls.py index 1f329074..a6008829 100644 --- a/demo-django/demo/urls.py +++ b/demo-django/demo/urls.py @@ -9,4 +9,3 @@ url(r'^attrs/$', attrs, name='attrs'), url(r'^metadata/$', metadata, name='metadata') ] - diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index 231061d2..e1e44e27 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -64,10 +64,10 @@ def index(request): return HttpResponseRedirect(auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq, name_id_format=name_id_format, spnq=name_id_spnq)) - # If LogoutRequest ID need to be stored in order to later validate it, do instead + # If LogoutRequest ID need to be stored in order to later validate it, do instead # slo_built_url = auth.logout(name_id=name_id, session_index=session_index) # request.session['LogoutRequestID'] = auth.get_last_request_id() - #return HttpResponseRedirect(slo_built_url) + # return HttpResponseRedirect(slo_built_url) elif 'acs' in req['get_data']: request_id = None if 'AuthNRequestID' in request.session: @@ -91,7 +91,7 @@ def index(request): # the value of the req['post_data']['RelayState'] is a trusted URL. return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState'])) elif auth.get_settings().is_debug_active(): - error_reason = auth.get_last_error_reason() + error_reason = auth.get_last_error_reason() elif 'sls' in req['get_data']: request_id = None if 'LogoutRequestID' in request.session: diff --git a/demo-django/demo/wsgi.py b/demo-django/demo/wsgi.py index bd706154..b58d97fe 100644 --- a/demo-django/demo/wsgi.py +++ b/demo-django/demo/wsgi.py @@ -6,9 +6,9 @@ For more information on this file, see https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ """ - import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") - from django.core.wsgi import get_wsgi_application + + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") application = get_wsgi_application() diff --git a/setup.py b/setup.py index abb93f1e..0a589898 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from setuptools import setup @@ -10,7 +9,7 @@ setup( name='python-saml', version='2.11.1', - description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', + description='Saml Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -18,10 +17,12 @@ 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', ], - author='OneLogin', - author_email='support@onelogin.com', + author='SAML-Toolkits', + author_email='contact@iamdigitalservices.com', + maintainer='Sixto Martin', + maintainer_email='sixto.martin.garcia@gmail.com', license='MIT', - url='https://github.com/onelogin/python-saml', + url='https://github.com/SAML-Toolkits/python-saml', packages=['onelogin', 'onelogin/saml2'], include_package_data=True, package_data={ @@ -32,17 +33,17 @@ }, test_suite='tests', install_requires=[ - 'lxml<4.7.1', + 'lxml>=4.6.5, !=4.7.0', 'dm.xmlsec.binding==1.3.7', 'isodate>=0.6.1', 'defusedxml>=0.7.1', ], extras_require={ 'test': ( - 'coverage>=3.6, <5.0', - 'freezegun==0.3.5', - 'flake8==3.6.0', - 'coveralls==1.1', + 'coverage>=5.5, <6.0', + 'freezegun>=0.3.5, <0.4', + 'flake8>=3.6.0, < 4.0', + 'coveralls>=1.1, < 2.0', ), }, keywords='saml saml2 xmlsec django flask', diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index a8aa5327..20ff48c5 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -267,8 +267,8 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): else: irt = sc_data.get('InResponseTo', None) if (in_response_to is None and irt is not None and - security.get('rejectUnsolicitedResponsesWithInResponseTo', False)) or \ - in_response_to and irt and irt != in_response_to: + security.get('rejectUnsolicitedResponsesWithInResponseTo', False)) \ + or in_response_to and irt and irt != in_response_to: continue recipient = sc_data.get('Recipient', None) if recipient and current_url not in recipient: From e73ce9b4e56df084b0435883c95e89fcc0c41729 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 28 Dec 2022 00:53:30 +0100 Subject: [PATCH 326/352] Update travis --- .travis.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.travis.yml b/.travis.yml index 93ffbcae..683b2117 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,16 @@ language: python python: - '2.7' +jobs: + include: + - name: "Python 2.7 on Focal Linux" + os: linux + dist: focal + python: 2.7 + - name: "Python 2.7 on macOS" + osx_image: xcode13.4 + python: 2.7 + install: - sudo apt-get update -qq - sudo apt-get install -qq swig python-dev libxml2-dev libxmlsec1-dev From 616ab2743f385e202893b895836b30b08bbb09d3 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 28 Dec 2022 00:56:20 +0100 Subject: [PATCH 327/352] Another try --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 683b2117..dc9a5fc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ jobs: dist: focal python: 2.7 - name: "Python 2.7 on macOS" - osx_image: xcode13.4 + os: osx python: 2.7 install: From 80e200c106140b2ce03dfe9592b46e27b8b936a2 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 28 Dec 2022 01:29:01 +0100 Subject: [PATCH 328/352] Pin ubuntu version for github workflow to 20.04 --- .github/workflows/python-package.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 174aef29..885bbc54 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -7,7 +7,7 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 @@ -27,5 +27,6 @@ jobs: run: | coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test coverage report -m --rcfile=tests/coverage.rc - pycodestyle tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc - pyflakes src/onelogin/saml2 demo-django demo-flask tests/src/OneLogin/saml2_tests + #pycodestyle tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc + #pyflakes src/onelogin/saml2 demo-django demo-flask tests/src/OneLogin/saml2_tests + flake8 --ignore E226,E302,E41,E731,E501,C901,W504 From fe9bac00725f8f3a9b6d74334388d67f860ab44d Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 28 Dec 2022 01:53:59 +0100 Subject: [PATCH 329/352] Add Poetry file --- .gitignore | 1 + pyproject.toml | 188 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore index ad382d79..c6000b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ __pycache_ /venv .coverage .pypirc +poetry.lock *.key *.crt diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..be66bac4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,188 @@ +[tool.poetry] +name = "python-saml" +version = "2.12.0" +description = "Saml Python Toolkit. Add SAML support to your Python software using this library" +license = "Apache-2.0" +authors = ["SAML-Toolkits "] +maintainers = ["Sixto Martin "] +readme = "README.md" +homepage = "https://saml.info" +repository = "https://github.com/SAML-Toolkits/python-saml" +keywords = [ + "saml", + "saml2", + "sso", + "xmlsec", + "federation", + "identity", +] +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules", +] +packages = [ + { include = "onelogin", from = "src" }, + { include = "onelogin/saml2", from = "src" }, +] + +include = [ + { path = "src/onelogin/saml2/schemas"}, + { path = "tests", format = "sdist" } +] + +[tool.poetry.urls] +"Bug Tracker" = "https://github.com/SAML-Toolkits/python-saml/issues" + +[tool.poetry.dependencies] +python = "2.7" +lxml = ">=4.6.5, !=4.7.0" +"dm.xmlsec.binding" = "1.3.7" +isodate = ">=0.6.1" +defusedxml = ">=0.7.1" + +#[tool.poetry.group.dev] +#optional = true + +#[tool.poetry.group.dev.dependencies] +#black = "*" +#isort = {version = "^5.10.1", extras = ["pyproject"]} +flake8 = { version = ">=3.6.0, <=4.0", optional = true} +#Flake8-pyproject = "^1.1.0.post0" +#flake8-bugbear = "^22.8.23" +#flake8-logging-format = "^0.7.5" +#ipdb = "^0.13.9" + +#[tool.poetry.group.test.dependencies] +freezegun= { version = ">=0.3.11, <=0.4", optional = true} +pytest = { version = ">=4.6.11", optional = true} +coverage = { version = ">=5.5, <6.0", optional = true} +coveralls = { version = ">=1.1, <2.0", optional = true} +#pylint = ">=1.9.4" + +[tool.poetry.extras] +test = ["flake8", "freezegun", "pytest", "coverage", "coveralls"] + +#[tool.poetry.group.test] +#optional = true + +#[tool.poetry.group.coverage] +#optional = true + +#[tool.poetry.group.coverage.dependencies] +#coverage = ">=4.5.2" +#pytest-cov = "*" + +#[tool.poetry.group.docs] +#optional = true + +#[tool.poetry.group.docs.dependencies] +#sphinx = "*" + +[build-system] +requires = [ + "poetry>=1.1.15", + "setuptools >= 40.1.0", + "wheel" +] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +minversion = "4.6.11" +addopts = "-ra -vvv" +testpaths = [ + "tests", +] +pythonpath = [ + "tests", +] + +[tool.coverage.run] +branch = true +source = ["src/onelogin/saml2"] +ignore_errors = true + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "def __str__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "if typing.TYPE_CHECKING:", +] + +[tool.coverage.html] +directory = "cov_html" + +[tool.flake8] +max-line-length = 210 +max-complexity = 22 +count = true +show-source = true +statistics = true +disable-noqa = false +# 'ignore' defaults to: E121,E123,E126,E226,E24,E704,W503,W504 +extend-ignore = [ + 'B904', + 'B006', + 'B950', + 'B017', + 'C901', + 'E501', + 'E731', +] +per-file-ignores = [ + '__init__.py:F401', +] +# 'select' defaults to: E,F,W,C90 +extend-select = [ + # * Default warnings reported by flake8-bugbear (B) - + # https://github.com/PyCQA/flake8-bugbear#list-of-warnings + 'B', + # * The B950 flake8-bugbear opinionated warnings - + # https://github.com/PyCQA/flake8-bugbear#opinionated-warnings + 'B9', +] +extend-exclude = [ + '.github', '.gitlab', + '.Python', '.*.pyc', '.*.pyo', '.*.pyd', '.*.py.class', '*.egg-info', + 'venv*', '.venv*', '.*_cache', + 'lib', 'lib64', '.*.so', + 'build', 'dist', 'sdist', 'wheels', +] + +[tool.black] +line-length = 200 +extend-exclude = ''' +# A regex preceded with ^/ will apply only to files and directories +# in the root of the project. +( + \.pytest_cache +) +''' + +[tool.isort] +profile = 'black' +# The 'black' profile means: +# multi_line_output = 3 +# include_trailing_comma = true +# force_grid_wrap = 0 +# use_parentheses = true +# ensure_newline_before_comments = true +# line_length = 88 +line_length = 200 # override black provile line_length +force_single_line = true # override black profile multi_line_output +star_first = true +group_by_package = true +force_sort_within_sections = true +lines_after_imports = 2 +honor_noqa = true +atomic = true +ignore_comments = true +skip_gitignore = true +src_paths = [ + 'src', + 'tests', +] From d6a3a3133c90e134cf25e99e22468ad38aef36ee Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 28 Dec 2022 01:58:23 +0100 Subject: [PATCH 330/352] Force lxml installation from non binary --- .github/workflows/python-package.yml | 5 +++-- .travis.yml | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 885bbc54..f31465d9 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,14 +15,15 @@ jobs: uses: actions/setup-python@v2 with: python-version: 2.7 - + - name: Install dependencies run: | sudo apt-get update -qq sudo apt-get install -qq swig python-dev libxml2-dev libxmlsec1-dev + pip install --force-reinstall --no-binary lxml lxml pip install . pip install -e ".[test]" - + - name: Test run: | coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test diff --git a/.travis.yml b/.travis.yml index dc9a5fc1..30686740 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ jobs: install: - sudo apt-get update -qq - sudo apt-get install -qq swig python-dev libxml2-dev libxmlsec1-dev + - 'travis_retry pip install --force-reinstall --no-binary lxml lxml' - 'travis_retry pip install .' - 'travis_retry pip install -e ".[test]"' From cafef169d4e3255f0d989f415cfa4284efb69996 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 28 Dec 2022 02:02:19 +0100 Subject: [PATCH 331/352] Prepare release 2.12.0 --- README.md | 27 ++++++++++++++++++--------- changelog.md | 15 ++++++--------- setup.py | 2 +- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d157b5b5..163728df 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ Installation ### Dependencies ### * python 2.7 +* [lxml](https://pypi.python.org/pypi/lxml) Python bindings for the libxml2 and libxslt libraries. * [dm.xmlsec.binding](https://pypi.python.org/pypi/dm.xmlsec.binding) Cython/lxml based binding for the XML security library (depends on python-dev libxml2-dev libxmlsec1-dev) * [isodate](https://pypi.python.org/pypi/isodate) An ISO 8601 date/time/duration parser and formater * [defusedxml](https://pypi.python.org/pypi/defusedxml) XML bomb protection for Python stdlib modules @@ -135,6 +136,14 @@ $ pip install python-saml If you want to know how a project can handle python packages review this [guide](https://packaging.python.org/en/latest/tutorial.html) and review this [sampleproject](https://github.com/pypa/sampleproject) +#### NOTE #### +To avoid ``libxml2`` library version incompatibilities between ``xmlsec`` and ``lxml`` it is recommended that ``lxml`` is not installed from binary. + +This can be ensured by executing: +``` +$ pip install --force-reinstall --no-binary lxml lxml +``` + Security Warning ---------------- @@ -164,10 +173,10 @@ SAML Messages have a limited timelife (NotBefore, NotOnOrAfter) that make harder this kind of attacks, but they are still possible. In order to avoid them, the SP can keep a list of SAML Messages or Assertion IDs alredy valdidated and processed. Those values only need -to be stored the amount of time of the SAML Message life time, so +to be stored the amount of time of the SAML Message life time, so we don't need to store all processed message/assertion Ids, but the most recent ones. -The OneLogin_Saml2_Auth class contains the [get_last_request_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L352), [get_last_message_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L359) and [get_last_assertion_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L366) methods to retrieve the IDs +The OneLogin_Saml2_Auth class contains the [get_last_request_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L352), [get_last_message_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L359) and [get_last_assertion_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L366) methods to retrieve the IDs Checking that the ID of the current Message/Assertion does not exists in the lis of the ones already processed will prevent replay attacks. @@ -334,7 +343,7 @@ This is the ``settings.json`` file: /* * Key rollover * If you plan to update the SP X.509 cert and privateKey - * you can define here the new X.509 cert and it will be + * you can define here the new X.509 cert and it will be * published on the SP metadata so Identity Providers can * read them and get ready for rollover. */ @@ -467,7 +476,7 @@ In addition to the required settings data (idp, sp), extra settings can be defin "wantAttributeStatement": true, // Rejects SAML responses with a InResponseTo attribute when request_id - // not provided in the process_response method that later call the + // not provided in the process_response method that later call the // response is_valid method with that parameter. "rejectUnsolicitedResponsesWithInResponseTo": false, @@ -582,7 +591,7 @@ There's an easier method -- use a metadata exchange. Metadata is just an XML fi Using ````parse_remote```` IdP metadata can be obtained and added to the settings withouth further ado. -But take in mind that the OneLogin_Saml2_IdPMetadataParser class does not validate in any way the URL that is introduced in order to be parsed. +But take in mind that the OneLogin_Saml2_IdPMetadataParser class does not validate in any way the URL that is introduced in order to be parsed. Usually the same administrator that handles the Service Provider also sets the URL to the IdP, which should be a trusted resource. @@ -967,7 +976,7 @@ else: ### SP Key rollover ### -If you plan to update the SP X.509 cert and privateKey you can define the new X.509 cert as ``settings['sp']['x509certNew']`` and it will be +If you plan to update the SP X.509 cert and privateKey you can define the new X.509 cert as ``settings['sp']['x509certNew']`` and it will be published on the SP metadata so Identity Providers can read them and get ready for rollover. @@ -981,14 +990,14 @@ In order to handle that the toolkit offers the ``settings['idp']['x509certMulti' When that parameter is used, ``x509cert`` and ``certFingerprint`` values will be ignored by the toolkit. The ``x509certMulti`` is an array with 2 keys: -- ``signing``. An array of certs that will be used to validate IdP signature +- ``signing``. An array of certs that will be used to validate IdP signature - ``encryption`` An array with one unique cert that will be used to encrypt data to be sent to the IdP ### Replay attacks ### - + In order to avoid replay attacks, you can store the ID of the SAML messages already processed, to avoid processing them twice. Since the Messages expires and will be invalidated due that fact, you don't need to store those IDs longer than the time frame that you currently accepting. - + Get the ID of the last processed message/assertion with the ``get_last_message_id``/``get_last_assertion_id method`` of the ``Auth`` object. diff --git a/changelog.md b/changelog.md index 6ffd3f12..ccf16b21 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,9 @@ # python-saml changelog +### 2.12.0 (Dec 28, 2022) +- Remove version restriction on lxml dependency +- Update Demo Bottle +- Updated Travis file. Forced lxml to be installed using no-validate_binary + ### 2.11.1 (Jan 28, 2022) - lxml fixed to be lower than 4.7.1 since it seems to have issues validating the signature of encrypted elements See https://github.com/onelogin/python3-saml/issues/292 @@ -120,7 +125,7 @@ Implement a more specific exception class for handling some validation errors. I * Improve Signature validation process * [#149](https://github.com/onelogin/python-saml/pull/149) Work-around for xmlsec.initialize * [#151](https://github.com/onelogin/python-saml/pull/151) Fix flask demo error handling and improve documentation -* [#152](https://github.com/onelogin/python-saml/pull/152) Update LICENSE to include MIT rather than BSD license +* [#152](https://github.com/onelogin/python-saml/pull/152) Update LICENSE to include MIT rather than BSD license * [#155](https://github.com/onelogin/python-saml/pull/155) Fix typographical errors in docstring * Fix RequestedAttribute Issue * Fix __build_signature method. If relay_state is null not be part of the SignQuery @@ -226,11 +231,3 @@ Implement a more specific exception class for handling some validation errors. I ### 1.0.0 (Jun 26, 2014) * OneLogin's SAML Python Toolkit v1.0.0 - - - - - - - - diff --git a/setup.py b/setup.py index 0a589898..1e9aba9a 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='python-saml', - version='2.11.1', + version='2.12.0', description='Saml Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 5 - Production/Stable', From 222c03689247b9e298a4f9a3c010527d00c1c370 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 3 Jan 2023 15:01:30 +0100 Subject: [PATCH 332/352] Update CI status badget --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 163728df..ede1b47c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # OneLogin's SAML Python Toolkit -[![Build Status](https://api.travis-ci.org/onelogin/python-saml.png?branch=master)](http://travis-ci.org/onelogin/python-saml) -[![Coverage Status](https://coveralls.io/repos/onelogin/python-saml/badge.png)](https://coveralls.io/r/onelogin/python-saml) +[![Python package](https://github.com/SAML-Toolkits/python-saml/actions/workflows/python-package.yml/badge.svg)](https://github.com/SAML-Toolkits/python-saml/actions/workflows/python-package.yml) +[![Coverage Status](https://coveralls.io/repos/SAML-Toolkits/python-saml/badge.png)](https://coveralls.io/r/SAML-Toolkits/python-saml) [![PyPi Version](https://img.shields.io/pypi/v/python-saml.svg)](https://pypi.python.org/pypi/python-saml) ![Python versions](https://img.shields.io/pypi/pyversions/python-saml.svg) From eb18bb8b55782340577c4cfe523b90cb300981da Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 3 Jan 2023 15:02:43 +0100 Subject: [PATCH 333/352] Update coverage budget --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ede1b47c..499746d9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # OneLogin's SAML Python Toolkit [![Python package](https://github.com/SAML-Toolkits/python-saml/actions/workflows/python-package.yml/badge.svg)](https://github.com/SAML-Toolkits/python-saml/actions/workflows/python-package.yml) -[![Coverage Status](https://coveralls.io/repos/SAML-Toolkits/python-saml/badge.png)](https://coveralls.io/r/SAML-Toolkits/python-saml) +[![Coverage Status](https://coveralls.io/repos/github/SAML-Toolkits/python-saml/badge.svg?branch=master)](https://coveralls.io/github/SAML-Toolkits/python-saml?branch=master) [![PyPi Version](https://img.shields.io/pypi/v/python-saml.svg)](https://pypi.python.org/pypi/python-saml) ![Python versions](https://img.shields.io/pypi/pyversions/python-saml.svg) From efd1d64d24c6c07a86b96e755ce2214a85464d5d Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 3 Jan 2023 15:59:25 +0100 Subject: [PATCH 334/352] Update License --- LICENSE | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 1c8f814e..c141165e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ -Copyright (c) 2010-2018 OneLogin, Inc. +Copyright (c) 2010-2022 OneLogin, Inc. +Copyright (c) 2023 IAM Digital Services, SL. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation From d085ea6619b546e30f341074496506366a97d5eb Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 4 Jan 2023 20:25:02 +0100 Subject: [PATCH 335/352] Add pypi downloads badget --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 499746d9..24e9e95f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # OneLogin's SAML Python Toolkit [![Python package](https://github.com/SAML-Toolkits/python-saml/actions/workflows/python-package.yml/badge.svg)](https://github.com/SAML-Toolkits/python-saml/actions/workflows/python-package.yml) +![PyPI Downloads](https://img.shields.io/pypi/dm/python-saml.svg?label=PyPI%20Downloads) [![Coverage Status](https://coveralls.io/repos/github/SAML-Toolkits/python-saml/badge.svg?branch=master)](https://coveralls.io/github/SAML-Toolkits/python-saml?branch=master) [![PyPi Version](https://img.shields.io/pypi/v/python-saml.svg)](https://pypi.python.org/pypi/python-saml) ![Python versions](https://img.shields.io/pypi/pyversions/python-saml.svg) From bdae8cb14991a687ab1b6782b6925d0a7fb5e524 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 4 Jan 2023 21:34:19 +0100 Subject: [PATCH 336/352] Remove references of OneLogin as maintainer --- README.md | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 24e9e95f..09eee6cf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OneLogin's SAML Python Toolkit +# SAML Python Toolkit [![Python package](https://github.com/SAML-Toolkits/python-saml/actions/workflows/python-package.yml/badge.svg)](https://github.com/SAML-Toolkits/python-saml/actions/workflows/python-package.yml) ![PyPI Downloads](https://img.shields.io/pypi/dm/python-saml.svg?label=PyPI%20Downloads) @@ -13,11 +13,10 @@ to Python 3 and use python3-saml Add SAML support to your Python software using this library. -Forget those complicated libraries and use the open source library provided -and supported by OneLogin Inc. +Forget those complicated libraries and use the open source library. This version supports Python2. There is a separate version that supports -Python3: [python3-saml](https://github.com/onelogin/python3-saml). +Python3: [python3-saml](https://github.com/SAML-Toolkits/python3-saml). #### Warning #### @@ -37,7 +36,7 @@ Update ``python-saml`` to ``2.2.0``, this version includes a security patch that #### Security Guidelines #### -If you believe you have discovered a security vulnerability in this toolkit, please report it at https://www.onelogin.com/security with a description. We follow responsible disclosure guidelines, and will work with you to quickly find a resolution. +If you believe you have discovered a security vulnerability in this toolkit, please report it by mail to the maintainer: sixto.martin.garcia+security@gmail.com Why add SAML support to my software? ------------------------------------ @@ -65,7 +64,7 @@ since 2002, but lately it is becoming popular due its advantages: General Description ------------------- -OneLogin's SAML Python toolkit lets you turn your Python application into a SP +SAML Python toolkit lets you turn your Python application into a SP (Service Provider) that can be connected to an IdP (Identity Provider). **Supports:** @@ -86,7 +85,7 @@ OneLogin's SAML Python toolkit lets you turn your Python application into a SP * **Easy to use** - Programmer will be allowed to code high-level and low-level programming, 2 easy to use APIs are available. * **Tested** - Thoroughly tested. - * **Popular** - OneLogin's customers use it. Add easy support to your Django/Flask/Bottle/Pyramid web projects. + * **Popular** - Developers use it. Add easy support to your Django/Flask/Bottle/Pyramid web projects. Installation @@ -120,8 +119,8 @@ $ brew install libxmlsec1 The toolkit is hosted on Github. You can download it from: - * Lastest release: https://github.com/onelogin/python-saml/releases/latest - * Master repo: https://github.com/onelogin/python-saml/tree/master + * Lastest release: https://github.com/SAML-Toolkits/python-saml/releases/latest + * Master repo: https://github.com/SAML-Toolkits/python-saml/tree/master Copy the core of the library ``(src/onelogin/saml2 folder)`` and merge the setup.py inside the Python application. (Each application has its structure so take your time to locate the Python SAML toolkit in the best place). @@ -177,7 +176,7 @@ In order to avoid them, the SP can keep a list of SAML Messages or Assertion IDs to be stored the amount of time of the SAML Message life time, so we don't need to store all processed message/assertion Ids, but the most recent ones. -The OneLogin_Saml2_Auth class contains the [get_last_request_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L352), [get_last_message_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L359) and [get_last_assertion_id](https://github.com/onelogin/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L366) methods to retrieve the IDs +The OneLogin_Saml2_Auth class contains the [get_last_request_id](https://github.com/SAML-Toolkits/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L352), [get_last_message_id](https://github.com/SAML-Toolkits/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L359) and [get_last_assertion_id](https://github.com/SAML-Toolkits/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L366) methods to retrieve the IDs Checking that the ID of the current Message/Assertion does not exists in the lis of the ones already processed will prevent replay attacks. @@ -187,7 +186,7 @@ Getting Started ### Knowing the toolkit ### -The new OneLogin SAML Toolkit contains different folders (``cert``, ``lib``, ``demo-django``, ``demo-flask``, ``demo-bottle`` and ``tests``) and some files. +The SAML Toolkit contains different folders (``cert``, ``lib``, ``demo-django``, ``demo-flask``, ``demo-bottle`` and ``tests``) and some files. Let's start describing them: @@ -300,7 +299,7 @@ This is the ``settings.json`` file: // URL Location where the from the IdP will be returned "url": "https:///?acs", // SAML protocol binding to be used when returning the - // message. OneLogin Toolkit supports this endpoint for the + // message. SAML Toolkit supports this endpoint for the // HTTP-POST binding only. "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, @@ -328,7 +327,7 @@ This is the ``settings.json`` file: // OPTIONAL: only specify if different from url parameter //"responseUrl": "https:///?sls", // SAML protocol binding to be used when returning the - // message. OneLogin Toolkit supports the HTTP-Redirect binding + // message. SAML Toolkit supports the HTTP-Redirect binding // only for this endpoint. "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, @@ -361,7 +360,7 @@ This is the ``settings.json`` file: // will be sent. "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", // SAML protocol binding to be used when returning the - // message. OneLogin Toolkit supports the HTTP-Redirect binding + // message. SAML Toolkit supports the HTTP-Redirect binding // only for this endpoint. "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, @@ -373,7 +372,7 @@ This is the ``settings.json`` file: // OPTIONAL: only specify if different from url parameter "responseUrl": "https://app.onelogin.com/trust/saml2/http-redirect/slo_return/", // SAML protocol binding to be used when returning the - // message. OneLogin Toolkit supports the HTTP-Redirect binding + // message. SAML Toolkit supports the HTTP-Redirect binding // only for this endpoint. "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, @@ -1008,7 +1007,7 @@ Described below are the main classes and methods that can be invoked from the SA #### OneLogin_Saml2_Auth - auth.py #### -Main class of OneLogin Python Toolkit +Main class of SAML Python Toolkit * `__init__` Initializes the SP SAML instance. * ***login*** Initiates the SSO process. @@ -1100,7 +1099,7 @@ SAML 2 Logout Response class #### OneLogin_Saml2_Settings - settings.py #### -Configuration of the OneLogin Python Toolkit +Configuration of the SAML Python Toolkit * `__init__` Initializes the settings: Sets the paths of the different folders and Loads settings info from settings file or array/object provided. * ***check_settings*** Checks the settings info. @@ -1263,7 +1262,7 @@ The flask project contains: #### SP setup #### -The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the ``demo-flask``, it uses the first method. +The SAML Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the ``demo-flask``, it uses the first method. In the index.py file we define the ``app.config['SAML_PATH']``, that will target to the ``saml`` folder. We require it in order to load the settings files. @@ -1336,7 +1335,7 @@ The django project contains: #### SP setup #### -The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the ``demo-django``, it uses the first method. +The SAML Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the ``demo-django``, it uses the first method. After set the ``SAML_FOLDER`` in the ``demo/settings.py``, the settings of the python toolkit will be loaded on the django web. @@ -1391,7 +1390,7 @@ The Pyramid project contains: #### SP setup #### -The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In ``demo_pyramid`` the first method is used. +The Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In ``demo_pyramid`` the first method is used. In the views.py file we define the ``SAML_PATH``, which will target the ``saml`` folder. We require it in order to load the settings files. From 1a3b040ea8f650316574d1957a0941f01912c444 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 6 Jan 2023 03:34:58 +0100 Subject: [PATCH 337/352] Remove references to onelogin provided support. --- changelog.md | 2 +- demo-bottle/templates/base.html | 4 ++-- demo-django/templates/base.html | 4 ++-- demo-flask/templates/base.html | 4 ++-- src/onelogin/__init__.py | 8 +++++--- src/onelogin/saml2/__init__.py | 8 +++++--- src/onelogin/saml2/auth.py | 3 +-- src/onelogin/saml2/authn_request.py | 3 +-- src/onelogin/saml2/constants.py | 5 ++--- src/onelogin/saml2/errors.py | 3 +-- src/onelogin/saml2/idp_metadata_parser.py | 3 +-- src/onelogin/saml2/logout_request.py | 3 +-- src/onelogin/saml2/logout_response.py | 3 +-- src/onelogin/saml2/metadata.py | 3 +-- src/onelogin/saml2/response.py | 3 +-- src/onelogin/saml2/settings.py | 3 +-- src/onelogin/saml2/utils.py | 3 +-- tests/src/OneLogin/saml2_tests/auth_test.py | 1 - tests/src/OneLogin/saml2_tests/authn_request_test.py | 1 - tests/src/OneLogin/saml2_tests/error_test.py | 1 - .../src/OneLogin/saml2_tests/idp_metadata_parser_test.py | 1 - tests/src/OneLogin/saml2_tests/logout_request_test.py | 1 - tests/src/OneLogin/saml2_tests/logout_response_test.py | 1 - tests/src/OneLogin/saml2_tests/metadata_test.py | 1 - tests/src/OneLogin/saml2_tests/response_test.py | 1 - tests/src/OneLogin/saml2_tests/settings_test.py | 1 - tests/src/OneLogin/saml2_tests/signed_response_test.py | 1 - tests/src/OneLogin/saml2_tests/utils_test.py | 1 - 28 files changed, 29 insertions(+), 47 deletions(-) diff --git a/changelog.md b/changelog.md index ccf16b21..c5500e97 100644 --- a/changelog.md +++ b/changelog.md @@ -230,4 +230,4 @@ Implement a more specific exception class for handling some validation errors. I * Security improved, added more checks at the SAMLResponse validation ### 1.0.0 (Jun 26, 2014) -* OneLogin's SAML Python Toolkit v1.0.0 +* SAML Python Toolkit v1.0.0 diff --git a/demo-bottle/templates/base.html b/demo-bottle/templates/base.html index a55dbf0b..960ca0bc 100644 --- a/demo-bottle/templates/base.html +++ b/demo-bottle/templates/base.html @@ -5,7 +5,7 @@ - A Python SAML Toolkit by OneLogin demo + A Python SAML Toolkit demo @@ -18,7 +18,7 @@
    -

    A Python SAML Toolkit by OneLogin demo

    +

    A Python SAML Toolkit demo

    {% block content %}{% endblock %}
    diff --git a/demo-django/templates/base.html b/demo-django/templates/base.html index a55dbf0b..960ca0bc 100644 --- a/demo-django/templates/base.html +++ b/demo-django/templates/base.html @@ -5,7 +5,7 @@ - A Python SAML Toolkit by OneLogin demo + A Python SAML Toolkit demo @@ -18,7 +18,7 @@
    -

    A Python SAML Toolkit by OneLogin demo

    +

    A Python SAML Toolkit demo

    {% block content %}{% endblock %}
    diff --git a/demo-flask/templates/base.html b/demo-flask/templates/base.html index a55dbf0b..960ca0bc 100644 --- a/demo-flask/templates/base.html +++ b/demo-flask/templates/base.html @@ -5,7 +5,7 @@ - A Python SAML Toolkit by OneLogin demo + A Python SAML Toolkit demo @@ -18,7 +18,7 @@
    -

    A Python SAML Toolkit by OneLogin demo

    +

    A Python SAML Toolkit demo

    {% block content %}{% endblock %}
    diff --git a/src/onelogin/__init__.py b/src/onelogin/__init__.py index 52ea1212..3ceff6ba 100644 --- a/src/onelogin/__init__.py +++ b/src/onelogin/__init__.py @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- """ -Copyright (c) 2010-2021 OneLogin, Inc. +Copyright (c) 2010-2022 OneLogin, Inc. +Copyright (c) 2023 IAM DIgital Services, SL + MIT License Add SAML support to your Python softwares using this library. Forget those complicated libraries and use that open source -library provided and supported by OneLogin Inc. +library. -OneLogin's SAML Python toolkit let you build a SP (Service Provider) +SAML Python toolkit let you build a SP (Service Provider) over your Python application and connect it to any IdP (Identity Provider). Supports: diff --git a/src/onelogin/saml2/__init__.py b/src/onelogin/saml2/__init__.py index 52ea1212..7ddcbd2e 100644 --- a/src/onelogin/saml2/__init__.py +++ b/src/onelogin/saml2/__init__.py @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- """ -Copyright (c) 2010-2021 OneLogin, Inc. +Copyright (c) 2010-2022 OneLogin, Inc. +Copyright (c) 2023 IAM Digital Services, SL. + MIT License Add SAML support to your Python softwares using this library. Forget those complicated libraries and use that open source -library provided and supported by OneLogin Inc. +library. -OneLogin's SAML Python toolkit let you build a SP (Service Provider) +SAML Python toolkit let you build a SP (Service Provider) over your Python application and connect it to any IdP (Identity Provider). Supports: diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 4911da6f..24d3b2b8 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -2,10 +2,9 @@ """ OneLogin_Saml2_Auth class -Copyright (c) 2010-2021 OneLogin, Inc. MIT License -Main class of OneLogin's Python Toolkit. +Main class of Python Toolkit. Initializes the SP SAML instance diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 21bb746c..5e2c5355 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -2,10 +2,9 @@ """ OneLogin_Saml2_Authn_Request class -Copyright (c) 2010-2021 OneLogin, Inc. MIT License -AuthNRequest class of OneLogin's Python Toolkit. +AuthNRequest class of Python Toolkit. """ from base64 import b64encode diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py index 5bff8d4d..0e4844d0 100644 --- a/src/onelogin/saml2/constants.py +++ b/src/onelogin/saml2/constants.py @@ -2,10 +2,9 @@ """ OneLogin_Saml2_Constants class -Copyright (c) 2010-2021 OneLogin, Inc. MIT License -Constants class of OneLogin's Python Toolkit. +Constants class of Python Toolkit. """ @@ -14,7 +13,7 @@ class OneLogin_Saml2_Constants(object): """ This class defines all the constants that will be used - in the OneLogin's Python Toolkit. + in the Python Toolkit. """ diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index f5dca8f2..a4ffd7e2 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -2,10 +2,9 @@ """ OneLogin_Saml2_Error class -Copyright (c) 2010-2021 OneLogin, Inc. MIT License -Error class of OneLogin's Python Toolkit. +Error class of Python Toolkit. Defines common Error codes and has a custom initializator. diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index 7197ef5e..7d03bf5a 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -2,10 +2,9 @@ """ OneLogin_Saml2_IdPMetadataParser class -Copyright (c) 2010-2021 OneLogin, Inc. MIT License -Metadata class of OneLogin's Python Toolkit. +Metadata class of Python Toolkit. """ diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index fa034a1b..9426d5d9 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -2,10 +2,9 @@ """ OneLogin_Saml2_Logout_Request class -Copyright (c) 2010-2021 OneLogin, Inc. MIT License -Logout Request class of OneLogin's Python Toolkit. +Logout Request class of Python Toolkit. """ diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 50e07f1e..66191caf 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -2,10 +2,9 @@ """ OneLogin_Saml2_Logout_Response class -Copyright (c) 2010-2021 OneLogin, Inc. MIT License -Logout Response class of OneLogin's Python Toolkit. +Logout Response class of Python Toolkit. """ diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 84493428..ae7c5ffb 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -2,10 +2,9 @@ """ OneLogin_Saml2_Metadata class -Copyright (c) 2010-2021 OneLogin, Inc. MIT License -Metadata class of OneLogin's Python Toolkit. +Metadata class of Python Toolkit. """ diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 20ff48c5..0da8c06c 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -2,10 +2,9 @@ """ OneLogin_Saml2_Response class -Copyright (c) 2010-2021 OneLogin, Inc. MIT License -SAML Response class of OneLogin's Python Toolkit. +SAML Response class of Python Toolkit. """ diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 054612de..fb194546 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -2,10 +2,9 @@ """ OneLogin_Saml2_Settings class -Copyright (c) 2010-2021 OneLogin, Inc. MIT License -Setting class of OneLogin's Python Toolkit. +Setting class of Python Toolkit. """ diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index d97edf15..bfdb10a8 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -2,10 +2,9 @@ """ OneLogin_Saml2_Utils class -Copyright (c) 2010-2021 OneLogin, Inc. MIT License -Auxiliary class of OneLogin's Python Toolkit. +Auxiliary class of Python Toolkit. """ diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 90e78d75..e1f4b062 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64decode, b64encode diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 3e10e71e..edbad6f4 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64decode diff --git a/tests/src/OneLogin/saml2_tests/error_test.py b/tests/src/OneLogin/saml2_tests/error_test.py index 2c18354d..07edfcff 100644 --- a/tests/src/OneLogin/saml2_tests/error_test.py +++ b/tests/src/OneLogin/saml2_tests/error_test.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License import unittest diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py index 545f0d12..c88e9d03 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 2b37f0a1..8aa83154 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64encode diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index f92111f7..ba950be9 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License import json diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 91b9047a..b578a73b 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index acd13d34..1fca1580 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64decode, b64encode diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index fa308684..4da3363e 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License import json diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py index ea011457..29a61006 100644 --- a/tests/src/OneLogin/saml2_tests/signed_response_test.py +++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64encode diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 6a4dd94c..6beb4c9b 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010-2021 OneLogin, Inc. # MIT License from base64 import b64decode From a87db77ba55d564857c1ad39714112546a52a380 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 8 Jan 2023 00:07:16 +0100 Subject: [PATCH 338/352] Add CI coveralls support (#302) * Add coveralls step to Github Actions * Fix typo * Add COVERALLS_REPO_TOKEN * Add environment * Add toml to avoid error * Fix tool.coverage.run section * Fix Coveralls * Fix Coveralls * Try running coveralls with py3 * New strategy * Typo * Typo * Use python-coveralls instead * Remove coveralls from pyproject * Downgrade coverage and remove coveralls --- .github/workflows/python-package.yml | 17 +++++++++++++---- pyproject.toml | 7 +++---- setup.py | 3 +-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f31465d9..18fd1e09 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -8,7 +8,7 @@ on: [push, pull_request] jobs: build: runs-on: ubuntu-20.04 - + environment: CI steps: - uses: actions/checkout@v2 - name: Set up Python 2.7 @@ -24,10 +24,19 @@ jobs: pip install . pip install -e ".[test]" - - name: Test + - name: Lint run: | - coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test - coverage report -m --rcfile=tests/coverage.rc #pycodestyle tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc #pyflakes src/onelogin/saml2 demo-django demo-flask tests/src/OneLogin/saml2_tests flake8 --ignore E226,E302,E41,E731,E501,C901,W504 + + - name: Test + run: | + coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test + + - name: Coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + run: | + pip install python-coveralls + coveralls diff --git a/pyproject.toml b/pyproject.toml index be66bac4..17e91a4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,12 +55,11 @@ flake8 = { version = ">=3.6.0, <=4.0", optional = true} #[tool.poetry.group.test.dependencies] freezegun= { version = ">=0.3.11, <=0.4", optional = true} pytest = { version = ">=4.6.11", optional = true} -coverage = { version = ">=5.5, <6.0", optional = true} -coveralls = { version = ">=1.1, <2.0", optional = true} +coverage = { version = ">=4.5.0, <5.0", optional = true} #pylint = ">=1.9.4" [tool.poetry.extras] -test = ["flake8", "freezegun", "pytest", "coverage", "coveralls"] +test = ["flake8", "freezegun", "pytest", "coverage"] #[tool.poetry.group.test] #optional = true @@ -99,9 +98,9 @@ pythonpath = [ [tool.coverage.run] branch = true source = ["src/onelogin/saml2"] -ignore_errors = true [tool.coverage.report] +ignore_errors = true exclude_lines = [ "pragma: no cover", "def __repr__", diff --git a/setup.py b/setup.py index 1e9aba9a..14987126 100644 --- a/setup.py +++ b/setup.py @@ -40,10 +40,9 @@ ], extras_require={ 'test': ( - 'coverage>=5.5, <6.0', + 'coverage>=4.5, <5.0', 'freezegun>=0.3.5, <0.4', 'flake8>=3.6.0, < 4.0', - 'coveralls>=1.1, < 2.0', ), }, keywords='saml saml2 xmlsec django flask', From a6a21109179571c9ca23f92e03017759741603c2 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 13 May 2023 09:54:43 +0200 Subject: [PATCH 339/352] Fix payloads of tests that had expiration dates on May 2023 --- tests/data/logout_requests/invalids/invalid_issuer.xml | 2 +- tests/data/logout_requests/invalids/invalid_issuer.xml.base64 | 2 +- tests/data/logout_requests/invalids/no_nameId.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/data/logout_requests/invalids/invalid_issuer.xml b/tests/data/logout_requests/invalids/invalid_issuer.xml index e1edabde..4b7714d1 100644 --- a/tests/data/logout_requests/invalids/invalid_issuer.xml +++ b/tests/data/logout_requests/invalids/invalid_issuer.xml @@ -5,7 +5,7 @@ Version="2.0" IssueInstant="2013-12-10T04:39:31Z" Destination="http://stuff.com/endpoints/endpoints/sls.php" - NotOnOrAfter="2023-05-10T04:39:31Z" + NotOnOrAfter="2053-05-10T04:39:31Z" > https://example.hello.com/access/saml https://example.hello.com/access/saml From 29d0e96f29f172dc05c8019331ac1773bcf3accb Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 21 Jul 2023 12:09:36 +0200 Subject: [PATCH 340/352] Add support github-action python2.7 by the use of a container (#309) Adding support to python2.7 by the use of a container, github-actions deprecated py2.7 --- .github/workflows/python-package.yml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 18fd1e09..0359fe32 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -8,21 +8,19 @@ on: [push, pull_request] jobs: build: runs-on: ubuntu-20.04 + container: + image: python:2.7.18-buster environment: CI steps: - - uses: actions/checkout@v2 - - name: Set up Python 2.7 - uses: actions/setup-python@v2 - with: - python-version: 2.7 - + - name: Checkout project + uses: actions/checkout@v3 - name: Install dependencies run: | - sudo apt-get update -qq - sudo apt-get install -qq swig python-dev libxml2-dev libxmlsec1-dev - pip install --force-reinstall --no-binary lxml lxml - pip install . - pip install -e ".[test]" + apt-get update -qq + apt-get install -qq swig python-dev libxml2-dev libxmlsec1-dev + pip install --disable-pip-version-check --no-cache-dir --force-reinstall --no-binary lxml lxml + pip install --disable-pip-version-check --no-cache-dir . + pip install --disable-pip-version-check --no-cache-dir -e ".[test]" - name: Lint run: | From 0baa89987860830411d5f8bc7b1e64f39c947eec Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 1 Oct 2023 01:10:02 +0200 Subject: [PATCH 341/352] Add PDF version of Documentation --- docs/SAML_Python_Toolkit_Guide.pdf | Bin 0 -> 299953 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/SAML_Python_Toolkit_Guide.pdf diff --git a/docs/SAML_Python_Toolkit_Guide.pdf b/docs/SAML_Python_Toolkit_Guide.pdf new file mode 100644 index 0000000000000000000000000000000000000000..509e64a9cd49318d1b9a89682ba6343cf6a5e76f GIT binary patch literal 299953 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

    MhAGjJhGrJMZN1=*i{L( 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

    V7%36%Y>LM{H{=jg4Fs% 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 C+3V&2 literal 0 HcmV?d00001 From b9ce7d98c20f84a25b422d447d74d48c1caf176b Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 1 Oct 2023 02:19:27 +0200 Subject: [PATCH 342/352] Remove outdated docs --- docs/saml2/.buildinfo | 4 - docs/saml2/_modules/index.html | 101 - docs/saml2/_modules/saml2/auth.html | 460 - docs/saml2/_modules/saml2/authn_request.html | 176 - docs/saml2/_modules/saml2/constants.html | 166 - docs/saml2/_modules/saml2/errors.html | 135 - docs/saml2/_modules/saml2/logout_request.html | 372 - .../saml2/_modules/saml2/logout_response.html | 282 - docs/saml2/_modules/saml2/metadata.html | 289 - docs/saml2/_modules/saml2/response.html | 532 - docs/saml2/_modules/saml2/settings.html | 695 -- docs/saml2/_modules/saml2/utils.html | 805 -- docs/saml2/_sources/index.txt | 23 - docs/saml2/_sources/saml2.txt | 83 - docs/saml2/_static/ajax-loader.gif | Bin 673 -> 0 bytes docs/saml2/_static/basic.css | 540 - docs/saml2/_static/comment-bright.png | Bin 3500 -> 0 bytes docs/saml2/_static/comment-close.png | Bin 3578 -> 0 bytes docs/saml2/_static/comment.png | Bin 3445 -> 0 bytes docs/saml2/_static/default.css | 256 - docs/saml2/_static/doctools.js | 247 - docs/saml2/_static/down-pressed.png | Bin 368 -> 0 bytes docs/saml2/_static/down.png | Bin 363 -> 0 bytes docs/saml2/_static/file.png | Bin 392 -> 0 bytes docs/saml2/_static/jquery.js | 9404 ----------------- docs/saml2/_static/minus.png | Bin 199 -> 0 bytes docs/saml2/_static/plus.png | Bin 199 -> 0 bytes docs/saml2/_static/pygments.css | 62 - docs/saml2/_static/searchtools.js | 567 - docs/saml2/_static/sidebar.js | 151 - docs/saml2/_static/underscore.js | 1226 --- docs/saml2/_static/up-pressed.png | Bin 372 -> 0 bytes docs/saml2/_static/up.png | Bin 363 -> 0 bytes docs/saml2/_static/websupport.js | 808 -- docs/saml2/genindex.html | 941 -- docs/saml2/index.html | 142 - docs/saml2/objects.inv | Bin 1959 -> 0 bytes docs/saml2/py-modindex.html | 154 - docs/saml2/saml2.html | 2044 ---- docs/saml2/search.html | 107 - docs/saml2/searchindex.js | 1 - 41 files changed, 20773 deletions(-) delete mode 100644 docs/saml2/.buildinfo delete mode 100644 docs/saml2/_modules/index.html delete mode 100644 docs/saml2/_modules/saml2/auth.html delete mode 100644 docs/saml2/_modules/saml2/authn_request.html delete mode 100644 docs/saml2/_modules/saml2/constants.html delete mode 100644 docs/saml2/_modules/saml2/errors.html delete mode 100644 docs/saml2/_modules/saml2/logout_request.html delete mode 100644 docs/saml2/_modules/saml2/logout_response.html delete mode 100644 docs/saml2/_modules/saml2/metadata.html delete mode 100644 docs/saml2/_modules/saml2/response.html delete mode 100644 docs/saml2/_modules/saml2/settings.html delete mode 100644 docs/saml2/_modules/saml2/utils.html delete mode 100644 docs/saml2/_sources/index.txt delete mode 100644 docs/saml2/_sources/saml2.txt delete mode 100644 docs/saml2/_static/ajax-loader.gif delete mode 100644 docs/saml2/_static/basic.css delete mode 100644 docs/saml2/_static/comment-bright.png delete mode 100644 docs/saml2/_static/comment-close.png delete mode 100644 docs/saml2/_static/comment.png delete mode 100644 docs/saml2/_static/default.css delete mode 100644 docs/saml2/_static/doctools.js delete mode 100644 docs/saml2/_static/down-pressed.png delete mode 100644 docs/saml2/_static/down.png delete mode 100644 docs/saml2/_static/file.png delete mode 100644 docs/saml2/_static/jquery.js delete mode 100644 docs/saml2/_static/minus.png delete mode 100644 docs/saml2/_static/plus.png delete mode 100644 docs/saml2/_static/pygments.css delete mode 100644 docs/saml2/_static/searchtools.js delete mode 100644 docs/saml2/_static/sidebar.js delete mode 100644 docs/saml2/_static/underscore.js delete mode 100644 docs/saml2/_static/up-pressed.png delete mode 100644 docs/saml2/_static/up.png delete mode 100644 docs/saml2/_static/websupport.js delete mode 100644 docs/saml2/genindex.html delete mode 100644 docs/saml2/index.html delete mode 100644 docs/saml2/objects.inv delete mode 100644 docs/saml2/py-modindex.html delete mode 100644 docs/saml2/saml2.html delete mode 100644 docs/saml2/search.html delete mode 100644 docs/saml2/searchindex.js diff --git a/docs/saml2/.buildinfo b/docs/saml2/.buildinfo deleted file mode 100644 index 5e197dbf..00000000 --- a/docs/saml2/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: e10660514f5c62e16e90878c60a15170 -tags: fbb0d17656682115ca4d033fb2f83ba1 diff --git a/docs/saml2/_modules/index.html b/docs/saml2/_modules/index.html deleted file mode 100644 index d672e465..00000000 --- a/docs/saml2/_modules/index.html +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - Overview: module code — OneLogin SAML Python library classes and methods - - - - - - - - - - - -

    - -
    - -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/_modules/saml2/auth.html b/docs/saml2/_modules/saml2/auth.html deleted file mode 100644 index 243bd4db..00000000 --- a/docs/saml2/_modules/saml2/auth.html +++ /dev/null @@ -1,460 +0,0 @@ - - - - - - - - - - saml2.auth — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Source code for saml2.auth

    -# -*- coding: utf-8 -*-
    -
    -# Copyright (c) 2010-2018 OneLogin, Inc.
    -# MIT License
    -
    -from base64 import b64encode
    -from urllib import urlencode, quote
    -from xml.etree.ElementTree import tostring
    -
    -import dm.xmlsec.binding as xmlsec
    -
    -from saml2.settings import OneLogin_Saml2_Settings
    -from saml2.response import OneLogin_Saml2_Response
    -from saml2.errors import OneLogin_Saml2_Error
    -from saml2.logout_response import OneLogin_Saml2_Logout_Response
    -from saml2.constants import OneLogin_Saml2_Constants
    -from saml2.utils import OneLogin_Saml2_Utils
    -from saml2.logout_request import OneLogin_Saml2_Logout_Request
    -from saml2.authn_request import OneLogin_Saml2_Authn_Request
    -
    -
    -
    [docs]class OneLogin_Saml2_Auth(object): - - def __init__(self, request_data, old_settings=None): - """ - Initializes the SP SAML instance. - - Arguments are: - * (dict) old_settings. Setting data - """ - self.__request_data = request_data - self.__settings = OneLogin_Saml2_Settings(old_settings) - self.__attributes = [] - self.__nameid = '' - self.__authenticated = False - self.__errors = [] - -
    [docs] def get_settings(self): - """ - Returns the settings info - :return: Setting info - :rtype: OneLogin_Saml2_Setting object - """ - return self.__settings -
    -
    [docs] def set_strict(self, value): - """ - Set the strict mode active/disable - - :param value: - :type value: bool - """ - assert isinstance(value, bool) - self.__settings.set_strict(value) -
    -
    [docs] def process_response(self, request_id=None): - """ - Process the SAML Response sent by the IdP. - - :param request_id: Is an optional argumen. Is the ID of the AuthNRequest sent by this SP to the IdP. - :type request_id: string - - :raises: OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND, when a POST with a SAMLResponse is not found - """ - self.__errors = [] - - 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']) - - if response.is_valid(request_id): - self.__attributes = response.get_attributes() - self.__nameid = response.get_nameid() - self.__authenticated = True - else: - self.__errors.append('invalid_response') - - else: - self.__errors.append('invalid_binding') - raise OneLogin_Saml2_Error( - 'SAML Response not found, Only supported HTTP_POST Binding', - OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND - ) -
    -
    [docs] def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None): - """ - Process the SAML Logout Response / Logout Request sent by the IdP. - - :param keep_local_session: When false will destroy the local session, otherwise will destroy it - :type keep_local_session: bool - - :param request_id: The ID of the LogoutRequest sent by this SP to the IdP - :type request_id: string - - :returns: Redirection url - """ - self.__errors = [] - - if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']: - logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) - if not logout_response.is_valid(self.__request_data, request_id): - self.__errors.append('invalid_logout_response') - elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS: - self.__errors.append('logout_not_success') - elif not keep_local_session: - OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) - - elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']: - request = OneLogin_Saml2_Utils.decode_base64_and_inflate(self.__request_data['get_data']['SAMLRequest']) - if not OneLogin_Saml2_Logout_Request.is_valid(self.__settings, request, self.__request_data): - self.__errors.append('invalid_logout_request') - else: - if not keep_local_session: - OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) - - in_response_to = OneLogin_Saml2_Logout_Request.get_id(request) - response_builder = OneLogin_Saml2_Logout_Response(self.__settings) - response_builder.build(in_response_to) - logout_response = response_builder.get_response() - - parameters = {'SAMLResponse': logout_response} - if 'RelayState' in self.__request_data['get_data']: - parameters['RelayState'] = self.__request_data['get_data']['RelayState'] - - security = self.__settings.get_security_data() - if 'logoutResponseSigned' in security and security['logoutResponseSigned']: - signature = self.build_response_signature(logout_response, parameters.get('RelayState', None)) - parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 - parameters['Signature'] = signature - - return self.redirect_to(self.get_slo_url(), parameters) - - else: - self.__errors.append('invalid_binding') - raise OneLogin_Saml2_Error( - 'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding', - OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND - ) -
    -
    [docs] def redirect_to(self, url=None, parameters={}): - """ - Redirects the user to the url past by parameter or to the url that we defined in our SSO Request. - - :param url: The target URL to redirect the user - :type url: string - :param parameters: Extra parameters to be passed as part of the url - :type parameters: dict - - :returns: Redirection url - """ - if url is None and 'RelayState' in self.__request_data['get_data']: - url = self.__request_data['get_data']['RelayState'] - return OneLogin_Saml2_Utils.redirect(url, parameters, request_data=self.__request_data) -
    -
    [docs] def is_authenticated(self): - """ - Checks if the user is authenticated or not. - - :returns: True if is authenticated, False if not - :rtype: bool - """ - return self.__authenticated -
    -
    [docs] def get_attributes(self): - """ - Returns the set of SAML attributes. - - :returns: SAML attributes - :rtype: dict - """ - return self.__attributes -
    -
    [docs] def get_nameid(self): - """ - Returns the nameID. - - :returns: NameID - :rtype: string - """ - return self.__nameid -
    -
    [docs] def get_errors(self): - """ - Returns a list with code errors if something went wrong - - :returns: List of errors - :rtype: list - """ - return self.__errors -
    -
    [docs] def get_attribute(self, name): - """ - Returns the requested SAML attribute. - - :param name: Name of the attribute - :type name: string - - :returns: Attribute value if exists or None - :rtype: string - """ - assert isinstance(name, basestring) - value = None - if name in self.__attributes.keys(): - value = self.__attributes[name] - return value -
    -
    [docs] def login(self, return_to=None): - """ - Initiates the SSO process. - - :param return_to: Optional argument. The target URL the user should be redirected to after login. - :type return_to: string - - :returns: Redirection url - """ - authn_request = OneLogin_Saml2_Authn_Request(self.__settings) - - saml_request = authn_request.get_request() - parameters = {'SAMLRequest': saml_request} - - if return_to is not None: - parameters['RelayState'] = return_to - else: - parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) - - security = self.__settings.get_security_data() - if security.get('authnRequestsSigned', False): - parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 - parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState']) - return self.redirect_to(self.get_sso_url(), parameters) -
    -
    [docs] def logout(self, return_to=None, name_id=None, session_index=None): - """ - Initiates the SLO process. - - :param return_to: Optional argument. The target URL the user should be redirected to after logout. - :type return_to: string - :param name_id: Optional argument. The NameID that will be set in the LogoutRequest. - :type name_id: string - :param session_index: Optional argument. SessionIndex that identifies the session of the user. - :type session_index: string - :returns: Redirection url - """ - slo_url = self.get_slo_url() - if slo_url is None: - raise OneLogin_Saml2_Error( - 'The IdP does not support Single Log Out', - OneLogin_Saml2_Error.SAML_SINGLE_LOGOUT_NOT_SUPPORTED - ) - - logout_request = OneLogin_Saml2_Logout_Request(self.__settings) - - saml_request = logout_request.get_request() - - parameters = {'SAMLRequest': logout_request.get_request()} - if return_to is not None: - parameters['RelayState'] = return_to - else: - parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) - - security = self.__settings.get_security_data() - if security.get('logoutRequestSigned', False): - parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 - parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState']) - return self.redirect_to(slo_url, parameters) -
    -
    [docs] def get_sso_url(self): - """ - Gets the SSO url. - - :returns: An URL, the SSO endpoint of the IdP - :rtype: string - """ - idp_data = self.__settings.get_idp_data() - return idp_data['singleSignOnService']['url'] -
    -
    [docs] def get_slo_url(self): - """ - Gets the SLO url. - - :returns: An URL, the SLO endpoint of the IdP - :rtype: string - """ - url = None - idp_data = self.__settings.get_idp_data() - if 'singleLogoutService' in idp_data.keys() and 'url' in idp_data['singleLogoutService']: - url = idp_data['singleLogoutService']['url'] - return url -
    -
    [docs] def build_request_signature(self, saml_request, relay_state): - """ - Builds the Signature of the SAML Request. - - :param saml_request: The SAML Request - :type saml_request: string - - :param relay_state: The target URL the user should be redirected to - :type relay_state: string - """ - if not self.__settings.check_sp_certs(): - raise OneLogin_Saml2_Error( - "Trying to sign the SAML Request but can't load the SP certs", - OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND - ) - - xmlsec.initialize() - - # Load the key into the xmlsec context - key = self.__settings.get_sp_key() - file_key = OneLogin_Saml2_Utils.write_temp_file(key) # FIXME avoid writing a file - - dsig_ctx = xmlsec.DSigCtx() - dsig_ctx.signKey = xmlsec.Key.load(file_key.name, xmlsec.KeyDataFormatPem, None) - file_key.close() - - data = { - 'SAMLRequest': quote(saml_request), - 'RelayState': quote(relay_state), - 'SignAlg': quote(OneLogin_Saml2_Constants.RSA_SHA1), - } - msg = urlencode(data) - signature = dsig_ctx.signBinary(msg, xmlsec.TransformRsaSha1) - return b64encode(signature) -
    -
    [docs] def build_response_signature(self, saml_response, relay_state): - """ - Builds the Signature of the SAML Response. - :param saml_request: The SAML Response - :type saml_request: string - - :param relay_state: The target URL the user should be redirected to - :type relay_state: string - """ - if not self.__settings.check_sp_certs(): - raise OneLogin_Saml2_Error( - "Trying to sign the SAML Response but can't load the SP certs", - OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND - ) - - xmlsec.initialize() - - # Load the key into the xmlsec context - key = self.__settings.get_sp_key() - file_key = OneLogin_Saml2_Utils.write_temp_file(key) # FIXME avoid writing a file - - dsig_ctx = xmlsec.DSigCtx() - dsig_ctx.signKey = xmlsec.Key.load(file_key.name, xmlsec.KeyDataFormatPem, None) - file_key.close() - - data = { - 'SAMLResponse': quote(saml_response), - 'RelayState': quote(relay_state), - 'SignAlg': quote(OneLogin_Saml2_Constants.RSA_SHA1), - } - msg = urlencode(data) - import pdb; dbp.set_trace() - print msg - data2 = { - 'SAMLResponse': saml_response, - 'RelayState': relay_state, - 'SignAlg': OneLogin_Saml2_Constants.RSA_SHA1, - } - msg2 = urlencode(data2) - print msg2 - signature = dsig_ctx.signBinary(msg, xmlsec.TransformRsaSha1) - return b64encode(signature)
    -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/_modules/saml2/authn_request.html b/docs/saml2/_modules/saml2/authn_request.html deleted file mode 100644 index 207ee7a6..00000000 --- a/docs/saml2/_modules/saml2/authn_request.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - saml2.authn_request — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Source code for saml2.authn_request

    -# -*- coding: utf-8 -*-
    -
    -# Copyright (c) 2010-2018 OneLogin, Inc.
    -# MIT License
    -
    -from base64 import b64encode
    -from datetime import datetime
    -from zlib import compress
    -
    -from saml2.utils import OneLogin_Saml2_Utils
    -from saml2.constants import OneLogin_Saml2_Constants
    -
    -
    -
    [docs]class OneLogin_Saml2_Authn_Request: - - def __init__(self, settings): - """ - Constructs the AuthnRequest object. - - Arguments are: - * (OneLogin_Saml2_Settings) settings. Setting data - """ - self.__settings = settings - - sp_data = self.__settings.get_sp_data() - security = self.__settings.get_security_data() - - uid = OneLogin_Saml2_Utils.generate_unique_id() - issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML( - int(datetime.now().strftime("%s")) - ) - - name_id_policy_format = sp_data['NameIDFormat'] - if 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted']: - name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED - - provider_name_str = '' - organization_data = settings.get_organization() - if isinstance(organization_data, dict): - langs = organization_data.keys() - if 'en-US' in langs: - lang = 'en-US' - else: - lang = langs[0] - if 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] is not None: - provider_name_str = 'ProviderName="%s"' % organization_data[lang]['displayname'] - - request = """<samlp:AuthnRequest - 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 - IssueInstant="%(issue_instant)s" - ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" - AssertionConsumerServiceURL="%(assertion_url)s"> - <saml:Issuer>%(entity_id)s</saml:Issuer> - <samlp:NameIDPolicy - Format="%(name_id_policy)s" - AllowCreate="true" /> - <samlp:RequestedAuthnContext Comparison="exact"> - <saml:AuthnContextMethodRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextMethodRef> - </samlp:RequestedAuthnContext> -</samlp:AuthnRequest>""" % { - 'id': uid, - 'provider_name': provider_name_str, - 'issue_instant': issue_instant, - 'assertion_url': sp_data['assertionConsumerService']['url'], - 'entity_id': sp_data['entityId'], - 'name_id_policy': name_id_policy_format, - } - - self.__authn_request = request - -
    [docs] def get_request(self): - """ - Returns unsigned AuthnRequest. - :return: Unsigned AuthnRequest - :rtype: str object - """ - deflated_request = compress(self.__authn_request)[2:-4] - return b64encode(deflated_request)
    -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/_modules/saml2/constants.html b/docs/saml2/_modules/saml2/constants.html deleted file mode 100644 index 306ec1b3..00000000 --- a/docs/saml2/_modules/saml2/constants.html +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - - - - saml2.constants — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Source code for saml2.constants

    -# -*- coding: utf-8 -*-
    -
    -# Copyright (c) 2010-2018 OneLogin, Inc.
    -# MIT License
    -
    -
    -
    [docs]class OneLogin_Saml2_Constants: - # Value added to the current time in time condition validations - ALOWED_CLOCK_DRIFT = 180 - - # NameID Formats - NAMEID_EMAIL_ADDRESS = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' - NAMEID_X509_SUBJECT_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName' - NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName' - NAMEID_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos' - NAMEID_ENTITY = 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity' - NAMEID_TRANSIENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' - NAMEID_PERSISTENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' - NAMEID_ENCRYPTED = 'urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted' - - # Attribute Name Formats - ATTRNAME_FORMAT_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified' - ATTRNAME_FORMAT_URI = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri' - ATTRNAME_FORMAT_BASIC = 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic' - - # Namespaces - NS_SAML = 'urn:oasis:names:tc:SAML:2.0:assertion' - NS_SAMLP = 'urn:oasis:names:tc:SAML:2.0:protocol' - NS_SOAP = 'http://schemas.xmlsoap.org/soap/envelope/' - NS_MD = 'urn:oasis:names:tc:SAML:2.0:metadata' - NS_XS = 'http://www.w3.org/2001/XMLSchema' - NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance' - NS_XENC = 'http://www.w3.org/2001/04/xmlenc#' - NS_DS = 'http://www.w3.org/2000/09/xmldsig#' - - # Bindings - BINDING_HTTP_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' - BINDING_HTTP_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' - BINDING_HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' - BINDING_SOAP = 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP' - BINDING_DEFLATE = 'urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE' - - # Auth Context Method - AC_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified' - AC_PASSWORD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password' - AC_X509 = 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509' - AC_SMARTCARD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard' - AC_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos' - - # Subject Confirmation - CM_BEARER = 'urn:oasis:names:tc:SAML:2.0:cm:bearer' - CM_HOLDER_KEY = 'urn:oasis:names:tc:SAML:2.0:cm:holder-of-key' - CM_SENDER_VOUCHES = 'urn:oasis:names:tc:SAML:2.0:cm:sender-vouches' - - # Status Codes - STATUS_SUCCESS = 'urn:oasis:names:tc:SAML:2.0:status:Success' - STATUS_REQUESTER = 'urn:oasis:names:tc:SAML:2.0:status:Requester' - STATUS_RESPONDER = 'urn:oasis:names:tc:SAML:2.0:status:Responder' - STATUS_VERSION_MISMATCH = 'urn:oasis:names:tc:SAML:2.0:status:VersionMismatch' - STATUS_NO_PASSIVE = 'urn:oasis:names:tc:SAML:2.0:status:NoPassive' - STATUS_PARTIAL_LOGOUT = 'urn:oasis:names:tc:SAML:2.0:status:PartialLogout' - STATUS_PROXY_COUNT_EXCEEDED = 'urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded' - - # Crypto - RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' - - NSMAP = { - 'samlp': NS_SAMLP, - 'saml': NS_SAML, - 'ds': NS_DS, - 'xenc': NS_XENC - }
    -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/_modules/saml2/errors.html b/docs/saml2/_modules/saml2/errors.html deleted file mode 100644 index ef958f31..00000000 --- a/docs/saml2/_modules/saml2/errors.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - saml2.errors — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Source code for saml2.errors

    -# -*- coding: utf-8 -*-
    -
    -# Copyright (c) 2010-2018 OneLogin, Inc.
    -# MIT License
    -
    -
    -
    [docs]class OneLogin_Saml2_Error(Exception): - - # Errors - SETTINGS_FILE_NOT_FOUND = 0 - SETTINGS_INVALID_SYNTAX = 1 - SETTINGS_INVALID = 2 - METADATA_SP_INVALID = 3 - SP_CERTS_NOT_FOUND = 4 - REDIRECT_INVALID_URL = 5 - PUBLIC_CERT_FILE_NOT_FOUND = 6 - PRIVATE_KEY_FILE_NOT_FOUND = 7 - SAML_RESPONSE_NOT_FOUND = 8 - SAML_LOGOUTMESSAGE_NOT_FOUND = 9 - SAML_LOGOUTREQUEST_INVALID = 10 - SAML_LOGOUTRESPONSE_INVALID = 11 - SAML_SINGLE_LOGOUT_NOT_SUPPORTED = 12 - - 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). - """ - from saml2.utils import _ - - assert isinstance(message, basestring) - assert isinstance(code, int) - - if errors is not None: - message = message % errors - - Exception.__init__(self, _(message)) - self.code = code
    -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/_modules/saml2/logout_request.html b/docs/saml2/_modules/saml2/logout_request.html deleted file mode 100644 index 1b690dd4..00000000 --- a/docs/saml2/_modules/saml2/logout_request.html +++ /dev/null @@ -1,372 +0,0 @@ - - - - - - - - - - saml2.logout_request — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Source code for saml2.logout_request

    -# -*- coding: utf-8 -*-
    -
    -# Copyright (c) 2010-2018 OneLogin, Inc.
    -# MIT License
    -
    -from base64 import b64decode
    -from datetime import datetime
    -from lxml import etree
    -from os.path import basename
    -from urllib import urlencode
    -from urlparse import parse_qs
    -from xml.dom.minidom import Document, parseString
    -
    -import dm.xmlsec.binding as xmlsec
    -
    -from saml2.constants import OneLogin_Saml2_Constants
    -from saml2.utils import OneLogin_Saml2_Utils
    -
    -
    -
    [docs]class OneLogin_Saml2_Logout_Request: - - def __init__(self, settings,request=None,name_id=None, session_index=None): - """ - Constructs the Logout Request object. - - Arguments are: - * (OneLogin_Saml2_Settings) settings. Setting data - """ - self.__settings = settings - - sp_data = self.__settings.get_sp_data() - idp_data = self.__settings.get_idp_data() - security = self.__settings.get_security_data() - - uid = OneLogin_Saml2_Utils.generate_unique_id() - name_id_value = OneLogin_Saml2_Utils.generate_unique_id() - issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(int(datetime.now().strftime("%s"))) - - key = None - if 'nameIdEncrypted' in security and security['nameIdEncrypted']: - key = idp_data['x509cert'] - - name_id = OneLogin_Saml2_Utils.generate_name_id( - name_id_value, - sp_data['entityId'], - sp_data['NameIDFormat'], - key - ) - - logout_request = """<samlp:LogoutRequest - 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"> - <saml:Issuer>%(entity_id)s</saml:Issuer> - %(name_id)s -</samlp:LogoutRequest>""" % { - 'id': uid, - 'issue_instant': issue_instant, - 'single_logout_url': idp_data['singleLogoutService']['url'], - 'entity_id': sp_data['entityId'], - 'name_id': name_id, - } - - self.__logout_request = logout_request - -
    [docs] def get_request(self): - """ - Returns the Logout Request defated, base64encoded - :return: Deflated base64 encoded Logout Request - :rtype: str object - """ - return OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_request) -
    - @staticmethod -
    [docs] def get_id(request): - """ - Returns the ID of the Logout Request - :param request: Logout Request Message - :type request: string|DOMDocument - :return: string ID - :rtype: str object - """ - if isinstance(request, Document): - dom = request - else: - dom = parseString(request) - return dom.documentElement.getAttribute('ID') -
    - @staticmethod -
    [docs] def get_name_id_data(request, key=None): - """ - Gets the NameID Data of the the Logout Request - :param request: Logout Request Message - :type request: string|DOMDocument - :param key: The SP key - :type key: string - :return: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) - :rtype: dict - """ - if isinstance(request, Document): - request = request.toxml() - dom = etree.fromstring(request) - name_id = None - - encrypted_entries = OneLogin_Saml2_Utils.query(dom, '/samlp:LogoutRequest/saml:EncryptedID') - - if len(encrypted_entries) == 1: - if key is None: - raise Exception('Key is required in order to decrypt the NameID') - - elem = parseString(etree.tostring(encrypted_entries[0])) - encrypted_data_nodes = elem.documentElement.getElementsByTagName('xenc:EncryptedData') - encrypted_data = encrypted_data_nodes[0] - - xmlsec.initialize() - - # Load the key into the xmlsec context - file_key = OneLogin_Saml2_Utils.write_temp_file(key) # FIXME avoid writing a file - enc_key = xmlsec.Key.load(file_key.name, xmlsec.KeyDataFormatPem, None) - enc_key.name = basename(file_key.name) - file_key.close() - enc_ctx = xmlsec.EncCtx() - enc_ctx.encKey = enc_key - - name_id = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, enc_ctx) - else: - entries = OneLogin_Saml2_Utils.query(dom, '/samlp:LogoutRequest/saml:NameID') - if len(entries) == 1: - name_id = entries[0] - - if name_id is None: - raise Exception('Not NameID found in the Logout Request') - - name_id_data = { - 'Value': name_id.text - } - for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: - if attr in name_id.attrib.keys(): - name_id_data[attr] = name_id.attrib[attr] - - return name_id_data -
    - @staticmethod -
    [docs] def get_name_id(request, key=None): - """ - Gets the NameID of the Logout Request Message - :param request: Logout Request Message - :type request: string|DOMDocument - :param key: The SP key - :type key: string - :return: Name ID Value - :rtype: string - """ - name_id = OneLogin_Saml2_Logout_Request.get_name_id_data(request, key) - return name_id['Value'] -
    - @staticmethod -
    [docs] def get_issuer(request): - """ - Gets the Issuer of the Logout Request Message - :param request: Logout Request Message - :type request: string|DOMDocument - :return: The Issuer - :rtype: string - """ - if isinstance(request, Document): - request = request.toxml() - dom = etree.fromstring(request) - - issuer = None - issuer_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:LogoutRequest/saml:Issuer') - if len(issuer_nodes) == 1: - issuer = issuer_nodes[0].text - return issuer -
    - @staticmethod -
    [docs] def get_session_indexes(request): - """ - Gets the SessionIndexes from the Logout Request - :param request: Logout Request Message - :type request: string|DOMDocument - :return: The SessionIndex value - :rtype: list - """ - if isinstance(request, Document): - request = request.toxml() - dom = etree.fromstring(request) - - session_indexes = [] - session_index_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:LogoutRequest/samlp:SessionIndex') - for session_index_node in session_index_nodes: - session_indexes.append(session_index_node.text) - return session_indexes -
    - @staticmethod -
    [docs] def is_valid(settings, request, get_data, debug=False): - """ - Checks if the Logout Request recieved is valid - :param settings: Settings - :type settings: OneLogin_Saml2_Settings - :param request: Logout Request Message - :type request: string|DOMDocument - :return: If the Logout Request is or not valid - :rtype: boolean - """ - try: - if isinstance(request, Document): - dom = request - else: - dom = parseString(request) - - idp_data = settings.get_idp_data() - idp_entity_id = idp_data['entityId'] - - if settings.is_strict(): - res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', debug) - if not isinstance(res, Document): - raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') - - security = settings.get_security_data() - - current_url = OneLogin_Saml2_Utils.get_self_url_no_query(get_data) - - # Check NotOnOrAfter - if dom.documentElement.hasAttribute('NotOnOrAfter'): - na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.documentElement.getAttribute('NotOnOrAfter')) - if na <= datetime.now(): - raise Exception('Timing issues (please check your clock settings)') - - # Check destination - if dom.documentElement.hasAttribute('Destination'): - destination = dom.documentElement.getAttribute('Destination') - if destination is not None: - if current_url not in destination: - raise Exception('The LogoutRequest was received at $currentURL instead of $destination') - - # Check issuer - issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom) - if issuer is None or issuer != idp_entity_id: - raise Exception('Invalid issuer in the Logout Request') - - 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') - - if 'Signature' in get_data: - if 'SigAlg' not in get_data: - sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 - else: - sign_alg = get_data['SigAlg'] - - if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: - raise Exception('Invalid signAlg in the recieved Logout Request') - - signed_query = 'SAMLRequest=%s' % urlencode(get_data['SAMLRequest']) - if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, urlencode(get_data['RelayState'])) - signed_query = '%s&SigAlg=%s' % (signed_query, urlencode(sign_alg)) - - if 'x509cert' not in idp_data or idp_data['x509cert'] is None: - raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') - cert = idp_data['x509cert'] - - xmlsec.initialize() - objkey = xmlsec.Key.load(cert, xmlsec.KeyDataFormatPem, None) # FIXME is this right? - - if not objkey.verifySignature(signed_query, b64decode(get_data['Signature'])): - raise Exception('Signature validation failed. Logout Request rejected') - - return True - except Exception as e: - debug = settings.is_debug_active() - if debug: - print(e.strerror) - return False
    -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/_modules/saml2/logout_response.html b/docs/saml2/_modules/saml2/logout_response.html deleted file mode 100644 index 3ea3bbf7..00000000 --- a/docs/saml2/_modules/saml2/logout_response.html +++ /dev/null @@ -1,282 +0,0 @@ - - - - - - - - - - saml2.logout_response — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Source code for saml2.logout_response

    -# -*- coding: utf-8 -*-
    -
    -# Copyright (c) 2010-2018 OneLogin, Inc.
    -# MIT License
    -
    -from base64 import b64decode
    -from datetime import datetime
    -from lxml import etree
    -from urllib import quote_plus
    -from xml.dom.minidom import Document, parseString
    -
    -import dm.xmlsec.binding as xmlsec
    -
    -from saml2.constants import OneLogin_Saml2_Constants
    -from saml2.utils import OneLogin_Saml2_Utils
    -
    -
    -
    [docs]class OneLogin_Saml2_Logout_Response(): - - def __init__(self, settings, response=None): - """ - Constructs a Logout Response object (Initialize params from settings - and if provided load the Logout Response. - - Arguments are: - * (OneLogin_Saml2_Settings) settings. Setting data - * (string) response. An UUEncoded SAML Logout - response from the IdP. - """ - self.__settings = settings - if response is not None: - self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response) - self.document = parseString(self.__logout_response) - -
    [docs] def get_issuer(self): - """ - Gets the Issuer of the Logout Response Message - :return: The Issuer - :rtype: string - """ - issuer = None - issuer_nodes = self.__query('/samlp:LogoutResponse/saml:Issuer') - if len(issuer_nodes) == 1: - issuer = issuer_nodes[0].text - return issuer -
    -
    [docs] def get_status(self): - """ - Gets the Status - :return: The Status - :rtype: string - """ - entries = self.__query('/samlp:LogoutResponse/samlp:Status/samlp:StatusCode') - if len(entries) == 0: - return None - status = entries[0].attrib['Value'] - return status -
    -
    [docs] def is_valid(self, request_data, request_id=None): - """ - 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 - :return: Returns if the SAML LogoutResponse is or not valid - :rtype: boolean - """ - try: - idp_data = self.__settings.get_idp_data() - idp_entity_id = idp_data['entityId'] - get_data = request_data['get_data'] - - if self.__settings.is_strict(): - res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) - if not isinstance(res, Document): - raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') - - security = self.__settings.get_security_data() - - # Check if the InResponseTo of the Logout Response matchs the ID of the Logout Request (requestId) if provided - if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'): - in_response_to = self.document.documentElement.getAttribute('InResponseTo') - if request_id != in_response_to: - 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)) - - # Check issuer - issuer = self.get_issuer() - if issuer is None or issuer != idp_entity_id: - raise Exception('Invalid issuer in the Logout Request') - - current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) - - # Check destination - if self.document.documentElement.hasAttribute('Destination'): - destination = self.document.documentElement.getAttribute('Destination') - if destination is not None: - if current_url not in destination: - raise Exception('The LogoutRequest was received at $currentURL instead of $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') - - if 'Signature' in get_data: - if 'SigAlg' not in get_data: - sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 - else: - sign_alg = get_data['SigAlg'] - - if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: - raise Exception('Invalid signAlg in the recieved Logout Response') - - signed_query = 'SAMLResponse=%s' % quote_plus(get_data['SAMLResponse']) - if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, quote_plus(get_data['RelayState'])) - signed_query = '%s&SigAlg=%s' % (signed_query, quote_plus(sign_alg)) - - if 'x509cert' not in idp_data or idp_data['x509cert'] is None: - raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') - cert = idp_data['x509cert'] - - xmlsec.initialize() - objkey = xmlsec.Key.load(cert, xmlsec.KeyDataFormatPem, None) # FIXME is this right? - - if not objkey.verifySignature(signed_query, b64decode(get_data['Signature'])): - raise Exception('Signature validation failed. Logout Response rejected') - - return True - except Exception as e: - debug = self.__settings.is_debug_active() - if debug: - print(e.strerror) - return False -
    - def __query(self, query): - """ - Extracts a node from the DOMDocument (Logout Response Menssage) - :param query: Xpath Expresion - :type query: string - :return: The queried node - :rtype: DOMNodeList - """ - # Switch to lxml for querying - xml = self.document.toxml() - return OneLogin_Saml2_Utils.query(etree.fromstring(xml), query) - -
    [docs] def build(self, in_response_to): - """ - Creates a Logout Response object. - :param in_response_to: InResponseTo value for the Logout Response. - :type in_response_to: string - """ - sp_data = self.__settings.get_sp_data() - idp_data = self.__settings.get_idp_data() - - uid = OneLogin_Saml2_Utils.generate_unique_id() - issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML( - int(datetime.now().strftime("%s")) - ) - - logout_response = """<samlp:LogoutResponse 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" -> - <saml:Issuer>%(entity_id)s</saml:Issuer> - <samlp:Status> - <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /> - </samlp:Status> -</samlp:LogoutResponse>""" % { - 'id': uid, - 'issue_instant': issue_instant, - 'destination': idp_data['singleLogoutService']['url'], - 'in_response_to': in_response_to, - 'entity_id': sp_data['entityId'], - } - - self.__logout_response = logout_response -
    -
    [docs] def get_response(self): - """ - Returns a Logout Response object. - :return: Logout Response deflated and base64 encoded - :rtype: string - """ - return OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_response)
    -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/_modules/saml2/metadata.html b/docs/saml2/_modules/saml2/metadata.html deleted file mode 100644 index 7b0a1be5..00000000 --- a/docs/saml2/_modules/saml2/metadata.html +++ /dev/null @@ -1,289 +0,0 @@ - - - - - - - - - - saml2.metadata — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Source code for saml2.metadata

    -# -*- coding: utf-8 -*-
    -
    -# Copyright (c) 2010-2018 OneLogin, Inc.
    -# MIT License
    -
    -from time import gmtime, strftime
    -from datetime import datetime
    -from xml.dom.minidom import parseString
    -
    -from saml2.constants import OneLogin_Saml2_Constants
    -from saml2.utils import OneLogin_Saml2_Utils
    -
    -
    -
    [docs]class OneLogin_Saml2_Metadata: - - TIME_VALID = 172800 # 2 days - TIME_CACHED = 604800 # 1 week - - @staticmethod -
    [docs] def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None): - """ - Build the metadata of the SP - - :param sp: The SP data - :type sp: string - - :param authnsign: authnRequestsSigned attribute - :type authnsign: string - - :param wsign: wantAssertionsSigned attribute - :type wsign: string - - :param valid_until: Metadata's valid time - :type valid_until: DateTime - - :param cache_duration: Duration of the cache in seconds - :type cache_duration: Timestamp - - :param contacts: Contacts info - :type contacts: dict - - :param organization: Organization ingo - :type organization: dict - """ - if valid_until is None: - valid_until = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_VALID - valid_until_time = gmtime(valid_until) - valid_until_time = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time) - if cache_duration is None: - cache_duration = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_CACHED - if contacts is None: - contacts = {} - if organization is None: - organization = {} - - sls = '' - if 'singleLogoutService' in sp: - sls = """<md:SingleLogoutService Binding="%(binding)s" - Location="%(location)s" />""" % { - 'binding': sp['singleLogoutService']['binding'], - 'location': sp['singleLogoutService']['url'], - } - - str_authnsign = 'true' if authnsign else 'false' - str_wsign = 'true' if wsign else 'false' - - str_organization = '' - if len(organization) > 0: - organization_info = [] - for (lang, info) in organization.items(): - organization_info.append(""" <md:Organization> - <md:OrganizationName xml:lang="%(lang)s">%(name)s</md:OrganizationName> - <md:OrganizationDisplayName xml:lang="%(lang)s">%(display_name)s</md:OrganizationDisplayName> - <md:OrganizationURL xml:lang="%(lang)s">%(url)s</md:OrganizationURL> - </md:Organization>""" % { - 'lang': lang, - 'name': info['name'], - 'display_name': info['displayname'], - 'url': info['url'], - }) - str_organization = '\n'.join(organization_info) - - str_contacts = '' - if len(contacts) > 0: - contacts_info = [] - for (ctype, info) in contacts.items(): - contacts_info.append(""" <md:ContactPerson contactType="%(type)s"> - <md:GivenName>%(name)s</md:GivenName> - <md:EmailAddress>%(email)s</md:EmailAddress> - </md:ContactPerson>""" % { - 'type': ctype, - 'name': info['givenName'], - 'email': info['emailAddress'], - }) - str_contacts = '\n'.join(contacts_info) - - metadata = """<?xml version="1.0"?> -<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" - validUntil="%(valid)s" - cacheDuration="PT%(cache)sS" - entityID="%(entity_id)s"> - <md:SPSSODescriptor AuthnRequestsSigned="%(authnsign)s" WantAssertionsSigned="%(wsign)s" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> - <md:NameIDFormat>%(name_id_format)s</md:NameIDFormat> - <md:AssertionConsumerService Binding="%(binding)s" - Location="%(location)s" - index="1" /> -%(sls)s - </md:SPSSODescriptor> -%(organization)s -%(contacts)s -</md:EntityDescriptor>""" % { - 'valid': valid_until_time, - 'cache': cache_duration, - 'entity_id': sp['entityId'], - 'authnsign': str_authnsign, - 'wsign': str_wsign, - 'name_id_format': sp['NameIDFormat'], - 'binding': sp['assertionConsumerService']['binding'], - 'location': sp['assertionConsumerService']['url'], - 'sls': sls, - 'organization': str_organization, - 'contacts': str_contacts, - } - - return metadata -
    - @staticmethod -
    [docs] def sign_metadata(metadata, key, cert): - """ - Sign the metadata with the key/cert provided - - :param metadata: SAML Metadata XML - :type metadata: string - - :param key: x509 key - :type key: string - - :param cert: x509 cert - :type cert: string - - :returns: Signed Metadata - :rtype: string - """ - return OneLogin_Saml2_Utils.add_sign(metadata, key, cert) -
    - @staticmethod -
    [docs] def add_x509_key_descriptors(metadata, cert): - """ - Add the x509 descriptors (sign/encriptation to the metadata - The same cert will be used for sign/encrypt - - :param metadata: SAML Metadata XML - :type metadata: string - - :param cert: x509 cert - :type cert: string - - :returns: Metadata with KeyDescriptors - :rtype: string - """ - try: - xml = parseString(metadata) - except Exception as e: - raise Exception('Error parsing metadata. ' + e.message) - - formated_cert = OneLogin_Saml2_Utils.format_cert(cert, False) - x509_certificate = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Certificate') - content = xml.createTextNode(formated_cert) - x509_certificate.appendChild(content) - - key_data = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Data') - key_data.appendChild(x509_certificate) - - key_info = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:KeyInfo') - key_info.appendChild(key_data) - - key_descriptor = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'md:KeyDescriptor') - - entity_descriptor = sp_sso_descriptor = xml.getElementsByTagName('md:EntityDescriptor')[0] - entity_descriptor.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) - - sp_sso_descriptor = xml.getElementsByTagName('md:SPSSODescriptor')[0] - sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) - sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) - - signing = xml.getElementsByTagName('md:KeyDescriptor')[0] - signing.setAttribute('use', 'signing') - - encryption = xml.getElementsByTagName('md:KeyDescriptor')[1] - encryption.setAttribute('use', 'encryption') - - signing.appendChild(key_info) - encryption.appendChild(key_info.cloneNode(True)) - - return xml.toxml()
    -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/_modules/saml2/response.html b/docs/saml2/_modules/saml2/response.html deleted file mode 100644 index d3fee4e3..00000000 --- a/docs/saml2/_modules/saml2/response.html +++ /dev/null @@ -1,532 +0,0 @@ - - - - - - - - - - saml2.response — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Source code for saml2.response

    -# -*- coding: utf-8 -*-
    -
    -# Copyright (c) 2010-2018 OneLogin, Inc.
    -# MIT License
    -
    -from base64 import b64decode
    -from copy import deepcopy
    -from lxml import etree
    -from os.path import basename
    -from time import time
    -import sys
    -from xml.dom.minidom import Document
    -
    -import dm.xmlsec.binding as xmlsec
    -
    -from saml2.constants import OneLogin_Saml2_Constants
    -from saml2.utils import OneLogin_Saml2_Utils
    -
    -
    -
    [docs]class OneLogin_Saml2_Response(object): - - def __init__(self, settings, response): - """ - Constructs the response object. - - :param settings: The setting info - :type settings: OneLogin_Saml2_Setting object - - :param response: The base64 encoded, XML string containing the samlp:Response - :type response: string - """ - self.__settings = settings - self.response = b64decode(response) - self.document = etree.fromstring(self.response) - self.decrypted_document = None - self.encrypted = None - - # Quick check for the presence of EncryptedAssertion - encrypted_assertion_nodes = self.__query('//saml:EncryptedAssertion') - if encrypted_assertion_nodes: - decrypted_document = deepcopy(self.document) - self.encrypted = True - self.decrypted_document = self.__decrypt_assertion(decrypted_document) - -
    [docs] def is_valid(self, request_data, request_id=None): - """ - Constructs the response object. - - :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP - :type request_id: string - - :returns: True if the SAML Response is valid, False if not - :rtype: bool - """ - try: - # Checks SAML version - if self.document.get('Version', None) != '2.0': - raise Exception('Unsupported SAML version') - - # Checks that ID exists - if self.document.get('ID', None) is None: - raise Exception('Missing ID attribute on SAML Response') - - # Checks that the response only has one assertion - if not self.validate_num_assertions(): - raise Exception('Multiple assertions are not supported') - - # Checks that the response has the SUCCESS status - self.check_status() - - idp_data = self.__settings.get_idp_data() - idp_entityid = idp_data.get('entityId', '') - sp_data = self.__settings.get_sp_data() - sp_entityid = sp_data.get('entityId', '') - - sign_nodes = self.__query('//ds:Signature') - - signed_elements = [] - for sign_node in sign_nodes: - signed_elements.append(sign_node.getparent().tag) - - if self.__settings.is_strict(): - res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) - if not isinstance(res, Document): - raise Exception('Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd') - - security = self.__settings.get_security_data() - current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) - - # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided - in_response_to = self.document.get('InResponseTo', None) - if in_response_to and request_id: - 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)) - - if not self.encrypted and security.get('wantAssertionsEncrypted', False): - raise Exception('The assertion of the Response is not encrypted and the SP require it') - - if security.get('wantNameIdEncrypted', False): - encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') - if not encrypted_nameid_nodes: - raise Exception('The NameID of the Response is not encrypted and the SP require it') - - # Checks that there is at least one AttributeStatement - attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement') - if not attribute_statement_nodes: - raise Exception('There is no AttributeStatement on the Response') - - # Validates Asserion timestamps - if not self.validate_timestamps(): - raise Exception('Timing issues (please check your clock settings)') - - 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') - - # Checks destination - destination = self.document.get('Destination', None) - if destination: - if destination not in current_url: - raise Exception('The response was received at %s instead of %s' % (current_url, destination)) - - # Checks audience - valid_audiences = self.get_audiences() - if valid_audiences and sp_entityid not in valid_audiences: - raise Exception('%s is not a valid audience for this Response' % sp_entityid) - - # Checks the issuers - issuers = self.get_issuers() - for issuer in issuers: - if not issuer or issuer != idp_entityid: - raise Exception('Invalid issuer in the Assertion/Response') - - # Checks the session Expiration - session_expiration = self.get_session_not_on_or_after() - if not session_expiration and session_expiration <= time(): - raise Exception('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response') - - # Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid - any_subject_confirmation = False - subject_confirmation_nodes = self.__query_assertion('/saml:Subject/saml:SubjectConfirmation') - - for scn in subject_confirmation_nodes: - method = scn.get('Method', None) - if method and method != OneLogin_Saml2_Constants.CM_BEARER: - continue - scData = scn.find('saml:SubjectConfirmationData', namespaces=OneLogin_Saml2_Constants.NSMAP) - if scData is None: - continue - else: - irt = scData.get('InResponseTo', None) - if irt != in_response_to: - continue - recipient = scData.get('Recipient', None) - if recipient not in current_url: - continue - nooa = scData.get('NotOnOrAfter', None) - if nooa: - parsed_nooa = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa) - if parsed_nooa <= time(): - continue - nb = scData.get('NotBefore', None) - if nb: - parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time(nb) - if (parsed_nb > time()): - continue - any_subject_confirmation = True - break - - if not any_subject_confirmation: - raise Exception('A valid SubjectConfirmation was not found on this Response') - - if security.get('wantAssertionsSigned', False) and 'saml:Assertion' not in signed_elements: - raise Exception('The Assertion of the Response is not signed and the SP require it') - - if security.get('wantMessagesSigned', False) and 'samlp:Response' not in signed_elements: - raise Exception('The Message of the Response is not signed and the SP require it') - - document_to_validate = None - if len(signed_elements) > 0: - cert = idp_data.get('x509cert', None) - fingerprint = idp_data.get('certFingerprint', None) - - # Only validates the first sign found - if 'samlp:Response' in signed_elements: - document_to_validate = self.document - else: - if self.encrypted: - document_to_validate = self.decrypted_document - else: - document_to_validate = self.document - - if document_to_validate is not None: - if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint): - raise Exception('Signature validation failed. SAML Response rejected') - return True - except: - debug = self.__settings.is_debug_active() - if debug: - print sys.exc_info()[0] - return False -
    -
    [docs] def check_status(self): - """ - Check if the status of the response is success or not - - :raises: Exception. If the status is not success - """ - status = OneLogin_Saml2_Utils.get_status(self.document) - code = status.get('code', None) - if code and code != OneLogin_Saml2_Constants.STATUS_SUCCESS: - splited_code = code.split(':') - printable_code = splited_code.pop() - status_exception_msg = 'The status code of the Response was not Success, was %s' % printable_code - status_msg = status.get('msg', None) - if status_msg: - status_exception_msg += ' -> ' + status_msg - raise Exception(status_exception_msg) -
    -
    [docs] def get_audiences(self): - """ - Gets the audiences - - :returns: The valid audiences for the SAML Response - :rtype: list - """ - audiences = [] - - audience_nodes = self.__query_assertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience') - for audience_node in audience_nodes: - audiences.append(audience_node.text) -
    -
    [docs] def get_issuers(self): - """ - Gets the issuers (from message and from assertion) - - :returns: The issuers - :rtype: list - """ - issuers = [] - - message_issuer_nodes = self.__query('/samlp:Response/saml:Issuer') - if message_issuer_nodes: - issuers.append(message_issuer_nodes[0].text) - - assertion_issuer_nodes = self.__query_assertion('/saml:Issuer') - if assertion_issuer_nodes: - issuers.append(assertion_issuer_nodes[0].text) - - return list(set(issuers)) -
    -
    [docs] def get_nameid_data(self): - """ - Gets the NameID Data provided by the SAML Response from the IdP - - :returns: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) - :rtype: dict - """ - nameid = None - encrypted_id_data_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') - if encrypted_id_data_nodes: - encrypted_data = encrypted_id_data_nodes[0] - - xmlsec.initialize() - - # Load the key into the xmlsec context - key = self.__settings.get_sp_key() - file_key = OneLogin_Saml2_Utils.write_temp_file(key) # FIXME avoid writing a file - enc_key = xmlsec.Key.load(file_key.name, xmlsec.KeyDataFormatPem, None) - enc_key.name = basename(file_key.name) - file_key.close() - enc_ctx = xmlsec.EncCtx() - enc_ctx.encKey = enc_key - - nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, enc_ctx) - else: - nameid_nodes = self.__query_assertion('/saml:Subject/saml:NameID') - if nameid_nodes: - nameid = nameid_nodes[0] - if nameid is None: - raise Exception('Not NameID found in the assertion of the Response') - - nameid_data = {'Value': nameid.text} - for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: - value = nameid.get(attr, None) - if value: - nameid_data[attr] = value - return nameid_data -
    -
    [docs] def get_nameid(self): - """ - Gets the NameID provided by the SAML Response from the IdP - - :returns: NameID (value) - :rtype: string - """ - nameid_data = self.get_nameid_data() - return nameid_data['Value'] -
    -
    [docs] def get_session_not_on_or_after(self): - """ - Gets the SessionNotOnOrAfter from the AuthnStatement - Could be used to set the local session expiration - - :returns: The SessionNotOnOrAfter value - :rtype: time|None - """ - not_on_or_after = None - authn_statement_nodes = self.__query_assertion('/saml:AuthnStatement[@SessionNotOnOrAfter]') - if authn_statement_nodes: - not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(authn_statement_nodes[0].get('SessionNotOnOrAfter')) - return not_on_or_after -
    -
    [docs] def get_session_index(self): - """ - Gets the SessionIndex from the AuthnStatement - Could be used to be stored in the local session in order - to be used in a future Logout Request that the SP could - send to the SP, to set what specific session must be deleted - - :returns: The SessionIndex value - :rtype: string|None - """ - session_index = None - authn_statement_nodes = self.__query_assertion('/saml:AuthnStatement[@SessionIndex]') - if authn_statement_nodes: - session_index = authn_statement_nodes[0].get('SessionIndex') - return session_index -
    -
    [docs] def get_attributes(self): - """ - Gets the Attributes from the AttributeStatement element. - EncryptedAttributes are not supported - """ - attributes = {} - attribute_nodes = self.__query_assertion('/saml:AttributeStatement/saml:Attribute') - for attribute_node in attribute_nodes: - attr_name = attribute_node.get('Name') - values = [] - for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP['saml']): - values.append(attr.text) - attributes[attr_name] = values - return attributes -
    -
    [docs] def validate_num_assertions(self): - """ - Verifies that the document only contains a single Assertion (encrypted or not) - - :returns: True if only 1 assertion encrypted or not - :rtype: bool - """ - encrypted_assertion_nodes = self.__query('//saml:EncryptedAssertion') - assertion_nodes = self.__query('//saml:Assertion') - return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1 -
    -
    [docs] def validate_timestamps(self): - """ - Verifies that the document is valid according to Conditions Element - - :returns: True if the condition is valid, False otherwise - :rtype: bool - """ - conditions_nodes = self.__query('//saml:Conditions') - for conditions_node in conditions_nodes: - 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) > time() + OneLogin_Saml2_Constants.ALOWED_CLOCK_DRIFT: - return False - if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALOWED_CLOCK_DRIFT <= time(): - return False - return True -
    - def __query_assertion(self, xpath_expr): - """ - Extracts nodes that match the query from the Assertion - - :param query: Xpath Expresion - :type query: String - - :returns: The queried nodes - :rtype: list - """ - if self.encrypted: - assertion_expr = '/saml:EncryptedAssertion/saml:Assertion' - else: - assertion_expr = '/saml:Assertion' - signature_expr = '/ds:Signature/ds:SignedInfo/ds:Reference' - signed_assertion_query = '/samlp:Response' + assertion_expr + signature_expr - assertion_reference_nodes = self.__query(signed_assertion_query) - - if not assertion_reference_nodes: - # Check if the message is signed - signed_message_query = '/samlp:Response' + signature_expr - message_reference_nodes = self.__query(signed_message_query) - if message_reference_nodes: - id = message_reference_nodes[0].get('URI') - final_query = "/samlp:Response[@ID='%s']/" % id[1:] - else: - final_query = "/samlp:Response/" - final_query += assertion_expr - else: - id = assertion_reference_nodes[0].get('URI') - final_query = '/samlp:Response' + assertion_expr + "[@ID='%s']" % id[1:] - final_query += xpath_expr - return self.__query(final_query) - - def __query(self, query): - """ - Extracts nodes that match the query from the Response - - :param query: Xpath Expresion - :type query: String - - :returns: The queried nodes - :rtype: list - """ - if self.encrypted: - document = self.decrypted_document - else: - document = self.document - return OneLogin_Saml2_Utils.query(document, query) - - def __decrypt_assertion(self, dom): - """ - Decrypts the Assertion - - :raises: Exception if no private key available - :param dom: Encrypted Assertion - :type dom: Element - :returns: Decrypted Assertion - :rtype: Element - """ - key = self.__settings.get_sp_key() - - if not key: - raise Exception('No private key available, check settings') - - # TODO Study how decrypt assertion
    -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/_modules/saml2/settings.html b/docs/saml2/_modules/saml2/settings.html deleted file mode 100644 index 5f8f5c2c..00000000 --- a/docs/saml2/_modules/saml2/settings.html +++ /dev/null @@ -1,695 +0,0 @@ - - - - - - - - - - saml2.settings — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Source code for saml2.settings

    -# -*- coding: utf-8 -*-
    -
    -# Copyright (c) 2010-2018 OneLogin, Inc.
    -# MIT License
    -
    -from datetime import datetime
    -import json
    -import re
    -from os.path import dirname, exists, join, sep
    -from xml.dom.minidom import Document
    -
    -from saml2.constants import OneLogin_Saml2_Constants
    -from saml2.errors import OneLogin_Saml2_Error
    -from saml2.metadata import OneLogin_Saml2_Metadata
    -from saml2.utils import OneLogin_Saml2_Utils
    -
    -
    -# Regex from Django Software Foundation and individual contributors.
    -# Released under a BSD 3-Clause License
    -url_regex = re.compile(
    -    r'^(?:[a-z0-9\.\-]*)://'  # scheme is validated separately
    -    r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # domain...
    -    r'localhost|'  # localhost...
    -    r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|'  # ...or ipv4
    -    r'\[?[A-F0-9]*:[A-F0-9:]+\]?)'  # ...or ipv6
    -    r'(?::\d+)?'  # optional port
    -    r'(?:/?|[/?]\S+)$', re.IGNORECASE)
    -url_schemes = ['http', 'https', 'ftp', 'ftps']
    -
    -
    -
    [docs]def validate_url(url): - scheme = url.split('://')[0].lower() - if scheme not in url_schemes: - return False - if not bool(url_regex.search(url)): - return False - return True - -
    -
    [docs]class OneLogin_Saml2_Settings: - - def __init__(self, settings=None, custom_base_path=None): - """ - Initializes the settings: - - Sets the paths of the different folders - - Loads settings info from settings file or array/object provided - - :param settings: SAML Toolkit Settings - :type settings: dict|object - """ - self.__paths = {} - self.__strict = False - self.__debug = False - self.__sp = {} - self.__idp = {} - self.__contacts = {} - self.__organization = {} - self.__errors = [] - - self.__load_paths(base_path=custom_base_path) - self.__update_paths(settings) - - if settings is None: - if not self.__load_settings_from_file(): - raise OneLogin_Saml2_Error( - 'Invalid file settings: %s', - OneLogin_Saml2_Error.SETTINGS_INVALID, - ','.join(self.__errors) - ) - self.__add_default_values() - elif isinstance(settings, dict): - if not self.__load_settings_from_dict(settings): - raise OneLogin_Saml2_Error( - 'Invalid dict settings: %s', - OneLogin_Saml2_Error.SETTINGS_INVALID, - ','.join(self.__errors) - ) - else: - raise Exception('Unsupported settings object') - - self.format_idp_cert() - - def __load_paths(self, base_path=None): - """ - Sets the paths of the different folders - """ - if base_path is None: - base_path = dirname(dirname(dirname(__file__))) - base_path += sep - self.__paths = { - 'base': base_path, - 'cert': base_path + 'certs' + sep, - 'lib': base_path + 'lib' + sep, - 'extlib': base_path + 'extlib' + sep, - } - - def __update_paths(self, settings): - """ - Set custom paths if necessary - """ - if not isinstance(settings, dict): - return - - if 'custom_base_path' in settings: - base_path = settings['custom_base_path'] - base_path = join(dirname(__file__), base_path) - self.__load_paths(base_path) - -
    [docs] def get_base_path(self): - """ - Returns base path - - :return: The base toolkit folder path - :rtype: string - """ - return self.__paths['base'] -
    -
    [docs] def get_cert_path(self): - """ - Returns cert path - - :return: The cert folder path - :rtype: string - """ - return self.__paths['cert'] -
    -
    [docs] def get_lib_path(self): - """ - Returns lib path - - :return: The library folder path - :rtype: string - """ - return self.__paths['lib'] -
    -
    [docs] def get_ext_lib_path(self): - """ - Returns external lib path - - :return: The external library folder path - :rtype: string - """ - return self.__paths['extlib'] -
    -
    [docs] def get_schemas_path(self): - """ - Returns schema path - - :return: The schema folder path - :rtype: string - """ - return self.__paths['lib'] + 'schemas/' -
    - def __load_settings_from_dict(self, settings): - """ - Loads settings info from a settings Dict - - :param settings: SAML Toolkit Settings - :type settings: dict - - :returns: True if the settings info is valid - :rtype: boolean - """ - errors = self.check_settings(settings) - if len(errors) == 0: - self.__errors = [] - self.__sp = settings['sp'] - self.__idp = settings['idp'] - - if 'strict' in settings: - self.__strict = settings['strict'] - if 'debug' in settings: - self.__debug = settings['debug'] - if 'security' in settings: - self.__security = settings['security'] - if 'contactPerson' in settings: - self.__contacts = settings['contactPerson'] - if 'organization' in settings: - self.__organization = settings['organization'] - - self.__add_default_values() - return True - - self.__errors = errors - return False - - def __load_settings_from_file(self): - """ - Loads settings info from the settings json file - - :returns: True if the settings info is valid - :rtype: boolean - """ - filename = self.get_base_path() + 'settings.json' - - if not exists(filename): - raise OneLogin_Saml2_Error( - 'Settings file not found: %s', - OneLogin_Saml2_Error.SETTINGS_FILE_NOT_FOUND, - filename - ) - - # In the php toolkit instead of being a json file it is a php file and - # it is directly included - json_data = open(filename, 'r') - settings = json.load(json_data) - json_data.close() - - advanced_filename = self.get_base_path() + 'advanced_settings.json' - if exists(advanced_filename): - json_data = open(advanced_filename, 'r') - settings.update(json.load(json_data)) # Merge settings - json_data.close() - - return self.__load_settings_from_dict(settings) - - def __add_default_values(self): - """ - Add default values if the settings info is not complete - """ - if 'binding' not in self.__sp['assertionConsumerService']: - self.__sp['assertionConsumerService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_POST - if 'singleLogoutService' in self.__sp and 'binding' not in self.__sp['singleLogoutService']: - self.__sp['singleLogoutService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT - - # Related to nameID - if 'NameIDFormat' not in self.__sp: - self.__sp['NameIDFormat'] = OneLogin_Saml2_Constants.NAMEID_PERSISTENT - if 'nameIdEncrypted' not in self.__security: - self.__security['nameIdEncrypted'] = False - - # Sign provided - if 'authnRequestsSigned' not in self.__security: - self.__security['authnRequestsSigned'] = False - if 'logoutRequestSigned' not in self.__security: - self.__security['logoutRequestSigned'] = False - if 'logoutResponseSigned' not in self.__security: - self.__security['logoutResponseSigned'] = False - if 'signMetadata' not in self.__security: - self.__security['signMetadata'] = False - - # Sign expected - if 'wantMessagesSigned' not in self.__security: - self.__security['wantMessagesSigned'] = False - if 'wantAssertionsSigned' not in self.__security: - self.__security['wantAssertionsSigned'] = False - - # Encrypt expected - if 'wantAssertionsEncrypted' not in self.__security: - self.__security['wantAssertionsEncrypted'] = False - if 'wantNameIdEncrypted' not in self.__security: - self.__security['wantNameIdEncrypted'] = False - - if 'x509cert' not in self.__idp: - self.__idp['x509cert'] = '' - if 'certFingerprint' not in self.__idp: - self.__idp['certFingerprint'] = '' - -
    [docs] def check_settings(self, settings): - """ - Checks the settings info. - - :param settings: Dict with settings data - :type settings: dict - - :returns: Errors found on the settings data - :rtype: list - """ - assert isinstance(settings, dict) - - errors = [] - if not isinstance(settings, dict) or len(settings) == 0: - errors.append('invalid_syntax') - return errors - - if 'idp' not in settings or len(settings['idp']) == 0: - errors.append('idp_not_found') - else: - idp = settings['idp'] - if 'entityId' not in idp or len(idp['entityId']) == 0: - errors.append('idp_entityId_not_found') - - if ('singleSignOnService' not in idp or - 'url' not in idp['singleSignOnService'] or - len(idp['singleSignOnService']['url']) == 0): - errors.append('idp_sso_not_found') - elif not validate_url(idp['singleSignOnService']['url']): - errors.append('idp_sso_url_invalid') - - if ('singleLogoutService' in idp and - 'url' in idp['singleLogoutService'] and - len(idp['singleLogoutService']['url']) > 0 and - not validate_url(idp['singleLogoutService']['url'])): - errors.append('idp_slo_url_invalid') - - if 'sp' not in settings or len(settings['sp']) == 0: - errors.append('sp_not_found') - else: - sp = settings['sp'] - security = {} - if 'security' in settings: - security = settings['security'] - - if 'entityId' not in sp or len(sp['entityId']) == 0: - errors.append('sp_entityId_not_found') - - if ('assertionConsumerService' not in sp or - 'url' not in sp['assertionConsumerService'] or - len(sp['assertionConsumerService']['url']) == 0): - errors.append('sp_acs_not_found') - elif not validate_url(sp['assertionConsumerService']['url']): - errors.append('sp_acs_url_invalid') - - if ('singleLogoutService' in sp and - 'url' in sp['singleLogoutService'] and - len(sp['singleLogoutService']['url']) > 0 and - not validate_url(sp['singleLogoutService']['url'])): - errors.append('sp_sls_url_invalid') - - if 'signMetadata' in security and isinstance(security['signMetadata'], dict): - if ('keyFileName' not in security['signMetadata'] or - 'certFileName' not in security['signMetadata']): - errors.append('sp_signMetadata_invalid') - - if ((('authnRequestsSigned' in security and security['authnRequestsSigned']) or - ('logoutRequestSigned' in security and security['logoutRequestSigned']) or - ('logoutResponseSigned' in security and security['logoutResponseSigned']) or - ('wantAssertionsEncrypted' in security and security['wantAssertionsEncrypted']) or - ('wantNameIdEncrypted' in security and security['wantNameIdEncrypted'])) and - not self.check_sp_certs()): - errors.append('sp_cert_not_found_and_required') - - exists_X509 = ('idp' in settings and - 'x509cert' in settings['idp'] and - len(settings['idp']['x509cert']) > 0) - exists_fingerprint = ('idp' in settings and - 'certFingerprint' in settings['idp'] and - len(settings['idp']['certFingerprint']) > 0) - if ((('wantAssertionsSigned' in security and security['wantAssertionsSigned']) or - ('wantMessagesSigned' in security and security['wantMessagesSigned'])) and - not(exists_X509 or exists_fingerprint)): - errors.append('idp_cert_or_fingerprint_not_found_and_required') - if ('nameIdEncrypted' in security and security['nameIdEncrypted']) and not exists_X509: - errors.append('idp_cert_not_found_and_required') - - if 'contactPerson' in settings: - types = settings['contactPerson'].keys() - valid_types = ['technical', 'support', 'administrative', 'billing', 'other'] - for t in types: - if t not in valid_types: - errors.append('contact_type_invalid') - break - - for t in settings['contactPerson']: - contact = settings['contactPerson'][t] - if (('givenName' not in contact or len(contact['givenName']) == 0) or - ('emailAddress' not in contact or len(contact['emailAddress']) == 0)): - errors.append('contact_not_enough_data') - break - - if 'organization' in settings: - for o in settings['organization']: - organization = settings['organization'][o] - if (('name' not in organization or len(organization['name']) == 0) or - ('displayname' not in organization or len(organization['displayname']) == 0) or - ('url' not in organization or len(organization['url']) == 0)): - errors.append('organization_not_enough_data') - break - - return errors -
    -
    [docs] def check_sp_certs(self): - """ - Checks if the x509 certs of the SP exists and are valid. - - :returns: If the x509 certs of the SP exists and are valid - :rtype: boolean - """ - key = self.get_sp_key() - cert = self.get_sp_cert() - return key is not None and cert is not None -
    -
    [docs] def get_sp_key(self): - """ - Returns the x509 private key of the SP. - - :returns: SP private key - :rtype: string - """ - key = None - key_file = self.__paths['cert'] + 'sp.key' - - if exists(key_file): - f = open(key_file, 'r') - key = f.read() - f.close() - - return key -
    -
    [docs] def get_sp_cert(self): - """ - Returns the x509 public cert of the SP. - - :returns: SP public cert - :rtype: string - """ - cert = None - cert_file = self.__paths['cert'] + 'sp.crt' - - if exists(cert_file): - f = open(cert_file, 'r') - cert = f.read() - f.close() - - return cert -
    -
    [docs] def get_idp_data(self): - """ - Gets the IdP data. - - :returns: IdP info - :rtype: dict - """ - return self.__idp -
    -
    [docs] def get_sp_data(self): - """ - Gets the SP data. - - :returns: SP info - :rtype: dict - """ - return self.__sp -
    -
    [docs] def get_security_data(self): - """ - Gets security data. - - :returns: Security info - :rtype: dict - """ - return self.__security -
    -
    [docs] def get_contacts(self): - """ - Gets contact data. - - :returns: Contacts info - :rtype: dict - """ - return self.__contacts -
    -
    [docs] def get_organization(self): - """ - Gets organization data. - - :returns: Organization info - :rtype: dict - """ - return self.__organization -
    -
    [docs] def get_sp_metadata(self): - """ - Gets the SP metadata. The XML representation. - - :returns: SP metadata (xml) - :rtype: string - """ - metadata = OneLogin_Saml2_Metadata.builder( - self.__sp, self.__security['authnRequestsSigned'], - self.__security['wantAssertionsSigned'], None, None, - self.get_contacts(), self.get_organization() - ) - cert = self.get_sp_cert() - if cert is not None: - metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert) - - # Sign metadata - if 'signMetadata' in self.__security and self.__security['signMetadata'] is not False: - if self.__security['signMetadata'] is True: - key_file_name = 'sp.key' - cert_file_name = 'sp.crt' - else: - if ('keyFileName' not in self.__security['signMetadata'] or - 'certFileName' not in self.__security['signMetadata']): - raise OneLogin_Saml2_Error( - 'Invalid Setting: signMetadata value of the sp is not valid', - OneLogin_Saml2_Error.SETTINGS_INVALID_SYNTAX - ) - key_file_name = self.__security['signMetadata']['keyFileName'] - cert_file_name = self.__security['signMetadata']['certFileName'] - key_metadata_file = self.__paths['cert'] + key_file_name - cert_metadata_file = self.__paths['cert'] + cert_file_name - - if not exists(key_metadata_file): - raise OneLogin_Saml2_Error( - 'Private key file not found: %s', - OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND, - key_metadata_file - ) - - if not exists(cert_metadata_file): - raise OneLogin_Saml2_Error( - 'Public cert file not found: %s', - OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND, - cert_metadata_file - ) - - f = open(key_metadata_file, 'r') - key_metadata = f.read() - f.close() - f = open(cert_metadata_file, 'r') - cert_metadata = f.read() - f.close() - metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key_metadata, cert_metadata) - - return metadata -
    -
    [docs] def validate_metadata(self, xml): - """ - Validates an XML SP Metadata. - - :param xml: Metadata's XML that will be validate - :type xml: string - - :returns: The list of found errors - :rtype: list - """ - - assert isinstance(xml, basestring) - - if len(xml) == 0: - raise Exception('Empty string supplied as input') - - errors = [] - res = OneLogin_Saml2_Utils.validate_xml(xml, 'saml-schema-metadata-2.0.xsd', self.__debug) - if not isinstance(res, Document): - errors.append(res) - else: - dom = res - element = dom.documentElement - if element.tagName != 'md:EntityDescriptor': - errors.append('noEntityDescriptor_xml') - else: - valid_until = cache_duration = expire_time = None - - if element.hasAttribute('validUntil'): - valid_until = OneLogin_Saml2_Utils.parse_SAML_to_time(element.getAttribute('validUntil')) - if element.hasAttribute('cacheDuration'): - cache_duration = element.getAttribute('cacheDuration') - - expire_time = OneLogin_Saml2_Utils.get_expire_time(cache_duration, valid_until) - if expire_time is not None and int(datetime.now().strftime('%s')) > int(expire_time): - errors.append('expired_xml') - - return errors -
    -
    [docs] def format_idp_cert(self): - """ - Formats the IdP cert. - """ - if self.__idp['x509cert'] is not None: - self.__idp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509cert']) -
    -
    [docs] def get_errors(self): - """ - Returns an array with the errors, the array is empty when the settings is ok. - - :returns: Errors - :rtype: list - """ - return self.__errors -
    -
    [docs] def set_strict(self, value): - """ - Activates or deactivates the strict mode. - - :param xml: Strict parameter - :type xml: boolean - """ - assert isinstance(value, bool) - - self.__strict = value -
    -
    [docs] def is_strict(self): - """ - Returns if the 'strict' mode is active. - - :returns: Strict parameter - :rtype: boolean - """ - return self.__strict -
    -
    [docs] def is_debug_active(self): - """ - Returns if the debug is active. - - :returns: Debug parameter - :rtype: boolean - """ - return self.__debug
    -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - - diff --git a/docs/saml2/_modules/saml2/utils.html b/docs/saml2/_modules/saml2/utils.html deleted file mode 100644 index 613d2a09..00000000 --- a/docs/saml2/_modules/saml2/utils.html +++ /dev/null @@ -1,805 +0,0 @@ - - - - - - - - - - saml2.utils — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Source code for saml2.utils

    -# -*- coding: utf-8 -*-
    -
    -# Copyright (c) 2010-2018 OneLogin, Inc.
    -# MIT License
    -
    -import base64
    -from datetime import datetime
    -import calendar
    -from hashlib import sha1
    -from isodate import parse_duration as duration_parser
    -from lxml import etree
    -from lxml.etree import ElementBase
    -from os.path import basename, dirname, join
    -import re
    -from sys import stderr
    -from tempfile import NamedTemporaryFile
    -from textwrap import wrap
    -from urllib import quote_plus
    -from uuid import uuid4
    -from xml.dom.minidom import Document, parseString, Element
    -from xml.etree.ElementTree import tostring
    -import zlib
    -
    -import dm.xmlsec.binding as xmlsec
    -from dm.xmlsec.binding.tmpl import EncData, Signature
    -from M2Crypto import X509
    -
    -from saml2.constants import OneLogin_Saml2_Constants
    -from saml2.errors import OneLogin_Saml2_Error
    -
    -
    -def _(msg):
    -    # Fixme Add i18n support
    -    return msg
    -
    -
    -
    [docs]class OneLogin_Saml2_Utils: - - @staticmethod -
    [docs] def decode_base64_and_inflate(value): - """ base64 decodes and then inflates according to RFC1951 - :param value: a deflated and encoded string - :return: the string after decoding and inflating - """ - - return zlib.decompress(base64.b64decode(value), -15) -
    - @staticmethod -
    [docs] def deflate_and_base64_encode(value): - """ - Deflates and the base64 encodes a string - :param value: The string to deflate and encode - :return: The deflated and encoded string - """ - return base64.b64encode(zlib.compress(value)[2:-4]) -
    - @staticmethod -
    [docs] def validate_xml(xml, schema, debug=False): - """ - """ - assert (isinstance(xml, basestring) or isinstance(xml, Document)) - assert isinstance(schema, basestring) - - if isinstance(xml, Document): - xml = xml.toxml() - - # Switch to lxml for schema validation - try: - dom = etree.fromstring(xml) - except Exception: - return 'unloaded_xml' - - schema_file = join(dirname(__file__), 'schemas', schema) - f = open(schema_file, 'r') - schema_doc = etree.parse(f) - f.close() - xmlschema = etree.XMLSchema(schema_doc) - - if not xmlschema.validate(dom): - xml_errors = [xmlschema.error_log] - if debug: - stderr.write('Errors validating the metadata') - stderr.write(':\n\n') - for error in xml_errors: - stderr.write('%s\n' % error.message) - - return 'invalid_xml' - - return parseString(etree.tostring(dom)) -
    - @staticmethod -
    [docs] def format_cert(cert, heads=True): - """ - Returns a x509 cert (adding header & footer if required). - - :param cert: A x509 unformated cert - :type: string - - :param heads: True if we want to include head and footer - :type: boolean - - :returns: Formated cert - :rtype: string - """ - x509_cert = cert.replace('\x0D', '') - x509_cert = x509_cert.replace('\r', '') - x509_cert = x509_cert.replace('\n', '') - if len(x509_cert) > 0: - x509_cert = x509_cert.replace('-----BEGIN CERTIFICATE-----', '') - x509_cert = x509_cert.replace('-----END CERTIFICATE-----', '') - x509_cert = x509_cert.replace(' ', '') - - if heads: - x509_cert = '-----BEGIN CERTIFICATE-----\n' + '\n'.join(wrap(x509_cert, 64)) + '\n-----END CERTIFICATE-----\n' - - return x509_cert -
    - @staticmethod -
    [docs] def redirect(url, parameters={}, request_data={}): - """ - Executes a redirection to the provided url (or return the target url). - - :param url: The target url - :type: string - - :param parameters: Extra parameters to be passed as part of the url - :type: dict - - :param request_data: The request as a dict - :type: dict - - :returns: Url - :rtype: string - """ - assert isinstance(url, basestring) - assert isinstance(parameters, dict) - - if url.startswith('/'): - url = '%s%s' % (OneLogin_Saml2_Utils.get_self_url_host(request_data), url) - - # Verify that the URL is to a http or https site. - if re.search('^https?://', url) is None: - raise OneLogin_Saml2_Error( - 'Redirect to invalid URL: ' + url, - OneLogin_Saml2_Error.REDIRECT_INVALID_URL - ) - - # Add encoded parameters - if url.find('?') < 0: - param_prefix = '?' - else: - param_prefix = '&' - - for name, value in parameters.items(): - - if value is None: - param = urlencode(name) - elif isinstance(value, list): - param = '' - for val in value: - param += quote_plus(name) + '[]=' + quote_plus(val) + '&' - if len(param) > 0: - param = param[0:-1] - else: - param = quote_plus(name) + '=' + quote_plus(value) - - url += param_prefix + param - param_prefix = '&' - - return url -
    - @staticmethod -
    [docs] def get_self_url_host(request_data): - """ - Returns the protocol + the current host + the port (if different than - common ports). - - :param request_data: The request as a dict - :type: dict - - :return: Url - :rtype: string - """ - current_host = OneLogin_Saml2_Utils.get_self_host(request_data) - port = '' - if OneLogin_Saml2_Utils.is_https(request_data): - protocol = 'https' - else: - protocol = 'http' - - if 'server_port' in request_data: - port_number = request_data['server_port'] - port = ':' + port_number - - if protocol == 'http' and port_number == '80': - port = '' - elif protocol == 'https' and port_number == '443': - port = '' - - return '%s://%s%s' % (protocol, current_host, port) -
    - @staticmethod -
    [docs] def get_self_host(request_data): - """ - Returns the current host. - - :param request_data: The request as a dict - :type: dict - - :return: The current host - :rtype: string - """ - if 'http_host' in request_data: - current_host = request_data['http_host'] - elif 'server_name' in request_data: - current_host = request_data['server_name'] - else: - raise Exception('No hostname defined') - - if ':' in current_host: - current_host_data = current_host.split(':') - possible_port = current_host_data[-1] - try: - possible_port = float(possible_port) - current_host = current_host_data[0] - except ValueError: - current_host = ':'.join(current_host_data) - - return current_host -
    - @staticmethod -
    [docs] def is_https(request_data): - """ - Checks if https or http. - - :param request_data: The request as a dict - :type: dict - - :return: False if https is not active - :rtype: boolean - """ - is_https = 'https' in request_data and request_data['https'] != 'off' - is_https = is_https or ('server_port' in request_data and request_data['server_port'] == '443') - return is_https -
    - @staticmethod -
    [docs] def get_self_url_no_query(request_data): - """ - Returns the URL of the current host + current view. - - :param request_data: The request as a dict - :type: dict - - :return: The url of current host + current view - :rtype: string - """ - self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) - script_name = request_data['script_name'] - if script_name[0] != '/': - script_name = '/' + script_name - self_url_host += script_name - if 'path_info' in request_data: - self_url_host += request_data['path_info'] - - return self_url_host -
    - @staticmethod -
    [docs] def get_self_url(request_data): - """ - Returns the URL of the current host + current view + query. - - :param request_data: The request as a dict - :type: dict - - :return: The url of current host + current view + query - :rtype: string - """ - self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) - - request_uri = '' - if 'request_uri' in request_data: - request_uri = request_data['request_uri'] - if not request_uri.startswith('/'): - match = re.search('^https?://[^/]*(/.*)', request_uri) - if match is not None: - request_uri = match.groups()[0] - - return self_url_host + request_uri -
    - @staticmethod -
    [docs] def generate_unique_id(): - """ - Generates an unique string (used for example as ID for assertions). - - :return: A unique string - :rtype: string - """ - return 'ONELOGIN_%s' % sha1(uuid4().hex).hexdigest() -
    - @staticmethod -
    [docs] def parse_time_to_SAML(time): - """ - Converts a UNIX timestamp to SAML2 timestamp on the form - yyyy-mm-ddThh:mm:ss(\.s+)?Z. - - :param time: The time we should convert (DateTime). - :type: string - - :return: SAML2 timestamp. - :rtype: string - """ - data = datetime.utcfromtimestamp(float(time)) - return data.strftime('%Y-%m-%dT%H:%M:%SZ') -
    - @staticmethod -
    [docs] def parse_SAML_to_time(timestr): - """ - Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(\.s+)?Z - to a UNIX timestamp. The sub-second part is ignored. - - :param time: The time we should convert (SAML Timestamp). - :type: string - - :return: Converted to a unix timestamp. - :rtype: int - """ - try: - data = datetime.strptime(timestr, '%Y-%m-%dT%H:%M:%SZ') - except ValueError: - data = datetime.strptime(timestr, '%Y-%m-%dT%H:%M:%S.%fZ') - return calendar.timegm(data.utctimetuple()) -
    - @staticmethod -
    [docs] def parse_duration(duration, timestamp=None): - """ - Interprets a ISO8601 duration value relative to a given timestamp. - - :param duration: The duration, as a string. - :type: string - - :param timestamp: The unix timestamp we should apply the duration to. - Optional, default to the current time. - :type: string - - :return: The new timestamp, after the duration is applied. - :rtype: int - """ - assert isinstance(duration, basestring) - assert (timestamp is None or isinstance(timestamp, int)) - - timedelta = duration_parser(duration) - if timestamp is None: - data = datetime.utcnow() + timedelta - else: - data = datetime.utcfromtimestamp(timestamp) + timedelta - return calendar.timegm(data.utctimetuple()) -
    - @staticmethod -
    [docs] def get_expire_time(cache_duration=None, valid_until=None): - """ - Compares 2 dates and returns the earliest. - - :param cache_duration: The duration, as a string. - :type: string - - :param valid_until: The valid until date, as a string or as a timestamp - :type: string - - :return: The expiration time. - :rtype: int - """ - expire_time = None - - if cache_duration is not None: - expire_time = OneLogin_Saml2_Utils.parse_duration(cache_duration) - - if valid_until is not None: - if isinstance(valid_until, int): - valid_until_time = valid_until - else: - valid_until_time = OneLogin_Saml2_Utils.parse_SAML_to_time(valid_until) - if expire_time is None or expire_time > valid_until_time: - expire_time = valid_until_time - - if expire_time is not None: - return '%d' % expire_time - return None -
    - @staticmethod -
    [docs] def query(dom, query, context=None): - """ - Extracts nodes that match the query from the Element - - :param dom: The root of the lxml objet - :type: Element - - :param query: Xpath Expresion - :type: string - - :param context: Context Node - :type: DOMElement - - :returns: The queried nodes - :rtype: list - """ - if context is None: - return dom.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) - else: - return context.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) -
    - @staticmethod -
    [docs] def delete_local_session(callback=None): - """ - Deletes the local session. - """ - - if callback is not None: - callback() -
    - @staticmethod -
    [docs] def calculate_x509_fingerprint(x509_cert): - """ - Calculates the fingerprint of a x509cert. - - :param x509_cert: x509 cert - :type: string - - :returns: Formated fingerprint - :rtype: string - """ - assert isinstance(x509_cert, basestring) - - lines = x509_cert.split('\n') - data = '' - - for line in lines: - # Remove '\r' from end of line if present. - line = line.rstrip() - if line == '-----BEGIN CERTIFICATE-----': - # Delete junk from before the certificate. - data = '' - elif line == '-----END CERTIFICATE-----': - # Ignore data after the certificate. - break - elif line == '-----BEGIN PUBLIC KEY-----' or line == '-----BEGIN RSA PRIVATE KEY-----': - # This isn't an X509 certificate. - return None - else: - # Append the current line to the certificate data. - data += line - # "data" now contains the certificate as a base64-encoded string. The - # fingerprint of the certificate is the sha1-hash of the certificate. - return sha1(base64.b64decode(data)).hexdigest().lower() -
    - @staticmethod -
    [docs] def format_finger_print(fingerprint): - """ - Formates a fingerprint. - - :param fingerprint: fingerprint - :type: string - - :returns: Formated fingerprint - :rtype: string - """ - formated_fingerprint = fingerprint.replace(':', '') - return formated_fingerprint.lower() -
    - @staticmethod -
    [docs] def generate_name_id(value, sp_nq, sp_format, key=None): - """ - Generates a nameID. - - :param value: fingerprint - :type: string - - :param sp_nq: SP Name Qualifier - :type: string - - :param sp_format: SP Format - :type: string - - :param key: SP Key to encrypt the nameID - :type: string - - :returns: DOMElement | XMLSec nameID - :rtype: string - """ - doc = Document() - - name_id = doc.createElement('saml:NameID') - name_id.setAttribute('SPNameQualifier', sp_nq) - name_id.setAttribute('Format', sp_format) - name_id.appendChild(doc.createTextNode(value)) - - doc.appendChild(name_id) - - if key is not None: - xmlsec.initialize() - - # Load the private key - mngr = xmlsec.KeysMngr() - key = OneLogin_Saml2_Utils.format_cert(key, heads=False) - file_key = OneLogin_Saml2_Utils.write_temp_file(key) - key_data = xmlsec.Key.load(file_key.name, xmlsec.KeyDataFormatPem, None) - key_data.name = key_name = basename(file_key.name) - mngr.addKey(key_data) - file_key.close() - - # Prepare for encryption - enc_data = EncData(xmlsec.TransformAes128Cbc, type=xmlsec.TypeEncElement) - enc_data.ensureCipherValue() - key_info = enc_data.ensureKeyInfo() - enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaPkcs1) - enc_key.ensureCipherValue() - enc_key_info = enc_key.ensureKeyInfo() - enc_key_info.addKeyName(key_name) - - # Encrypt! - enc_ctx = xmlsec.EncCtx(mngr) - enc_ctx.enc_key = xmlsec.Key.generate(xmlsec.KeyDataAes, 128, xmlsec.KeyDataTypeSession) - ed = enc_ctx.encryptXml(enc_data, doc.getroot()) - - # Build XML with encrypted data - newdoc = Document() - encrypted_id = newdoc.createElement('saml:EncryptedID') - newdoc.appendChild(encrypted_id) - encrypted_id.appendChild(encrypted_id.ownerDocument.importNode(ed, True)) - - return newdoc.saveXML(encrypted_id) - else: - return doc.saveXML(name_id) -
    - @staticmethod -
    [docs] def get_status(dom): - """ - Gets Status from a Response. - - :param dom: The Response as XML - :type: Document - - :returns: The Status, an array with the code and a message. - :rtype: dict - """ - status = {} - - status_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status') - if len(status_entry) == 0: - raise Exception('Missing Status on response') - - code_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode', status_entry[0]) - if len(code_entry) == 0: - raise Exception('Missing Status Code on response') - code = code_entry[0].values()[0] - status['code'] = code - - message_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusMessage', status_entry[0]) - if len(message_entry) == 0: - status['msg'] = '' - else: - status['msg'] = message_entry[0].text - - return status -
    - @staticmethod -
    [docs] def decrypt_element(encrypted_data, enc_ctx): - """ - Decrypts an encrypted element. - - :param encrypted_data: The encrypted data. - :type: DOMElement - - :param enc_ctx: The encryption context. - :type: Encryption Context - - :returns: The decrypted element. - :rtype: DOMElement - """ - if isinstance(encrypted_data, Element): - # Minidom element - encrypted_data = etree.fromstring(encrypted_data.toxml()) - - decrypted = enc_ctx.decrypt(encrypted_data) - if isinstance(decrypted, ElementBase): - # lxml element, decrypted xml data - return tostring(decrypted.getroottree()) - else: - # decrypted binary data - return decrypted -
    - @staticmethod -
    [docs] def write_temp_file(content): - """ - Writes some content into a temporary file and returns it. - - :param content: The file content - :type: string - - :returns: The temporary file - :rtype: file-like object - """ - f = NamedTemporaryFile(delete=True) - f.file.write(content) - f.file.flush() - return f -
    - @staticmethod -
    [docs] def add_sign(xml, key, cert): - """ - Adds signature key and senders certificate to an element (Message or - Assertion). - - :param xml: The element we should sign - :type: string | Document - - :param key: The private key - :type: string - - :param cert: The public - :type: string - """ - if isinstance(xml, Document): - dom = xml - else: - if xml == '': - raise Exception('Empty string supplied as input') - - try: - dom = parseString(xml) - except Exception: - raise Exception('Error parsing xml string') - - xmlsec.initialize() - - # TODO the key and cert could be file descriptors instead - # Load the private key. - file_key = OneLogin_Saml2_Utils.write_temp_file(key) - sign_key = xmlsec.Key.load(file_key.name, xmlsec.KeyDataFormatPem, None) - file_key.close() - # Add the certificate to the signature. - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) - sign_key.loadCert(file_cert.name, xmlsec.KeyDataFormatPem) - file_cert.close() - - # Get the EntityDescriptor node we should sign. - root_node = dom.firstChild - - # Sign the metadata with our private key. - signature = Signature(xmlsec.TransformExclC14N, xmlsec.TransformRsaSha1) - ref = signature.addReference(xmlsec.TransformSha1) - ref.addTransform(xmlsec.TransformEnveloped) - - key_info = signature.ensureKeyInfo() - key_info.addX509Data() - - dsig_ctx = xmlsec.DSigCtx() - dsig_ctx.signKey = sign_key - dsig_ctx.sign(signature) - - signature = tostring(signature).replace('ns0:', 'ds:').replace(':ns0', ':ds') - signature = parseString(signature).firstChild - - insert_before = root_node.getElementsByTagName('saml:Issuer') - if len(insert_before) > 0: - insert_before = insert_before[0].nextSibling - else: - insert_before = root_node.firstChild.nextSibling.nextSibling - root_node.insertBefore(signature, insert_before) - - return dom.toxml() -
    - @staticmethod -
    [docs] def validate_sign(xml, cert=None, fingerprint=None): - """ - Validates a signature (Message or Assertion). - - :param xml: The element we should validate - :type: string | Document - - :param cert: The pubic cert - :type: string - - :param fingerprint: The fingerprint of the public cert - :type: string - """ - if isinstance(xml, Document): - dom = etree.fromstring(xml.toxml()) - else: - if xml == '': - raise Exception('Empty string supplied as input') - - try: - dom = etree.fromstring(xml) - except Exception: - raise Exception('Error parsing xml string') - - xmlsec.initialize() - - # Find signature in the dom - signature_node = OneLogin_Saml2_Utils.query(dom, 'ds:Signature')[0] - - # Prepare context and load cert into it - dsig_ctx = xmlsec.DSigCtx() - sign_cert = X509.load_cert_string(str(cert), X509.FORMAT_PEM) - pub_key = sign_cert.get_pubkey().get_rsa() - sign_key = xmlsec.Key.loadMemory(pub_key.as_pem(cipher=None), - xmlsec.KeyDataFormatPem) - dsig_ctx.signKey = sign_key - - # Verify signature - dsig_ctx.verify(signature_node)
    -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/_sources/index.txt b/docs/saml2/_sources/index.txt deleted file mode 100644 index 43d48f47..00000000 --- a/docs/saml2/_sources/index.txt +++ /dev/null @@ -1,23 +0,0 @@ -.. saml2 documentation master file, created by - sphinx-quickstart on Thu Oct 23 03:29:00 2014. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to OneLogin SAML Python library documentation -===================================================== - -Contents: - -.. toctree:: - :maxdepth: 4 - - saml2 - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/saml2/_sources/saml2.txt b/docs/saml2/_sources/saml2.txt deleted file mode 100644 index bf444d4d..00000000 --- a/docs/saml2/_sources/saml2.txt +++ /dev/null @@ -1,83 +0,0 @@ -OneLogin saml2 Module -====================== - -:mod:`auth` Class ------------------- - -.. automodule:: saml2.auth - :members: - :undoc-members: - :show-inheritance: - -:mod:`authn_request` Class ---------------------------- - -.. automodule:: saml2.authn_request - :members: - :undoc-members: - :show-inheritance: - -:mod:`constants` Class ------------------------ - -.. automodule:: saml2.constants - :members: - :undoc-members: - :show-inheritance: - -:mod:`errors` Class --------------------- - -.. automodule:: saml2.errors - :members: - :undoc-members: - :show-inheritance: - -:mod:`logout_request` Class ----------------------------- - -.. automodule:: saml2.logout_request - :members: - :undoc-members: - :show-inheritance: - -:mod:`logout_response` Class ------------------------------ - -.. automodule:: saml2.logout_response - :members: - :undoc-members: - :show-inheritance: - -:mod:`metadata` Class ----------------------- - -.. automodule:: saml2.metadata - :members: - :undoc-members: - :show-inheritance: - -:mod:`response` Class ----------------------- - -.. automodule:: saml2.response - :members: - :undoc-members: - :show-inheritance: - -:mod:`settings` Class ----------------------- - -.. automodule:: saml2.settings - :members: - :undoc-members: - :show-inheritance: - -:mod:`utils` Class -------------------- - -.. automodule:: saml2.utils - :members: - :undoc-members: - :show-inheritance: - diff --git a/docs/saml2/_static/ajax-loader.gif b/docs/saml2/_static/ajax-loader.gif deleted file mode 100644 index 61faf8cab23993bd3e1560bff0668bd628642330..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 673 zcmZ?wbhEHb6krfw_{6~Q|Nno%(3)e{?)x>&1u}A`t?OF7Z|1gRivOgXi&7IyQd1Pl zGfOfQ60;I3a`F>X^fL3(@);C=vM_KlFfb_o=k{|A33hf2a5d61U}gjg=>Rd%XaNQW zW@Cw{|b%Y*pl8F?4B9 zlo4Fz*0kZGJabY|>}Okf0}CCg{u4`zEPY^pV?j2@h+|igy0+Kz6p;@SpM4s6)XEMg z#3Y4GX>Hjlml5ftdH$4x0JGdn8~MX(U~_^d!Hi)=HU{V%g+mi8#UGbE-*ao8f#h+S z2a0-5+vc7MU$e-NhmBjLIC1v|)9+Im8x1yacJ7{^tLX(ZhYi^rpmXm0`@ku9b53aN zEXH@Y3JaztblgpxbJt{AtE1ad1Ca>{v$rwwvK(>{m~Gf_=-Ro7Fk{#;i~+{{>QtvI yb2P8Zac~?~=sRA>$6{!(^3;ZP0TPFR(G_-UDU(8Jl0?(IXu$~#4A!880|o%~Al1tN diff --git a/docs/saml2/_static/basic.css b/docs/saml2/_static/basic.css deleted file mode 100644 index 43e8bafa..00000000 --- a/docs/saml2/_static/basic.css +++ /dev/null @@ -1,540 +0,0 @@ -/* - * basic.css - * ~~~~~~~~~ - * - * Sphinx stylesheet -- basic theme. - * - * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar #searchbox input[type="text"] { - width: 170px; -} - -div.sphinxsidebar #searchbox input[type="submit"] { - width: 30px; -} - -img { - border: 0; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li div.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable { - width: 100%; -} - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable dl, table.indextable dd { - margin-top: 0; - margin-bottom: 0; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -div.genindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -/* -- general body styles --------------------------------------------------- */ - -a.headerlink { - visibility: hidden; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.field-list ul { - padding-left: 1em; -} - -.first { - margin-top: 0 !important; -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -img.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px 7px 0 7px; - background-color: #ffe; - width: 40%; - float: right; -} - -p.sidebar-title { - font-weight: bold; -} - -/* -- topics ---------------------------------------------------------------- */ - -div.topic { - border: 1px solid #ccc; - padding: 7px 7px 0 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -div.admonition dl { - margin-bottom: 0; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - border: 0; - border-collapse: collapse; -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -table.field-list td, table.field-list th { - border: 0 !important; -} - -table.footnote td, table.footnote th { - border: 0 !important; -} - -th { - text-align: left; - padding-right: 5px; -} - -table.citation { - border-left: solid 1px gray; - margin-left: 1px; -} - -table.citation td { - border-bottom: none; -} - -/* -- other body styles ----------------------------------------------------- */ - -ol.arabic { - list-style: decimal; -} - -ol.loweralpha { - list-style: lower-alpha; -} - -ol.upperalpha { - list-style: upper-alpha; -} - -ol.lowerroman { - list-style: lower-roman; -} - -ol.upperroman { - list-style: upper-roman; -} - -dl { - margin-bottom: 15px; -} - -dd p { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -dt:target, .highlighted { - background-color: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.refcount { - color: #060; -} - -.optional { - font-size: 1.3em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.guilabel, .menuselection { - font-family: sans-serif; -} - -.accelerator { - text-decoration: underline; -} - -.classifier { - font-style: oblique; -} - -abbr, acronym { - border-bottom: dotted 1px; - cursor: help; -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; - overflow-y: hidden; /* fixes display issues on Chrome browsers */ -} - -td.linenos pre { - padding: 5px 0px; - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - margin-left: 0.5em; -} - -table.highlighttable td { - padding: 0 0.5em 0 0.5em; -} - -tt.descname { - background-color: transparent; - font-weight: bold; - font-size: 1.2em; -} - -tt.descclassname { - background-color: transparent; -} - -tt.xref, a tt { - background-color: transparent; - font-weight: bold; -} - -h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { - background-color: transparent; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: sans-serif; -} - -div.viewcode-block:target { - margin: -1px -10px; - padding: 0 10px; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} \ No newline at end of file diff --git a/docs/saml2/_static/comment-bright.png b/docs/saml2/_static/comment-bright.png deleted file mode 100644 index 551517b8c83b76f734ff791f847829a760ad1903..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3500 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 A2LJ#7 diff --git a/docs/saml2/_static/comment.png b/docs/saml2/_static/comment.png deleted file mode 100644 index 92feb52b8824c6b0f59b658b1196c61de9162a95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3445 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 Xn!ZpXD%P(H00000NkvXXu0mjf= 0 && !jQuery(node.parentNode).hasMethod(className)) { - var span = document.createElement("span"); - span.className = className; - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - } - } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this); - }); - } - } - return this.each(function() { - highlight(this); - }); -}; - -/** - * Small JavaScript module for the documentation. - */ -var Documentation = { - - init : function() { - this.fixFirefoxAnchorBug(); - this.highlightSearchWords(); - this.initIndexTable(); - }, - - /** - * i18n support - */ - TRANSLATIONS : {}, - PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; }, - LOCALE : 'unknown', - - // gettext and ngettext don't access this so that the functions - // can safely bound to a different name (_ = Documentation.gettext) - gettext : function(string) { - var translated = Documentation.TRANSLATIONS[string]; - if (typeof translated == 'undefined') - return string; - return (typeof translated == 'string') ? translated : translated[0]; - }, - - ngettext : function(singular, plural, n) { - var translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated == 'undefined') - return (n == 1) ? singular : plural; - return translated[Documentation.PLURALEXPR(n)]; - }, - - addTranslations : function(catalog) { - for (var key in catalog.messages) - this.TRANSLATIONS[key] = catalog.messages[key]; - this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); - this.LOCALE = catalog.locale; - }, - - /** - * add context elements like header anchor links - */ - addContextElements : function() { - $('div[id] > :header:first').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this headline')). - appendTo(this); - }); - $('dt[id]').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this definition')). - appendTo(this); - }); - }, - - /** - * workaround a firefox stupidity - */ - fixFirefoxAnchorBug : function() { - if (document.location.hash && $.browser.mozilla) - window.setTimeout(function() { - document.location.href += ''; - }, 10); - }, - - /** - * highlight the search words provided in the url in the text - */ - highlightSearchWords : function() { - var params = $.getQueryParameters(); - var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; - if (terms.length) { - var body = $('div.body'); - window.setTimeout(function() { - $.each(terms, function() { - body.highlightText(this.toLowerCase(), 'highlighted'); - }); - }, 10); - $('') - .appendTo($('#searchbox')); - } - }, - - /** - * init the domain index toggle buttons - */ - initIndexTable : function() { - var togglers = $('img.toggler').click(function() { - var src = $(this).attr('src'); - var idnum = $(this).attr('id').substr(7); - $('tr.cg-' + idnum).toggle(); - if (src.substr(-9) == 'minus.png') - $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); - else - $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); - }).css('display', ''); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { - togglers.click(); - } - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords : function() { - $('#searchbox .highlight-link').fadeOut(300); - $('span.highlighted').removeMethod('highlighted'); - }, - - /** - * make the url absolute - */ - makeURL : function(relativeURL) { - return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; - }, - - /** - * get the current relative url - */ - getCurrentURL : function() { - var path = document.location.pathname; - var parts = path.split(/\//); - $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { - if (this == '..') - parts.pop(); - }); - var url = parts.join('/'); - return path.substring(url.lastIndexOf('/') + 1, path.length - 1); - } -}; - -// quick alias for translations -_ = Documentation.gettext; - -$(document).ready(function() { - Documentation.init(); -}); diff --git a/docs/saml2/_static/down-pressed.png b/docs/saml2/_static/down-pressed.png deleted file mode 100644 index 6f7ad782782e4f8e39b0c6e15c7344700cdd2527..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 368 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6U4S$Y z{B+)352QE?JR*yM+OLB!qm#z$3ZNi+iKnkC`z>}Z23@f-Ava~9&<9T!#}JFtXD=!G zGdl{fK6ro2OGiOl+hKvH6i=D3%%Y^j`yIkRn!8O>@bG)IQR0{Kf+mxNd=_WScA8u_ z3;8(7x2){m9`nt+U(Nab&1G)!{`SPVpDX$w8McLTzAJ39wprG3p4XLq$06M`%}2Yk zRPPsbES*dnYm1wkGL;iioAUB*Or2kz6(-M_r_#Me-`{mj$Z%( diff --git a/docs/saml2/_static/down.png b/docs/saml2/_static/down.png deleted file mode 100644 index 3003a88770de3977d47a2ba69893436a2860f9e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 363 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6U4S$Y z{B+)352QE?JR*yM+OLB!qm#z$3ZNi+iKnkC`z>}xaV3tUZ$qnrLa#kt978NlpS`ru z&)HFc^}^>{UOEce+71h5nn>6&w6A!ieNbu1wh)UGh{8~et^#oZ1# z>T7oM=FZ~xXWnTo{qnXm$ZLOlqGswI_m2{XwVK)IJmBjW{J3-B3x@C=M{ShWt#fYS9M?R;8K$~YwlIqwf>VA7q=YKcwf2DS4Zj5inDKXXB1zl=(YO3ST6~rDq)&z z*o>z)=hxrfG-cDBW0G$!?6{M<$@{_4{m1o%Ub!naEtn|@^frU1tDnm{r-UW|!^@B8 diff --git a/docs/saml2/_static/file.png b/docs/saml2/_static/file.png deleted file mode 100644 index d18082e397e7e54f20721af768c4c2983258f1b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 392 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP$HyOL$D9)yc9|lc|nKf<9@eUiWd>3GuTC!a5vdfWYEazjncPj5ZQX%+1 zt8B*4=d)!cdDz4wr^#OMYfqGz$1LDFF>|#>*O?AGil(WEs?wLLy{Gj2J_@opDm%`dlax3yA*@*N$G&*ukFv>P8+2CBWO(qz zD0k1@kN>hhb1_6`&wrCswzINE(evt-5C1B^STi2@PmdKI;Vst0PQB6!2kdN diff --git a/docs/saml2/_static/jquery.js b/docs/saml2/_static/jquery.js deleted file mode 100644 index dc2f3dac..00000000 --- a/docs/saml2/_static/jquery.js +++ /dev/null @@ -1,9404 +0,0 @@ -/*! - * jQuery JavaScript Library v1.7.2 - * http://jquery.com/ - * - * Copyright 2011, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * Copyright 2011, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * - * Date: Fri Jul 5 14:07:58 UTC 2013 - */ -(function( window, undefined ) { - -// Use the correct document accordingly with window argument (sandbox) -var document = window.document, - navigator = window.navigator, - location = window.location; -var jQuery = (function() { - -// Define a local copy of jQuery -var jQuery = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context, rootjQuery ); - }, - - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$, - - // A central reference to the root jQuery(document) - rootjQuery, - - // A simple way to check for HTML strings or ID strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, - - // Check if a string has a non-whitespace character in it - rnotwhite = /\S/, - - // Used for trimming whitespace - trimLeft = /^\s+/, - trimRight = /\s+$/, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, - - // JSON RegExp - rvalidchars = /^[\],:{}\s]*$/, - rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, - rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, - rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, - - // Useragent RegExp - rwebkit = /(webkit)[ \/]([\w.]+)/, - ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, - rmsie = /(msie) ([\w.]+)/, - rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, - - // Matches dashed string for camelizing - rdashAlpha = /-([a-z]|[0-9])/ig, - rmsPrefix = /^-ms-/, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function( all, letter ) { - return ( letter + "" ).toUpperCase(); - }, - - // Keep a UserAgent string for use with jQuery.browser - userAgent = navigator.userAgent, - - // For matching the engine and version of the browser - browserMatch, - - // The deferred used on DOM ready - readyList, - - // The ready event handler - DOMContentLoaded, - - // Save a reference to some core methods - toString = Object.prototype.toString, - hasOwn = Object.prototype.hasOwnProperty, - push = Array.prototype.push, - slice = Array.prototype.slice, - trim = String.prototype.trim, - indexOf = Array.prototype.indexOf, - - // [[Method]] -> type pairs - class2type = {}; - -jQuery.fn = jQuery.prototype = { - constructor: jQuery, - init: function( selector, context, rootjQuery ) { - var match, elem, ret, doc; - - // Handle $(""), $(null), or $(undefined) - if ( !selector ) { - return this; - } - - // Handle $(DOMElement) - if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - } - - // The body element only exists once, optimize finding it - if ( selector === "body" && !context && document.body ) { - this.context = document; - this[0] = document.body; - this.selector = selector; - this.length = 1; - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - // Are we dealing with HTML string or an ID? - if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = quickExpr.exec( selector ); - } - - // Verify a match, and that no context was specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - doc = ( context ? context.ownerDocument || context : document ); - - // If a single string is passed in and it's a single tag - // just do a createElement and skip the rest - ret = rsingleTag.exec( selector ); - - if ( ret ) { - if ( jQuery.isPlainObject( context ) ) { - selector = [ document.createElement( ret[1] ) ]; - jQuery.fn.attr.call( selector, context, true ); - - } else { - selector = [ doc.createElement( ret[1] ) ]; - } - - } else { - ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); - selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; - } - - return jQuery.merge( this, selector ); - - // HANDLE: $("#id") - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return rootjQuery.ready( selector ); - } - - if ( selector.selector !== undefined ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }, - - // Start with an empty selector - selector: "", - - // The current version of jQuery being used - jquery: "1.7.2", - - // The default length of a jQuery object is 0 - length: 0, - - // The number of elements contained in the matched element set - size: function() { - return this.length; - }, - - toArray: function() { - return slice.call( this, 0 ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num == null ? - - // Return a 'clean' array - this.toArray() : - - // Return just the object - ( num < 0 ? this[ this.length + num ] : this[ num ] ); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems, name, selector ) { - // Build a new jQuery matched element set - var ret = this.constructor(); - - if ( jQuery.isArray( elems ) ) { - push.apply( ret, elems ); - - } else { - jQuery.merge( ret, elems ); - } - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - ret.context = this.context; - - if ( name === "find" ) { - ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; - } else if ( name ) { - ret.selector = this.selector + "." + name + "(" + selector + ")"; - } - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - ready: function( fn ) { - // Attach the listeners - jQuery.bindReady(); - - // Add the callback - readyList.add( fn ); - - return this; - }, - - eq: function( i ) { - i = +i; - return i === -1 ? - this.slice( i ) : - this.slice( i, i + 1 ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ), - "slice", slice.call(arguments).join(",") ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - - end: function() { - return this.prevObject || this.constructor(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: [].sort, - splice: [].splice -}; - -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - -jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if ( length === i ) { - target = this; - --i; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { - if ( copyIsArray ) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend({ - noConflict: function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } - - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } - - return jQuery; - }, - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } - }, - - // Handle when the DOM is ready - ready: function( wait ) { - // Either a released hold or an DOMready/load event and not yet ready - if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready, 1 ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.fireWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.trigger ) { - jQuery( document ).trigger( "ready" ).off( "ready" ); - } - } - }, - - bindReady: function() { - if ( readyList ) { - return; - } - - readyList = jQuery.Callbacks( "once memory" ); - - // Catch cases where $(document).ready() is called after the - // browser event has already occurred. - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - return setTimeout( jQuery.ready, 1 ); - } - - // Mozilla, Opera and webkit nightlies currently support this event - if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", jQuery.ready, false ); - - // If IE event model is used - } else if ( document.attachEvent ) { - // ensure firing before onload, - // maybe late but safe also for iframes - document.attachEvent( "onreadystatechange", DOMContentLoaded ); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", jQuery.ready ); - - // If IE and not a frame - // continually check to see if the document is ready - var toplevel = false; - - try { - toplevel = window.frameElement == null; - } catch(e) {} - - if ( document.documentElement.doScroll && toplevel ) { - doScrollCheck(); - } - } - }, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return jQuery.type(obj) === "function"; - }, - - isArray: Array.isArray || function( obj ) { - return jQuery.type(obj) === "array"; - }, - - isWindow: function( obj ) { - return obj != null && obj == obj.window; - }, - - isNumeric: function( obj ) { - return !isNaN( parseFloat(obj) ) && isFinite( obj ); - }, - - type: function( obj ) { - return obj == null ? - String( obj ) : - class2type[ toString.call(obj) ] || "object"; - }, - - isPlainObject: function( obj ) { - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { - return false; - } - - try { - // Not own constructor property must be Object - if ( obj.constructor && - !hasOwn.call(obj, "constructor") && - !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - } catch ( e ) { - // IE8,9 Will throw exceptions on certain host objects #9897 - return false; - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - - var key; - for ( key in obj ) {} - - return key === undefined || hasOwn.call( obj, key ); - }, - - isEmptyObject: function( obj ) { - for ( var name in obj ) { - return false; - } - return true; - }, - - error: function( msg ) { - throw new Error( msg ); - }, - - parseJSON: function( data ) { - if ( typeof data !== "string" || !data ) { - return null; - } - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); - - // Attempt to parse using the native JSON parser first - if ( window.JSON && window.JSON.parse ) { - return window.JSON.parse( data ); - } - - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test( data.replace( rvalidescape, "@" ) - .replace( rvalidtokens, "]" ) - .replace( rvalidbraces, "")) ) { - - return ( new Function( "return " + data ) )(); - - } - jQuery.error( "Invalid JSON: " + data ); - }, - - // Cross-browser xml parsing - parseXML: function( data ) { - if ( typeof data !== "string" || !data ) { - return null; - } - var xml, tmp; - try { - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data , "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); - } - } catch( e ) { - xml = undefined; - } - if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; - }, - - noop: function() {}, - - // Evaluates a script in a global context - // Workarounds based on findings by Jim Driscoll - // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context - globalEval: function( data ) { - if ( data && rnotwhite.test( data ) ) { - // We use execScript on Internet Explorer - // We use an anonymous function so that context is window - // rather than jQuery in Firefox - ( window.execScript || function( data ) { - window[ "eval" ].call( window, data ); - } )( data ); - } - }, - - // Convert dashed to camelCase; used by the css and data modules - // Microsoft forgot to hump their vendor prefix (#9572) - camelCase: function( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); - }, - - // args is for internal usage only - each: function( object, callback, args ) { - var name, i = 0, - length = object.length, - isObj = length === undefined || jQuery.isFunction( object ); - - if ( args ) { - if ( isObj ) { - for ( name in object ) { - if ( callback.apply( object[ name ], args ) === false ) { - break; - } - } - } else { - for ( ; i < length; ) { - if ( callback.apply( object[ i++ ], args ) === false ) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if ( isObj ) { - for ( name in object ) { - if ( callback.call( object[ name ], name, object[ name ] ) === false ) { - break; - } - } - } else { - for ( ; i < length; ) { - if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { - break; - } - } - } - } - - return object; - }, - - // Use native String.trim function wherever possible - trim: trim ? - function( text ) { - return text == null ? - "" : - trim.call( text ); - } : - - // Otherwise use our own trimming functionality - function( text ) { - return text == null ? - "" : - text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); - }, - - // results is for internal usage only - makeArray: function( array, results ) { - var ret = results || []; - - if ( array != null ) { - // The window, strings (and functions) also have 'length' - // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 - var type = jQuery.type( array ); - - if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { - push.call( ret, array ); - } else { - jQuery.merge( ret, array ); - } - } - - return ret; - }, - - inArray: function( elem, array, i ) { - var len; - - if ( array ) { - if ( indexOf ) { - return indexOf.call( array, elem, i ); - } - - len = array.length; - i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; - - for ( ; i < len; i++ ) { - // Skip accessing in sparse arrays - if ( i in array && array[ i ] === elem ) { - return i; - } - } - } - - return -1; - }, - - merge: function( first, second ) { - var i = first.length, - j = 0; - - if ( typeof second.length === "number" ) { - for ( var l = second.length; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, inv ) { - var ret = [], retVal; - inv = !!inv; - - // Go through the array, only saving the items - // that pass the validator function - for ( var i = 0, length = elems.length; i < length; i++ ) { - retVal = !!callback( elems[ i ], i ); - if ( inv !== retVal ) { - ret.push( elems[ i ] ); - } - } - - return ret; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var value, key, ret = [], - i = 0, - length = elems.length, - // jquery objects are treated as arrays - isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; - - // Go through the array, translating each of the items to their - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - - // Go through every key on the object, - } else { - for ( key in elems ) { - value = callback( elems[ key ], key, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - } - - // Flatten any nested arrays - return ret.concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function( fn, context ) { - if ( typeof context === "string" ) { - var tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !jQuery.isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - var args = slice.call( arguments, 2 ), - proxy = function() { - return fn.apply( context, args.concat( slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - - return proxy; - }, - - // Mutifunctional method to get and set values to a collection - // The value/s can optionally be executed if it's a function - access: function( elems, fn, key, value, chainable, emptyGet, pass ) { - var exec, - bulk = key == null, - i = 0, - length = elems.length; - - // Sets many values - if ( key && typeof key === "object" ) { - for ( i in key ) { - jQuery.access( elems, fn, i, key[i], 1, emptyGet, value ); - } - chainable = 1; - - // Sets one value - } else if ( value !== undefined ) { - // Optionally, function values get executed if exec is true - exec = pass === undefined && jQuery.isFunction( value ); - - if ( bulk ) { - // Bulk operations only iterate when executing function values - if ( exec ) { - exec = fn; - fn = function( elem, key, value ) { - return exec.call( jQuery( elem ), value ); - }; - - // Otherwise they run against the entire set - } else { - fn.call( elems, value ); - fn = null; - } - } - - if ( fn ) { - for (; i < length; i++ ) { - fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); - } - } - - chainable = 1; - } - - return chainable ? - elems : - - // Gets - bulk ? - fn.call( elems ) : - length ? fn( elems[0], key ) : emptyGet; - }, - - now: function() { - return ( new Date() ).getTime(); - }, - - // Use of jQuery.browser is frowned upon. - // More details: http://docs.jquery.com/Utilities/jQuery.browser - uaMatch: function( ua ) { - ua = ua.toLowerCase(); - - var match = rwebkit.exec( ua ) || - ropera.exec( ua ) || - rmsie.exec( ua ) || - ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || - []; - - return { browser: match[1] || "", version: match[2] || "0" }; - }, - - sub: function() { - function jQuerySub( selector, context ) { - return new jQuerySub.fn.init( selector, context ); - } - jQuery.extend( true, jQuerySub, this ); - jQuerySub.superclass = this; - jQuerySub.fn = jQuerySub.prototype = this(); - jQuerySub.fn.constructor = jQuerySub; - jQuerySub.sub = this.sub; - jQuerySub.fn.init = function init( selector, context ) { - if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { - context = jQuerySub( context ); - } - - return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); - }; - jQuerySub.fn.init.prototype = jQuerySub.fn; - var rootjQuerySub = jQuerySub(document); - return jQuerySub; - }, - - browser: {} -}); - -// Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -browserMatch = jQuery.uaMatch( userAgent ); -if ( browserMatch.browser ) { - jQuery.browser[ browserMatch.browser ] = true; - jQuery.browser.version = browserMatch.version; -} - -// Deprecated, use jQuery.browser.webkit instead -if ( jQuery.browser.webkit ) { - jQuery.browser.safari = true; -} - -// IE doesn't match non-breaking spaces with \s -if ( rnotwhite.test( "\xA0" ) ) { - trimLeft = /^[\s\xA0]+/; - trimRight = /[\s\xA0]+$/; -} - -// All jQuery objects should point back to these -rootjQuery = jQuery(document); - -// Cleanup functions for the document ready method -if ( document.addEventListener ) { - DOMContentLoaded = function() { - document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - jQuery.ready(); - }; - -} else if ( document.attachEvent ) { - DOMContentLoaded = function() { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( document.readyState === "complete" ) { - document.detachEvent( "onreadystatechange", DOMContentLoaded ); - jQuery.ready(); - } - }; -} - -// The DOM ready check for Internet Explorer -function doScrollCheck() { - if ( jQuery.isReady ) { - return; - } - - try { - // If IE is used, use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - document.documentElement.doScroll("left"); - } catch(e) { - setTimeout( doScrollCheck, 1 ); - return; - } - - // and execute any waiting functions - jQuery.ready(); -} - -return jQuery; - -})(); - - -// String to Object flags format cache -var flagsCache = {}; - -// Convert String-formatted flags into Object-formatted ones and store in cache -function createFlags( flags ) { - var object = flagsCache[ flags ] = {}, - i, length; - flags = flags.split( /\s+/ ); - for ( i = 0, length = flags.length; i < length; i++ ) { - object[ flags[i] ] = true; - } - return object; -} - -/* - * Create a callback list using the following parameters: - * - * flags: an optional list of space-separated flags that will change how - * the callback list behaves - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible flags: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( flags ) { - - // Convert flags from String-formatted to Object-formatted - // (we check in cache first) - flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; - - var // Actual callback list - list = [], - // Stack of fire calls for repeatable lists - stack = [], - // Last fire value (for non-forgettable lists) - memory, - // Flag to know if list was already fired - fired, - // Flag to know if list is currently firing - firing, - // First callback to fire (used internally by add and fireWith) - firingStart, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // Add one or several callbacks to the list - add = function( args ) { - var i, - length, - elem, - type, - actual; - for ( i = 0, length = args.length; i < length; i++ ) { - elem = args[ i ]; - type = jQuery.type( elem ); - if ( type === "array" ) { - // Inspect recursively - add( elem ); - } else if ( type === "function" ) { - // Add if not in unique mode and callback is not in - if ( !flags.unique || !self.has( elem ) ) { - list.push( elem ); - } - } - } - }, - // Fire callbacks - fire = function( context, args ) { - args = args || []; - memory = !flags.memory || [ context, args ]; - fired = true; - firing = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - for ( ; list && firingIndex < firingLength; firingIndex++ ) { - if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { - memory = true; // Mark as halted - break; - } - } - firing = false; - if ( list ) { - if ( !flags.once ) { - if ( stack && stack.length ) { - memory = stack.shift(); - self.fireWith( memory[ 0 ], memory[ 1 ] ); - } - } else if ( memory === true ) { - self.disable(); - } else { - list = []; - } - } - }, - // Actual Callbacks object - self = { - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - var length = list.length; - add( arguments ); - // Do we need to add the callbacks to the - // current firing batch? - if ( firing ) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away, unless previous - // firing was halted (stopOnFalse) - } else if ( memory && memory !== true ) { - firingStart = length; - fire( memory[ 0 ], memory[ 1 ] ); - } - } - return this; - }, - // Remove a callback from the list - remove: function() { - if ( list ) { - var args = arguments, - argIndex = 0, - argLength = args.length; - for ( ; argIndex < argLength ; argIndex++ ) { - for ( var i = 0; i < list.length; i++ ) { - if ( args[ argIndex ] === list[ i ] ) { - // Handle firingIndex and firingLength - if ( firing ) { - if ( i <= firingLength ) { - firingLength--; - if ( i <= firingIndex ) { - firingIndex--; - } - } - } - // Remove the element - list.splice( i--, 1 ); - // If we have some unicity property then - // we only need to do this once - if ( flags.unique ) { - break; - } - } - } - } - } - return this; - }, - // Control if a given callback is in the list - has: function( fn ) { - if ( list ) { - var i = 0, - length = list.length; - for ( ; i < length; i++ ) { - if ( fn === list[ i ] ) { - return true; - } - } - } - return false; - }, - // Remove all callbacks from the list - empty: function() { - list = []; - return this; - }, - // Have the list do nothing anymore - disable: function() { - list = stack = memory = undefined; - return this; - }, - // Is it disabled? - disabled: function() { - return !list; - }, - // Lock the list in its current state - lock: function() { - stack = undefined; - if ( !memory || memory === true ) { - self.disable(); - } - return this; - }, - // Is it locked? - locked: function() { - return !stack; - }, - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( stack ) { - if ( firing ) { - if ( !flags.once ) { - stack.push( [ context, args ] ); - } - } else if ( !( flags.once && memory ) ) { - fire( context, args ); - } - } - return this; - }, - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; - - - - -var // Static reference to slice - sliceDeferred = [].slice; - -jQuery.extend({ - - Deferred: function( func ) { - var doneList = jQuery.Callbacks( "once memory" ), - failList = jQuery.Callbacks( "once memory" ), - progressList = jQuery.Callbacks( "memory" ), - state = "pending", - lists = { - resolve: doneList, - reject: failList, - notify: progressList - }, - promise = { - done: doneList.add, - fail: failList.add, - progress: progressList.add, - - state: function() { - return state; - }, - - // Deprecated - isResolved: doneList.fired, - isRejected: failList.fired, - - then: function( doneCallbacks, failCallbacks, progressCallbacks ) { - deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); - return this; - }, - always: function() { - deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); - return this; - }, - pipe: function( fnDone, fnFail, fnProgress ) { - return jQuery.Deferred(function( newDefer ) { - jQuery.each( { - done: [ fnDone, "resolve" ], - fail: [ fnFail, "reject" ], - progress: [ fnProgress, "notify" ] - }, function( handler, data ) { - var fn = data[ 0 ], - action = data[ 1 ], - returned; - if ( jQuery.isFunction( fn ) ) { - deferred[ handler ](function() { - returned = fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); - } else { - newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); - } - }); - } else { - deferred[ handler ]( newDefer[ action ] ); - } - }); - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - if ( obj == null ) { - obj = promise; - } else { - for ( var key in promise ) { - obj[ key ] = promise[ key ]; - } - } - return obj; - } - }, - deferred = promise.promise({}), - key; - - for ( key in lists ) { - deferred[ key ] = lists[ key ].fire; - deferred[ key + "With" ] = lists[ key ].fireWith; - } - - // Handle state - deferred.done( function() { - state = "resolved"; - }, failList.disable, progressList.lock ).fail( function() { - state = "rejected"; - }, doneList.disable, progressList.lock ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( firstParam ) { - var args = sliceDeferred.call( arguments, 0 ), - i = 0, - length = args.length, - pValues = new Array( length ), - count = length, - pCount = length, - deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? - firstParam : - jQuery.Deferred(), - promise = deferred.promise(); - function resolveFunc( i ) { - return function( value ) { - args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; - if ( !( --count ) ) { - deferred.resolveWith( deferred, args ); - } - }; - } - function progressFunc( i ) { - return function( value ) { - pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; - deferred.notifyWith( promise, pValues ); - }; - } - if ( length > 1 ) { - for ( ; i < length; i++ ) { - if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { - args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); - } else { - --count; - } - } - if ( !count ) { - deferred.resolveWith( deferred, args ); - } - } else if ( deferred !== firstParam ) { - deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); - } - return promise; - } -}); - - - - -jQuery.support = (function() { - - var support, - all, - a, - select, - opt, - input, - fragment, - tds, - events, - eventName, - i, - isSupported, - div = document.createElement( "div" ), - documentElement = document.documentElement; - - // Preliminary tests - div.setAttribute("className", "t"); - div.innerHTML = "
    a"; - - all = div.getElementsByTagName( "*" ); - a = div.getElementsByTagName( "a" )[ 0 ]; - - // Can't get basic test support - if ( !all || !all.length || !a ) { - return {}; - } - - // First batch of supports tests - select = document.createElement( "select" ); - opt = select.appendChild( document.createElement("option") ); - input = div.getElementsByTagName( "input" )[ 0 ]; - - support = { - // IE strips leading whitespace when .innerHTML is used - leadingWhitespace: ( div.firstChild.nodeType === 3 ), - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - tbody: !div.getElementsByTagName("tbody").length, - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - htmlSerialize: !!div.getElementsByTagName("link").length, - - // Get the style information from getAttribute - // (IE uses .cssText instead) - style: /top/.test( a.getAttribute("style") ), - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - hrefNormalized: ( a.getAttribute("href") === "/a" ), - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.55/.test( a.style.opacity ), - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - cssFloat: !!a.style.cssFloat, - - // Make sure that if no value is specified for a checkbox - // that it defaults to "on". - // (WebKit defaults to "" instead) - checkOn: ( input.value === "on" ), - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - optSelected: opt.selected, - - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - getSetAttribute: div.className !== "t", - - // Tests for enctype support on a form(#6743) - enctype: !!document.createElement("form").enctype, - - // Makes sure cloning an html5 element does not cause problems - // Where outerHTML is undefined, this still works - html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", - - // Will be defined later - submitBubbles: true, - changeBubbles: true, - focusinBubbles: false, - deleteExpando: true, - noCloneEvent: true, - inlineBlockNeedsLayout: false, - shrinkWrapBlocks: false, - reliableMarginRight: true, - pixelMargin: true - }; - - // jQuery.boxModel DEPRECATED in 1.3, use jQuery.support.boxModel instead - jQuery.boxModel = support.boxModel = (document.compatMode === "CSS1Compat"); - - // Make sure checked status is properly cloned - input.checked = true; - support.noCloneChecked = input.cloneNode( true ).checked; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; - - // Test to see if it's possible to delete an expando from an element - // Fails in Internet Explorer - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; - } - - if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { - div.attachEvent( "onclick", function() { - // Cloning a node shouldn't copy over any - // bound event handlers (IE does this) - support.noCloneEvent = false; - }); - div.cloneNode( true ).fireEvent( "onclick" ); - } - - // Check if a radio maintains its value - // after being appended to the DOM - input = document.createElement("input"); - input.value = "t"; - input.setAttribute("type", "radio"); - support.radioValue = input.value === "t"; - - input.setAttribute("checked", "checked"); - - // #11217 - WebKit loses check when the name is after the checked attribute - input.setAttribute( "name", "t" ); - - div.appendChild( input ); - fragment = document.createDocumentFragment(); - fragment.appendChild( div.lastChild ); - - // WebKit doesn't clone checked state correctly in fragments - support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - support.appendChecked = input.checked; - - fragment.removeChild( input ); - fragment.appendChild( div ); - - // Technique from Juriy Zaytsev - // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ - // We only care about the case where non-standard event systems - // are used, namely in IE. Short-circuiting here helps us to - // avoid an eval call (in setAttribute) which can cause CSP - // to go haywire. See: https://developer.mozilla.org/en/Security/CSP - if ( div.attachEvent ) { - for ( i in { - submit: 1, - change: 1, - focusin: 1 - }) { - eventName = "on" + i; - isSupported = ( eventName in div ); - if ( !isSupported ) { - div.setAttribute( eventName, "return;" ); - isSupported = ( typeof div[ eventName ] === "function" ); - } - support[ i + "Bubbles" ] = isSupported; - } - } - - fragment.removeChild( div ); - - // Null elements to avoid leaks in IE - fragment = select = opt = div = input = null; - - // Run tests that need a body at doc ready - jQuery(function() { - var container, outer, inner, table, td, offsetSupport, - marginDiv, conMarginTop, style, html, positionTopLeftWidthHeight, - paddingMarginBorderVisibility, paddingMarginBorder, - body = document.getElementsByTagName("body")[0]; - - if ( !body ) { - // Return for frameset docs that don't have a body - return; - } - - conMarginTop = 1; - paddingMarginBorder = "padding:0;margin:0;border:"; - positionTopLeftWidthHeight = "position:absolute;top:0;left:0;width:1px;height:1px;"; - paddingMarginBorderVisibility = paddingMarginBorder + "0;visibility:hidden;"; - style = "style='" + positionTopLeftWidthHeight + paddingMarginBorder + "5px solid #000;"; - html = "
    " + - "" + - "
    "; - - container = document.createElement("div"); - container.style.cssText = paddingMarginBorderVisibility + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; - body.insertBefore( container, body.firstChild ); - - // Construct the test element - div = document.createElement("div"); - container.appendChild( div ); - - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - // (only IE 8 fails this test) - div.innerHTML = "
    t
    "; - tds = div.getElementsByTagName( "td" ); - isSupported = ( tds[ 0 ].offsetHeight === 0 ); - - tds[ 0 ].style.display = ""; - tds[ 1 ].style.display = "none"; - - // Check if empty table cells still have offsetWidth/Height - // (IE <= 8 fail this test) - support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); - - // Check if div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container. For more - // info see bug #3333 - // Fails in WebKit before Feb 2011 nightlies - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - if ( window.getComputedStyle ) { - div.innerHTML = ""; - marginDiv = document.createElement( "div" ); - marginDiv.style.width = "0"; - marginDiv.style.marginRight = "0"; - div.style.width = "2px"; - div.appendChild( marginDiv ); - support.reliableMarginRight = - ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; - } - - if ( typeof div.style.zoom !== "undefined" ) { - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - // (IE < 8 does this) - div.innerHTML = ""; - div.style.width = div.style.padding = "1px"; - div.style.border = 0; - div.style.overflow = "hidden"; - div.style.display = "inline"; - div.style.zoom = 1; - support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); - - // Check if elements with layout shrink-wrap their children - // (IE 6 does this) - div.style.display = "block"; - div.style.overflow = "visible"; - div.innerHTML = "
    "; - support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); - } - - div.style.cssText = positionTopLeftWidthHeight + paddingMarginBorderVisibility; - div.innerHTML = html; - - outer = div.firstChild; - inner = outer.firstChild; - td = outer.nextSibling.firstChild.firstChild; - - offsetSupport = { - doesNotAddBorder: ( inner.offsetTop !== 5 ), - doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) - }; - - inner.style.position = "fixed"; - inner.style.top = "20px"; - - // safari subtracts parent border width here which is 5px - offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); - inner.style.position = inner.style.top = ""; - - outer.style.overflow = "hidden"; - outer.style.position = "relative"; - - offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); - offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); - - if ( window.getComputedStyle ) { - div.style.marginTop = "1%"; - support.pixelMargin = ( window.getComputedStyle( div, null ) || { marginTop: 0 } ).marginTop !== "1%"; - } - - if ( typeof container.style.zoom !== "undefined" ) { - container.style.zoom = 1; - } - - body.removeChild( container ); - marginDiv = div = container = null; - - jQuery.extend( support, offsetSupport ); - }); - - return support; -})(); - - - - -var rbrace = /^(?:\{.*\}|\[.*\])$/, - rmultiDash = /([A-Z])/g; - -jQuery.extend({ - cache: {}, - - // Please use with caution - uuid: 0, - - // Unique for each copy of jQuery on the page - // Non-digits removed to match rinlinejQuery - expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), - - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "embed": true, - // Ban all objects except for Flash (which handle expandos) - "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", - "applet": true - }, - - hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - return !!elem && !isEmptyDataObject( elem ); - }, - - data: function( elem, name, data, pvt /* Internal Use Only */ ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var privateCache, thisCache, ret, - internalKey = jQuery.expando, - getByName = typeof name === "string", - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, - - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, - isEvents = name === "events"; - - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { - return; - } - - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - elem[ internalKey ] = id = ++jQuery.uuid; - } else { - id = internalKey; - } - } - - if ( !cache[ id ] ) { - cache[ id ] = {}; - - // Avoids exposing jQuery metadata on plain JS objects when the object - // is serialized using JSON.stringify - if ( !isNode ) { - cache[ id ].toJSON = jQuery.noop; - } - } - - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" || typeof name === "function" ) { - if ( pvt ) { - cache[ id ] = jQuery.extend( cache[ id ], name ); - } else { - cache[ id ].data = jQuery.extend( cache[ id ].data, name ); - } - } - - privateCache = thisCache = cache[ id ]; - - // jQuery data() is stored in a separate object inside the object's internal data - // cache in order to avoid key collisions between internal data and user-defined - // data. - if ( !pvt ) { - if ( !thisCache.data ) { - thisCache.data = {}; - } - - thisCache = thisCache.data; - } - - if ( data !== undefined ) { - thisCache[ jQuery.camelCase( name ) ] = data; - } - - // Users should not attempt to inspect the internal events object using jQuery.data, - // it is undocumented and subject to change. But does anyone listen? No. - if ( isEvents && !thisCache[ name ] ) { - return privateCache.events; - } - - // Check for both converted-to-camel and non-converted data property names - // If a data property was specified - if ( getByName ) { - - // First Try to find as-is property data - ret = thisCache[ name ]; - - // Test for null|undefined property data - if ( ret == null ) { - - // Try to find the camelCased property - ret = thisCache[ jQuery.camelCase( name ) ]; - } - } else { - ret = thisCache; - } - - return ret; - }, - - removeData: function( elem, name, pvt /* Internal Use Only */ ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var thisCache, i, l, - - // Reference to internal data cache key - internalKey = jQuery.expando, - - isNode = elem.nodeType, - - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - - // See jQuery.data for more information - id = isNode ? elem[ internalKey ] : internalKey; - - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } - - if ( name ) { - - thisCache = pvt ? cache[ id ] : cache[ id ].data; - - if ( thisCache ) { - - // Support array or space separated string names for data keys - if ( !jQuery.isArray( name ) ) { - - // try the string as a key before any manipulation - if ( name in thisCache ) { - name = [ name ]; - } else { - - // split the camel cased version by spaces unless a key with the spaces exists - name = jQuery.camelCase( name ); - if ( name in thisCache ) { - name = [ name ]; - } else { - name = name.split( " " ); - } - } - } - - for ( i = 0, l = name.length; i < l; i++ ) { - delete thisCache[ name[i] ]; - } - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { - return; - } - } - } - - // See jQuery.data for more information - if ( !pvt ) { - delete cache[ id ].data; - - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !isEmptyDataObject(cache[ id ]) ) { - return; - } - } - - // Browsers that fail expando deletion also refuse to delete expandos on - // the window, but it will allow it on all other JS objects; other browsers - // don't care - // Ensure that `cache` is not a window object #10080 - if ( jQuery.support.deleteExpando || !cache.setInterval ) { - delete cache[ id ]; - } else { - cache[ id ] = null; - } - - // We destroyed the cache and need to eliminate the expando on the node to avoid - // false lookups in the cache for entries that no longer exist - if ( isNode ) { - // IE does not allow us to delete expando properties from nodes, - // nor does it have a removeAttribute function on Document nodes; - // we must handle all of these cases - if ( jQuery.support.deleteExpando ) { - delete elem[ internalKey ]; - } else if ( elem.removeAttribute ) { - elem.removeAttribute( internalKey ); - } else { - elem[ internalKey ] = null; - } - } - }, - - // For internal use only. - _data: function( elem, name, data ) { - return jQuery.data( elem, name, data, true ); - }, - - // A method for determining if a DOM node can handle the data expando - acceptData: function( elem ) { - if ( elem.nodeName ) { - var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; - - if ( match ) { - return !(match === true || elem.getAttribute("classid") !== match); - } - } - - return true; - } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - var parts, part, attr, name, l, - elem = this[0], - i = 0, - data = null; - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = jQuery.data( elem ); - - if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { - attr = elem.attributes; - for ( l = attr.length; i < l; i++ ) { - name = attr[i].name; - - if ( name.indexOf( "data-" ) === 0 ) { - name = jQuery.camelCase( name.substring(5) ); - - dataAttr( elem, name, data[ name ] ); - } - } - jQuery._data( elem, "parsedAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); - } - - parts = key.split( ".", 2 ); - parts[1] = parts[1] ? "." + parts[1] : ""; - part = parts[1] + "!"; - - return jQuery.access( this, function( value ) { - - if ( value === undefined ) { - data = this.triggerHandler( "getData" + part, [ parts[0] ] ); - - // Try to fetch any internally stored data first - if ( data === undefined && elem ) { - data = jQuery.data( elem, key ); - data = dataAttr( elem, key, data ); - } - - return data === undefined && parts[1] ? - this.data( parts[0] ) : - data; - } - - parts[1] = value; - this.each(function() { - var self = jQuery( this ); - - self.triggerHandler( "setData" + part, parts ); - jQuery.data( this, key, value ); - self.triggerHandler( "changeData" + part, parts ); - }); - }, null, value, arguments.length > 1, null, false ); - }, - - removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); - } -}); - -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - - var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - jQuery.isNumeric( data ) ? +data : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); - - } else { - data = undefined; - } - } - - return data; -} - -// checks a cache object for emptiness -function isEmptyDataObject( obj ) { - for ( var name in obj ) { - - // if the public data object is empty, the private is still empty - if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { - continue; - } - if ( name !== "toJSON" ) { - return false; - } - } - - return true; -} - - - - -function handleQueueMarkDefer( elem, type, src ) { - var deferDataKey = type + "defer", - queueDataKey = type + "queue", - markDataKey = type + "mark", - defer = jQuery._data( elem, deferDataKey ); - if ( defer && - ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && - ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { - // Give room for hard-coded callbacks to fire first - // and eventually mark/queue something else on the element - setTimeout( function() { - if ( !jQuery._data( elem, queueDataKey ) && - !jQuery._data( elem, markDataKey ) ) { - jQuery.removeData( elem, deferDataKey, true ); - defer.fire(); - } - }, 0 ); - } -} - -jQuery.extend({ - - _mark: function( elem, type ) { - if ( elem ) { - type = ( type || "fx" ) + "mark"; - jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); - } - }, - - _unmark: function( force, elem, type ) { - if ( force !== true ) { - type = elem; - elem = force; - force = false; - } - if ( elem ) { - type = type || "fx"; - var key = type + "mark", - count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); - if ( count ) { - jQuery._data( elem, key, count ); - } else { - jQuery.removeData( elem, key, true ); - handleQueueMarkDefer( elem, type, "mark" ); - } - } - }, - - queue: function( elem, type, data ) { - var q; - if ( elem ) { - type = ( type || "fx" ) + "queue"; - q = jQuery._data( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !q || jQuery.isArray(data) ) { - q = jQuery._data( elem, type, jQuery.makeArray(data) ); - } else { - q.push( data ); - } - } - return q || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - fn = queue.shift(), - hooks = {}; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - } - - if ( fn ) { - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - jQuery._data( elem, type + ".run", hooks ); - fn.call( elem, function() { - jQuery.dequeue( elem, type ); - }, hooks ); - } - - if ( !queue.length ) { - jQuery.removeData( elem, type + "queue " + type + ".run", true ); - handleQueueMarkDefer( elem, type, "queue" ); - } - } -}); - -jQuery.fn.extend({ - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); - } - - return data === undefined ? - this : - this.each(function() { - var queue = jQuery.queue( this, type, data ); - - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = setTimeout( next, time ); - hooks.stop = function() { - clearTimeout( timeout ); - }; - }); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, object ) { - if ( typeof type !== "string" ) { - object = type; - type = undefined; - } - type = type || "fx"; - var defer = jQuery.Deferred(), - elements = this, - i = elements.length, - count = 1, - deferDataKey = type + "defer", - queueDataKey = type + "queue", - markDataKey = type + "mark", - tmp; - function resolve() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - } - while( i-- ) { - if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || - ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || - jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && - jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { - count++; - tmp.add( resolve ); - } - } - resolve(); - return defer.promise( object ); - } -}); - - - - -var rclass = /[\n\t\r]/g, - rspace = /\s+/, - rreturn = /\r/g, - rtype = /^(?:button|input)$/i, - rfocusable = /^(?:button|input|object|select|textarea)$/i, - rclickable = /^a(?:rea)?$/i, - rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, - getSetAttribute = jQuery.support.getSetAttribute, - nodeHook, boolHook, fixSpecified; - -jQuery.fn.extend({ - attr: function( name, value ) { - return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each(function() { - jQuery.removeAttr( this, name ); - }); - }, - - prop: function( name, value ) { - return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - name = jQuery.propFix[ name ] || name; - return this.each(function() { - // try/catch handles cases where IE balks (such as removing a property on window) - try { - this[ name ] = undefined; - delete this[ name ]; - } catch( e ) {} - }); - }, - - addMethod: function( value ) { - var classNames, i, l, elem, - setMethod, c, cl; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).addMethod( value.call(this, j, this.className) ); - }); - } - - if ( value && typeof value === "string" ) { - classNames = value.split( rspace ); - - for ( i = 0, l = this.length; i < l; i++ ) { - elem = this[ i ]; - - if ( elem.nodeType === 1 ) { - if ( !elem.className && classNames.length === 1 ) { - elem.className = value; - - } else { - setMethod = " " + elem.className + " "; - - for ( c = 0, cl = classNames.length; c < cl; c++ ) { - if ( !~setMethod.indexOf( " " + classNames[ c ] + " " ) ) { - setMethod += classNames[ c ] + " "; - } - } - elem.className = jQuery.trim( setMethod ); - } - } - } - } - - return this; - }, - - removeMethod: function( value ) { - var classNames, i, l, elem, className, c, cl; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).removeMethod( value.call(this, j, this.className) ); - }); - } - - if ( (value && typeof value === "string") || value === undefined ) { - classNames = ( value || "" ).split( rspace ); - - for ( i = 0, l = this.length; i < l; i++ ) { - elem = this[ i ]; - - if ( elem.nodeType === 1 && elem.className ) { - if ( value ) { - className = (" " + elem.className + " ").replace( rclass, " " ); - for ( c = 0, cl = classNames.length; c < cl; c++ ) { - className = className.replace(" " + classNames[ c ] + " ", " "); - } - elem.className = jQuery.trim( className ); - - } else { - elem.className = ""; - } - } - } - } - - return this; - }, - - toggleMethod: function( value, stateVal ) { - var type = typeof value, - isBool = typeof stateVal === "boolean"; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( i ) { - jQuery( this ).toggleMethod( value.call(this, i, this.className, stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - state = stateVal, - classNames = value.split( rspace ); - - while ( (className = classNames[ i++ ]) ) { - // check each className given, space seperated list - state = isBool ? state : !self.hasMethod( className ); - self[ state ? "addMethod" : "removeMethod" ]( className ); - } - - } else if ( type === "undefined" || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery._data( this, "__className__", this.className ); - } - - // toggle whole className - this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; - } - }); - }, - - hasMethod: function( selector ) { - var className = " " + selector + " ", - i = 0, - l = this.length; - for ( ; i < l; i++ ) { - if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { - return true; - } - } - - return false; - }, - - val: function( value ) { - var hooks, ret, isFunction, - elem = this[0]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { - return ret; - } - - ret = elem.value; - - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; - } - - return; - } - - isFunction = jQuery.isFunction( value ); - - return this.each(function( i ) { - var self = jQuery(this), val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call( this, i, self.val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map(val, function ( value ) { - return value == null ? "" : value + ""; - }); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - }); - } -}); - -jQuery.extend({ - valHooks: { - option: { - get: function( elem ) { - // attributes.value is undefined in Blackberry 4.7 but - // uses .value. See #6932 - var val = elem.attributes.value; - return !val || val.specified ? elem.value : elem.text; - } - }, - select: { - get: function( elem ) { - var value, i, max, option, - index = elem.selectedIndex, - values = [], - options = elem.options, - one = elem.type === "select-one"; - - // Nothing was selected - if ( index < 0 ) { - return null; - } - - // Loop through all the selected options - i = one ? index : 0; - max = one ? index + 1 : options.length; - for ( ; i < max; i++ ) { - option = options[ i ]; - - // Don't return options that are disabled or in a disabled optgroup - if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && - (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - // Fixes Bug #2551 -- select.val() broken in IE after form.reset() - if ( one && !values.length && options.length ) { - return jQuery( options[ index ] ).val(); - } - - return values; - }, - - set: function( elem, value ) { - var values = jQuery.makeArray( value ); - - jQuery(elem).find("option").each(function() { - this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; - }); - - if ( !values.length ) { - elem.selectedIndex = -1; - } - return values; - } - } - }, - - attrFn: { - val: true, - css: true, - html: true, - text: true, - data: true, - width: true, - height: true, - offset: true - }, - - attr: function( elem, name, value, pass ) { - var ret, hooks, notxml, - nType = elem.nodeType; - - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - if ( pass && name in jQuery.attrFn ) { - return jQuery( elem )[ name ]( value ); - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === "undefined" ) { - return jQuery.prop( elem, name, value ); - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - // All attributes are lowercase - // Grab necessary hook if one is defined - if ( notxml ) { - name = name.toLowerCase(); - hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); - } - - if ( value !== undefined ) { - - if ( value === null ) { - jQuery.removeAttr( elem, name ); - return; - - } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - elem.setAttribute( name, "" + value ); - return value; - } - - } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - - ret = elem.getAttribute( name ); - - // Non-existent attributes return null, we normalize to undefined - return ret === null ? - undefined : - ret; - } - }, - - removeAttr: function( elem, value ) { - var propName, attrNames, name, l, isBool, - i = 0; - - if ( value && elem.nodeType === 1 ) { - attrNames = value.toLowerCase().split( rspace ); - l = attrNames.length; - - for ( ; i < l; i++ ) { - name = attrNames[ i ]; - - if ( name ) { - propName = jQuery.propFix[ name ] || name; - isBool = rboolean.test( name ); - - // See #9699 for explanation of this approach (setting first, then removal) - // Do not do this for boolean attributes (see #10870) - if ( !isBool ) { - jQuery.attr( elem, name, "" ); - } - elem.removeAttribute( getSetAttribute ? name : propName ); - - // Set corresponding property to false for boolean attributes - if ( isBool && propName in elem ) { - elem[ propName ] = false; - } - } - } - } - }, - - attrHooks: { - type: { - set: function( elem, value ) { - // We can't allow the type property to be changed (since it causes problems in IE) - if ( rtype.test( elem.nodeName ) && elem.parentNode ) { - jQuery.error( "type property can't be changed" ); - } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to it's default in case type is set after value - // This is for element creation - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - }, - // Use the value property for back compat - // Use the nodeHook for button elements in IE6/7 (#1954) - value: { - get: function( elem, name ) { - if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { - return nodeHook.get( elem, name ); - } - return name in elem ? - elem.value : - null; - }, - set: function( elem, value, name ) { - if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { - return nodeHook.set( elem, value, name ); - } - // Does not return so that setAttribute is also used - elem.value = value; - } - } - }, - - propFix: { - tabindex: "tabIndex", - readonly: "readOnly", - "for": "htmlFor", - "class": "className", - maxlength: "maxLength", - cellspacing: "cellSpacing", - cellpadding: "cellPadding", - rowspan: "rowSpan", - colspan: "colSpan", - usemap: "useMap", - frameborder: "frameBorder", - contenteditable: "contentEditable" - }, - - prop: function( elem, name, value ) { - var ret, hooks, notxml, - nType = elem.nodeType; - - // don't get/set properties on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - if ( notxml ) { - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - return ( elem[ name ] = value ); - } - - } else { - if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - return elem[ name ]; - } - } - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - var attributeNode = elem.getAttributeNode("tabindex"); - - return attributeNode && attributeNode.specified ? - parseInt( attributeNode.value, 10 ) : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; - } - } - } -}); - -// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) -jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; - -// Hook for boolean attributes -boolHook = { - get: function( elem, name ) { - // Align boolean attributes with corresponding properties - // Fall back to attribute presence where some booleans are not supported - var attrNode, - property = jQuery.prop( elem, name ); - return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? - name.toLowerCase() : - undefined; - }, - set: function( elem, value, name ) { - var propName; - if ( value === false ) { - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else { - // value is true since we know at this point it's type boolean and not false - // Set boolean attributes to the same name and set the DOM property - propName = jQuery.propFix[ name ] || name; - if ( propName in elem ) { - // Only set the IDL specifically if it already exists on the element - elem[ propName ] = true; - } - - elem.setAttribute( name, name.toLowerCase() ); - } - return name; - } -}; - -// IE6/7 do not support getting/setting some attributes with get/setAttribute -if ( !getSetAttribute ) { - - fixSpecified = { - name: true, - id: true, - coords: true - }; - - // Use this for any attribute in IE6/7 - // This fixes almost every IE6/7 issue - nodeHook = jQuery.valHooks.button = { - get: function( elem, name ) { - var ret; - ret = elem.getAttributeNode( name ); - return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? - ret.nodeValue : - undefined; - }, - set: function( elem, value, name ) { - // Set the existing or create a new attribute node - var ret = elem.getAttributeNode( name ); - if ( !ret ) { - ret = document.createAttribute( name ); - elem.setAttributeNode( ret ); - } - return ( ret.nodeValue = value + "" ); - } - }; - - // Apply the nodeHook to tabindex - jQuery.attrHooks.tabindex.set = nodeHook.set; - - // Set width and height to auto instead of 0 on empty string( Bug #8150 ) - // This is for removals - jQuery.each([ "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - set: function( elem, value ) { - if ( value === "" ) { - elem.setAttribute( name, "auto" ); - return value; - } - } - }); - }); - - // Set contenteditable to false on removals(#10429) - // Setting to empty string throws an error as an invalid value - jQuery.attrHooks.contenteditable = { - get: nodeHook.get, - set: function( elem, value, name ) { - if ( value === "" ) { - value = "false"; - } - nodeHook.set( elem, value, name ); - } - }; -} - - -// Some attributes require a special call on IE -if ( !jQuery.support.hrefNormalized ) { - jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - get: function( elem ) { - var ret = elem.getAttribute( name, 2 ); - return ret === null ? undefined : ret; - } - }); - }); -} - -if ( !jQuery.support.style ) { - jQuery.attrHooks.style = { - get: function( elem ) { - // Return undefined in the case of empty string - // Normalize to lowercase since IE uppercases css property names - return elem.style.cssText.toLowerCase() || undefined; - }, - set: function( elem, value ) { - return ( elem.style.cssText = "" + value ); - } - }; -} - -// Safari mis-reports the default selected property of an option -// Accessing the parent's selectedIndex property fixes it -if ( !jQuery.support.optSelected ) { - jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { - get: function( elem ) { - var parent = elem.parentNode; - - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - return null; - } - }); -} - -// IE6/7 call enctype encoding -if ( !jQuery.support.enctype ) { - jQuery.propFix.enctype = "encoding"; -} - -// Radios and checkboxes getter/setter -if ( !jQuery.support.checkOn ) { - jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - get: function( elem ) { - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - } - }; - }); -} -jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); - } - } - }); -}); - - - - -var rformElems = /^(?:textarea|input|select)$/i, - rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, - rhoverHack = /(?:^|\s)hover(\.\S+)?\b/, - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, - quickParse = function( selector ) { - var quick = rquickIs.exec( selector ); - if ( quick ) { - // 0 1 2 3 - // [ _, tag, id, class ] - quick[1] = ( quick[1] || "" ).toLowerCase(); - quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); - } - return quick; - }, - quickIs = function( elem, m ) { - var attrs = elem.attributes || {}; - return ( - (!m[1] || elem.nodeName.toLowerCase() === m[1]) && - (!m[2] || (attrs.id || {}).value === m[2]) && - (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) - ); - }, - hoverHack = function( events ) { - return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); - }; - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - add: function( elem, types, handler, data, selector ) { - - var elemData, eventHandle, events, - t, tns, type, namespaces, handleObj, - handleObjIn, quick, handlers, special; - - // Don't attach events to noData or text/comment nodes (allow plain objects tho) - if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - events = elemData.events; - if ( !events ) { - elemData.events = events = {}; - } - eventHandle = elemData.handle; - if ( !eventHandle ) { - elemData.handle = eventHandle = function( e ) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : - undefined; - }; - // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events - eventHandle.elem = elem; - } - - // Handle multiple events separated by a space - // jQuery(...).bind("mouseover mouseout", fn); - types = jQuery.trim( hoverHack(types) ).split( " " ); - for ( t = 0; t < types.length; t++ ) { - - tns = rtypenamespace.exec( types[t] ) || []; - type = tns[1]; - namespaces = ( tns[2] || "" ).split( "." ).sort(); - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend({ - type: type, - origType: tns[1], - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - quick: selector && quickParse( selector ), - namespace: namespaces.join(".") - }, handleObjIn ); - - // Init the event handler queue if we're the first - handlers = events[ type ]; - if ( !handlers ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener/attachEvent if the special events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - global: {}, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - - var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), - t, tns, type, origType, namespaces, origCount, - j, events, special, handle, eventType, handleObj; - - if ( !elemData || !(events = elemData.events) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = jQuery.trim( hoverHack( types || "" ) ).split(" "); - for ( t = 0; t < types.length; t++ ) { - tns = rtypenamespace.exec( types[t] ) || []; - type = origType = tns[1]; - namespaces = tns[2]; - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector? special.delegateType : special.bindType ) || type; - eventType = events[ type ] || []; - origCount = eventType.length; - namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; - - // Remove matching events - for ( j = 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !namespaces || namespaces.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { - eventType.splice( j--, 1 ); - - if ( handleObj.selector ) { - eventType.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( eventType.length === 0 && origCount !== eventType.length ) { - if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - handle = elemData.handle; - if ( handle ) { - handle.elem = null; - } - - // removeData also checks for emptiness and clears the expando if empty - // so use it instead of delete - jQuery.removeData( elem, [ "events", "handle" ], true ); - } - }, - - // Events that are safe to short-circuit if no handlers are attached. - // Native DOM events should not be added, they may have inline handlers. - customEvent: { - "getData": true, - "setData": true, - "changeData": true - }, - - trigger: function( event, data, elem, onlyHandlers ) { - // Don't do events on text and comment nodes - if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { - return; - } - - // Event object or event type - var type = event.type || event, - namespaces = [], - cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf( "!" ) >= 0 ) { - // Exclusive events trigger only for the exact event (no namespaces) - type = type.slice(0, -1); - exclusive = true; - } - - if ( type.indexOf( "." ) >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - - if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { - // No jQuery handlers for this event type, and it can't have inline handlers - return; - } - - // Caller can pass in an Event, Object, or just an event type string - event = typeof event === "object" ? - // jQuery.Event object - event[ jQuery.expando ] ? event : - // Object literal - new jQuery.Event( type, event ) : - // Just the event type (string) - new jQuery.Event( type ); - - event.type = type; - event.isTrigger = true; - event.exclusive = exclusive; - event.namespace = namespaces.join( "." ); - event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; - ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; - - // Handle a global trigger - if ( !elem ) { - - // TODO: Stop taunting the data cache; remove global events and always attach to document - cache = jQuery.cache; - for ( i in cache ) { - if ( cache[ i ].events && cache[ i ].events[ type ] ) { - jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); - } - } - return; - } - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data != null ? jQuery.makeArray( data ) : []; - data.unshift( event ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - eventPath = [[ elem, special.bindType || type ]]; - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; - old = null; - for ( ; cur; cur = cur.parentNode ) { - eventPath.push([ cur, bubbleType ]); - old = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( old && old === elem.ownerDocument ) { - eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); - } - } - - // Fire handlers on the event path - for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { - - cur = eventPath[i][0]; - event.type = eventPath[i][1]; - - handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - // Note that this is a bare JS function and not a jQuery handler - handle = ontype && cur[ ontype ]; - if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { - event.preventDefault(); - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && - !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction() check here because IE6/7 fails that test. - // Don't do default actions on window, that's where global variables be (#6170) - // IE<9 dies on focus/blur to hidden element (#1486) - if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - old = elem[ ontype ]; - - if ( old ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - elem[ type ](); - jQuery.event.triggered = undefined; - - if ( old ) { - elem[ ontype ] = old; - } - } - } - } - - return event.result; - }, - - dispatch: function( event ) { - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event || window.event ); - - var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), - delegateCount = handlers.delegateCount, - args = [].slice.call( arguments, 0 ), - run_all = !event.exclusive && !event.namespace, - special = jQuery.event.special[ event.type ] || {}, - handlerQueue = [], - i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers that should run if there are delegated events - // Avoid non-left-click bubbling in Firefox (#3861) - if ( delegateCount && !(event.button && event.type === "click") ) { - - // Pregenerate a single jQuery object for reuse with .is() - jqcur = jQuery(this); - jqcur.context = this.ownerDocument || this; - - for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { - - // Don't process events on disabled elements (#6911, #8165) - if ( cur.disabled !== true ) { - selMatch = {}; - matches = []; - jqcur[0] = cur; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - sel = handleObj.selector; - - if ( selMatch[ sel ] === undefined ) { - selMatch[ sel ] = ( - handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) - ); - } - if ( selMatch[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push({ elem: cur, matches: matches }); - } - } - } - } - - // Add the remaining (directly-bound) handlers - if ( handlers.length > delegateCount ) { - handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); - } - - // Run delegates first; they may want to stop propagation beneath us - for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { - matched = handlerQueue[ i ]; - event.currentTarget = matched.elem; - - for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { - handleObj = matched.matches[ j ]; - - // Triggered event must either 1) be non-exclusive and have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). - if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { - - event.data = handleObj.data; - event.handleObj = handleObj; - - ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) - .apply( matched.elem, args ); - - if ( ret !== undefined ) { - event.result = ret; - if ( ret === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - // Includes some event props shared by KeyEvent and MouseEvent - // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** - props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), - - fixHooks: {}, - - keyHooks: { - props: "char charCode key keyCode".split(" "), - filter: function( event, original ) { - - // Add which for key events - if ( event.which == null ) { - event.which = original.charCode != null ? original.charCode : original.keyCode; - } - - return event; - } - }, - - mouseHooks: { - props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), - filter: function( event, original ) { - var eventDoc, doc, body, - button = original.button, - fromElement = original.fromElement; - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && original.clientX != null ) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; - - event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); - } - - // Add relatedTarget, if necessary - if ( !event.relatedTarget && fromElement ) { - event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && button !== undefined ) { - event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); - } - - return event; - } - }, - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, - originalEvent = event, - fixHook = jQuery.event.fixHooks[ event.type ] || {}, - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - - event = jQuery.Event( originalEvent ); - - for ( i = copy.length; i; ) { - prop = copy[ --i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) - if ( !event.target ) { - event.target = originalEvent.srcElement || document; - } - - // Target should not be a text node (#504, Safari) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) - if ( event.metaKey === undefined ) { - event.metaKey = event.ctrlKey; - } - - return fixHook.filter? fixHook.filter( event, originalEvent ) : event; - }, - - special: { - ready: { - // Make sure the ready event is setup - setup: jQuery.bindReady - }, - - load: { - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - - focus: { - delegateType: "focusin" - }, - blur: { - delegateType: "focusout" - }, - - beforeunload: { - setup: function( data, namespaces, eventHandle ) { - // We only want to do this special case on windows - if ( jQuery.isWindow( this ) ) { - this.onbeforeunload = eventHandle; - } - }, - - teardown: function( namespaces, eventHandle ) { - if ( this.onbeforeunload === eventHandle ) { - this.onbeforeunload = null; - } - } - } - }, - - simulate: function( type, elem, event, bubble ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, - { type: type, - isSimulated: true, - originalEvent: {} - } - ); - if ( bubble ) { - jQuery.event.trigger( e, null, elem ); - } else { - jQuery.event.dispatch.call( elem, e ); - } - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } - } -}; - -// Some plugins are using, but it's undocumented/deprecated and will be removed. -// The 1.7 special event interface should provide all the hooks needed now. -jQuery.event.handle = jQuery.event.dispatch; - -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } - } : - function( elem, type, handle ) { - if ( elem.detachEvent ) { - elem.detachEvent( "on" + type, handle ); - } - }; - -jQuery.Event = function( src, props ) { - // Allow instantiation without the 'new' keyword - if ( !(this instanceof jQuery.Event) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -function returnFalse() { - return false; -} -function returnTrue() { - return true; -} - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - preventDefault: function() { - this.isDefaultPrevented = returnTrue; - - var e = this.originalEvent; - if ( !e ) { - return; - } - - // if preventDefault exists run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); - - // otherwise set the returnValue property of the original event to false (IE) - } else { - e.returnValue = false; - } - }, - stopPropagation: function() { - this.isPropagationStopped = returnTrue; - - var e = this.originalEvent; - if ( !e ) { - return; - } - // if stopPropagation exists run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } - // otherwise set the cancelBubble property of the original event to true (IE) - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); - }, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse -}; - -// Create mouseenter/leave events using mouseover/out and event-time checks -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var target = this, - related = event.relatedTarget, - handleObj = event.handleObj, - selector = handleObj.selector, - ret; - - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !jQuery.contains( target, related )) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -}); - -// IE submit delegation -if ( !jQuery.support.submitBubbles ) { - - jQuery.event.special.submit = { - setup: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Lazy-add a submit handler when a descendant form may potentially be submitted - jQuery.event.add( this, "click._submit keypress._submit", function( e ) { - // Node name check avoids a VML-related crash in IE (#9807) - var elem = e.target, - form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; - if ( form && !form._submit_attached ) { - jQuery.event.add( form, "submit._submit", function( event ) { - event._submit_bubble = true; - }); - form._submit_attached = true; - } - }); - // return undefined since we don't need an event listener - }, - - postDispatch: function( event ) { - // If form was submitted by the user, bubble the event up the tree - if ( event._submit_bubble ) { - delete event._submit_bubble; - if ( this.parentNode && !event.isTrigger ) { - jQuery.event.simulate( "submit", this.parentNode, event, true ); - } - } - }, - - teardown: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Remove delegated handlers; cleanData eventually reaps submit handlers attached above - jQuery.event.remove( this, "._submit" ); - } - }; -} - -// IE change delegation and checkbox/radio fix -if ( !jQuery.support.changeBubbles ) { - - jQuery.event.special.change = { - - setup: function() { - - if ( rformElems.test( this.nodeName ) ) { - // IE doesn't fire change on a check/radio until blur; trigger it on click - // after a propertychange. Eat the blur-change in special.change.handle. - // This still fires onchange a second time for check/radio after blur. - if ( this.type === "checkbox" || this.type === "radio" ) { - jQuery.event.add( this, "propertychange._change", function( event ) { - if ( event.originalEvent.propertyName === "checked" ) { - this._just_changed = true; - } - }); - jQuery.event.add( this, "click._change", function( event ) { - if ( this._just_changed && !event.isTrigger ) { - this._just_changed = false; - jQuery.event.simulate( "change", this, event, true ); - } - }); - } - return false; - } - // Delegated event; lazy-add a change handler on descendant inputs - jQuery.event.add( this, "beforeactivate._change", function( e ) { - var elem = e.target; - - if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { - jQuery.event.add( elem, "change._change", function( event ) { - if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { - jQuery.event.simulate( "change", this.parentNode, event, true ); - } - }); - elem._change_attached = true; - } - }); - }, - - handle: function( event ) { - var elem = event.target; - - // Swallow native change events from checkbox/radio, we already triggered them above - if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { - return event.handleObj.handler.apply( this, arguments ); - } - }, - - teardown: function() { - jQuery.event.remove( this, "._change" ); - - return rformElems.test( this.nodeName ); - } - }; -} - -// Create "bubbling" focus and blur events -if ( !jQuery.support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler while someone wants focusin/focusout - var attaches = 0, - handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - if ( attaches++ === 0 ) { - document.addEventListener( orig, handler, true ); - } - }, - teardown: function() { - if ( --attaches === 0 ) { - document.removeEventListener( orig, handler, true ); - } - } - }; - }); -} - -jQuery.fn.extend({ - - on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { // && selector != null - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - this.on( type, selector, data, types[ type ], one ); - } - return this; - } - - if ( data == null && fn == null ) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return this; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return this.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - }); - }, - one: function( types, selector, data, fn ) { - return this.on( types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - if ( types && types.preventDefault && types.handleObj ) { - // ( event ) dispatched jQuery.Event - var handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - // ( types-object [, selector] ) - for ( var type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each(function() { - jQuery.event.remove( this, types, fn, selector ); - }); - }, - - bind: function( types, data, fn ) { - return this.on( types, null, data, fn ); - }, - unbind: function( types, fn ) { - return this.off( types, null, fn ); - }, - - live: function( types, data, fn ) { - jQuery( this.context ).on( types, this.selector, data, fn ); - return this; - }, - die: function( types, fn ) { - jQuery( this.context ).off( types, this.selector || "**", fn ); - return this; - }, - - delegate: function( selector, types, data, fn ) { - return this.on( types, selector, data, fn ); - }, - undelegate: function( selector, types, fn ) { - // ( namespace ) or ( selector, types [, fn] ) - return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - triggerHandler: function( type, data ) { - if ( this[0] ) { - return jQuery.event.trigger( type, data, this[0], true ); - } - }, - - toggle: function( fn ) { - // Save reference to arguments for access in closure - var args = arguments, - guid = fn.guid || jQuery.guid++, - i = 0, - toggler = function( event ) { - // Figure out which function to execute - var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; - jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); - - // Make sure that clicks stop - event.preventDefault(); - - // and execute the function - return args[ lastToggle ].apply( this, arguments ) || false; - }; - - // link all the functions, so any of them can unbind this click handler - toggler.guid = guid; - while ( i < args.length ) { - args[ i++ ].guid = guid; - } - - return this.click( toggler ); - }, - - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); - } -}); - -jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( data, fn ) { - if ( fn == null ) { - fn = data; - data = null; - } - - return arguments.length > 0 ? - this.on( name, null, data, fn ) : - this.trigger( name ); - }; - - if ( jQuery.attrFn ) { - jQuery.attrFn[ name ] = true; - } - - if ( rkeyEvent.test( name ) ) { - jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; - } - - if ( rmouseEvent.test( name ) ) { - jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; - } -}); - - - -/*! - * Sizzle CSS Selector Engine - * Copyright 2011, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * More information: http://sizzlejs.com/ - */ -(function(){ - -var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, - expando = "sizcache" + (Math.random() + '').replace('.', ''), - done = 0, - toString = Object.prototype.toString, - hasDuplicate = false, - baseHasDuplicate = true, - rBackslash = /\\/g, - rReturn = /\r\n/g, - rNonWord = /\W/; - -// Here we check if the JavaScript engine is using some sort of -// optimization where it does not always call our comparision -// function. If that is the case, discard the hasDuplicate value. -// Thus far that includes Google Chrome. -[0, 0].sort(function() { - baseHasDuplicate = false; - return 0; -}); - -var Sizzle = function( selector, context, results, seed ) { - results = results || []; - context = context || document; - - var origContext = context; - - if ( context.nodeType !== 1 && context.nodeType !== 9 ) { - return []; - } - - if ( !selector || typeof selector !== "string" ) { - return results; - } - - var m, set, checkSet, extra, ret, cur, pop, i, - prune = true, - contextXML = Sizzle.isXML( context ), - parts = [], - soFar = selector; - - // Reset the position of the chunker regexp (start from head) - do { - chunker.exec( "" ); - m = chunker.exec( soFar ); - - if ( m ) { - soFar = m[3]; - - parts.push( m[1] ); - - if ( m[2] ) { - extra = m[3]; - break; - } - } - } while ( m ); - - if ( parts.length > 1 && origPOS.exec( selector ) ) { - - if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context, seed ); - - } else { - set = Expr.relative[ parts[0] ] ? - [ context ] : - Sizzle( parts.shift(), context ); - - while ( parts.length ) { - selector = parts.shift(); - - if ( Expr.relative[ selector ] ) { - selector += parts.shift(); - } - - set = posProcess( selector, set, seed ); - } - } - - } else { - // Take a shortcut and set the context if the root selector is an ID - // (but not if it'll be faster if the inner selector is an ID) - if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && - Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { - - ret = Sizzle.find( parts.shift(), context, contextXML ); - context = ret.expr ? - Sizzle.filter( ret.expr, ret.set )[0] : - ret.set[0]; - } - - if ( context ) { - ret = seed ? - { expr: parts.pop(), set: makeArray(seed) } : - Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); - - set = ret.expr ? - Sizzle.filter( ret.expr, ret.set ) : - ret.set; - - if ( parts.length > 0 ) { - checkSet = makeArray( set ); - - } else { - prune = false; - } - - while ( parts.length ) { - cur = parts.pop(); - pop = cur; - - if ( !Expr.relative[ cur ] ) { - cur = ""; - } else { - pop = parts.pop(); - } - - if ( pop == null ) { - pop = context; - } - - Expr.relative[ cur ]( checkSet, pop, contextXML ); - } - - } else { - checkSet = parts = []; - } - } - - if ( !checkSet ) { - checkSet = set; - } - - if ( !checkSet ) { - Sizzle.error( cur || selector ); - } - - if ( toString.call(checkSet) === "[object Array]" ) { - if ( !prune ) { - results.push.apply( results, checkSet ); - - } else if ( context && context.nodeType === 1 ) { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { - results.push( set[i] ); - } - } - - } else { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && checkSet[i].nodeType === 1 ) { - results.push( set[i] ); - } - } - } - - } else { - makeArray( checkSet, results ); - } - - if ( extra ) { - Sizzle( extra, origContext, results, seed ); - Sizzle.uniqueSort( results ); - } - - return results; -}; - -Sizzle.uniqueSort = function( results ) { - if ( sortOrder ) { - hasDuplicate = baseHasDuplicate; - results.sort( sortOrder ); - - if ( hasDuplicate ) { - for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[ i - 1 ] ) { - results.splice( i--, 1 ); - } - } - } - } - - return results; -}; - -Sizzle.matches = function( expr, set ) { - return Sizzle( expr, null, null, set ); -}; - -Sizzle.matchesSelector = function( node, expr ) { - return Sizzle( expr, null, null, [node] ).length > 0; -}; - -Sizzle.find = function( expr, context, isXML ) { - var set, i, len, match, type, left; - - if ( !expr ) { - return []; - } - - for ( i = 0, len = Expr.order.length; i < len; i++ ) { - type = Expr.order[i]; - - if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - left = match[1]; - match.splice( 1, 1 ); - - if ( left.substr( left.length - 1 ) !== "\\" ) { - match[1] = (match[1] || "").replace( rBackslash, "" ); - set = Expr.find[ type ]( match, context, isXML ); - - if ( set != null ) { - expr = expr.replace( Expr.match[ type ], "" ); - break; - } - } - } - } - - if ( !set ) { - set = typeof context.getElementsByTagName !== "undefined" ? - context.getElementsByTagName( "*" ) : - []; - } - - return { set: set, expr: expr }; -}; - -Sizzle.filter = function( expr, set, inplace, not ) { - var match, anyFound, - type, found, item, filter, left, - i, pass, - old = expr, - result = [], - curLoop = set, - isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); - - while ( expr && set.length ) { - for ( type in Expr.filter ) { - if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - filter = Expr.filter[ type ]; - left = match[1]; - - anyFound = false; - - match.splice(1,1); - - if ( left.substr( left.length - 1 ) === "\\" ) { - continue; - } - - if ( curLoop === result ) { - result = []; - } - - if ( Expr.preFilter[ type ] ) { - match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); - - if ( !match ) { - anyFound = found = true; - - } else if ( match === true ) { - continue; - } - } - - if ( match ) { - for ( i = 0; (item = curLoop[i]) != null; i++ ) { - if ( item ) { - found = filter( item, match, i, curLoop ); - pass = not ^ found; - - if ( inplace && found != null ) { - if ( pass ) { - anyFound = true; - - } else { - curLoop[i] = false; - } - - } else if ( pass ) { - result.push( item ); - anyFound = true; - } - } - } - } - - if ( found !== undefined ) { - if ( !inplace ) { - curLoop = result; - } - - expr = expr.replace( Expr.match[ type ], "" ); - - if ( !anyFound ) { - return []; - } - - break; - } - } - } - - // Improper expression - if ( expr === old ) { - if ( anyFound == null ) { - Sizzle.error( expr ); - - } else { - break; - } - } - - old = expr; - } - - return curLoop; -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Utility function for retreiving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -var getText = Sizzle.getText = function( elem ) { - var i, node, - nodeType = elem.nodeType, - ret = ""; - - if ( nodeType ) { - if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - // Use textContent || innerText for elements - if ( typeof elem.textContent === 'string' ) { - return elem.textContent; - } else if ( typeof elem.innerText === 'string' ) { - // Replace IE's carriage returns - return elem.innerText.replace( rReturn, '' ); - } else { - // Traverse it's children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - } else { - - // If no nodeType, this is expected to be an array - for ( i = 0; (node = elem[i]); i++ ) { - // Do not traverse comment nodes - if ( node.nodeType !== 8 ) { - ret += getText( node ); - } - } - } - return ret; -}; - -var Expr = Sizzle.selectors = { - order: [ "ID", "NAME", "TAG" ], - - match: { - ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, - ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, - TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, - CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, - POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, - PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ - }, - - leftMatch: {}, - - attrMap: { - "class": "className", - "for": "htmlFor" - }, - - attrHandle: { - href: function( elem ) { - return elem.getAttribute( "href" ); - }, - type: function( elem ) { - return elem.getAttribute( "type" ); - } - }, - - relative: { - "+": function(checkSet, part){ - var isPartStr = typeof part === "string", - isTag = isPartStr && !rNonWord.test( part ), - isPartStrNotTag = isPartStr && !isTag; - - if ( isTag ) { - part = part.toLowerCase(); - } - - for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { - if ( (elem = checkSet[i]) ) { - while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} - - checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? - elem || false : - elem === part; - } - } - - if ( isPartStrNotTag ) { - Sizzle.filter( part, checkSet, true ); - } - }, - - ">": function( checkSet, part ) { - var elem, - isPartStr = typeof part === "string", - i = 0, - l = checkSet.length; - - if ( isPartStr && !rNonWord.test( part ) ) { - part = part.toLowerCase(); - - for ( ; i < l; i++ ) { - elem = checkSet[i]; - - if ( elem ) { - var parent = elem.parentNode; - checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; - } - } - - } else { - for ( ; i < l; i++ ) { - elem = checkSet[i]; - - if ( elem ) { - checkSet[i] = isPartStr ? - elem.parentNode : - elem.parentNode === part; - } - } - - if ( isPartStr ) { - Sizzle.filter( part, checkSet, true ); - } - } - }, - - "": function(checkSet, part, isXML){ - var nodeCheck, - doneName = done++, - checkFn = dirCheck; - - if ( typeof part === "string" && !rNonWord.test( part ) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; - } - - checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); - }, - - "~": function( checkSet, part, isXML ) { - var nodeCheck, - doneName = done++, - checkFn = dirCheck; - - if ( typeof part === "string" && !rNonWord.test( part ) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; - } - - checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); - } - }, - - find: { - ID: function( match, context, isXML ) { - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; - } - }, - - NAME: function( match, context ) { - if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], - results = context.getElementsByName( match[1] ); - - for ( var i = 0, l = results.length; i < l; i++ ) { - if ( results[i].getAttribute("name") === match[1] ) { - ret.push( results[i] ); - } - } - - return ret.length === 0 ? null : ret; - } - }, - - TAG: function( match, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( match[1] ); - } - } - }, - preFilter: { - CLASS: function( match, curLoop, inplace, result, not, isXML ) { - match = " " + match[1].replace( rBackslash, "" ) + " "; - - if ( isXML ) { - return match; - } - - for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { - if ( elem ) { - if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { - if ( !inplace ) { - result.push( elem ); - } - - } else if ( inplace ) { - curLoop[i] = false; - } - } - } - - return false; - }, - - ID: function( match ) { - return match[1].replace( rBackslash, "" ); - }, - - TAG: function( match, curLoop ) { - return match[1].replace( rBackslash, "" ).toLowerCase(); - }, - - CHILD: function( match ) { - if ( match[1] === "nth" ) { - if ( !match[2] ) { - Sizzle.error( match[0] ); - } - - match[2] = match[2].replace(/^\+|\s*/g, ''); - - // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' - var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( - match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || - !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); - - // calculate the numbers (first)n+(last) including if they are negative - match[2] = (test[1] + (test[2] || 1)) - 0; - match[3] = test[3] - 0; - } - else if ( match[2] ) { - Sizzle.error( match[0] ); - } - - // TODO: Move to normal caching system - match[0] = done++; - - return match; - }, - - ATTR: function( match, curLoop, inplace, result, not, isXML ) { - var name = match[1] = match[1].replace( rBackslash, "" ); - - if ( !isXML && Expr.attrMap[name] ) { - match[1] = Expr.attrMap[name]; - } - - // Handle if an un-quoted value was used - match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); - - if ( match[2] === "~=" ) { - match[4] = " " + match[4] + " "; - } - - return match; - }, - - PSEUDO: function( match, curLoop, inplace, result, not ) { - if ( match[1] === "not" ) { - // If we're dealing with a complex expression, or a simple one - if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { - match[3] = Sizzle(match[3], null, null, curLoop); - - } else { - var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); - - if ( !inplace ) { - result.push.apply( result, ret ); - } - - return false; - } - - } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { - return true; - } - - return match; - }, - - POS: function( match ) { - match.unshift( true ); - - return match; - } - }, - - filters: { - enabled: function( elem ) { - return elem.disabled === false && elem.type !== "hidden"; - }, - - disabled: function( elem ) { - return elem.disabled === true; - }, - - checked: function( elem ) { - return elem.checked === true; - }, - - selected: function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - parent: function( elem ) { - return !!elem.firstChild; - }, - - empty: function( elem ) { - return !elem.firstChild; - }, - - has: function( elem, i, match ) { - return !!Sizzle( match[3], elem ).length; - }, - - header: function( elem ) { - return (/h\d/i).test( elem.nodeName ); - }, - - text: function( elem ) { - var attr = elem.getAttribute( "type" ), type = elem.type; - // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) - // use getAttribute instead to test this case - return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); - }, - - radio: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; - }, - - checkbox: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; - }, - - file: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; - }, - - password: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; - }, - - submit: function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && "submit" === elem.type; - }, - - image: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; - }, - - reset: function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && "reset" === elem.type; - }, - - button: function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && "button" === elem.type || name === "button"; - }, - - input: function( elem ) { - return (/input|select|textarea|button/i).test( elem.nodeName ); - }, - - focus: function( elem ) { - return elem === elem.ownerDocument.activeElement; - } - }, - setFilters: { - first: function( elem, i ) { - return i === 0; - }, - - last: function( elem, i, match, array ) { - return i === array.length - 1; - }, - - even: function( elem, i ) { - return i % 2 === 0; - }, - - odd: function( elem, i ) { - return i % 2 === 1; - }, - - lt: function( elem, i, match ) { - return i < match[3] - 0; - }, - - gt: function( elem, i, match ) { - return i > match[3] - 0; - }, - - nth: function( elem, i, match ) { - return match[3] - 0 === i; - }, - - eq: function( elem, i, match ) { - return match[3] - 0 === i; - } - }, - filter: { - PSEUDO: function( elem, match, i, array ) { - var name = match[1], - filter = Expr.filters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - - } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; - - } else if ( name === "not" ) { - var not = match[3]; - - for ( var j = 0, l = not.length; j < l; j++ ) { - if ( not[j] === elem ) { - return false; - } - } - - return true; - - } else { - Sizzle.error( name ); - } - }, - - CHILD: function( elem, match ) { - var first, last, - doneName, parent, cache, - count, diff, - type = match[1], - node = elem; - - switch ( type ) { - case "only": - case "first": - while ( (node = node.previousSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - - if ( type === "first" ) { - return true; - } - - node = elem; - - /* falls through */ - case "last": - while ( (node = node.nextSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - - return true; - - case "nth": - first = match[2]; - last = match[3]; - - if ( first === 1 && last === 0 ) { - return true; - } - - doneName = match[0]; - parent = elem.parentNode; - - if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { - count = 0; - - for ( node = parent.firstChild; node; node = node.nextSibling ) { - if ( node.nodeType === 1 ) { - node.nodeIndex = ++count; - } - } - - parent[ expando ] = doneName; - } - - diff = elem.nodeIndex - last; - - if ( first === 0 ) { - return diff === 0; - - } else { - return ( diff % first === 0 && diff / first >= 0 ); - } - } - }, - - ID: function( elem, match ) { - return elem.nodeType === 1 && elem.getAttribute("id") === match; - }, - - TAG: function( elem, match ) { - return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; - }, - - CLASS: function( elem, match ) { - return (" " + (elem.className || elem.getAttribute("class")) + " ") - .indexOf( match ) > -1; - }, - - ATTR: function( elem, match ) { - var name = match[1], - result = Sizzle.attr ? - Sizzle.attr( elem, name ) : - Expr.attrHandle[ name ] ? - Expr.attrHandle[ name ]( elem ) : - elem[ name ] != null ? - elem[ name ] : - elem.getAttribute( name ), - value = result + "", - type = match[2], - check = match[4]; - - return result == null ? - type === "!=" : - !type && Sizzle.attr ? - result != null : - type === "=" ? - value === check : - type === "*=" ? - value.indexOf(check) >= 0 : - type === "~=" ? - (" " + value + " ").indexOf(check) >= 0 : - !check ? - value && result !== false : - type === "!=" ? - value !== check : - type === "^=" ? - value.indexOf(check) === 0 : - type === "$=" ? - value.substr(value.length - check.length) === check : - type === "|=" ? - value === check || value.substr(0, check.length + 1) === check + "-" : - false; - }, - - POS: function( elem, match, i, array ) { - var name = match[2], - filter = Expr.setFilters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - } - } - } -}; - -var origPOS = Expr.match.POS, - fescape = function(all, num){ - return "\\" + (num - 0 + 1); - }; - -for ( var type in Expr.match ) { - Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); - Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); -} -// Expose origPOS -// "global" as in regardless of relation to brackets/parens -Expr.match.globalPOS = origPOS; - -var makeArray = function( array, results ) { - array = Array.prototype.slice.call( array, 0 ); - - if ( results ) { - results.push.apply( results, array ); - return results; - } - - return array; -}; - -// Perform a simple check to determine if the browser is capable of -// converting a NodeList to an array using builtin methods. -// Also verifies that the returned array holds DOM nodes -// (which is not the case in the Blackberry browser) -try { - Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; - -// Provide a fallback method if it does not work -} catch( e ) { - makeArray = function( array, results ) { - var i = 0, - ret = results || []; - - if ( toString.call(array) === "[object Array]" ) { - Array.prototype.push.apply( ret, array ); - - } else { - if ( typeof array.length === "number" ) { - for ( var l = array.length; i < l; i++ ) { - ret.push( array[i] ); - } - - } else { - for ( ; array[i]; i++ ) { - ret.push( array[i] ); - } - } - } - - return ret; - }; -} - -var sortOrder, siblingCheck; - -if ( document.documentElement.compareDocumentPosition ) { - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { - return a.compareDocumentPosition ? -1 : 1; - } - - return a.compareDocumentPosition(b) & 4 ? -1 : 1; - }; - -} else { - sortOrder = function( a, b ) { - // The nodes are identical, we can exit early - if ( a === b ) { - hasDuplicate = true; - return 0; - - // Fallback to using sourceIndex (in IE) if it's available on both nodes - } else if ( a.sourceIndex && b.sourceIndex ) { - return a.sourceIndex - b.sourceIndex; - } - - var al, bl, - ap = [], - bp = [], - aup = a.parentNode, - bup = b.parentNode, - cur = aup; - - // If the nodes are siblings (or identical) we can do a quick check - if ( aup === bup ) { - return siblingCheck( a, b ); - - // If no parents were found then the nodes are disconnected - } else if ( !aup ) { - return -1; - - } else if ( !bup ) { - return 1; - } - - // Otherwise they're somewhere else in the tree so we need - // to build up a full list of the parentNodes for comparison - while ( cur ) { - ap.unshift( cur ); - cur = cur.parentNode; - } - - cur = bup; - - while ( cur ) { - bp.unshift( cur ); - cur = cur.parentNode; - } - - al = ap.length; - bl = bp.length; - - // Start walking down the tree looking for a discrepancy - for ( var i = 0; i < al && i < bl; i++ ) { - if ( ap[i] !== bp[i] ) { - return siblingCheck( ap[i], bp[i] ); - } - } - - // We ended someplace up the tree so do a sibling check - return i === al ? - siblingCheck( a, bp[i], -1 ) : - siblingCheck( ap[i], b, 1 ); - }; - - siblingCheck = function( a, b, ret ) { - if ( a === b ) { - return ret; - } - - var cur = a.nextSibling; - - while ( cur ) { - if ( cur === b ) { - return -1; - } - - cur = cur.nextSibling; - } - - return 1; - }; -} - -// Check to see if the browser returns elements by name when -// querying by getElementById (and provide a workaround) -(function(){ - // We're going to inject a fake input element with a specified name - var form = document.createElement("div"), - id = "script" + (new Date()).getTime(), - root = document.documentElement; - - form.innerHTML = ""; - - // Inject it into the root element, check its status, and remove it quickly - root.insertBefore( form, root.firstChild ); - - // The workaround has to do additional checks after a getElementById - // Which slows things down for other browsers (hence the branching) - if ( document.getElementById( id ) ) { - Expr.find.ID = function( match, context, isXML ) { - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - - return m ? - m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? - [m] : - undefined : - []; - } - }; - - Expr.filter.ID = function( elem, match ) { - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); - - return elem.nodeType === 1 && node && node.nodeValue === match; - }; - } - - root.removeChild( form ); - - // release memory in IE - root = form = null; -})(); - -(function(){ - // Check to see if the browser returns only elements - // when doing getElementsByTagName("*") - - // Create a fake element - var div = document.createElement("div"); - div.appendChild( document.createComment("") ); - - // Make sure no comments are found - if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function( match, context ) { - var results = context.getElementsByTagName( match[1] ); - - // Filter out possible comments - if ( match[1] === "*" ) { - var tmp = []; - - for ( var i = 0; results[i]; i++ ) { - if ( results[i].nodeType === 1 ) { - tmp.push( results[i] ); - } - } - - results = tmp; - } - - return results; - }; - } - - // Check to see if an attribute returns normalized href attributes - div.innerHTML = ""; - - if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && - div.firstChild.getAttribute("href") !== "#" ) { - - Expr.attrHandle.href = function( elem ) { - return elem.getAttribute( "href", 2 ); - }; - } - - // release memory in IE - div = null; -})(); - -if ( document.querySelectorAll ) { - (function(){ - var oldSizzle = Sizzle, - div = document.createElement("div"), - id = "__sizzle__"; - - div.innerHTML = "

    "; - - // Safari can't handle uppercase or unicode characters when - // in quirks mode. - if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { - return; - } - - Sizzle = function( query, context, extra, seed ) { - context = context || document; - - // Only use querySelectorAll on non-XML documents - // (ID selectors don't work in non-HTML documents) - if ( !seed && !Sizzle.isXML(context) ) { - // See if we find a selector to speed up - var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); - - if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { - // Speed-up: Sizzle("TAG") - if ( match[1] ) { - return makeArray( context.getElementsByTagName( query ), extra ); - - // Speed-up: Sizzle(".CLASS") - } else if ( match[2] && Expr.find.CLASS && context.getElementsByMethodName ) { - return makeArray( context.getElementsByMethodName( match[2] ), extra ); - } - } - - if ( context.nodeType === 9 ) { - // Speed-up: Sizzle("body") - // The body element only exists once, optimize finding it - if ( query === "body" && context.body ) { - return makeArray( [ context.body ], extra ); - - // Speed-up: Sizzle("#ID") - } else if ( match && match[3] ) { - var elem = context.getElementById( match[3] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id === match[3] ) { - return makeArray( [ elem ], extra ); - } - - } else { - return makeArray( [], extra ); - } - } - - try { - return makeArray( context.querySelectorAll(query), extra ); - } catch(qsaError) {} - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - var oldContext = context, - old = context.getAttribute( "id" ), - nid = old || id, - hasParent = context.parentNode, - relativeHierarchySelector = /^\s*[+~]/.test( query ); - - if ( !old ) { - context.setAttribute( "id", nid ); - } else { - nid = nid.replace( /'/g, "\\$&" ); - } - if ( relativeHierarchySelector && hasParent ) { - context = context.parentNode; - } - - try { - if ( !relativeHierarchySelector || hasParent ) { - return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); - } - - } catch(pseudoError) { - } finally { - if ( !old ) { - oldContext.removeAttribute( "id" ); - } - } - } - } - - return oldSizzle(query, context, extra, seed); - }; - - for ( var prop in oldSizzle ) { - Sizzle[ prop ] = oldSizzle[ prop ]; - } - - // release memory in IE - div = null; - })(); -} - -(function(){ - var html = document.documentElement, - matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; - - if ( matches ) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9 fails this) - var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), - pseudoWorks = false; - - try { - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( document.documentElement, "[test!='']:sizzle" ); - - } catch( pseudoError ) { - pseudoWorks = true; - } - - Sizzle.matchesSelector = function( node, expr ) { - // Make sure that attribute selectors are quoted - expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); - - if ( !Sizzle.isXML( node ) ) { - try { - if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { - var ret = matches.call( node, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || !disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9, so check for that - node.document && node.document.nodeType !== 11 ) { - return ret; - } - } - } catch(e) {} - } - - return Sizzle(expr, null, null, [node]).length > 0; - }; - } -})(); - -(function(){ - var div = document.createElement("div"); - - div.innerHTML = "
    "; - - // Opera can't find a second classname (in 9.6) - // Also, make sure that getElementsByMethodName actually exists - if ( !div.getElementsByMethodName || div.getElementsByMethodName("e").length === 0 ) { - return; - } - - // Safari caches class attributes, doesn't catch changes (in 3.2) - div.lastChild.className = "e"; - - if ( div.getElementsByMethodName("e").length === 1 ) { - return; - } - - Expr.order.splice(1, 0, "CLASS"); - Expr.find.CLASS = function( match, context, isXML ) { - if ( typeof context.getElementsByMethodName !== "undefined" && !isXML ) { - return context.getElementsByMethodName(match[1]); - } - }; - - // release memory in IE - div = null; -})(); - -function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - - if ( elem ) { - var match = false; - - elem = elem[dir]; - - while ( elem ) { - if ( elem[ expando ] === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 && !isXML ){ - elem[ expando ] = doneName; - elem.sizset = i; - } - - if ( elem.nodeName.toLowerCase() === cur ) { - match = elem; - break; - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - - if ( elem ) { - var match = false; - - elem = elem[dir]; - - while ( elem ) { - if ( elem[ expando ] === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 ) { - if ( !isXML ) { - elem[ expando ] = doneName; - elem.sizset = i; - } - - if ( typeof cur !== "string" ) { - if ( elem === cur ) { - match = true; - break; - } - - } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { - match = elem; - break; - } - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -if ( document.documentElement.contains ) { - Sizzle.contains = function( a, b ) { - return a !== b && (a.contains ? a.contains(b) : true); - }; - -} else if ( document.documentElement.compareDocumentPosition ) { - Sizzle.contains = function( a, b ) { - return !!(a.compareDocumentPosition(b) & 16); - }; - -} else { - Sizzle.contains = function() { - return false; - }; -} - -Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; - - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -var posProcess = function( selector, context, seed ) { - var match, - tmpSet = [], - later = "", - root = context.nodeType ? [context] : context; - - // Position selectors must be done after the filter - // And so must :not(positional) so we move all PSEUDOs to the end - while ( (match = Expr.match.PSEUDO.exec( selector )) ) { - later += match[0]; - selector = selector.replace( Expr.match.PSEUDO, "" ); - } - - selector = Expr.relative[selector] ? selector + "*" : selector; - - for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet, seed ); - } - - return Sizzle.filter( later, tmpSet ); -}; - -// EXPOSE -// Override sizzle attribute retrieval -Sizzle.attr = jQuery.attr; -Sizzle.selectors.attrMap = {}; -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.filters; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; - - -})(); - - -var runtil = /Until$/, - rparentsprev = /^(?:parents|prevUntil|prevAll)/, - // Note: This RegExp should be improved, or likely pulled from Sizzle - rmultiselector = /,/, - isSimple = /^.[^:#\[\.,]*$/, - slice = Array.prototype.slice, - POS = jQuery.expr.match.globalPOS, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend({ - find: function( selector ) { - var self = this, - i, l; - - if ( typeof selector !== "string" ) { - return jQuery( selector ).filter(function() { - for ( i = 0, l = self.length; i < l; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - }); - } - - var ret = this.pushStack( "", "find", selector ), - length, n, r; - - for ( i = 0, l = this.length; i < l; i++ ) { - length = ret.length; - jQuery.find( selector, this[i], ret ); - - if ( i > 0 ) { - // Make sure that the results are unique - for ( n = length; n < ret.length; n++ ) { - for ( r = 0; r < length; r++ ) { - if ( ret[r] === ret[n] ) { - ret.splice(n--, 1); - break; - } - } - } - } - } - - return ret; - }, - - has: function( target ) { - var targets = jQuery( target ); - return this.filter(function() { - for ( var i = 0, l = targets.length; i < l; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, - - not: function( selector ) { - return this.pushStack( winnow(this, selector, false), "not", selector); - }, - - filter: function( selector ) { - return this.pushStack( winnow(this, selector, true), "filter", selector ); - }, - - is: function( selector ) { - return !!selector && ( - typeof selector === "string" ? - // If this is a positional selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - POS.test( selector ) ? - jQuery( selector, this.context ).index( this[0] ) >= 0 : - jQuery.filter( selector, this ).length > 0 : - this.filter( selector ).length > 0 ); - }, - - closest: function( selectors, context ) { - var ret = [], i, l, cur = this[0]; - - // Array (deprecated as of jQuery 1.7) - if ( jQuery.isArray( selectors ) ) { - var level = 1; - - while ( cur && cur.ownerDocument && cur !== context ) { - for ( i = 0; i < selectors.length; i++ ) { - - if ( jQuery( cur ).is( selectors[ i ] ) ) { - ret.push({ selector: selectors[ i ], elem: cur, level: level }); - } - } - - cur = cur.parentNode; - level++; - } - - return ret; - } - - // String - var pos = POS.test( selectors ) || typeof selectors !== "string" ? - jQuery( selectors, context || this.context ) : - 0; - - for ( i = 0, l = this.length; i < l; i++ ) { - cur = this[i]; - - while ( cur ) { - if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { - ret.push( cur ); - break; - - } else { - cur = cur.parentNode; - if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { - break; - } - } - } - } - - ret = ret.length > 1 ? jQuery.unique( ret ) : ret; - - return this.pushStack( ret, "closest", selectors ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; - } - - // index in selector - if ( typeof elem === "string" ) { - return jQuery.inArray( this[0], jQuery( elem ) ); - } - - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this ); - }, - - add: function( selector, context ) { - var set = typeof selector === "string" ? - jQuery( selector, context ) : - jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), - all = jQuery.merge( this.get(), set ); - - return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? - all : - jQuery.unique( all ) ); - }, - - andSelf: function() { - return this.add( this.prevObject ); - } -}); - -// A painfully simple check to see if an element is disconnected -// from a document (should be improved, where feasible). -function isDisconnected( node ) { - return !node || !node.parentNode || node.parentNode.nodeType === 11; -} - -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return jQuery.nth( elem, 2, "nextSibling" ); - }, - prev: function( elem ) { - return jQuery.nth( elem, 2, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return jQuery.nodeName( elem, "iframe" ) ? - elem.contentDocument || elem.contentWindow.document : - jQuery.makeArray( elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ); - - if ( !runtil.test( name ) ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - ret = jQuery.filter( selector, ret ); - } - - ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; - - if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { - ret = ret.reverse(); - } - - return this.pushStack( ret, name, slice.call( arguments ).join(",") ); - }; -}); - -jQuery.extend({ - filter: function( expr, elems, not ) { - if ( not ) { - expr = ":not(" + expr + ")"; - } - - return elems.length === 1 ? - jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : - jQuery.find.matches(expr, elems); - }, - - dir: function( elem, dir, until ) { - var matched = [], - cur = elem[ dir ]; - - while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { - if ( cur.nodeType === 1 ) { - matched.push( cur ); - } - cur = cur[dir]; - } - return matched; - }, - - nth: function( cur, result, dir, elem ) { - result = result || 1; - var num = 0; - - for ( ; cur; cur = cur[dir] ) { - if ( cur.nodeType === 1 && ++num === result ) { - break; - } - } - - return cur; - }, - - sibling: function( n, elem ) { - var r = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - r.push( n ); - } - } - - return r; - } -}); - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, keep ) { - - // Can't pass null or undefined to indexOf in Firefox 4 - // Set to 0 to skip string check - qualifier = qualifier || 0; - - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep(elements, function( elem, i ) { - var retVal = !!qualifier.call( elem, i, elem ); - return retVal === keep; - }); - - } else if ( qualifier.nodeType ) { - return jQuery.grep(elements, function( elem, i ) { - return ( elem === qualifier ) === keep; - }); - - } else if ( typeof qualifier === "string" ) { - var filtered = jQuery.grep(elements, function( elem ) { - return elem.nodeType === 1; - }); - - if ( isSimple.test( qualifier ) ) { - return jQuery.filter(qualifier, filtered, !keep); - } else { - qualifier = jQuery.filter( qualifier, filtered ); - } - } - - return jQuery.grep(elements, function( elem, i ) { - return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; - }); -} - - - - -function createSafeFragment( document ) { - var list = nodeNames.split( "|" ), - safeFrag = document.createDocumentFragment(); - - if ( safeFrag.createElement ) { - while ( list.length ) { - safeFrag.createElement( - list.pop() - ); - } - } - return safeFrag; -} - -var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + - "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", - rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, - rleadingWhitespace = /^\s+/, - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, - rtagName = /<([\w:]+)/, - rtbody = /]", "i"), - // checked="checked" or checked - rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, - rscriptType = /\/(java|ecma)script/i, - rcleanScript = /^\s*", "" ], - legend: [ 1, "
    ", "
    " ], - thead: [ 1, "", "
    " ], - tr: [ 2, "", "
    " ], - td: [ 3, "", "
    " ], - col: [ 2, "", "
    " ], - area: [ 1, "", "" ], - _default: [ 0, "", "" ] - }, - safeFragment = createSafeFragment( document ); - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// IE can't serialize and - - - - - - - - -
    -
    -
    -
    - - -

    Index

    - -
    - A - | B - | C - | D - | F - | G - | I - | L - | M - | N - | O - | P - | Q - | R - | S - | T - | V - | W - -
    -

    A

    - - - -
    - -
    AC_KERBEROS (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    AC_PASSWORD (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    AC_SMARTCARD (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    AC_UNSPECIFIED (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    AC_X509 (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    add_sign() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - -
    - -
    add_x509_key_descriptors() (saml2.metadata.OneLogin_Saml2_Metadata static method) -
    - - -
    ALOWED_CLOCK_DRIFT (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    ATTRNAME_FORMAT_BASIC (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    ATTRNAME_FORMAT_UNSPECIFIED (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    ATTRNAME_FORMAT_URI (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - -
    - -

    B

    - - - -
    - -
    BINDING_DEFLATE (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    BINDING_HTTP_ARTIFACT (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    BINDING_HTTP_POST (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    BINDING_HTTP_REDIRECT (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    BINDING_SOAP (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - -
    - -
    build() (saml2.logout_response.OneLogin_Saml2_Logout_Response method) -
    - - -
    build_request_signature() (saml2.auth.OneLogin_Saml2_Auth method) -
    - - -
    build_response_signature() (saml2.auth.OneLogin_Saml2_Auth method) -
    - - -
    builder() (saml2.metadata.OneLogin_Saml2_Metadata static method) -
    - -
    - -

    C

    - - - -
    - -
    calculate_x509_fingerprint() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    check_settings() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    check_sp_certs() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    check_status() (saml2.response.OneLogin_Saml2_Response method) -
    - -
    - -
    CM_BEARER (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    CM_HOLDER_KEY (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    CM_SENDER_VOUCHES (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - -
    - -

    D

    - - - -
    - -
    decode_base64_and_inflate() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    decrypt_element() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - -
    - -
    deflate_and_base64_encode() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    delete_local_session() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - -
    - -

    F

    - - - -
    - -
    format_cert() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    format_finger_print() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - -
    - -
    format_idp_cert() (saml2.settings.OneLogin_Saml2_Settings method) -
    - -
    - -

    G

    - - - -
    - -
    generate_name_id() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    generate_unique_id() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    get_attribute() (saml2.auth.OneLogin_Saml2_Auth method) -
    - - -
    get_attributes() (saml2.auth.OneLogin_Saml2_Auth method) -
    - -
    - -
    (saml2.response.OneLogin_Saml2_Response method) -
    - -
    - -
    get_audiences() (saml2.response.OneLogin_Saml2_Response method) -
    - - -
    get_base_path() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_cert_path() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_contacts() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_errors() (saml2.auth.OneLogin_Saml2_Auth method) -
    - -
    - -
    (saml2.settings.OneLogin_Saml2_Settings method) -
    - -
    - -
    get_expire_time() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    get_ext_lib_path() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_id() (saml2.logout_request.OneLogin_Saml2_Logout_Request static method) -
    - - -
    get_idp_data() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_issuer() (saml2.logout_request.OneLogin_Saml2_Logout_Request static method) -
    - -
    - -
    (saml2.logout_response.OneLogin_Saml2_Logout_Response method) -
    - -
    - -
    get_issuers() (saml2.response.OneLogin_Saml2_Response method) -
    - - -
    get_lib_path() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_name_id() (saml2.logout_request.OneLogin_Saml2_Logout_Request static method) -
    - - -
    get_name_id_data() (saml2.logout_request.OneLogin_Saml2_Logout_Request static method) -
    - - -
    get_nameid() (saml2.auth.OneLogin_Saml2_Auth method) -
    - -
    - -
    (saml2.response.OneLogin_Saml2_Response method) -
    - -
    - -
    get_nameid_data() (saml2.response.OneLogin_Saml2_Response method) -
    - -
    - -
    get_organization() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_request() (saml2.authn_request.OneLogin_Saml2_Authn_Request method) -
    - -
    - -
    (saml2.logout_request.OneLogin_Saml2_Logout_Request method) -
    - -
    - -
    get_response() (saml2.logout_response.OneLogin_Saml2_Logout_Response method) -
    - - -
    get_schemas_path() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_security_data() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_self_host() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    get_self_url() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    get_self_url_host() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    get_self_url_no_query() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    get_session_index() (saml2.response.OneLogin_Saml2_Response method) -
    - - -
    get_session_indexes() (saml2.logout_request.OneLogin_Saml2_Logout_Request static method) -
    - - -
    get_session_not_on_or_after() (saml2.response.OneLogin_Saml2_Response method) -
    - - -
    get_settings() (saml2.auth.OneLogin_Saml2_Auth method) -
    - - -
    get_slo_url() (saml2.auth.OneLogin_Saml2_Auth method) -
    - - -
    get_sp_cert() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_sp_data() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_sp_key() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_sp_metadata() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    get_sso_url() (saml2.auth.OneLogin_Saml2_Auth method) -
    - - -
    get_status() (saml2.logout_response.OneLogin_Saml2_Logout_Response method) -
    - -
    - -
    (saml2.utils.OneLogin_Saml2_Utils static method) -
    - -
    -
    - -

    I

    - - - -
    - -
    is_authenticated() (saml2.auth.OneLogin_Saml2_Auth method) -
    - - -
    is_debug_active() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    is_https() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - -
    - -
    is_strict() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    is_valid() (saml2.logout_request.OneLogin_Saml2_Logout_Request static method) -
    - -
    - -
    (saml2.logout_response.OneLogin_Saml2_Logout_Response method) -
    - - -
    (saml2.response.OneLogin_Saml2_Response method) -
    - -
    -
    - -

    L

    - - - -
    - -
    login() (saml2.auth.OneLogin_Saml2_Auth method) -
    - -
    - -
    logout() (saml2.auth.OneLogin_Saml2_Auth method) -
    - -
    - -

    M

    - - -
    - -
    METADATA_SP_INVALID (saml2.errors.OneLogin_Saml2_Error attribute) -
    - -
    - -

    N

    - - - -
    - -
    NAMEID_EMAIL_ADDRESS (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NAMEID_ENCRYPTED (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NAMEID_ENTITY (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NAMEID_KERBEROS (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NAMEID_PERSISTENT (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NAMEID_TRANSIENT (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NAMEID_X509_SUBJECT_NAME (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NS_DS (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - -
    - -
    NS_MD (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NS_SAML (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NS_SAMLP (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NS_SOAP (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NS_XENC (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NS_XS (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NS_XSI (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    NSMAP (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - -
    - -

    O

    - - - -
    - -
    OneLogin_Saml2_Auth (class in saml2.auth) -
    - - -
    OneLogin_Saml2_Authn_Request (class in saml2.authn_request) -
    - - -
    OneLogin_Saml2_Constants (class in saml2.constants) -
    - - -
    OneLogin_Saml2_Error -
    - - -
    OneLogin_Saml2_Logout_Request (class in saml2.logout_request) -
    - -
    - -
    OneLogin_Saml2_Logout_Response (class in saml2.logout_response) -
    - - -
    OneLogin_Saml2_Metadata (class in saml2.metadata) -
    - - -
    OneLogin_Saml2_Response (class in saml2.response) -
    - - -
    OneLogin_Saml2_Settings (class in saml2.settings) -
    - - -
    OneLogin_Saml2_Utils (class in saml2.utils) -
    - -
    - -

    P

    - - - -
    - -
    parse_duration() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    parse_SAML_to_time() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    parse_time_to_SAML() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    PRIVATE_KEY_FILE_NOT_FOUND (saml2.errors.OneLogin_Saml2_Error attribute) -
    - -
    - -
    process_response() (saml2.auth.OneLogin_Saml2_Auth method) -
    - - -
    process_slo() (saml2.auth.OneLogin_Saml2_Auth method) -
    - - -
    PUBLIC_CERT_FILE_NOT_FOUND (saml2.errors.OneLogin_Saml2_Error attribute) -
    - -
    - -

    Q

    - - -
    - -
    query() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - -
    - -

    R

    - - - -
    - -
    redirect() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - - -
    REDIRECT_INVALID_URL (saml2.errors.OneLogin_Saml2_Error attribute) -
    - -
    - -
    redirect_to() (saml2.auth.OneLogin_Saml2_Auth method) -
    - - -
    RSA_SHA1 (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - -
    - -

    S

    - - - -
    - -
    saml2.auth (module) -
    - - -
    saml2.authn_request (module) -
    - - -
    saml2.constants (module) -
    - - -
    saml2.errors (module) -
    - - -
    saml2.logout_request (module) -
    - - -
    saml2.logout_response (module) -
    - - -
    saml2.metadata (module) -
    - - -
    saml2.response (module) -
    - - -
    saml2.settings (module) -
    - - -
    saml2.utils (module) -
    - - -
    SAML_LOGOUTMESSAGE_NOT_FOUND (saml2.errors.OneLogin_Saml2_Error attribute) -
    - - -
    SAML_LOGOUTREQUEST_INVALID (saml2.errors.OneLogin_Saml2_Error attribute) -
    - - -
    SAML_LOGOUTRESPONSE_INVALID (saml2.errors.OneLogin_Saml2_Error attribute) -
    - - -
    SAML_RESPONSE_NOT_FOUND (saml2.errors.OneLogin_Saml2_Error attribute) -
    - -
    - -
    SAML_SINGLE_LOGOUT_NOT_SUPPORTED (saml2.errors.OneLogin_Saml2_Error attribute) -
    - - -
    set_strict() (saml2.auth.OneLogin_Saml2_Auth method) -
    - -
    - -
    (saml2.settings.OneLogin_Saml2_Settings method) -
    - -
    - -
    SETTINGS_FILE_NOT_FOUND (saml2.errors.OneLogin_Saml2_Error attribute) -
    - - -
    SETTINGS_INVALID (saml2.errors.OneLogin_Saml2_Error attribute) -
    - - -
    SETTINGS_INVALID_SYNTAX (saml2.errors.OneLogin_Saml2_Error attribute) -
    - - -
    sign_metadata() (saml2.metadata.OneLogin_Saml2_Metadata static method) -
    - - -
    SP_CERTS_NOT_FOUND (saml2.errors.OneLogin_Saml2_Error attribute) -
    - - -
    STATUS_NO_PASSIVE (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    STATUS_PARTIAL_LOGOUT (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    STATUS_PROXY_COUNT_EXCEEDED (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    STATUS_REQUESTER (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    STATUS_RESPONDER (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    STATUS_SUCCESS (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - - -
    STATUS_VERSION_MISMATCH (saml2.constants.OneLogin_Saml2_Constants attribute) -
    - -
    - -

    T

    - - - -
    - -
    TIME_CACHED (saml2.metadata.OneLogin_Saml2_Metadata attribute) -
    - -
    - -
    TIME_VALID (saml2.metadata.OneLogin_Saml2_Metadata attribute) -
    - -
    - -

    V

    - - - -
    - -
    validate_metadata() (saml2.settings.OneLogin_Saml2_Settings method) -
    - - -
    validate_num_assertions() (saml2.response.OneLogin_Saml2_Response method) -
    - - -
    validate_sign() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - -
    - -
    validate_timestamps() (saml2.response.OneLogin_Saml2_Response method) -
    - - -
    validate_url() (in module saml2.settings) -
    - - -
    validate_xml() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - -
    - -

    W

    - - -
    - -
    write_temp_file() (saml2.utils.OneLogin_Saml2_Utils static method) -
    - -
    - - - -
    -
    -
    -
    -
    - - - - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/index.html b/docs/saml2/index.html deleted file mode 100644 index d17bd654..00000000 --- a/docs/saml2/index.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - Welcome to OneLogin SAML Python library documentation - - - - - - - - - - - - - - -
    -
    -
    -
    - - -
    -

    Indices and tables

    - -
    - - -
    -
    -
    -
    -
    -

    Table Of Contents

    - - -

    Next topic

    -

    onelogin.saml2 Module

    -

    This Page

    - - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/objects.inv b/docs/saml2/objects.inv deleted file mode 100644 index f4df5df0cf8a1203e12c4a54b64e6f611ff0f28d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1959 zcmV;Y2Uz$cAX9K?X>NERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkUZe>hw zXK8LAQ$bBkAW(U9Xm4&HY-wV0VRCsOV{Bn_b7gZNVQyp~ZDn+5Z)9@{BOq2~a&u{K zZaN?eBOp|0Wgv28ZDDC{WMy(7Z)PBLXlZjGW@&6?AZc?TV{dJ6a%FRKWn>_Ab7^j8 zAbM?s|2V9Tf1|@6!eqtX@uVz%yH4whi=+}}pI!S5+dIDwDmJfIc?DEG0DJqur|?K!B{31A&7V8LW*>Hi?r-`~ zkfqjEOaSLKbZyaZjfr)g9g~9G6Y*`-rh2^}idyuG8Cw>KV31T9JSUv}@jI|8t+QT# z@>3#DPj7WFfkWX$tzL(bdvC=AzLF=nb>{^9Acosu;X9~pZ_M~mINhu(ofi2sG|A{w zla*?D#C3r9u^T_3_PFn%hmz;R(`bCE_M2x|&7zLa(={s-j#R-3Lr}ulOOv+@L|&q= z|ERq`cX^R*^a4CzRf*_YHffJ7%jEu2f!!S9#%&L7h!v3tb=wGmG`8Xe1-h_pLg`fZ zT-I%w2uN9rKD4$)R!PY;%<_1wm;`1tutIaFz?-ekF1EIeDIDCOq5{ag>cJALsgtq` zCQ-Mj)1&~-IM3?}D|Vy#L&?$3jig1w`GgC(aec@2zkow-=MupUJ4ZzRS`Zo|o$geG zp_Vs?dGIAz#GZ6F>x*uTIT^2iylo=5SaBSw03&x7c~%SypN7PCZ(ZV;((mTlj5*U; zTS9r`4o#dAo;WL(ia$YBgM_d4)qo>@Pl`NK1n8wKOi&(5;#l4Qy@c=RI1mj3-$6PM zR;}xrn+&c0v-ilI?C&0lEys#2pdq;ay)}0o4VmX-)md}SIdJ){bXsU>l9mrV3q%bH z#3u8tO(Y)z`Dr1Wr1emcw^@%}d2P03-C;$o}bb#r+D)5=uc|SWH6?Y51(}1Y2+YB~uA{o6-*DU=8N>qo~-l_YK zh~q}Y-rl+-l=JE3&$NAQWQJ0(QhI?YIG>(2jcP;RD+-2HNd#FbM0v|txRCjn0Qpn5 zkSv8EytF;I3+6J#QPtz0Q;OYezi%B49&g0tdm!nuoK-0XM^N&HNTd_x&;{vjjGIh@ z6+jQEDK%c}A1k4GhOO*rFhV3P(NV35BAg8Ip+)IK5aD$AMlaGF6~ZDo;#?7x`TL#q z!DR8m0AE^c2R(#Qel@CHGqk(QpHrTd|6@ijH?xF14u9OgPGF zHmx3+y_gDqbtPmT&5d4$XSs8*94Er|dhn@D)s-OeLt>qbQQRam53MM6tqqs{Ik9y& ze>AjhKj`7my(be*-vl;~1`tP+p7qA*wnZw!J#+Tkcw!%r!>q3supM z#@K*%3`xwijn(9Z!PzdlZM5O(4hr1Pp7to<@;bx{Zr_x#GWaNK+qD~iK z7)02tLW>RVdeFo?BTb65>&F#)EM7`=u(oOQN*Ehg8*Wr*UQHLD6#2fjY?JIpD`p&3 zLDSZ$WOe(KU6;>(E^g+o-SR=9_99ybN?&g7BHp>vvb?UvaaY4G+V75MwTV7}(YqONT$4;}cxgG8?2 zL8>+IEZ2v>7V-d!2ZDNTlnml63*93e3(-d1Y8aE&89TqA)`~}9P2qi%n>I!w<>^++ zjf@D}9#Eo2-rzo1*ms0Z?y diff --git a/docs/saml2/py-modindex.html b/docs/saml2/py-modindex.html deleted file mode 100644 index e7321859..00000000 --- a/docs/saml2/py-modindex.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - Python Class Index — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - - - -
    -
    -
    -
    - - -

    Python Class Index

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - saml2 -
        - saml2.auth -
        - saml2.authn_request -
        - saml2.constants -
        - saml2.errors -
        - saml2.logout_request -
        - saml2.logout_response -
        - saml2.metadata -
        - saml2.response -
        - saml2.settings -
        - saml2.utils -
    - - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/saml2.html b/docs/saml2/saml2.html deleted file mode 100644 index 6dd1e00a..00000000 --- a/docs/saml2/saml2.html +++ /dev/null @@ -1,2044 +0,0 @@ - - - - - - - - - - OneLogin saml2 Module — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - -
    -
    -
    -
    - -
    -

    OneLogin saml2 Module

    -
    -

    auth Class

    -
    -
    -class onelogin.saml2.auth.OneLogin_Saml2_Auth(request_data, old_settings=None)[source]
    -

    Bases: object

    -
    -
    -build_request_signature(saml_request, relay_state)[source]
    -

    Builds the Signature of the SAML Request.

    - --- - - - -
    Parameters:
      -
    • saml_request (string) – The SAML Request
    • -
    • relay_state (string) – The target URL the user should be redirected to
    • -
    -
    -
    - -
    -
    -build_response_signature(saml_response, relay_state)[source]
    -

    Builds the Signature of the SAML Response. -:param saml_request: The SAML Response -:type saml_request: string

    - --- - - - -
    Parameters:relay_state (string) – The target URL the user should be redirected to
    -
    - -
    -
    -get_attribute(name)[source]
    -

    Returns the requested SAML attribute.

    - --- - - - - - - - -
    Parameters:name (string) – Name of the attribute
    Returns:Attribute value if exists or None
    Return type:string
    -
    - -
    -
    -get_attributes()[source]
    -

    Returns the set of SAML attributes.

    - --- - - - - - -
    Returns:SAML attributes
    Return type:dict
    -
    - -
    -
    -get_errors()[source]
    -

    Returns a list with code errors if something went wrong

    - --- - - - - - -
    Returns:List of errors
    Return type:list
    -
    - -
    -
    -get_last_error_reason()[source]
    -

    Returns the reason for the last error

    - --- - - - - - -
    Returns:Error
    Return type:string
    -
    - -
    -
    -get_nameid()[source]
    -

    Returns the nameID.

    - --- - - - - - -
    Returns:NameID
    Return type:string
    -
    - -
    -
    -get_settings()[source]
    -

    Returns the settings info -:return: Setting info -:rtype: OneLogin_Saml2_Setting object

    -
    - -
    -
    -get_slo_url()[source]
    -

    Gets the SLO url.

    - --- - - - - - -
    Returns:An URL, the SLO endpoint of the IdP
    Return type:string
    -
    - -
    -
    -get_sso_url()[source]
    -

    Gets the SSO url.

    - --- - - - - - -
    Returns:An URL, the SSO endpoint of the IdP
    Return type:string
    -
    - -
    -
    -is_authenticated()[source]
    -

    Checks if the user is authenticated or not.

    - --- - - - - - -
    Returns:True if is authenticated, False if not
    Return type:bool
    -
    - -
    -
    -login(return_to=None, force_authn=False, is_passive=False)[source]
    -

    Initiates the SSO process.

    - --- - - - - - -
    Parameters:
      -
    • return_to (string) – Optional argument. The target URL the user should be redirected to after login.
    • -
    • force_authn (bool) – Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'.
    • -
    • is_passive (bool) – Optional argument. When true the AuthNReuqest will set the Ispassive='true'.
    • -
    -
    Returns:Redirection url
    -
    - -
    -
    -logout(return_to=None, name_id=None, session_index=None)[source]
    -

    Initiates the SLO process.

    - --- - - - - - - -
    Parameters:
      -
    • return_to (string) – Optional argument. The target URL the user should be redirected to after logout.
    • -
    • name_id (string) – Optional argument. The NameID that will be set in the LogoutRequest.
    • -
    • session_index (string) – Optional argument. SessionIndex that identifies the session of the user.
    • -
    Returns:Redirection url
    -
    - -
    -
    -process_response(request_id=None)[source]
    -

    Process the SAML Response sent by the IdP.

    - --- - - - - - -
    Parameters:request_id (string) – Is an optional argumen. Is the ID of the AuthNRequest sent by this SP to the IdP.
    Raises :OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND, when a POST with a SAMLResponse is not found
    -
    - -
    -
    -process_slo(keep_local_session=False, request_id=None, delete_session_cb=None)[source]
    -

    Process the SAML Logout Response / Logout Request sent by the IdP.

    - --- - - - - - -
    Parameters:
      -
    • keep_local_session (bool) – When false will destroy the local session, otherwise will destroy it
    • -
    • request_id (string) – The ID of the LogoutRequest sent by this SP to the IdP
    • -
    -
    Returns:

    Redirection url

    -
    -
    - -
    -
    -redirect_to(url=None, parameters={})[source]
    -

    Redirects the user to the url past by parameter or to the url that we defined in our SSO Request.

    - --- - - - - - -
    Parameters:
      -
    • url (string) – The target URL to redirect the user
    • -
    • parameters (dict) – Extra parameters to be passed as part of the url
    • -
    -
    Returns:

    Redirection url

    -
    -
    - -
    -
    -set_strict(value)[source]
    -

    Set the strict mode active/disable

    - --- - - - -
    Parameters:value (bool) –
    -
    - -
    - -
    -
    -

    authn_request Class

    -
    -
    -class onelogin.saml2.authn_request.OneLogin_Saml2_Authn_Request(settings, force_authn=False, is_passive=False)[source]
    -
    -
    -get_request()[source]
    -

    Returns unsigned AuthnRequest. -:return: Unsigned AuthnRequest -:rtype: str object

    -
    - -
    - -
    -
    -

    constants Class

    -
    -
    -class onelogin.saml2.constants.OneLogin_Saml2_Constants[source]
    -
    -
    -AC_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos'
    -
    - -
    -
    -AC_PASSWORD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
    -
    - -
    -
    -AC_SMARTCARD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard'
    -
    - -
    -
    -AC_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified'
    -
    - -
    -
    -AC_X509 = 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'
    -
    - -
    -
    -ALOWED_CLOCK_DRIFT = 180
    -
    - -
    -
    -ATTRNAME_FORMAT_BASIC = 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic'
    -
    - -
    -
    -ATTRNAME_FORMAT_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified'
    -
    - -
    -
    -ATTRNAME_FORMAT_URI = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'
    -
    - -
    -
    -BINDING_DEFLATE = 'urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE'
    -
    - -
    -
    -BINDING_HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'
    -
    - -
    -
    -BINDING_HTTP_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
    -
    - -
    -
    -BINDING_HTTP_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
    -
    - -
    -
    -BINDING_SOAP = 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP'
    -
    - -
    -
    -CM_BEARER = 'urn:oasis:names:tc:SAML:2.0:cm:bearer'
    -
    - -
    -
    -CM_HOLDER_KEY = 'urn:oasis:names:tc:SAML:2.0:cm:holder-of-key'
    -
    - -
    -
    -CM_SENDER_VOUCHES = 'urn:oasis:names:tc:SAML:2.0:cm:sender-vouches'
    -
    - -
    -
    -NAMEID_EMAIL_ADDRESS = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
    -
    - -
    -
    -NAMEID_ENCRYPTED = 'urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted'
    -
    - -
    -
    -NAMEID_ENTITY = 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity'
    -
    - -
    -
    -NAMEID_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos'
    -
    - -
    -
    -NAMEID_PERSISTENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
    -
    - -
    -
    -NAMEID_TRANSIENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
    -
    - -
    -
    -NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName'
    -
    - -
    -
    -NAMEID_X509_SUBJECT_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName'
    -
    - -
    -
    -NSMAP = {'xenc': 'http://www.w3.org/2001/04/xmlenc#', 'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol', 'ds': 'http://www.w3.org/2000/09/xmldsig#', 'saml': 'urn:oasis:names:tc:SAML:2.0:assertion'}
    -
    - -
    -
    -NS_DS = 'http://www.w3.org/2000/09/xmldsig#'
    -
    - -
    -
    -NS_MD = 'urn:oasis:names:tc:SAML:2.0:metadata'
    -
    - -
    -
    -NS_SAML = 'urn:oasis:names:tc:SAML:2.0:assertion'
    -
    - -
    -
    -NS_SAMLP = 'urn:oasis:names:tc:SAML:2.0:protocol'
    -
    - -
    -
    -NS_SOAP = 'http://schemas.xmlsoap.org/soap/envelope/'
    -
    - -
    -
    -NS_XENC = 'http://www.w3.org/2001/04/xmlenc#'
    -
    - -
    -
    -NS_XS = 'http://www.w3.org/2001/XMLSchema'
    -
    - -
    -
    -NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
    -
    - -
    -
    -RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
    -
    - -
    -
    -STATUS_NO_PASSIVE = 'urn:oasis:names:tc:SAML:2.0:status:NoPassive'
    -
    - -
    -
    -STATUS_PARTIAL_LOGOUT = 'urn:oasis:names:tc:SAML:2.0:status:PartialLogout'
    -
    - -
    -
    -STATUS_PROXY_COUNT_EXCEEDED = 'urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded'
    -
    - -
    -
    -STATUS_REQUESTER = 'urn:oasis:names:tc:SAML:2.0:status:Requester'
    -
    - -
    -
    -STATUS_RESPONDER = 'urn:oasis:names:tc:SAML:2.0:status:Responder'
    -
    - -
    -
    -STATUS_SUCCESS = 'urn:oasis:names:tc:SAML:2.0:status:Success'
    -
    - -
    -
    -STATUS_VERSION_MISMATCH = 'urn:oasis:names:tc:SAML:2.0:status:VersionMismatch'
    -
    - -
    - -
    -
    -

    errors Class

    -
    -
    -exception onelogin.saml2.errors.OneLogin_Saml2_Error(message, code=0, errors=None)[source]
    -

    Bases: exceptions.Exception

    -
    -
    -METADATA_SP_INVALID = 3
    -
    - -
    -
    -PRIVATE_KEY_FILE_NOT_FOUND = 7
    -
    - -
    -
    -PUBLIC_CERT_FILE_NOT_FOUND = 6
    -
    - -
    -
    -REDIRECT_INVALID_URL = 5
    -
    - -
    -
    -SAML_LOGOUTMESSAGE_NOT_FOUND = 9
    -
    - -
    -
    -SAML_LOGOUTREQUEST_INVALID = 10
    -
    - -
    -
    -SAML_LOGOUTRESPONSE_INVALID = 11
    -
    - -
    -
    -SAML_RESPONSE_NOT_FOUND = 8
    -
    - -
    -
    -SAML_SINGLE_LOGOUT_NOT_SUPPORTED = 12
    -
    - -
    -
    -SETTINGS_FILE_NOT_FOUND = 0
    -
    - -
    -
    -SETTINGS_INVALID = 2
    -
    - -
    -
    -SETTINGS_INVALID_SYNTAX = 1
    -
    - -
    -
    -SP_CERTS_NOT_FOUND = 4
    -
    - -
    - -
    -
    -

    logout_request Class

    -
    -
    -class onelogin.saml2.logout_request.OneLogin_Saml2_Logout_Request(settings, request=None, name_id=None, session_index=None)[source]
    -
    -
    -static get_id(request)[source]
    -

    Returns the ID of the Logout Request -:param request: Logout Request Message -:type request: string|DOMDocument -:return: string ID -:rtype: str object

    -
    - -
    -
    -static get_issuer(request)[source]
    -

    Gets the Issuer of the Logout Request Message -:param request: Logout Request Message -:type request: string|DOMDocument -:return: The Issuer -:rtype: string

    -
    - -
    -
    -static get_name_id(request, key=None)[source]
    -

    Gets the NameID of the Logout Request Message -:param request: Logout Request Message -:type request: string|DOMDocument -:param key: The SP key -:type key: string -:return: Name ID Value -:rtype: string

    -
    - -
    -
    -static get_name_id_data(request, key=None)[source]
    -

    Gets the NameID Data of the the Logout Request -:param request: Logout Request Message -:type request: string|DOMDocument -:param key: The SP key -:type key: string -:return: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) -:rtype: dict

    -
    - -
    -
    -get_request()[source]
    -

    Returns the Logout Request defated, base64encoded -:return: Deflated base64 encoded Logout Request -:rtype: str object

    -
    - -
    -
    -static get_session_indexes(request)[source]
    -

    Gets the SessionIndexes from the Logout Request -:param request: Logout Request Message -:type request: string|DOMDocument -:return: The SessionIndex value -:rtype: list

    -
    - -
    -
    -static is_valid(settings, request, get_data, debug=False)[source]
    -

    Checks if the Logout Request recieved is valid -:param settings: Settings -:type settings: OneLogin_Saml2_Settings -:param request: Logout Request Message -:type request: string|DOMDocument -:return: If the Logout Request is or not valid -:rtype: boolean

    -
    - -
    -
    -get_error()[source]
    -

    After execute a validation process, if fails this method returns the cause -:rtype: str object

    -
    - -
    - -
    -
    -

    logout_response Class

    -
    -
    -class onelogin.saml2.logout_response.OneLogin_Saml2_Logout_Response(settings, response=None)[source]
    -
    -
    -build(in_response_to)[source]
    -

    Creates a Logout Response object. -:param in_response_to: InResponseTo value for the Logout Response. -:type in_response_to: string

    -
    - -
    -
    -get_issuer()[source]
    -

    Gets the Issuer of the Logout Response Message -:return: The Issuer -:rtype: string

    -
    - -
    -
    -get_response()[source]
    -

    Returns a Logout Response object. -:return: Logout Response deflated and base64 encoded -:rtype: string

    -
    - -
    -
    -get_status()[source]
    -

    Gets the Status -:return: The Status -:rtype: string

    -
    - -
    -
    -is_valid(request_data, request_id=None)[source]
    -

    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 -:return: Returns if the SAML LogoutResponse is or not valid -:rtype: boolean

    -
    - -
    -
    -get_error()[source]
    -

    After execute a validation process, if fails this method returns the cause -:rtype: str object

    -
    - -
    - -
    -
    -

    metadata Class

    -
    -
    -class onelogin.saml2.metadata.OneLogin_Saml2_Metadata[source]
    -
    -
    -TIME_CACHED = 604800
    -
    - -
    -
    -TIME_VALID = 172800
    -
    - -
    -
    -static add_x509_key_descriptors(metadata, cert)[source]
    -

    Add the x509 descriptors (sign/encriptation to the metadata -The same cert will be used for sign/encrypt

    - --- - - - - - - - -
    Parameters:
      -
    • metadata (string) – SAML Metadata XML
    • -
    • cert (string) – x509 cert
    • -
    -
    Returns:

    Metadata with KeyDescriptors

    -
    Return type:

    string

    -
    -
    - -
    -
    -static builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None)[source]
    -

    Build the metadata of the SP

    - --- - - - -
    Parameters:
      -
    • sp (string) – The SP data
    • -
    • authnsign (string) – authnRequestsSigned attribute
    • -
    • wsign (string) – wantAssertionsSigned attribute
    • -
    • valid_until (DateTime) – Metadata’s valid time
    • -
    • cache_duration (Timestamp) – Duration of the cache in seconds
    • -
    • contacts (dict) – Contacts info
    • -
    • organization (dict) – Organization ingo
    • -
    -
    -
    - -
    -
    -static sign_metadata(metadata, key, cert)[source]
    -

    Sign the metadata with the key/cert provided

    - --- - - - - - - - -
    Parameters:
      -
    • metadata (string) – SAML Metadata XML
    • -
    • key (string) – x509 key
    • -
    • cert (string) – x509 cert
    • -
    -
    Returns:

    Signed Metadata

    -
    Return type:

    string

    -
    -
    - -
    - -
    -
    -

    response Class

    -
    -
    -class onelogin.saml2.response.OneLogin_Saml2_Response(settings, response)[source]
    -

    Bases: object

    -
    -
    -check_status()[source]
    -

    Check if the status of the response is success or not

    - --- - - - -
    Raises :Exception. If the status is not success
    -
    - -
    -
    -get_attributes()[source]
    -

    Gets the Attributes from the AttributeStatement element. -EncryptedAttributes are not supported

    -
    - -
    -
    -get_audiences()[source]
    -

    Gets the audiences

    - --- - - - - - -
    Returns:The valid audiences for the SAML Response
    Return type:list
    -
    - -
    -
    -get_issuers()[source]
    -

    Gets the issuers (from message and from assertion)

    - --- - - - - - -
    Returns:The issuers
    Return type:list
    -
    - -
    -
    -get_nameid()[source]
    -

    Gets the NameID provided by the SAML Response from the IdP

    - --- - - - - - -
    Returns:NameID (value)
    Return type:string
    -
    - -
    -
    -get_nameid_data()[source]
    -

    Gets the NameID Data provided by the SAML Response from the IdP

    - --- - - - - - -
    Returns:Name ID Data (Value, Format, NameQualifier, SPNameQualifier)
    Return type:dict
    -
    - -
    -
    -get_session_index()[source]
    -

    Gets the SessionIndex from the AuthnStatement -Could be used to be stored in the local session in order -to be used in a future Logout Request that the SP could -send to the SP, to set what specific session must be deleted

    - --- - - - - - -
    Returns:The SessionIndex value
    Return type:string|None
    -
    - -
    -
    -get_session_not_on_or_after()[source]
    -

    Gets the SessionNotOnOrAfter from the AuthnStatement -Could be used to set the local session expiration

    - --- - - - - - -
    Returns:The SessionNotOnOrAfter value
    Return type:time|None
    -
    - -
    -
    -is_valid(request_data, request_id=None)[source]
    -

    Constructs the response object.

    - --- - - - - - - - -
    Parameters:request_id (string) – Optional argument. The ID of the AuthNRequest sent by this SP to the IdP
    Returns:True if the SAML Response is valid, False if not
    Return type:bool
    -
    - -
    -
    -validate_num_assertions()[source]
    -

    Verifies that the document only contains a single Assertion (encrypted or not)

    - --- - - - - - -
    Returns:True if only 1 assertion encrypted or not
    Return type:bool
    -
    - -
    -
    -validate_timestamps()[source]
    -

    Verifies that the document is valid according to Conditions Element

    - --- - - - - - -
    Returns:True if the condition is valid, False otherwise
    Return type:bool
    -
    - -
    - -
    -
    -

    settings Class

    -
    -
    -class onelogin.saml2.settings.OneLogin_Saml2_Settings(settings=None, custom_base_path=None)[source]
    -
    -
    -check_settings(settings)[source]
    -

    Checks the settings info.

    - --- - - - - - - - -
    Parameters:settings (dict) – Dict with settings data
    Returns:Errors found on the settings data
    Return type:list
    -
    - -
    -
    -check_sp_certs()[source]
    -

    Checks if the x509 certs of the SP exists and are valid.

    - --- - - - - - -
    Returns:If the x509 certs of the SP exists and are valid
    Return type:boolean
    -
    - -
    -
    -format_idp_cert()[source]
    -

    Formats the IdP cert.

    -
    - -
    -
    -get_base_path()[source]
    -

    Returns base path

    - --- - - - - - -
    Returns:The base toolkit folder path
    Return type:string
    -
    - -
    -
    -get_cert_path()[source]
    -

    Returns cert path

    - --- - - - - - -
    Returns:The cert folder path
    Return type:string
    -
    - -
    -
    -get_contacts()[source]
    -

    Gets contact data.

    - --- - - - - - -
    Returns:Contacts info
    Return type:dict
    -
    - -
    -
    -get_errors()[source]
    -

    Returns an array with the errors, the array is empty when the settings is ok.

    - --- - - - - - -
    Returns:Errors
    Return type:list
    -
    - -
    -
    -get_ext_lib_path()[source]
    -

    Returns external lib path

    - --- - - - - - -
    Returns:The external library folder path
    Return type:string
    -
    - -
    -
    -get_idp_data()[source]
    -

    Gets the IdP data.

    - --- - - - - - -
    Returns:IdP info
    Return type:dict
    -
    - -
    -
    -get_lib_path()[source]
    -

    Returns lib path

    - --- - - - - - -
    Returns:The library folder path
    Return type:string
    -
    - -
    -
    -get_organization()[source]
    -

    Gets organization data.

    - --- - - - - - -
    Returns:Organization info
    Return type:dict
    -
    - -
    -
    -get_schemas_path()[source]
    -

    Returns schema path

    - --- - - - - - -
    Returns:The schema folder path
    Return type:string
    -
    - -
    -
    -get_security_data()[source]
    -

    Gets security data.

    - --- - - - - - -
    Returns:Security info
    Return type:dict
    -
    - -
    -
    -get_sp_cert()[source]
    -

    Returns the x509 public cert of the SP.

    - --- - - - - - -
    Returns:SP public cert
    Return type:string
    -
    - -
    -
    -get_sp_data()[source]
    -

    Gets the SP data.

    - --- - - - - - -
    Returns:SP info
    Return type:dict
    -
    - -
    -
    -get_sp_key()[source]
    -

    Returns the x509 private key of the SP.

    - --- - - - - - -
    Returns:SP private key
    Return type:string
    -
    - -
    -
    -get_sp_metadata()[source]
    -

    Gets the SP metadata. The XML representation.

    - --- - - - - - -
    Returns:SP metadata (xml)
    Return type:string
    -
    - -
    -
    -is_debug_active()[source]
    -

    Returns if the debug is active.

    - --- - - - - - -
    Returns:Debug parameter
    Return type:boolean
    -
    - -
    -
    -is_strict()[source]
    -

    Returns if the ‘strict’ mode is active.

    - --- - - - - - -
    Returns:Strict parameter
    Return type:boolean
    -
    - -
    -
    -set_strict(value)[source]
    -

    Activates or deactivates the strict mode.

    - --- - - - -
    Parameters:xml (boolean) – Strict parameter
    -
    - -
    -
    -validate_metadata(xml)[source]
    -

    Validates an XML SP Metadata.

    - --- - - - - - - - -
    Parameters:xml (string) – Metadata’s XML that will be validate
    Returns:The list of found errors
    Return type:list
    -
    - -
    - -
    -
    -onelogin.saml2.settings.validate_url(url)[source]
    -
    - -
    -
    -

    utils Class

    -
    -
    -class onelogin.saml2.utils.OneLogin_Saml2_Utils[source]
    -
    -
    -static add_sign(xml, key, cert)[source]
    -

    Adds signature key and senders certificate to an element (Message or -Assertion).

    - --- - - - - - - - - - -
    Parameters:
      -
    • xml – The element we should sign
    • -
    • key – The private key
    • -
    • cert – The public
    • -
    -
    Type :

    string | Document

    -
    Type :

    string

    -
    Type :

    string

    -
    -
    - -
    -
    -static calculate_x509_fingerprint(x509_cert)[source]
    -

    Calculates the fingerprint of a x509cert.

    - --- - - - - - - - - - -
    Parameters:x509_cert – x509 cert
    Type :string
    Returns:Formated fingerprint
    Return type:string
    -
    - -
    -
    -static decode_base64_and_inflate(value)[source]
    -

    base64 decodes and then inflates according to RFC1951 -:param value: a deflated and encoded string -:return: the string after decoding and inflating

    -
    - -
    -
    -static decrypt_element(encrypted_data, enc_ctx)[source]
    -

    Decrypts an encrypted element.

    - --- - - - - - - - - - - - -
    Parameters:
      -
    • encrypted_data – The encrypted data.
    • -
    • enc_ctx – The encryption context.
    • -
    -
    Type :

    DOMElement

    -
    Type :

    Encryption Context

    -
    Returns:

    The decrypted element.

    -
    Return type:

    DOMElement

    -
    -
    - -
    -
    -static deflate_and_base64_encode(value)[source]
    -

    Deflates and the base64 encodes a string -:param value: The string to deflate and encode -:return: The deflated and encoded string

    -
    - -
    -
    -static delete_local_session(callback=None)[source]
    -

    Deletes the local session.

    -
    - -
    -
    -static format_cert(cert, heads=True)[source]
    -

    Returns a x509 cert (adding header & footer if required).

    - --- - - - - - - - - - - - -
    Parameters:
      -
    • cert – A x509 unformated cert
    • -
    • heads – True if we want to include head and footer
    • -
    -
    Type :

    string

    -
    Type :

    boolean

    -
    Returns:

    Formated cert

    -
    Return type:

    string

    -
    -
    - -
    -
    -static format_finger_print(fingerprint)[source]
    -

    Formates a fingerprint.

    - --- - - - - - - - - - -
    Parameters:fingerprint – fingerprint
    Type :string
    Returns:Formated fingerprint
    Return type:string
    -
    - -
    -
    -static generate_name_id(value, sp_nq, sp_format, key=None)[source]
    -

    Generates a nameID.

    - --- - - - - - - - - - - - - - - - -
    Parameters:
      -
    • value – fingerprint
    • -
    • sp_nq – SP Name Qualifier
    • -
    • sp_format – SP Format
    • -
    • key – SP Key to encrypt the nameID
    • -
    -
    Type :

    string

    -
    Type :

    string

    -
    Type :

    string

    -
    Type :

    string

    -
    Returns:

    DOMElement | XMLSec nameID

    -
    Return type:

    string

    -
    -
    - -
    -
    -static generate_unique_id()[source]
    -

    Generates an unique string (used for example as ID for assertions).

    - --- - - - - - -
    Returns:A unique string
    Return type:string
    -
    - -
    -
    -static get_expire_time(cache_duration=None, valid_until=None)[source]
    -

    Compares 2 dates and returns the earliest.

    - --- - - - - - - - - - - - -
    Parameters:
      -
    • cache_duration – The duration, as a string.
    • -
    • valid_until – The valid until date, as a string or as a timestamp
    • -
    -
    Type :

    string

    -
    Type :

    string

    -
    Returns:

    The expiration time.

    -
    Return type:

    int

    -
    -
    - -
    -
    -static get_self_host(request_data)[source]
    -

    Returns the current host.

    - --- - - - - - - - - - -
    Parameters:request_data – The request as a dict
    Type :dict
    Returns:The current host
    Return type:string
    -
    - -
    -
    -static get_self_url(request_data)[source]
    -

    Returns the URL of the current host + current view + query.

    - --- - - - - - - - - - -
    Parameters:request_data – The request as a dict
    Type :dict
    Returns:The url of current host + current view + query
    Return type:string
    -
    - -
    -
    -static get_self_url_host(request_data)[source]
    -

    Returns the protocol + the current host + the port (if different than -common ports).

    - --- - - - - - - - - - -
    Parameters:request_data – The request as a dict
    Type :dict
    Returns:Url
    Return type:string
    -
    - -
    -
    -static get_self_url_no_query(request_data)[source]
    -

    Returns the URL of the current host + current view.

    - --- - - - - - - - - - -
    Parameters:request_data – The request as a dict
    Type :dict
    Returns:The url of current host + current view
    Return type:string
    -
    - -
    -
    -static get_status(dom)[source]
    -

    Gets Status from a Response.

    - --- - - - - - - - - - -
    Parameters:dom – The Response as XML
    Type :Document
    Returns:The Status, an array with the code and a message.
    Return type:dict
    -
    - -
    -
    -static is_https(request_data)[source]
    -

    Checks if https or http.

    - --- - - - - - - - - - -
    Parameters:request_data – The request as a dict
    Type :dict
    Returns:False if https is not active
    Return type:boolean
    -
    - -
    -
    -static parse_SAML_to_time(timestr)[source]
    -

    Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(.s+)?Z -to a UNIX timestamp. The sub-second part is ignored.

    - --- - - - - - - - - - -
    Parameters:time – The time we should convert (SAML Timestamp).
    Type :string
    Returns:Converted to a unix timestamp.
    Return type:int
    -
    - -
    -
    -static parse_duration(duration, timestamp=None)[source]
    -

    Interprets a ISO8601 duration value relative to a given timestamp.

    - --- - - - - - - - - - - - -
    Parameters:
      -
    • duration – The duration, as a string.
    • -
    • timestamp – The unix timestamp we should apply the duration to. -Optional, default to the current time.
    • -
    -
    Type :

    string

    -
    Type :

    string

    -
    Returns:

    The new timestamp, after the duration is applied.

    -
    Return type:

    int

    -
    -
    - -
    -
    -static parse_time_to_SAML(time)[source]
    -

    Converts a UNIX timestamp to SAML2 timestamp on the form -yyyy-mm-ddThh:mm:ss(.s+)?Z.

    - --- - - - - - - - - - -
    Parameters:time – The time we should convert (DateTime).
    Type :string
    Returns:SAML2 timestamp.
    Return type:string
    -
    - -
    -
    -static query(dom, query, context=None)[source]
    -

    Extracts nodes that match the query from the Element

    - --- - - - - - - - - - - - - - -
    Parameters:
      -
    • dom – The root of the lxml objet
    • -
    • query – Xpath Expresion
    • -
    • context – Context Node
    • -
    -
    Type :

    Element

    -
    Type :

    string

    -
    Type :

    DOMElement

    -
    Returns:

    The queried nodes

    -
    Return type:

    list

    -
    -
    - -
    -
    -static redirect(url, parameters={}, request_data={})[source]
    -

    Executes a redirection to the provided url (or return the target url).

    - --- - - - - - - - - - - - - - -
    Parameters:
      -
    • url – The target url
    • -
    • parameters – Extra parameters to be passed as part of the url
    • -
    • request_data – The request as a dict
    • -
    -
    Type :

    string

    -
    Type :

    dict

    -
    Type :

    dict

    -
    Returns:

    Url

    -
    Return type:

    string

    -
    -
    - -
    -
    -static validate_sign(xml, cert=None, fingerprint=None)[source]
    -

    Validates a signature (Message or Assertion).

    - --- - - - - - - - - - -
    Parameters:
      -
    • xml – The element we should validate
    • -
    • cert – The pubic cert
    • -
    • fingerprint – The fingerprint of the public cert
    • -
    -
    Type :

    string | Document

    -
    Type :

    string

    -
    Type :

    string

    -
    -
    - -
    -
    -static validate_xml(xml, schema, debug=False)[source]
    -
    - -
    -
    -static write_temp_file(content)[source]
    -

    Writes some content into a temporary file and returns it.

    - --- - - - - - - - - - -
    Parameters:content – The file content
    Type :string
    Returns:The temporary file
    Return type:file-like object
    -
    - -
    - -
    -
    - - -
    -
    -
    -
    -
    -

    Table Of Contents

    - - -

    Previous topic

    -

    Welcome to OneLogin SAML Python library documentation

    -

    This Page

    - - - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/search.html b/docs/saml2/search.html deleted file mode 100644 index 531d00de..00000000 --- a/docs/saml2/search.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - Search — OneLogin SAML Python library classes and methods - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    - -

    Search

    -
    - -

    - Please activate JavaScript to enable the search - functionality. -

    -
    -

    - From here you can search these documents. Enter your search - words into the box below and click "search". Note that the search - function will automatically search for all of the words. Pages - containing fewer words won't appear in the result list. -

    -
    - - - -
    - -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/docs/saml2/searchindex.js b/docs/saml2/searchindex.js deleted file mode 100644 index cf3c42ca..00000000 --- a/docs/saml2/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({objects:{"saml2.logout_response.OneLogin_Saml2_Logout_Response":{is_valid:[1,2,1,""],get_response:[1,2,1,""],get_status:[1,2,1,""],get_issuer:[1,2,1,""],build:[1,2,1,""]},"saml2.response.OneLogin_Saml2_Response":{get_audiences:[1,2,1,""],validate_num_assertions:[1,2,1,""],get_nameid_data:[1,2,1,""],get_session_index:[1,2,1,""],get_issuers:[1,2,1,""],is_valid:[1,2,1,""],check_status:[1,2,1,""],validate_timestamps:[1,2,1,""],get_nameid:[1,2,1,""],get_attributes:[1,2,1,""],get_session_not_on_or_after:[1,2,1,""]},"saml2.errors.OneLogin_Saml2_Error":{PUBLIC_CERT_FILE_NOT_FOUND:[1,1,1,""],SETTINGS_FILE_NOT_FOUND:[1,1,1,""],SAML_LOGOUTREQUEST_INVALID:[1,1,1,""],REDIRECT_INVALID_URL:[1,1,1,""],PRIVATE_KEY_FILE_NOT_FOUND:[1,1,1,""],SAML_LOGOUTMESSAGE_NOT_FOUND:[1,1,1,""],SAML_RESPONSE_NOT_FOUND:[1,1,1,""],METADATA_SP_INVALID:[1,1,1,""],SAML_LOGOUTRESPONSE_INVALID:[1,1,1,""],SETTINGS_INVALID:[1,1,1,""],SP_CERTS_NOT_FOUND:[1,1,1,""],SETTINGS_INVALID_SYNTAX:[1,1,1,""],SAML_SINGLE_LOGOUT_NOT_SUPPORTED:[1,1,1,""]},"saml2.errors":{OneLogin_Saml2_Error:[1,6,1,""]},"saml2.metadata.OneLogin_Saml2_Metadata":{sign_metadata:[1,3,1,""],builder:[1,3,1,""],add_x509_key_descriptors:[1,3,1,""],TIME_VALID:[1,1,1,""],TIME_CACHED:[1,1,1,""]},"saml2.response":{OneLogin_Saml2_Response:[1,4,1,""]},"saml2.settings.OneLogin_Saml2_Settings":{get_contacts:[1,2,1,""],get_security_data:[1,2,1,""],validate_metadata:[1,2,1,""],get_errors:[1,2,1,""],check_settings:[1,2,1,""],get_sp_data:[1,2,1,""],get_idp_data:[1,2,1,""],get_cert_path:[1,2,1,""],get_schemas_path:[1,2,1,""],set_strict:[1,2,1,""],get_base_path:[1,2,1,""],is_strict:[1,2,1,""],get_lib_path:[1,2,1,""],get_sp_key:[1,2,1,""],get_sp_metadata:[1,2,1,""],is_debug_active:[1,2,1,""],get_ext_lib_path:[1,2,1,""],get_sp_cert:[1,2,1,""],get_organization:[1,2,1,""],check_sp_certs:[1,2,1,""],format_idp_cert:[1,2,1,""]},"saml2.settings":{OneLogin_Saml2_Settings:[1,4,1,""],validate_url:[1,5,1,""]},"saml2.logout_response":{OneLogin_Saml2_Logout_Response:[1,4,1,""]},"saml2.authn_request.OneLogin_Saml2_Authn_Request":{get_request:[1,2,1,""]},"saml2.constants.OneLogin_Saml2_Constants":{NAMEID_EMAIL_ADDRESS:[1,1,1,""],CM_SENDER_VOUCHES:[1,1,1,""],CM_HOLDER_KEY:[1,1,1,""],NS_SAML:[1,1,1,""],RSA_SHA1:[1,1,1,""],NS_XS:[1,1,1,""],STATUS_PROXY_COUNT_EXCEEDED:[1,1,1,""],NS_SOAP:[1,1,1,""],NAMEID_ENCRYPTED:[1,1,1,""],STATUS_REQUESTER:[1,1,1,""],STATUS_NO_PASSIVE:[1,1,1,""],STATUS_PARTIAL_LOGOUT:[1,1,1,""],BINDING_HTTP_REDIRECT:[1,1,1,""],NAMEID_X509_SUBJECT_NAME:[1,1,1,""],AC_KERBEROS:[1,1,1,""],NAMEID_KERBEROS:[1,1,1,""],BINDING_HTTP_ARTIFACT:[1,1,1,""],NS_XENC:[1,1,1,""],BINDING_HTTP_POST:[1,1,1,""],CM_BEARER:[1,1,1,""],ALOWED_CLOCK_DRIFT:[1,1,1,""],BINDING_DEFLATE:[1,1,1,""],NAMEID_ENTITY:[1,1,1,""],AC_SMARTCARD:[1,1,1,""],AC_UNSPECIFIED:[1,1,1,""],NS_XSI:[1,1,1,""],NSMAP:[1,1,1,""],STATUS_RESPONDER:[1,1,1,""],AC_PASSWORD:[1,1,1,""],NS_SAMLP:[1,1,1,""],NS_DS:[1,1,1,""],STATUS_SUCCESS:[1,1,1,""],AC_X509:[1,1,1,""],NAMEID_TRANSIENT:[1,1,1,""],BINDING_SOAP:[1,1,1,""],ATTRNAME_FORMAT_UNSPECIFIED:[1,1,1,""],ATTRNAME_FORMAT_BASIC:[1,1,1,""],NS_MD:[1,1,1,""],ATTRNAME_FORMAT_URI:[1,1,1,""],NAMEID_PERSISTENT:[1,1,1,""],STATUS_VERSION_MISMATCH:[1,1,1,""],NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME:[1,1,1,""]},"saml2.authn_request":{OneLogin_Saml2_Authn_Request:[1,4,1,""]},"saml2.metadata":{OneLogin_Saml2_Metadata:[1,4,1,""]},"saml2.utils.OneLogin_Saml2_Utils":{generate_unique_id:[1,3,1,""],add_sign:[1,3,1,""],deflate_and_base64_encode:[1,3,1,""],get_status:[1,3,1,""],query:[1,3,1,""],redirect:[1,3,1,""],get_expire_time:[1,3,1,""],decode_base64_and_inflate:[1,3,1,""],parse_SAML_to_time:[1,3,1,""],parse_duration:[1,3,1,""],generate_name_id:[1,3,1,""],validate_xml:[1,3,1,""],get_self_host:[1,3,1,""],parse_time_to_SAML:[1,3,1,""],format_finger_print:[1,3,1,""],decrypt_element:[1,3,1,""],get_self_url_host:[1,3,1,""],get_self_url:[1,3,1,""],delete_local_session:[1,3,1,""],format_cert:[1,3,1,""],is_https:[1,3,1,""],calculate_x509_fingerprint:[1,3,1,""],get_self_url_no_query:[1,3,1,""],write_temp_file:[1,3,1,""],validate_sign:[1,3,1,""]},"saml2.logout_request.OneLogin_Saml2_Logout_Request":{get_issuer:[1,3,1,""],get_name_id:[1,3,1,""],get_request:[1,2,1,""],get_id:[1,3,1,""],is_valid:[1,3,1,""],get_session_indexes:[1,3,1,""],get_name_id_data:[1,3,1,""]},"saml2.utils":{OneLogin_Saml2_Utils:[1,4,1,""]},"saml2.constants":{OneLogin_Saml2_Constants:[1,4,1,""]},"saml2.auth.OneLogin_Saml2_Auth":{get_settings:[1,2,1,""],process_response:[1,2,1,""],get_errors:[1,2,1,""],build_request_signature:[1,2,1,""],redirect_to:[1,2,1,""],is_authenticated:[1,2,1,""],get_attribute:[1,2,1,""],build_response_signature:[1,2,1,""],set_strict:[1,2,1,""],process_slo:[1,2,1,""],get_sso_url:[1,2,1,""],logout:[1,2,1,""],login:[1,2,1,""],get_slo_url:[1,2,1,""],get_attributes:[1,2,1,""],get_nameid:[1,2,1,""]},"saml2.auth":{OneLogin_Saml2_Auth:[1,4,1,""]},saml2:{errors:[1,0,1,""],settings:[1,0,1,""],utils:[1,0,1,""],auth:[1,0,1,""],logout_request:[1,0,1,""],authn_request:[1,0,1,""],logout_response:[1,0,1,""],response:[1,0,1,""],constants:[1,0,1,""],metadata:[1,0,1,""]},"saml2.logout_request":{OneLogin_Saml2_Logout_Request:[1,4,1,""]}},terms:{represent:1,code:1,queri:1,issuer:1,privat:1,encryptedattribut:1,base64:1,ac_smartcard:1,specif:1,send:1,binding_defl:1,must:1,sent:1,deactiv:1,sourc:1,string:1,fals:1,parse_saml_to_tim:1,util:[0,1],xmlschema:1,public_cert_file_not_found:1,get_self_url_no_queri:1,settings_file_not_found:1,list:1,onelogin_saml2_util:1,sign_metadata:1,pubic:1,sign:1,past:1,second:1,pass:1,port:1,index:0,what:1,get_name_id:1,compar:1,get_idp_data:1,sp_nq:1,current:1,delet:1,x509subjectnam:1,"new":1,status_no_pass:1,"public":1,metadata:[0,1],redirect:1,keep_local_sess:1,gener:1,windowsdomainqualifiednam:1,logout:1,path:1,valu:1,search:0,sender:1,datetim:1,cert:1,is_debug_act:1,redirect_invalid_url:1,extra:1,appli:1,modul:[0,1],metadata_sp_invalid:1,is_authent:1,unix:1,"boolean":1,onelogin_saml2_respons:1,org:1,post:1,authnstat:1,from:1,ddthh:1,nameid_persist:1,emailaddress:1,status_request:1,type:1,until:1,keydescriptor:1,attrname_format_bas:1,iso8601:1,"transient":1,get_sp_cert:1,cach:1,status_proxy_count_exceed:1,none:1,endpoint:1,redirect_to:1,uniqu:1,descriptor:1,time_valid:1,root:1,status_success:1,request_data:1,objet:1,process:1,onelogin_saml2_auth:1,unform:1,indic:0,ac_unspecifi:1,want:1,unsign:1,lxml:1,secur:1,check_statu:1,status_respond:1,write:1,verifi:1,decrypt_el:1,ns_samlp:1,ac_x509:1,x509:1,after:1,validate_xml:1,nameid_x509_subject_nam:1,callback:1,date:1,data:1,domdocu:1,footer:1,bind:1,element:1,onelogin_saml2_logout_request:1,authn_request:[0,1],nameid:1,order:1,alowed_clock_drift:1,deflate_and_base64_encod:1,process_respons:1,paramet:1,settings_invalid_syntax:1,persist:1,get_slo_url:1,"return":1,timestamp:1,auth:[0,1],authnrequest:1,get_respons:1,get_self_url:1,format_idp_cert:1,namequalifi:1,authent:1,onelogin_saml2_authn_request:1,mode:1,request_id:1,debug:1,found:1,went:1,oasi:1,authnsign:1,"static":1,rsa_sha1:1,our:1,extract:1,check_sp_cert:1,content:[0,1],validate_metadata:1,rel:1,qualifi:1,generate_name_id:1,envelop:1,given:1,base:1,get_lib_path:1,format_finger_print:1,get_session_index:1,earliest:1,get_name_id_data:1,ns_x:1,could:1,wrong:1,domel:1,ns_d:1,time_cach:1,validate_sign:1,smartcard:1,arrai:1,messag:1,attrname_format_uri:1,kerbero:1,saml_single_logout_not_support:1,differ:1,construct:1,ns_xsi:1,parse_time_to_saml:1,store:1,schema:1,option:1,sessionnotonoraft:1,rsa:1,artifact:1,wantassertionssign:1,encrypted_data:1,vouch:1,part:1,is_strict:1,holder:1,than:1,target:1,provid:1,defat:1,str:1,get_nameid:1,initi:1,argument:1,packag:[0,1],expir:1,onelogin_saml2_error:1,tabl:0,onelogin_saml2_set:1,lib:1,build_response_signatur:1,destroi:1,contact:1,build:1,soap:1,singl:1,validate_timestamp:1,decode_base64_and_infl:1,object:1,nameid_kerbero:1,logout_request:[0,1],return_to:1,"class":1,sub:1,ns_saml:1,saml_logoutrequest_invalid:1,dom:1,url:1,urn:1,nsmap:1,uri:1,determin:1,saml_response_not_found:1,add_sign:1,session:1,nameid_ent:1,onelogin_saml2_const:1,xml:1,onli:1,get_sp_kei:1,timestr:1,activ:1,set_strict:1,should:1,get_sso_url:1,dict:1,folder:1,local:1,valid_until:1,nameid_transi:1,get:1,authnrequestssign:1,sso:1,expres:1,get_nameid_data:1,requir:1,organ:1,onelogin:[0,1],binding_http_redirect:1,common:1,contain:1,xmlenc:1,view:1,respond:1,certif:1,set:[0,1],get_set:1,respons:[0,1],statu:1,ingo:1,logoutrequest:1,check_set:1,someth:1,get_organ:1,saml_logoutresponse_invalid:1,entiti:1,attribut:1,signatur:1,accord:1,kei:1,samlrespons:1,old_set:1,wsign:1,sp_format:1,delete_session_cb:1,rtype:1,get_cert_path:1,format_cert:1,audienc:1,instanc:1,get_error:1,attrnam:1,login:1,validate_num_assert:1,generate_unique_id:1,settings_invalid:1,write_temp_fil:1,status_partial_logout:1,header:1,rfc1951:1,reciev:1,empti:1,interpret:1,basic:1,nameid_email_address:1,partiallogout:1,convert:1,assert:1,get_id:1,versionmismatch:1,saml_request:1,durat:1,defin:1,calcul:1,slo:1,error:[0,1],ns_xenc:1,cm_holder_kei:1,get_attribut:1,binding_soap:1,toolkit:1,sessionindex:1,cache_dur:1,sp_certs_not_found:1,get_statu:1,status_version_mismatch:1,welcom:0,saml:1,inresponseto:1,process_slo:1,same:1,decod:1,document:[0,1],get_security_data:1,http:1,context:1,inflat:1,onelogin_saml2_logout_respons:1,rais:1,temporari:1,user:1,binding_http_artifact:1,extern:1,get_self_url_host:1,sha1:1,builder:1,relay_st:1,calculate_x509_fingerprint:1,exampl:1,thi:1,get_contact:1,protocol:1,bearer:1,execut:1,private_key_file_not_found:1,fingerprint:1,ac_kerbero:1,get_audi:1,get_sp_data:1,except:1,param:1,proxycountexceed:1,add:1,is_valid:1,xenc:1,match:1,logoutrespons:1,nameid_encrypt:1,format:1,add_x509_key_descriptor:1,cm_sender_vouch:1,get_sp_metadata:1,password:1,enc_ctx:1,name:1,like:1,success:1,xmlsoap:1,nameid_windows_domain_qualified_nam:1,page:0,yyyi:1,is_http:1,www:1,some:1,unspecifi:1,librari:1,xmldsig:1,condit:1,get_ext_lib_path:1,get_expire_tim:1,cm_bearer:1,build_request_signatur:1,logout_respons:[0,1],host:1,get_base_path:1,x509cert:1,deflat:1,idp:1,get_request:1,disabl:1,encod:1,get_issu:1,validate_url:1,samlp:1,support:1,strict:1,encript:1,includ:1,delete_local_sess:1,xpath:1,head:1,form:1,spnamequalifi:1,saml_logoutmessage_not_found:1,parse_dur:1,ac_password:1,"true":1,info:1,binding_http_post:1,get_session_not_on_or_aft:1,"default":1,ns_md:1,otherwis:1,constant:[0,1],creat:1,"int":1,request:1,decrypt:1,onelogin_saml2_metadata:1,exist:1,file:1,check:1,saml2:[0,1],encrypt:1,attrname_format_unspecifi:1,get_schemas_path:1,get_data:1,when:1,valid:1,bool:1,futur:1,saml_respons:1,argumen:1,node:1,attributestat:1,get_self_host:1,x509_cert:1,nopass:1,ns_soap:1,xmlsec:1,ignor:1,base64encod:1,time:1,custom_base_path:1,in_response_to:1},objtypes:{"0":"py:module","1":"py:attribute","2":"py:method","3":"py:staticmethod","4":"py:class","5":"py:function","6":"py:exception"},titles:["Welcome to saml2’s documentation!","OneLogin saml2 Module"],objnames:{"0":["py","module","Python module"],"1":["py","attribute","Python attribute"],"2":["py","method","Python method"],"3":["py","staticmethod","Python static method"],"4":["py","class","Python class"],"5":["py","function","Python function"],"6":["py","exception","Python exception"]},filenames:["index","saml2"]}) \ No newline at end of file From 57d114542dbe2397c0e7e805ce6b9ed80005993c Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 1 Oct 2023 02:21:39 +0200 Subject: [PATCH 343/352] Updated doc --- docs/saml2/_modules/index.html | 110 + docs/saml2/_modules/onelogin/saml2/auth.html | 696 ++ .../onelogin/saml2/authn_request.html | 267 + .../_modules/onelogin/saml2/constants.html | 219 + .../saml2/_modules/onelogin/saml2/errors.html | 232 + .../onelogin/saml2/idp_metadata_parser.html | 362 + .../onelogin/saml2/logout_request.html | 549 + .../onelogin/saml2/logout_response.html | 377 + .../_modules/onelogin/saml2/metadata.html | 383 + .../_modules/onelogin/saml2/response.html | 1087 ++ .../_modules/onelogin/saml2/settings.html | 960 ++ docs/saml2/_modules/onelogin/saml2/utils.html | 1402 +++ .../_modules/onelogin/saml2/xmlparser.html | 244 + docs/saml2/_sources/index.rst.txt | 14 + docs/saml2/_sources/modules.rst.txt | 7 + docs/saml2/_sources/onelogin.rst.txt | 17 + docs/saml2/_sources/onelogin.saml2.rst.txt | 110 + docs/saml2/_static/ajax-loader.gif | Bin 0 -> 673 bytes docs/saml2/_static/basic.css | 676 + docs/saml2/_static/comment-bright.png | Bin 0 -> 756 bytes docs/saml2/_static/comment-close.png | Bin 0 -> 829 bytes docs/saml2/_static/comment.png | Bin 0 -> 641 bytes docs/saml2/_static/css/badge_only.css | 1 + .../_static/css/fonts/Roboto-Slab-Bold.woff | Bin 0 -> 87624 bytes .../_static/css/fonts/Roboto-Slab-Bold.woff2 | Bin 0 -> 67312 bytes .../css/fonts/Roboto-Slab-Regular.woff | Bin 0 -> 86288 bytes .../css/fonts/Roboto-Slab-Regular.woff2 | Bin 0 -> 66444 bytes .../_static/css/fonts/fontawesome-webfont.eot | Bin 0 -> 165742 bytes .../_static/css/fonts/fontawesome-webfont.svg | 2671 ++++ .../_static/css/fonts/fontawesome-webfont.ttf | Bin 0 -> 165548 bytes .../css/fonts/fontawesome-webfont.woff | Bin 0 -> 98024 bytes .../css/fonts/fontawesome-webfont.woff2 | Bin 0 -> 77160 bytes .../_static/css/fonts/lato-bold-italic.woff | Bin 0 -> 323344 bytes .../_static/css/fonts/lato-bold-italic.woff2 | Bin 0 -> 193308 bytes docs/saml2/_static/css/fonts/lato-bold.woff | Bin 0 -> 309728 bytes docs/saml2/_static/css/fonts/lato-bold.woff2 | Bin 0 -> 184912 bytes .../_static/css/fonts/lato-normal-italic.woff | Bin 0 -> 328412 bytes .../css/fonts/lato-normal-italic.woff2 | Bin 0 -> 195704 bytes docs/saml2/_static/css/fonts/lato-normal.woff | Bin 0 -> 309192 bytes .../saml2/_static/css/fonts/lato-normal.woff2 | Bin 0 -> 182708 bytes docs/saml2/_static/css/theme.css | 4 + docs/saml2/_static/doctools.js | 315 + docs/saml2/_static/documentation_options.js | 10 + docs/saml2/_static/down-pressed.png | Bin 0 -> 222 bytes docs/saml2/_static/down.png | Bin 0 -> 202 bytes docs/saml2/_static/file.png | Bin 0 -> 286 bytes docs/saml2/_static/jquery-3.2.1.js | 10253 ++++++++++++++++ docs/saml2/_static/jquery.js | 4 + docs/saml2/_static/js/badge_only.js | 1 + .../_static/js/html5shiv-printshiv.min.js | 4 + docs/saml2/_static/js/html5shiv.min.js | 4 + docs/saml2/_static/js/theme.js | 1 + docs/saml2/_static/language_data.js | 297 + docs/saml2/_static/minus.png | Bin 0 -> 90 bytes docs/saml2/_static/plus.png | Bin 0 -> 90 bytes docs/saml2/_static/pygments.css | 69 + docs/saml2/_static/searchtools.js | 481 + docs/saml2/_static/underscore-1.3.1.js | 999 ++ docs/saml2/_static/underscore.js | 31 + docs/saml2/_static/up-pressed.png | Bin 0 -> 214 bytes docs/saml2/_static/up.png | Bin 0 -> 203 bytes docs/saml2/_static/websupport.js | 808 ++ docs/saml2/genindex.html | 1020 ++ docs/saml2/index.html | 133 + docs/saml2/modules.html | 129 + docs/saml2/objects.inv | Bin 0 -> 3136 bytes docs/saml2/onelogin.html | 158 + docs/saml2/onelogin.saml2.html | 3811 ++++++ docs/saml2/py-modindex.html | 183 + docs/saml2/search.html | 118 + docs/saml2/searchindex.js | 1 + 71 files changed, 29218 insertions(+) create mode 100644 docs/saml2/_modules/index.html create mode 100644 docs/saml2/_modules/onelogin/saml2/auth.html create mode 100644 docs/saml2/_modules/onelogin/saml2/authn_request.html create mode 100644 docs/saml2/_modules/onelogin/saml2/constants.html create mode 100644 docs/saml2/_modules/onelogin/saml2/errors.html create mode 100644 docs/saml2/_modules/onelogin/saml2/idp_metadata_parser.html create mode 100644 docs/saml2/_modules/onelogin/saml2/logout_request.html create mode 100644 docs/saml2/_modules/onelogin/saml2/logout_response.html create mode 100644 docs/saml2/_modules/onelogin/saml2/metadata.html create mode 100644 docs/saml2/_modules/onelogin/saml2/response.html create mode 100644 docs/saml2/_modules/onelogin/saml2/settings.html create mode 100644 docs/saml2/_modules/onelogin/saml2/utils.html create mode 100644 docs/saml2/_modules/onelogin/saml2/xmlparser.html create mode 100644 docs/saml2/_sources/index.rst.txt create mode 100644 docs/saml2/_sources/modules.rst.txt create mode 100644 docs/saml2/_sources/onelogin.rst.txt create mode 100644 docs/saml2/_sources/onelogin.saml2.rst.txt create mode 100644 docs/saml2/_static/ajax-loader.gif create mode 100644 docs/saml2/_static/basic.css create mode 100644 docs/saml2/_static/comment-bright.png create mode 100644 docs/saml2/_static/comment-close.png create mode 100644 docs/saml2/_static/comment.png create mode 100644 docs/saml2/_static/css/badge_only.css create mode 100644 docs/saml2/_static/css/fonts/Roboto-Slab-Bold.woff create mode 100644 docs/saml2/_static/css/fonts/Roboto-Slab-Bold.woff2 create mode 100644 docs/saml2/_static/css/fonts/Roboto-Slab-Regular.woff create mode 100644 docs/saml2/_static/css/fonts/Roboto-Slab-Regular.woff2 create mode 100644 docs/saml2/_static/css/fonts/fontawesome-webfont.eot create mode 100644 docs/saml2/_static/css/fonts/fontawesome-webfont.svg create mode 100644 docs/saml2/_static/css/fonts/fontawesome-webfont.ttf create mode 100644 docs/saml2/_static/css/fonts/fontawesome-webfont.woff create mode 100644 docs/saml2/_static/css/fonts/fontawesome-webfont.woff2 create mode 100644 docs/saml2/_static/css/fonts/lato-bold-italic.woff create mode 100644 docs/saml2/_static/css/fonts/lato-bold-italic.woff2 create mode 100644 docs/saml2/_static/css/fonts/lato-bold.woff create mode 100644 docs/saml2/_static/css/fonts/lato-bold.woff2 create mode 100644 docs/saml2/_static/css/fonts/lato-normal-italic.woff create mode 100644 docs/saml2/_static/css/fonts/lato-normal-italic.woff2 create mode 100644 docs/saml2/_static/css/fonts/lato-normal.woff create mode 100644 docs/saml2/_static/css/fonts/lato-normal.woff2 create mode 100644 docs/saml2/_static/css/theme.css create mode 100644 docs/saml2/_static/doctools.js create mode 100644 docs/saml2/_static/documentation_options.js create mode 100644 docs/saml2/_static/down-pressed.png create mode 100644 docs/saml2/_static/down.png create mode 100644 docs/saml2/_static/file.png create mode 100644 docs/saml2/_static/jquery-3.2.1.js create mode 100644 docs/saml2/_static/jquery.js create mode 100644 docs/saml2/_static/js/badge_only.js create mode 100644 docs/saml2/_static/js/html5shiv-printshiv.min.js create mode 100644 docs/saml2/_static/js/html5shiv.min.js create mode 100644 docs/saml2/_static/js/theme.js create mode 100644 docs/saml2/_static/language_data.js create mode 100644 docs/saml2/_static/minus.png create mode 100644 docs/saml2/_static/plus.png create mode 100644 docs/saml2/_static/pygments.css create mode 100644 docs/saml2/_static/searchtools.js create mode 100644 docs/saml2/_static/underscore-1.3.1.js create mode 100644 docs/saml2/_static/underscore.js create mode 100644 docs/saml2/_static/up-pressed.png create mode 100644 docs/saml2/_static/up.png create mode 100644 docs/saml2/_static/websupport.js create mode 100644 docs/saml2/genindex.html create mode 100644 docs/saml2/index.html create mode 100644 docs/saml2/modules.html create mode 100644 docs/saml2/objects.inv create mode 100644 docs/saml2/onelogin.html create mode 100644 docs/saml2/onelogin.saml2.html create mode 100644 docs/saml2/py-modindex.html create mode 100644 docs/saml2/search.html create mode 100644 docs/saml2/searchindex.js diff --git a/docs/saml2/_modules/index.html b/docs/saml2/_modules/index.html new file mode 100644 index 00000000..8757e8fd --- /dev/null +++ b/docs/saml2/_modules/index.html @@ -0,0 +1,110 @@ + + + + + + Overview: module code — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    +
      +
    • + +
    • +
    • +
    +
    +
    + + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/auth.html b/docs/saml2/_modules/onelogin/saml2/auth.html new file mode 100644 index 00000000..b7c47d8b --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/auth.html @@ -0,0 +1,696 @@ + + + + + + onelogin.saml2.auth — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    + +

    Source code for onelogin.saml2.auth

    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_Auth class
    +
    +MIT License
    +
    +Main class of Python Toolkit.
    +
    +Initializes the SP SAML instance
    +
    +"""
    +
    +from base64 import b64encode
    +from urllib import quote_plus
    +
    +from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +from onelogin.saml2.errors import OneLogin_Saml2_Error
    +from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response
    +from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
    +from onelogin.saml2.response import OneLogin_Saml2_Response
    +from onelogin.saml2.settings import OneLogin_Saml2_Settings
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils, xmlsec
    +from onelogin.saml2.xmlparser import tostring
    +
    +
    +
    [docs]class OneLogin_Saml2_Auth(object): + """ + + This class implements the SP SAML instance. + + Defines the methods that you can invoke in your application in + order to add SAML support (initiates SSO, initiates SLO, processes a + SAML Response, a Logout Request or a Logout Response). + """ + + def __init__(self, request_data, old_settings=None, custom_base_path=None): + """ + Initializes the SP SAML instance. + + :param request_data: Request Data + :type request_data: dict + + :param old_settings: Optional. SAML Toolkit Settings + :type old_settings: dict + + :param custom_base_path: Optional. Path where are stored the settings file and the cert folder + :type custom_base_path: string + """ + self.__request_data = request_data + self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path) + self.__attributes = [] + self.__friendlyname_attributes = [] + self.__nameid = None + self.__nameid_format = None + self.__nameid_nq = None + self.__nameid_spnq = None + self.__session_index = None + self.__session_expiration = None + self.__authenticated = False + self.__errors = [] + self.__error_reason = None + self.__last_request_id = None + self.__last_message_id = None + self.__last_assertion_id = None + self.__last_assertion_not_on_or_after = None + self.__last_authn_contexts = [] + self.__last_request = None + self.__last_response = None + +
    [docs] def get_settings(self): + """ + Returns the settings info + :return: Setting info + :rtype: OneLogin_Saml2_Setting object + """ + return self.__settings
    + +
    [docs] def set_strict(self, value): + """ + Set the strict mode active/disable + + :param value: + :type value: bool + """ + assert isinstance(value, bool) + self.__settings.set_strict(value)
    + +
    [docs] def process_response(self, request_id=None): + """ + Process the SAML Response sent by the IdP. + + :param request_id: Is an optional argument. Is the ID of the AuthNRequest sent by this SP to the IdP. + :type request_id: string + + :raises: OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND, when a POST with a SAMLResponse is not found + """ + self.__errors = [] + self.__error_reason = 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() + self.__friendlyname_attributes = response.get_friendlyname_attributes() + self.__nameid = response.get_nameid() + self.__nameid_format = response.get_nameid_format() + self.__nameid_nq = response.get_nameid_nq() + self.__nameid_spnq = response.get_nameid_spnq() + self.__session_index = response.get_session_index() + self.__session_expiration = response.get_session_not_on_or_after() + self.__last_message_id = response.get_id() + self.__last_assertion_id = response.get_assertion_id() + self.__last_authn_contexts = response.get_authn_contexts() + self.__last_assertion_not_on_or_after = response.get_assertion_not_on_or_after() + self.__authenticated = True + else: + self.__errors.append('invalid_response') + self.__error_reason = response.get_error() + else: + self.__errors.append('invalid_binding') + raise OneLogin_Saml2_Error( + 'SAML Response not found, Only supported HTTP_POST Binding', + OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND + )
    + +
    [docs] def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None): + """ + Process the SAML Logout Response / Logout Request sent by the IdP. + + :param keep_local_session: When false will destroy the local session, otherwise will destroy it + :type keep_local_session: bool + + :param request_id: The ID of the LogoutRequest sent by this SP to the IdP + :type request_id: string + + :returns: Redirection URL + """ + self.__errors = [] + self.__error_reason = None + + if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']: + logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) + self.__last_response = logout_response.get_xml() + if not logout_response.is_valid(self.__request_data, request_id): + self.__errors.append('invalid_logout_response') + self.__error_reason = logout_response.get_error() + elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS: + self.__errors.append('logout_not_success') + else: + self.__last_message_id = logout_response.id + if not keep_local_session: + OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) + + elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']: + logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest']) + self.__last_request = logout_request.get_xml() + if not logout_request.is_valid(self.__request_data): + self.__errors.append('invalid_logout_request') + self.__error_reason = logout_request.get_error() + else: + if not keep_local_session: + OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) + + in_response_to = logout_request.id + self.__last_message_id = 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} + if 'RelayState' in self.__request_data['get_data']: + parameters['RelayState'] = self.__request_data['get_data']['RelayState'] + # else: + # parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) + + security = self.__settings.get_security_data() + if 'logoutResponseSigned' in security and security['logoutResponseSigned']: + parameters['SigAlg'] = security['signatureAlgorithm'] + parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None), security['signatureAlgorithm']) + + return self.redirect_to(self.get_slo_url(), parameters) + else: + self.__errors.append('invalid_binding') + raise OneLogin_Saml2_Error( + 'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding', + OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND + )
    + +
    [docs] def redirect_to(self, url=None, parameters={}): + """ + Redirects the user to the URL passed by parameter or to the URL that we defined in our SSO Request. + + :param url: The target URL to redirect the user + :type url: string + :param parameters: Extra parameters to be passed as part of the URL + :type parameters: dict + + :returns: Redirection URL + """ + if url is None and 'RelayState' in self.__request_data['get_data']: + url = self.__request_data['get_data']['RelayState'] + return OneLogin_Saml2_Utils.redirect(url, parameters, request_data=self.__request_data)
    + +
    [docs] def get_last_authn_contexts(self): + """ + :returns: The list of authentication contexts sent in the last SAML Response. + :rtype: list + """ + return self.__last_authn_contexts
    + +
    [docs] def is_authenticated(self): + """ + Checks if the user is authenticated or not. + + :returns: True if is authenticated, False if not + :rtype: bool + """ + return self.__authenticated
    + +
    [docs] def get_attributes(self): + """ + Returns the set of SAML attributes. + + :returns: SAML attributes + :rtype: dict + """ + return self.__attributes
    + +
    [docs] def get_friendlyname_attributes(self): + """ + Returns the set of SAML attributes indexed by FiendlyName. + + :returns: SAML attributes + :rtype: dict + """ + return self.__friendlyname_attributes
    + +
    [docs] def get_nameid(self): + """ + Returns the nameID. + + :returns: NameID + :rtype: string|None + """ + return self.__nameid
    + +
    [docs] def get_nameid_format(self): + """ + Returns the nameID Format. + + :returns: NameID Format + :rtype: string|None + """ + return self.__nameid_format
    + +
    [docs] def get_nameid_nq(self): + """ + Returns the nameID NameQualifier of the Assertion. + + :returns: NameID NameQualifier + :rtype: string|None + """ + return self.__nameid_nq
    + +
    [docs] def get_nameid_spnq(self): + """ + Returns the nameID SP NameQualifier of the Assertion. + + :returns: NameID SP NameQualifier + :rtype: string|None + """ + return self.__nameid_spnq
    + +
    [docs] def get_session_index(self): + """ + Returns the SessionIndex from the AuthnStatement. + :returns: The SessionIndex of the assertion + :rtype: string + """ + return self.__session_index
    + +
    [docs] def get_session_expiration(self): + """ + Returns the SessionNotOnOrAfter from the AuthnStatement. + :returns: The SessionNotOnOrAfter of the assertion + :rtype: unix/posix timestamp|None + """ + return self.__session_expiration
    + +
    [docs] def get_last_assertion_not_on_or_after(self): + """ + The NotOnOrAfter value of the valid SubjectConfirmationData node + (if any) of the last assertion processed + """ + return self.__last_assertion_not_on_or_after
    + +
    [docs] def get_errors(self): + """ + Returns a list with code errors if something went wrong + + :returns: List of errors + :rtype: list + """ + return self.__errors
    + +
    [docs] def get_last_error_reason(self): + """ + Returns the reason for the last error + + :returns: Reason of the last error + :rtype: None | string + """ + return self.__error_reason
    + +
    [docs] def get_attribute(self, name): + """ + Returns the requested SAML attribute. + + :param name: Name of the attribute + :type name: string + + :returns: Attribute value(s) if exists or None + :rtype: list + """ + assert isinstance(name, basestring) + value = None + if self.__attributes and name in self.__attributes.keys(): + value = self.__attributes[name] + return value
    + +
    [docs] def get_friendlyname_attribute(self, friendlyname): + """ + Returns the requested SAML attribute searched by FriendlyName. + + :param friendlyname: FriendlyName of the attribute + :type friendlyname: string + + :returns: Attribute value(s) if exists or None + :rtype: list + """ + assert isinstance(friendlyname, basestring) + value = None + if self.__friendlyname_attributes and friendlyname in self.__friendlyname_attributes.keys(): + value = self.__friendlyname_attributes[friendlyname] + return value
    + +
    [docs] def get_last_request_id(self): + """ + :returns: The ID of the last Request SAML message generated. + :rtype: string + """ + return self.__last_request_id
    + +
    [docs] def get_last_message_id(self): + """ + :returns: The ID of the last Response SAML message processed. + :rtype: string + """ + return self.__last_message_id
    + +
    [docs] def get_last_assertion_id(self): + """ + :returns: The ID of the last assertion processed. + :rtype: string + """ + return self.__last_assertion_id
    + +
    [docs] def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True, name_id_value_req=None): + """ + Initiates the SSO process. + + :param return_to: Optional argument. The target URL the user should be redirected to after login. + :type return_to: string + + :param force_authn: Optional argument. When true the AuthNRequest will set the ForceAuthn='true'. + :type force_authn: bool + + :param is_passive: Optional argument. When true the AuthNRequest will set the Ispassive='true'. + :type is_passive: bool + + :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element. + :type set_nameid_policy: bool + + :param name_id_value_req: Optional argument. Indicates to the IdP the subject that should be authenticated + :type name_id_value_req: string + + :returns: Redirection URL + :rtype: string + """ + authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy, name_id_value_req) + self.__last_request = authn_request.get_xml() + self.__last_request_id = authn_request.get_id() + saml_request = authn_request.get_request() + + parameters = {'SAMLRequest': saml_request} + if return_to is not None: + parameters['RelayState'] = return_to + else: + parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) + + security = self.__settings.get_security_data() + if security.get('authnRequestsSigned', False): + parameters['SigAlg'] = security['signatureAlgorithm'] + parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) + return self.redirect_to(self.get_sso_url(), parameters)
    + +
    [docs] def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None): + """ + Initiates the SLO process. + + :param return_to: Optional argument. The target URL the user should be redirected to after logout. + :type return_to: string + + :param name_id: The NameID that will be set in the LogoutRequest. + :type name_id: string + + :param session_index: SessionIndex that identifies the session of the user. + :type session_index: string + + :param nq: IDP Name Qualifier + :type: string + + :param name_id_format: The NameID Format that will be set in the LogoutRequest. + :type: string + + :param spnq: SP Name Qualifier + :type: string + + :returns: Redirection url + """ + slo_url = self.get_slo_url() + if slo_url is None: + raise OneLogin_Saml2_Error( + 'The IdP does not support Single Log Out', + OneLogin_Saml2_Error.SAML_SINGLE_LOGOUT_NOT_SUPPORTED + ) + + if name_id is None and self.__nameid is not None: + name_id = self.__nameid + if name_id_format is None and self.__nameid_format is not None: + name_id_format = self.__nameid_format + + logout_request = OneLogin_Saml2_Logout_Request( + self.__settings, + name_id=name_id, + session_index=session_index, + nq=nq, + name_id_format=name_id_format, + spnq=spnq + ) + self.__last_request = logout_request.get_xml() + self.__last_request_id = logout_request.id + saml_request = logout_request.get_request() + + parameters = {'SAMLRequest': logout_request.get_request()} + if return_to is not None: + parameters['RelayState'] = return_to + else: + parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) + + security = self.__settings.get_security_data() + if security.get('logoutRequestSigned', False): + parameters['SigAlg'] = security['signatureAlgorithm'] + parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) + return self.redirect_to(slo_url, parameters)
    + +
    [docs] def get_sso_url(self): + """ + Gets the IdP SSO URL. + + :returns: An URL, the SSO endpoint of the IdP + :rtype: string + """ + return self.__settings.get_idp_sso_url()
    + +
    [docs] def get_slo_url(self): + """ + Gets the IdP SLO URL. + + :returns: An URL, the SLO endpoint of the IdP + :rtype: string + """ + return self.__settings.get_idp_slo_url()
    + +
    [docs] def get_slo_response_url(self): + """ + Gets the SLO return URL for IdP-initiated logout. + + :returns: an URL, the SLO return endpoint of the IdP + :rtype: string + """ + return self.__settings.get_idp_slo_response_url()
    + +
    [docs] def build_request_signature(self, saml_request, relay_state, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): + """ + Builds the Signature of the SAML Request. + + :param saml_request: The SAML Request + :type saml_request: string + + :param relay_state: The target URL the user should be redirected to + :type relay_state: string + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + """ + return self.__build_signature(saml_request, relay_state, 'SAMLRequest', sign_algorithm)
    + +
    [docs] def build_response_signature(self, saml_response, relay_state, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): + """ + Builds the Signature of the SAML Response. + :param saml_request: The SAML Response + :type saml_request: string + + :param relay_state: The target URL the user should be redirected to + :type relay_state: string + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + """ + return self.__build_signature(saml_response, relay_state, 'SAMLResponse', sign_algorithm)
    + + def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): + """ + Builds the Signature + :param saml_data: The SAML Data + :type saml_data: string + + :param relay_state: The target URL the user should be redirected to + :type relay_state: string + + :param saml_type: The target URL the user should be redirected to + :type saml_type: string SAMLRequest | SAMLResponse + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + """ + assert saml_type in ['SAMLRequest', 'SAMLResponse'] + + # Load the key into the xmlsec context + key = self.__settings.get_sp_key() + + 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.PRIVATE_KEY_NOT_FOUND + ) + + dsig_ctx = xmlsec.DSigCtx() + dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + + msg = '%s=%s' % (saml_type, quote_plus(saml_data)) + if relay_state is not None: + msg += '&RelayState=%s' % quote_plus(relay_state) + msg += '&SigAlg=%s' % quote_plus(sign_algorithm) + + # Sign the metadata with our private key. + sign_algorithm_transform_map = { + OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1, + OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1, + OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256, + OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384, + OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512 + } + sign_algorithm_transform = sign_algorithm_transform_map.get(sign_algorithm, xmlsec.TransformRsaSha1) + + signature = dsig_ctx.signBinary(str(msg), sign_algorithm_transform) + return b64encode(signature) + +
    [docs] 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 = tostring(self.__last_response, pretty_print=pretty_print_if_possible) + return response
    + +
    [docs] 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
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/authn_request.html b/docs/saml2/_modules/onelogin/saml2/authn_request.html new file mode 100644 index 00000000..d9daac3d --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/authn_request.html @@ -0,0 +1,267 @@ + + + + + + onelogin.saml2.authn_request — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    + +

    Source code for onelogin.saml2.authn_request

    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_Authn_Request class
    +
    +MIT License
    +
    +AuthNRequest class of Python Toolkit.
    +
    +"""
    +from base64 import b64encode
    +
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +
    +
    +
    [docs]class OneLogin_Saml2_Authn_Request(object): + """ + + This class handles an AuthNRequest. It builds an + AuthNRequest object. + + """ + + def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_policy=True, name_id_value_req=None): + """ + Constructs the AuthnRequest object. + + :param settings: OSetting data + :type settings: OneLogin_Saml2_Settings + + :param force_authn: Optional argument. When true the AuthNRequest will set the ForceAuthn='true'. + :type force_authn: bool + + :param is_passive: Optional argument. When true the AuthNRequest will set the Ispassive='true'. + :type is_passive: bool + + :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element. + :type set_nameid_policy: bool + + :param name_id_value_req: Optional argument. Indicates to the IdP the subject that should be authenticated + :type name_id_value_req: string + """ + self.__settings = settings + + sp_data = self.__settings.get_sp_data() + idp_data = self.__settings.get_idp_data() + security = self.__settings.get_security_data() + + uid = OneLogin_Saml2_Utils.generate_unique_id() + self.__id = uid + issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) + + destination = idp_data['singleSignOnService']['url'] + + provider_name_str = '' + organization_data = settings.get_organization() + if isinstance(organization_data, dict) and organization_data: + langs = organization_data.keys() + if 'en-US' in langs: + lang = 'en-US' + else: + lang = langs[0] + if 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] is not None: + provider_name_str = "\n" + ' ProviderName="%s"' % organization_data[lang]['displayname'] + + force_authn_str = '' + if force_authn is True: + force_authn_str = "\n" + ' ForceAuthn="true"' + + is_passive_str = '' + if is_passive is True: + is_passive_str = "\n" + ' IsPassive="true"' + + subject_str = '' + if name_id_value_req: + subject_str = """ + <saml:Subject> + <saml:NameID Format="%s">%s</saml:NameID> + <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"></saml:SubjectConfirmation> + </saml:Subject>""" % (sp_data['NameIDFormat'], name_id_value_req) + + nameid_policy_str = '' + if set_nameid_policy: + name_id_policy_format = sp_data['NameIDFormat'] + if 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted']: + name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED + + nameid_policy_str = """ + <samlp:NameIDPolicy + Format="%s" + AllowCreate="true" />""" % name_id_policy_format + + requested_authn_context_str = '' + if 'requestedAuthnContext' in security.keys() and security['requestedAuthnContext'] is not False: + authn_comparison = security['requestedAuthnContextComparison'] + + if security['requestedAuthnContext'] is True: + requested_authn_context_str = "\n" + """ <samlp:RequestedAuthnContext Comparison="%s"> + <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef> + </samlp:RequestedAuthnContext>""" % authn_comparison + else: + requested_authn_context_str = "\n" + ' <samlp:RequestedAuthnContext Comparison="%s">' % authn_comparison + for authn_context in security['requestedAuthnContext']: + requested_authn_context_str += '<saml:AuthnContextClassRef>%s</saml:AuthnContextClassRef>' % authn_context + requested_authn_context_str += ' </samlp:RequestedAuthnContext>' + + attr_consuming_service_str = '' + if 'attributeConsumingService' in sp_data and sp_data['attributeConsumingService']: + attr_consuming_service_str = 'AttributeConsumingServiceIndex="1"' + + request = """<samlp:AuthnRequest + 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> + <saml:Issuer>%(entity_id)s</saml:Issuer>%(subject_str)s%(nameid_policy_str)s%(requested_authn_context_str)s +</samlp:AuthnRequest>""" % \ + { + 'id': uid, + 'provider_name': provider_name_str, + 'force_authn_str': force_authn_str, + 'is_passive_str': is_passive_str, + 'issue_instant': issue_instant, + 'destination': destination, + 'assertion_url': sp_data['assertionConsumerService']['url'], + 'entity_id': sp_data['entityId'], + 'subject_str': subject_str, + 'nameid_policy_str': nameid_policy_str, + 'requested_authn_context_str': requested_authn_context_str, + 'attr_consuming_service_str': attr_consuming_service_str + } + + self.__authn_request = request + +
    [docs] def get_request(self, deflate=True): + """ + Returns unsigned AuthnRequest. + :param deflate: It makes the deflate process optional + :type: bool + :return: AuthnRequest maybe deflated and base64 encoded + :rtype: str object + """ + if deflate: + request = OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__authn_request) + else: + request = b64encode(self.__authn_request) + return request
    + +
    [docs] def get_id(self): + """ + Returns the AuthNRequest ID. + :return: AuthNRequest ID + :rtype: string + """ + return self.__id
    + +
    [docs] 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
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/constants.html b/docs/saml2/_modules/onelogin/saml2/constants.html new file mode 100644 index 00000000..0f092f82 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/constants.html @@ -0,0 +1,219 @@ + + + + + + onelogin.saml2.constants — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    + +

    Source code for onelogin.saml2.constants

    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_Constants class
    +
    +MIT License
    +
    +Constants class of Python Toolkit.
    +
    +"""
    +
    +
    +
    [docs]class OneLogin_Saml2_Constants(object): + """ + + This class defines all the constants that will be used + in the Python Toolkit. + + """ + + # Value added to the current time in time condition validations + ALLOWED_CLOCK_DRIFT = 300 + + XML = 'http://www.w3.org/XML/1998/namespace' + XSI = 'http://www.w3.org/2001/XMLSchema-instance' + + # NameID Formats + NAMEID_EMAIL_ADDRESS = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' + NAMEID_X509_SUBJECT_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName' + NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName' + NAMEID_UNSPECIFIED = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' + NAMEID_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos' + NAMEID_ENTITY = 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity' + NAMEID_TRANSIENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + NAMEID_PERSISTENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' + NAMEID_ENCRYPTED = 'urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted' + + # Attribute Name Formats + ATTRNAME_FORMAT_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified' + ATTRNAME_FORMAT_URI = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri' + ATTRNAME_FORMAT_BASIC = 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic' + + # Namespaces + NS_SAML = 'urn:oasis:names:tc:SAML:2.0:assertion' + NS_SAMLP = 'urn:oasis:names:tc:SAML:2.0:protocol' + NS_SOAP = 'http://schemas.xmlsoap.org/soap/envelope/' + NS_MD = 'urn:oasis:names:tc:SAML:2.0:metadata' + NS_XS = 'http://www.w3.org/2001/XMLSchema' + NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance' + NS_XENC = 'http://www.w3.org/2001/04/xmlenc#' + NS_DS = 'http://www.w3.org/2000/09/xmldsig#' + + # Namespace Prefixes + NS_PREFIX_SAML = 'saml' + NS_PREFIX_SAMLP = 'samlp' + NS_PREFIX_MD = 'md' + NS_PREFIX_XS = 'xs' + NS_PREFIX_XSI = 'xsi' + NS_PREFIX_XENC = 'xenc' + NS_PREFIX_DS = 'ds' + + # Prefix:Namespace Mappings + NSMAP = { + NS_PREFIX_SAMLP: NS_SAMLP, + NS_PREFIX_SAML: NS_SAML, + NS_PREFIX_DS: NS_DS, + NS_PREFIX_XENC: NS_XENC, + NS_PREFIX_MD: NS_MD + } + + # Bindings + BINDING_HTTP_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + BINDING_HTTP_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + BINDING_HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' + BINDING_SOAP = 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP' + BINDING_DEFLATE = 'urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE' + + # Auth Context Class + AC_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified' + AC_PASSWORD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password' + AC_PASSWORD_PROTECTED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + AC_X509 = 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509' + AC_SMARTCARD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard' + AC_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos' + + # Subject Confirmation + CM_BEARER = 'urn:oasis:names:tc:SAML:2.0:cm:bearer' + CM_HOLDER_KEY = 'urn:oasis:names:tc:SAML:2.0:cm:holder-of-key' + CM_SENDER_VOUCHES = 'urn:oasis:names:tc:SAML:2.0:cm:sender-vouches' + + # Status Codes + STATUS_SUCCESS = 'urn:oasis:names:tc:SAML:2.0:status:Success' + STATUS_REQUESTER = 'urn:oasis:names:tc:SAML:2.0:status:Requester' + STATUS_RESPONDER = 'urn:oasis:names:tc:SAML:2.0:status:Responder' + STATUS_VERSION_MISMATCH = 'urn:oasis:names:tc:SAML:2.0:status:VersionMismatch' + STATUS_NO_PASSIVE = 'urn:oasis:names:tc:SAML:2.0:status:NoPassive' + STATUS_PARTIAL_LOGOUT = 'urn:oasis:names:tc:SAML:2.0:status:PartialLogout' + STATUS_PROXY_COUNT_EXCEEDED = 'urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded' + + # Sign & Crypto + SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1' + SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256' + SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384' + SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512' + + DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' + RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + RSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' + RSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' + + # Enc + TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' + AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' + AES192_CBC = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' + AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' + RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' + RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' + + # Define here the deprecated algorithms + DEPRECATED_ALGORITHMS = [DSA_SHA1, RSA_SHA1, SHA1]
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/errors.html b/docs/saml2/_modules/onelogin/saml2/errors.html new file mode 100644 index 00000000..0b40179e --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/errors.html @@ -0,0 +1,232 @@ + + + + + + onelogin.saml2.errors — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    + +

    Source code for onelogin.saml2.errors

    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_Error class
    +
    +MIT License
    +
    +Error class of Python Toolkit.
    +
    +Defines common Error codes and has a custom initializator.
    +
    +"""
    +
    +
    +
    [docs]class OneLogin_Saml2_Error(Exception): + """ + + This class implements a custom Exception handler. + Defines custom error codes. + + """ + + # Errors + SETTINGS_FILE_NOT_FOUND = 0 + 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 + PRIVATE_KEY_FILE_NOT_FOUND = 7 + SAML_RESPONSE_NOT_FOUND = 8 + SAML_LOGOUTMESSAGE_NOT_FOUND = 9 + 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): + """ + Initializes the Exception instance. + + Arguments are: + * (str) message. Describes the error. + * (int) code. The code error (defined in the error class). + """ + assert isinstance(message, basestring) + assert isinstance(code, int) + + if errors is not None: + message = message % errors + + Exception.__init__(self, message) + self.code = code
    + + +
    [docs]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_MULTIPLE_IN_RESPONSE = 27 + ISSUER_NOT_FOUND_IN_ASSERTION = 28 + WRONG_ISSUER = 29 + SESSION_EXPIRED = 30 + WRONG_SUBJECTCONFIRMATION = 31 + NO_SIGNED_MESSAGE = 32 + NO_SIGNED_ASSERTION = 33 + NO_SIGNATURE_FOUND = 34 + KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35 + CHILDREN_NODE_NOT_FOUND_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 + AUTHN_CONTEXT_MISMATCH = 45 + DEPRECATED_SIGNATURE_METHOD = 46 + DEPRECATED_DIGEST_METHOD = 47 + + 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(message, basestring) + assert isinstance(code, int) + + if errors is not None: + message = message % errors + + Exception.__init__(self, message) + self.code = code
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/idp_metadata_parser.html b/docs/saml2/_modules/onelogin/saml2/idp_metadata_parser.html new file mode 100644 index 00000000..f6622e58 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/idp_metadata_parser.html @@ -0,0 +1,362 @@ + + + + + + onelogin.saml2.idp_metadata_parser — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    +
      +
    • + + +
    • +
    • +
    +
    +
    +
    +
    + +

    Source code for onelogin.saml2.idp_metadata_parser

    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_IdPMetadataParser class
    +
    +MIT License
    +
    +Metadata class of Python Toolkit.
    +
    +"""
    +
    +import urllib2
    +import ssl
    +
    +from copy import deepcopy
    +
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +from onelogin.saml2.xmlparser import fromstring
    +
    +
    +
    [docs]class OneLogin_Saml2_IdPMetadataParser(object): + """ + A class that contain methods related to obtaining and parsing metadata from IdP + + This class does not validate in any way the URL that is introduced, + make sure to validate it properly before use it in a get_metadata method. + """ + +
    [docs] @staticmethod + def get_metadata(url, validate_cert=True): + """ + Gets the metadata XML from the provided URL + + :param url: Url where the XML of the Identity Provider Metadata is published. + :type url: string + + :param validate_cert: If the url uses https schema, that flag enables or not the verification of the associated certificate. + :type validate_cert: bool + + :returns: metadata XML + :rtype: string + """ + valid = False + if validate_cert: + response = urllib2.urlopen(url) + else: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + response = urllib2.urlopen(url, context=ctx) + xml = response.read() + + if xml: + try: + dom = fromstring(xml, forbid_dtd=True) + idp_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:IDPSSODescriptor') + if idp_descriptor_nodes: + valid = True + except Exception: + pass + + if not valid: + raise Exception('Not valid IdP XML found from URL: %s' % (url)) + + return xml
    + +
    [docs] @staticmethod + def parse_remote(url, validate_cert=True, entity_id=None, **kwargs): + """ + Gets the metadata XML from the provided URL and parse it, returning a dict with extracted data + + :param url: Url where the XML of the Identity Provider Metadata is published. + :type url: string + + :param validate_cert: If the url uses https schema, that flag enables or not the verification of the associated certificate. + :type validate_cert: bool + + :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML + that contains multiple EntityDescriptor. + :type entity_id: string + + :returns: settings dict with extracted data + :rtype: dict + """ + idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url, validate_cert) + return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata, entity_id=entity_id, **kwargs)
    + +
    [docs] @staticmethod + def parse( + idp_metadata, + required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, + required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, + entity_id=None): + """ + Parse the Identity Provider metadata and return a dict with extracted data. + + If there are multiple <IDPSSODescriptor> tags, parse only the first. + + Parse only those SSO endpoints with the same binding as given by + the `required_sso_binding` parameter. + + Parse only those SLO endpoints with the same binding as given by + the `required_slo_binding` parameter. + + If the metadata specifies multiple SSO endpoints with the required + binding, extract only the first (the same holds true for SLO + endpoints). + + :param idp_metadata: XML of the Identity Provider Metadata. + :type idp_metadata: string + + :param required_sso_binding: Parse only POST or REDIRECT SSO endpoints. + :type required_sso_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT + or OneLogin_Saml2_Constants.BINDING_HTTP_POST + + :param required_slo_binding: Parse only POST or REDIRECT SLO endpoints. + :type required_slo_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT + or OneLogin_Saml2_Constants.BINDING_HTTP_POST + + :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML + that contains multiple EntityDescriptor. + :type entity_id: string + + :returns: settings dict with extracted data + :rtype: dict + """ + data = {} + + dom = fromstring(idp_metadata, forbid_dtd=True) + + entity_desc_path = '//md:EntityDescriptor' + if entity_id: + entity_desc_path += "[@entityID='%s']" % entity_id + + entity_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, entity_desc_path) + + idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = certs = None + + if len(entity_descriptor_nodes) > 0: + entity_descriptor_node = entity_descriptor_nodes[0] + idp_descriptor_nodes = OneLogin_Saml2_Utils.query(entity_descriptor_node, './md:IDPSSODescriptor') + if len(idp_descriptor_nodes) > 0: + idp_descriptor_node = idp_descriptor_nodes[0] + + idp_entity_id = entity_descriptor_node.get('entityID', None) + + want_authn_requests_signed = entity_descriptor_node.get('WantAuthnRequestsSigned', None) + + name_id_format_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, './md:NameIDFormat') + if len(name_id_format_nodes) > 0: + idp_name_id_format = OneLogin_Saml2_Utils.element_text(name_id_format_nodes[0]) + + sso_nodes = OneLogin_Saml2_Utils.query( + idp_descriptor_node, + "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding + ) + + if len(sso_nodes) > 0: + idp_sso_url = sso_nodes[0].get('Location', None) + + slo_nodes = OneLogin_Saml2_Utils.query( + idp_descriptor_node, + "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding + ) + if len(slo_nodes) > 0: + idp_slo_url = slo_nodes[0].get('Location', None) + + signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") + encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") + + if len(signing_nodes) > 0 or len(encryption_nodes) > 0: + certs = {} + if len(signing_nodes) > 0: + certs['signing'] = [] + for cert_node in signing_nodes: + certs['signing'].append(''.join(OneLogin_Saml2_Utils.element_text(cert_node).split())) + if len(encryption_nodes) > 0: + certs['encryption'] = [] + for cert_node in encryption_nodes: + certs['encryption'].append(''.join(OneLogin_Saml2_Utils.element_text(cert_node).split())) + + data['idp'] = {} + + if idp_entity_id is not None: + data['idp']['entityId'] = idp_entity_id + + if idp_sso_url is not None: + data['idp']['singleSignOnService'] = {} + data['idp']['singleSignOnService']['url'] = idp_sso_url + data['idp']['singleSignOnService']['binding'] = required_sso_binding + + if idp_slo_url is not None: + data['idp']['singleLogoutService'] = {} + data['idp']['singleLogoutService']['url'] = idp_slo_url + data['idp']['singleLogoutService']['binding'] = required_slo_binding + + if certs is not None: + if (len(certs) == 1 and + (('signing' in certs and len(certs['signing']) == 1) or + ('encryption' in certs and len(certs['encryption']) == 1))) or \ + (('signing' in certs and len(certs['signing']) == 1) and + ('encryption' in certs and len(certs['encryption']) == 1 and + certs['signing'][0] == certs['encryption'][0])): + if 'signing' in certs: + data['idp']['x509cert'] = certs['signing'][0] + else: + data['idp']['x509cert'] = certs['encryption'][0] + else: + data['idp']['x509certMulti'] = certs + + if want_authn_requests_signed is not None: + data['security'] = {} + data['security']['authnRequestsSigned'] = want_authn_requests_signed + + if idp_name_id_format: + data['sp'] = {} + data['sp']['NameIDFormat'] = idp_name_id_format + return data
    + +
    [docs] @staticmethod + def merge_settings(settings, new_metadata_settings): + """ + Will update the settings with the provided new settings data extracted from the IdP metadata + + :param settings: Current settings dict data + :type settings: dict + + :param new_metadata_settings: Settings to be merged (extracted from IdP metadata after parsing) + :type new_metadata_settings: dict + + :returns: merged settings + :rtype: dict + """ + for d in (settings, new_metadata_settings): + if not isinstance(d, dict): + raise TypeError('Both arguments must be dictionaries.') + + # Guarantee to not modify original data (`settings.copy()` would not + # be sufficient, as it's just a shallow copy). + result_settings = deepcopy(settings) + + # previously I will take care of cert stuff + if 'idp' in new_metadata_settings and 'idp' in result_settings: + if new_metadata_settings['idp'].get('x509cert', None) and result_settings['idp'].get('x509certMulti', None): + del result_settings['idp']['x509certMulti'] + if new_metadata_settings['idp'].get('x509certMulti', None) and result_settings['idp'].get('x509cert', None): + del result_settings['idp']['x509cert'] + + # Merge `new_metadata_settings` into `result_settings`. + dict_deep_merge(result_settings, new_metadata_settings) + return result_settings
    + + +
    [docs]def dict_deep_merge(lhs, rhs): + """Deep-merge dictionary `rhs` into dictionary `lhs`.""" + updated_rhs = {} + for key in rhs: + if key in lhs and isinstance(lhs[key], dict) and isinstance(rhs[key], dict): + updated_rhs[key] = dict_deep_merge(lhs[key], rhs[key]) + else: + updated_rhs[key] = rhs[key] + lhs.update(updated_rhs) + return lhs
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/logout_request.html b/docs/saml2/_modules/onelogin/saml2/logout_request.html new file mode 100644 index 00000000..3c896717 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/logout_request.html @@ -0,0 +1,549 @@ + + + + + + onelogin.saml2.logout_request — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    +
      +
    • + + +
    • +
    • +
    +
    +
    +
    +
    + +

    Source code for onelogin.saml2.logout_request

    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_Logout_Request class
    +
    +MIT License
    +
    +Logout Request class of Python Toolkit.
    +
    +"""
    +
    +from zlib import decompress
    +from base64 import b64encode, b64decode
    +from lxml import etree
    +from xml.dom.minidom import Document
    +
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +from onelogin.saml2.xmlparser import fromstring
    +
    +
    +
    [docs]class OneLogin_Saml2_Logout_Request(object): + """ + + This class handles a Logout Request. + + Builds a Logout Response object and validates it. + + """ + + def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None): + """ + Constructs the Logout Request object. + + :param settings: Setting data + :type request_data: OneLogin_Saml2_Settings + + :param request: Optional. A LogoutRequest to be loaded instead build one. + :type request: string + + :param name_id: The NameID that will be set in the LogoutRequest. + :type name_id: string + + :param session_index: SessionIndex that identifies the session of the user. + :type session_index: string + + :param nq: IDP Name Qualifier + :type: string + + :param name_id_format: The NameID Format that will be set in the LogoutRequest. + :type: string + + :param spnq: SP Name Qualifier + :type: string + + """ + self.__settings = settings + self.__error = None + self.id = None + + if request is None: + sp_data = self.__settings.get_sp_data() + idp_data = self.__settings.get_idp_data() + security = self.__settings.get_security_data() + + uid = OneLogin_Saml2_Utils.generate_unique_id() + self.id = uid + + issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) + + cert = None + if 'nameIdEncrypted' in security and security['nameIdEncrypted']: + exists_multix509enc = 'x509certMulti' in idp_data and \ + 'encryption' in idp_data['x509certMulti'] and \ + idp_data['x509certMulti']['encryption'] + if exists_multix509enc: + cert = idp_data['x509certMulti']['encryption'][0] + else: + cert = idp_data['x509cert'] + + if name_id is not None: + if not name_id_format and sp_data['NameIDFormat'] != OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED: + name_id_format = sp_data['NameIDFormat'] + else: + name_id = idp_data['entityId'] + name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY + + # From saml-core-2.0-os 8.3.6, when the entity Format is used: + # "The NameQualifier, SPNameQualifier, and SPProvidedID attributes + # MUST be omitted. + if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY: + nq = None + spnq = None + + # NameID Format UNSPECIFIED omitted + if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED: + name_id_format = None + + name_id_obj = OneLogin_Saml2_Utils.generate_name_id( + name_id, + spnq, + name_id_format, + cert, + False, + nq + ) + + if session_index: + session_index_str = '<samlp:SessionIndex>%s</samlp:SessionIndex>' % session_index + else: + session_index_str = '' + + logout_request = """<samlp:LogoutRequest + 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"> + <saml:Issuer>%(entity_id)s</saml:Issuer> + %(name_id)s + %(session_index)s + </samlp:LogoutRequest>""" % \ + { + 'id': uid, + 'issue_instant': issue_instant, + 'single_logout_url': self.__settings.get_idp_slo_url(), + 'entity_id': sp_data['entityId'], + 'name_id': name_id_obj, + 'session_index': session_index_str, + } + else: + decoded = b64decode(request) + # We try to inflate + try: + inflated = decompress(decoded, -15) + logout_request = inflated + except Exception: + logout_request = decoded + self.id = self.get_id(logout_request) + + self.__logout_request = logout_request + +
    [docs] def get_request(self, deflate=True): + """ + Returns the Logout Request deflated, base64encoded + :param deflate: It makes the deflate process optional + :type: bool + :return: Logout Request maybe deflated and base64 encoded + :rtype: str object + """ + if deflate: + request = OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_request) + else: + request = b64encode(self.__logout_request) + return request
    + +
    [docs] 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
    + +
    [docs] @staticmethod + def get_id(request): + """ + Returns the ID of the Logout Request + :param request: Logout Request Message + :type request: string|DOMDocument + :return: string ID + :rtype: str object + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + elem = fromstring(request, forbid_dtd=True) + return elem.get('ID', None)
    + +
    [docs] @staticmethod + def get_nameid_data(request, key=None): + """ + Gets the NameID Data of the the Logout Request + :param request: Logout Request Message + :type request: string|DOMDocument + :param key: The SP key + :type key: string + :return: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) + :rtype: dict + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + elem = fromstring(request, forbid_dtd=True) + + name_id = None + encrypted_entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID') + + if len(encrypted_entries) == 1: + if key is None: + 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_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData') + if len(encrypted_data_nodes) == 1: + encrypted_data = encrypted_data_nodes[0] + name_id = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) + else: + entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:NameID') + if len(entries) == 1: + name_id = entries[0] + + if name_id is None: + raise OneLogin_Saml2_ValidationError( + 'NameID not found in the Logout Request', + OneLogin_Saml2_ValidationError.NO_NAMEID + ) + + name_id_data = { + 'Value': OneLogin_Saml2_Utils.element_text(name_id) + } + for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: + if attr in name_id.attrib.keys(): + name_id_data[attr] = name_id.attrib[attr] + + return name_id_data
    + +
    [docs] @staticmethod + def get_nameid(request, key=None): + """ + Gets the NameID of the Logout Request Message + :param request: Logout Request Message + :type request: string|DOMDocument + :param key: The SP key + :type key: string + :return: Name ID Value + :rtype: string + """ + name_id = OneLogin_Saml2_Logout_Request.get_nameid_data(request, key) + return name_id['Value']
    + +
    [docs] @staticmethod + def get_nameid_format(request, key=None): + """ + Gets the NameID Format of the Logout Request Message + :param request: Logout Request Message + :type request: string|DOMDocument + :param key: The SP key + :type key: string + :return: Name ID Value + :rtype: string + """ + name_id_format = None + name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(request, key) + if name_id_data and 'Format' in name_id_data.keys(): + name_id_format = name_id_data['Format'] + return name_id_format
    + +
    [docs] @staticmethod + def get_issuer(request): + """ + Gets the Issuer of the Logout Request Message + :param request: Logout Request Message + :type request: string|DOMDocument + :return: The Issuer + :rtype: string + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + elem = fromstring(request, forbid_dtd=True) + + issuer = None + issuer_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:Issuer') + if len(issuer_nodes) == 1: + issuer = OneLogin_Saml2_Utils.element_text(issuer_nodes[0]) + return issuer
    + +
    [docs] @staticmethod + def get_session_indexes(request): + """ + Gets the SessionIndexes from the Logout Request + :param request: Logout Request Message + :type request: string|DOMDocument + :return: The SessionIndex value + :rtype: list + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + elem = fromstring(request, forbid_dtd=True) + + session_indexes = [] + session_index_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex') + for session_index_node in session_index_nodes: + session_indexes.append(OneLogin_Saml2_Utils.element_text(session_index_node)) + return session_indexes
    + +
    [docs] 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 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 + """ + self.__error = None + lowercase_urlencoding = False + try: + dom = fromstring(self.__logout_request, forbid_dtd=True) + + idp_data = self.__settings.get_idp_data() + idp_entity_id = idp_data['entityId'] + + if 'get_data' in request_data.keys(): + get_data = request_data['get_data'] + else: + get_data = {} + + if 'lowercase_urlencoding' in request_data.keys(): + lowercase_urlencoding = request_data['lowercase_urlencoding'] + + security = self.__settings.get_security_data() + if self.__settings.is_strict(): + res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) + if not isinstance(res, Document): + raise OneLogin_Saml2_ValidationError( + 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd', + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) + + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + + # Check NotOnOrAfter + if dom.get('NotOnOrAfter', None): + na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.get('NotOnOrAfter')) + if na <= OneLogin_Saml2_Utils.now(): + raise OneLogin_Saml2_ValidationError( + 'Could not validate timestamp: expired. Check system clock.', + OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED + ) + + # Check destination + destination = dom.get('Destination') + if destination: + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=current_url)): + raise Exception( + '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(dom) + if issuer is not None and issuer != idp_entity_id: + raise OneLogin_Saml2_ValidationError( + 'Invalid issuer in the Logout Request (expected %(idpEntityId)s, got %(issuer)s)' % + { + 'idpEntityId': idp_entity_id, + 'issuer': issuer + }, + OneLogin_Saml2_ValidationError.WRONG_ISSUER + ) + + if security['wantMessagesSigned']: + 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_MESSAGE + ) + + if 'Signature' in get_data: + if 'SigAlg' not in get_data: + sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 + else: + sign_alg = get_data['SigAlg'] + + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + if reject_deprecated_alg: + if sign_alg in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sign_alg, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + + signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding) + if 'RelayState' in get_data: + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', sign_alg, lowercase_urlencoding=lowercase_urlencoding)) + + exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert'] + exists_multix509sign = 'x509certMulti' in idp_data and \ + 'signing' in idp_data['x509certMulti'] and \ + idp_data['x509certMulti']['signing'] + + if not (exists_x509cert or exists_multix509sign): + raise OneLogin_Saml2_Error( + 'In order to validate the sign on the Logout Request, the x509cert of the IdP is required', + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) + if exists_multix509sign: + for cert in idp_data['x509certMulti']['signing']: + if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): + return True + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Request rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) + else: + cert = idp_data['x509cert'] + + if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Request rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) + return True + except Exception as err: + # pylint: disable=R0801sign_alg + self.__error = err.__str__() + debug = self.__settings.is_debug_active() + if debug: + print(err.__str__()) + if raise_exceptions: + raise err + return False
    + +
    [docs] def get_error(self): + """ + After executing a validation process, if it fails this method returns the cause + """ + return self.__error
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/logout_response.html b/docs/saml2/_modules/onelogin/saml2/logout_response.html new file mode 100644 index 00000000..714c701a --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/logout_response.html @@ -0,0 +1,377 @@ + + + + + + onelogin.saml2.logout_response — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    +
      +
    • + + +
    • +
    • +
    +
    +
    +
    +
    + +

    Source code for onelogin.saml2.logout_response

    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_Logout_Response class
    +
    +MIT License
    +
    +Logout Response class of Python Toolkit.
    +
    +"""
    +
    +from base64 import b64encode, b64decode
    +from xml.dom.minidom import Document
    +from defusedxml.minidom import parseString
    +
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +from onelogin.saml2.xmlparser import fromstring
    +
    +
    +
    [docs]class OneLogin_Saml2_Logout_Response(object): + """ + + This class handles a Logout Response. It Builds or parses a Logout Response object + and validates it. + + """ + + def __init__(self, settings, response=None): + """ + Constructs a Logout Response object (Initialize params from settings + and if provided load the Logout Response. + + Arguments are: + * (OneLogin_Saml2_Settings) settings. Setting data + * (string) response. An UUEncoded SAML Logout + response from the IdP. + """ + self.__settings = settings + self.__error = None + self.id = None + + if response is not None: + self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response) + self.document = parseString(self.__logout_response, forbid_dtd=True) + self.id = self.document.documentElement.getAttribute('ID') + +
    [docs] def get_issuer(self): + """ + Gets the Issuer of the Logout Response Message + :return: The Issuer + :rtype: string + """ + issuer = None + issuer_nodes = self.__query('/samlp:LogoutResponse/saml:Issuer') + if len(issuer_nodes) == 1: + issuer = OneLogin_Saml2_Utils.element_text(issuer_nodes[0]) + return issuer
    + +
    [docs] def get_status(self): + """ + Gets the Status + :return: The Status + :rtype: string + """ + entries = self.__query('/samlp:LogoutResponse/samlp:Status/samlp:StatusCode') + if len(entries) == 0: + return None + status = entries[0].attrib['Value'] + return status
    + +
    [docs] 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 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 + """ + self.__error = None + lowercase_urlencoding = False + try: + idp_data = self.__settings.get_idp_data() + idp_entity_id = idp_data['entityId'] + get_data = request_data['get_data'] + + if 'lowercase_urlencoding' in request_data.keys(): + lowercase_urlencoding = request_data['lowercase_urlencoding'] + + security = self.__settings.get_security_data() + if self.__settings.is_strict(): + res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) + if not isinstance(res, Document): + raise OneLogin_Saml2_ValidationError( + 'Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd', + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) + + in_response_to = self.get_in_response_to() + # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided + if request_id is not None and in_response_to and 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 OneLogin_Saml2_ValidationError( + 'Invalid issuer in the Logout Response (expected %(idpEntityId)s, got %(issuer)s)' % + { + 'idpEntityId': idp_entity_id, + 'issuer': issuer + }, + OneLogin_Saml2_ValidationError.WRONG_ISSUER + ) + + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + + # Check destination + if self.document.documentElement.hasAttribute('Destination'): + destination = self.document.documentElement.getAttribute('Destination') + if destination: + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=current_url)): + 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 OneLogin_Saml2_ValidationError( + 'The Message of the Logout Response is not signed and the SP require it', + OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE + ) + + if 'Signature' in get_data: + if 'SigAlg' not in get_data: + sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 + else: + sign_alg = get_data['SigAlg'] + + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + if reject_deprecated_alg: + if sign_alg in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sign_alg, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + + signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse', lowercase_urlencoding=lowercase_urlencoding) + if 'RelayState' in get_data: + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding)) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', sign_alg, lowercase_urlencoding=lowercase_urlencoding)) + + exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert'] + exists_multix509sign = 'x509certMulti' in idp_data and \ + 'signing' in idp_data['x509certMulti'] and \ + idp_data['x509certMulti']['signing'] + + if not (exists_x509cert or exists_multix509sign): + raise OneLogin_Saml2_Error( + 'In order to validate the sign on the Logout Response, the x509cert of the IdP is required', + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) + if exists_multix509sign: + for cert in idp_data['x509certMulti']['signing']: + if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): + return True + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) + else: + cert = idp_data['x509cert'] + + if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) + + return True + # pylint: disable=R0801 + except Exception as err: + self.__error = err.__str__() + debug = self.__settings.is_debug_active() + if debug: + print(err.__str__()) + if raise_exceptions: + raise err + return False
    + + def __query(self, query): + """ + Extracts a node from the DOMDocument (Logout Response Menssage) + :param query: Xpath Expresion + :type query: string + :return: The queried node + :rtype: DOMNodeList + """ + # Switch to lxml for querying + xml = self.document.toxml() + return OneLogin_Saml2_Utils.query(fromstring(xml, forbid_dtd=True), query) + +
    [docs] def build(self, in_response_to): + """ + Creates a Logout Response object. + :param in_response_to: InResponseTo value for the Logout Response. + :type in_response_to: string + """ + sp_data = self.__settings.get_sp_data() + + uid = OneLogin_Saml2_Utils.generate_unique_id() + issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) + + logout_response = """<samlp:LogoutResponse 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" +> + <saml:Issuer>%(entity_id)s</saml:Issuer> + <samlp:Status> + <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /> + </samlp:Status> +</samlp:LogoutResponse>""" % \ + { + 'id': uid, + 'issue_instant': issue_instant, + 'destination': self.__settings.get_idp_slo_response_url(), + 'in_response_to': in_response_to, + 'entity_id': sp_data['entityId'], + } + + self.__logout_response = logout_response
    + +
    [docs] def get_in_response_to(self): + """ + Gets the ID of the LogoutRequest which this response is in response to + :returns: ID of LogoutRequest this LogoutResponse is in response to or None if it is not present + :rtype: str + """ + return self.document.documentElement.getAttribute('InResponseTo')
    + +
    [docs] def get_response(self, deflate=True): + """ + Returns the Logout Response defated, base64encoded + :param deflate: It makes the deflate process optional + :type: bool + :return: Logout Response maybe deflated and base64 encoded + :rtype: string + """ + if deflate: + response = OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_response) + else: + response = b64encode(self.__logout_response) + return response
    + +
    [docs] 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
    + +
    [docs] def get_error(self): + """ + After executing a validation process, if it fails this method returns the cause + """ + return self.__error
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/metadata.html b/docs/saml2/_modules/onelogin/saml2/metadata.html new file mode 100644 index 00000000..6e16c080 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/metadata.html @@ -0,0 +1,383 @@ + + + + + + onelogin.saml2.metadata — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    + +

    Source code for onelogin.saml2.metadata

    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_Metadata class
    +
    +MIT License
    +
    +Metadata class of Python Toolkit.
    +
    +"""
    +
    +from time import gmtime, strftime, time
    +from datetime import datetime
    +from defusedxml.minidom import parseString
    +
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +
    +
    +
    [docs]class OneLogin_Saml2_Metadata(object): + """ + + A class that contains methods related to the metadata of the SP + + """ + + TIME_VALID = 172800 # 2 days + TIME_CACHED = 604800 # 1 week + +
    [docs] @staticmethod + def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None): + """ + Builds the metadata of the SP + + :param sp: The SP data + :type sp: string + + :param authnsign: authnRequestsSigned attribute + :type authnsign: string + + :param wsign: wantAssertionsSigned attribute + :type wsign: string + + :param valid_until: Metadata's expiry date + :type valid_until: string|DateTime|Timestamp + + :param cache_duration: Duration of the cache in seconds + :type cache_duration: int|string + + :param contacts: Contacts info + :type contacts: dict + + :param organization: Organization info + :type organization: dict + """ + if valid_until is None: + valid_until = int(time()) + OneLogin_Saml2_Metadata.TIME_VALID + if not isinstance(valid_until, basestring): + if isinstance(valid_until, datetime): + valid_until_time = valid_until.timetuple() + else: + valid_until_time = gmtime(valid_until) + valid_until_str = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time) + else: + valid_until_str = valid_until + + if cache_duration is None: + cache_duration = OneLogin_Saml2_Metadata.TIME_CACHED + if not isinstance(cache_duration, basestring): + cache_duration_str = 'PT%sS' % cache_duration # 'P'eriod of 'T'ime x 'S'econds + else: + cache_duration_str = cache_duration + + if contacts is None: + contacts = {} + if organization is None: + organization = {} + + str_attribute_consuming_service = '' + if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']): + attr_cs_desc_str = '' + if "serviceDescription" in sp['attributeConsumingService']: + attr_cs_desc_str = """ <md:ServiceDescription xml:lang="en">%s</md:ServiceDescription> +""" % sp['attributeConsumingService']['serviceDescription'] + + requested_attribute_data = [] + for req_attribs in sp['attributeConsumingService']['requestedAttributes']: + req_attr_nameformat_str = req_attr_friendlyname_str = req_attr_isrequired_str = '' + req_attr_aux_str = ' />' + + if 'nameFormat' in req_attribs.keys() and req_attribs['nameFormat']: + req_attr_nameformat_str = " NameFormat=\"%s\"" % req_attribs['nameFormat'] + if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']: + req_attr_friendlyname_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName'] + if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']: + req_attr_isrequired_str = " isRequired=\"%s\"" % 'true' if req_attribs['isRequired'] else 'false' + + if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']: + if isinstance(req_attribs['attributeValue'], basestring): + req_attribs['attributeValue'] = [req_attribs['attributeValue']] + + req_attr_aux_str = ">" + for attrValue in req_attribs['attributeValue']: + req_attr_aux_str += """ + <saml:AttributeValue xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">%(attributeValue)s</saml:AttributeValue>""" % \ + { + 'attributeValue': attrValue + } + req_attr_aux_str += """ + </md:RequestedAttribute>""" + + requested_attribute = """ <md:RequestedAttribute Name="%(req_attr_name)s"%(req_attr_nameformat_str)s%(req_attr_friendlyname_str)s%(req_attr_isrequired_str)s%(req_attr_aux_str)s""" % \ + { + 'req_attr_name': req_attribs['name'], + 'req_attr_nameformat_str': req_attr_nameformat_str, + 'req_attr_friendlyname_str': req_attr_friendlyname_str, + 'req_attr_isrequired_str': req_attr_isrequired_str, + 'req_attr_aux_str': req_attr_aux_str + } + + requested_attribute_data.append(requested_attribute) + + str_attribute_consuming_service = """ <md:AttributeConsumingService index="1"> + <md:ServiceName xml:lang="en">%(service_name)s</md:ServiceName> +%(attr_cs_desc)s%(requested_attribute_str)s + </md:AttributeConsumingService> +""" % \ + { + 'service_name': sp['attributeConsumingService']['serviceName'], + 'attr_cs_desc': attr_cs_desc_str, + 'requested_attribute_str': '\n'.join(requested_attribute_data) + } + + sls = '' + if 'singleLogoutService' in sp and 'url' in sp['singleLogoutService']: + sls = """ <md:SingleLogoutService Binding="%(binding)s" + Location="%(location)s" />\n""" % \ + { + 'binding': sp['singleLogoutService']['binding'], + 'location': sp['singleLogoutService']['url'], + } + + str_authnsign = 'true' if authnsign else 'false' + str_wsign = 'true' if wsign else 'false' + + str_organization = '' + if len(organization) > 0: + organization_names = [] + organization_displaynames = [] + organization_urls = [] + for (lang, info) in organization.items(): + organization_names.append(""" <md:OrganizationName xml:lang="%s">%s</md:OrganizationName>""" % (lang, info['name'])) + organization_displaynames.append(""" <md:OrganizationDisplayName xml:lang="%s">%s</md:OrganizationDisplayName>""" % (lang, info['displayname'])) + organization_urls.append(""" <md:OrganizationURL xml:lang="%s">%s</md:OrganizationURL>""" % (lang, info['url'])) + org_data = '\n'.join(organization_names) + '\n' + '\n'.join(organization_displaynames) + '\n' + '\n'.join(organization_urls) + str_organization = """ <md:Organization> +%(org)s + </md:Organization>\n""" % {'org': org_data} + + str_contacts = '' + if len(contacts) > 0: + contacts_info = [] + for (ctype, info) in contacts.items(): + contact = """ <md:ContactPerson contactType="%(type)s"> + <md:GivenName>%(name)s</md:GivenName> + <md:EmailAddress>%(email)s</md:EmailAddress> + </md:ContactPerson>""" % \ + { + 'type': ctype, + 'name': info['givenName'], + 'email': info['emailAddress'], + } + contacts_info.append(contact) + str_contacts = '\n'.join(contacts_info) + '\n' + + metadata = u"""<?xml version="1.0"?> +<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" + %(valid)s + %(cache)s + entityID="%(entity_id)s"> + <md:SPSSODescriptor AuthnRequestsSigned="%(authnsign)s" WantAssertionsSigned="%(wsign)s" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> +%(sls)s <md:NameIDFormat>%(name_id_format)s</md:NameIDFormat> + <md:AssertionConsumerService Binding="%(binding)s" + Location="%(location)s" + index="1" /> +%(attribute_consuming_service)s </md:SPSSODescriptor> +%(organization)s%(contacts)s</md:EntityDescriptor>""" % \ + { + 'valid': ('validUntil="%s"' % valid_until_str) if valid_until_str else '', + 'cache': ('cacheDuration="%s"' % cache_duration_str) if cache_duration_str else '', + 'entity_id': sp['entityId'], + 'authnsign': str_authnsign, + 'wsign': str_wsign, + 'name_id_format': sp['NameIDFormat'], + 'binding': sp['assertionConsumerService']['binding'], + 'location': sp['assertionConsumerService']['url'], + 'sls': sls, + 'organization': str_organization, + 'contacts': str_contacts, + 'attribute_consuming_service': str_attribute_consuming_service + } + return metadata
    + +
    [docs] @staticmethod + def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA256, digest_algorithm=OneLogin_Saml2_Constants.SHA256): + """ + Signs the metadata with the key/cert provided + + :param metadata: SAML Metadata XML + :type metadata: string + + :param key: x509 key + :type key: string + + :param cert: x509 cert + :type cert: string + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + + :param digest_algorithm: Digest algorithm method + :type digest_algorithm: string + + :returns: Signed Metadata + :rtype: string + """ + return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm, digest_algorithm)
    + +
    [docs] @staticmethod + def add_x509_key_descriptors(metadata, cert=None, add_encryption=True): + """ + Adds the x509 descriptors (sign/encryption) to the metadata + The same cert will be used for sign/encrypt + + :param metadata: SAML Metadata XML + :type metadata: string + + :param cert: x509 cert + :type cert: string + + :param add_encryption: Determines if the KeyDescriptor[use="encryption"] should be added. + :type add_encryption: boolean + + :returns: Metadata with KeyDescriptors + :rtype: string + """ + if cert is None or cert == '': + return metadata + try: + xml = parseString(metadata.encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True) + except Exception as e: + raise Exception('Error parsing metadata. ' + e.message) + + formated_cert = OneLogin_Saml2_Utils.format_cert(cert, False) + x509_certificate = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Certificate') + content = xml.createTextNode(formated_cert) + x509_certificate.appendChild(content) + + key_data = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Data') + key_data.appendChild(x509_certificate) + + key_info = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:KeyInfo') + key_info.appendChild(key_data) + + key_descriptor = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'md:KeyDescriptor') + + entity_descriptor = xml.getElementsByTagName('md:EntityDescriptor')[0] + + sp_sso_descriptor = entity_descriptor.getElementsByTagName('md:SPSSODescriptor')[0] + sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) + if add_encryption: + sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) + + signing = xml.getElementsByTagName('md:KeyDescriptor')[0] + signing.setAttribute('use', 'signing') + signing.appendChild(key_info) + signing.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) + + if add_encryption: + encryption = xml.getElementsByTagName('md:KeyDescriptor')[1] + encryption.setAttribute('use', 'encryption') + encryption.appendChild(key_info.cloneNode(True)) + encryption.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) + + return xml.toxml()
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/response.html b/docs/saml2/_modules/onelogin/saml2/response.html new file mode 100644 index 00000000..57e2502c --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/response.html @@ -0,0 +1,1087 @@ + + + + + + onelogin.saml2.response — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    + +

    Source code for onelogin.saml2.response

    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_Response class
    +
    +MIT License
    +
    +SAML Response class of Python Toolkit.
    +
    +"""
    +
    +from base64 import b64decode
    +from copy import deepcopy
    +from xml.dom.minidom import Document
    +
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception
    +from onelogin.saml2.xmlparser import tostring, fromstring
    +
    +
    +
    [docs]class OneLogin_Saml2_Response(object): + """ + + This class handles a SAML Response. It parses or validates + a Logout Response object. + + """ + + def __init__(self, settings, response): + """ + Constructs the response object. + + :param settings: The setting info + :type settings: OneLogin_Saml2_Setting object + + :param response: The base64 encoded, XML string containing the samlp:Response + :type response: string + """ + self.__settings = settings + self.__error = None + self.response = b64decode(response) + self.document = fromstring(self.response, forbid_dtd=True) + self.decrypted_document = None + self.encrypted = None + self.valid_scd_not_on_or_after = None + + # Quick check for the presence of EncryptedAssertion + encrypted_assertion_nodes = self.__query('/samlp:Response/saml:EncryptedAssertion') + if encrypted_assertion_nodes: + decrypted_document = deepcopy(self.document) + self.encrypted = True + self.decrypted_document = self.__decrypt_assertion(decrypted_document) + +
    [docs] def is_valid(self, request_data, request_id=None, raise_exceptions=False): + """ + Validates the response object. + + :param request_data: Request Data + :type request_data: dict + + :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP + :type request_id: string + + :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 + """ + self.__error = None + try: + # Checks SAML version + if self.document.get('Version', None) != '2.0': + 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 OneLogin_Saml2_ValidationError( + 'Missing ID attribute on SAML Response', + OneLogin_Saml2_ValidationError.MISSING_ID + ) + + # Checks that the response has the SUCCESS status + self.check_status() + + # Checks that the response only has one assertion + if not self.validate_num_assertions(): + raise OneLogin_Saml2_ValidationError( + 'SAML Response must contain 1 assertion', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS + ) + + idp_data = self.__settings.get_idp_data() + idp_entity_id = idp_data.get('entityId', '') + sp_data = self.__settings.get_sp_data() + sp_entity_id = sp_data.get('entityId', '') + + signed_elements = self.process_signed_elements() + + has_signed_response = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements + has_signed_assertion = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML in signed_elements + + security = self.__settings.get_security_data() + if self.__settings.is_strict(): + no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' + res = OneLogin_Saml2_Utils.validate_xml( + tostring(self.document), + 'saml-schema-protocol-2.0.xsd', + self.__settings.is_debug_active() + ) + if not isinstance(res, Document): + 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_Utils.validate_xml( + tostring(self.decrypted_document), + 'saml-schema-protocol-2.0.xsd', + self.__settings.is_debug_active() + ) + if not isinstance(res, Document): + raise OneLogin_Saml2_ValidationError( + no_valid_xml_msg, + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) + + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + + in_response_to = self.get_in_response_to() + if request_id is None and in_response_to is not None and security.get('rejectUnsolicitedResponsesWithInResponseTo', False): + raise OneLogin_Saml2_ValidationError( + 'The Response has an InResponseTo attribute: %s while no InResponseTo was expected' % in_response_to, + OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO + ) + + # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided + if request_id is not None and 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.get('wantAssertionsEncrypted', False): + 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.get('wantNameIdEncrypted', False): + encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') + if len(encrypted_nameid_nodes) != 1: + 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 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 OneLogin_Saml2_ValidationError( + 'The Assertion must include an AuthnStatement element', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_AUTHSTATEMENTS + ) + + # Checks that the response has all of the AuthnContexts that we provided in the request. + # Only check if failOnAuthnContextMismatch is true and requestedAuthnContext is set to a list. + requested_authn_contexts = security.get('requestedAuthnContext', True) + + if security.get('failOnAuthnContextMismatch', False) and requested_authn_contexts and requested_authn_contexts is not True: + authn_contexts = self.get_authn_contexts() + unmatched_contexts = set(authn_contexts).difference(requested_authn_contexts) + if unmatched_contexts: + raise OneLogin_Saml2_ValidationError( + 'The AuthnContext "%s" was not a requested context "%s"' % (', '.join(unmatched_contexts), ', '.join(requested_authn_contexts)), + OneLogin_Saml2_ValidationError.AUTHN_CONTEXT_MISMATCH + ) + + # 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 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 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) + if destination: + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=current_url)): + # TODO: Review if following lines are required, since we can control the + # request_data + # current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data) + # if not destination.startswith(current_url_routed): + raise OneLogin_Saml2_ValidationError( + 'The response was received at %s instead of %s' % (current_url, destination), + OneLogin_Saml2_ValidationError.WRONG_DESTINATION + ) + elif destination == '': + 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 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 OneLogin_Saml2_ValidationError( + 'Invalid issuer in the Assertion/Response (expected %(idpEntityId)s, got %(issuer)s)' % + { + 'idpEntityId': idp_entity_id, + 'issuer': issuer + }, + 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 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 + subject_confirmation_nodes = self.__query_assertion('/saml:Subject/saml:SubjectConfirmation') + + for scn in subject_confirmation_nodes: + method = scn.get('Method', None) + if method and method != OneLogin_Saml2_Constants.CM_BEARER: + continue + sc_data = scn.find('saml:SubjectConfirmationData', namespaces=OneLogin_Saml2_Constants.NSMAP) + if sc_data is None: + continue + else: + irt = sc_data.get('InResponseTo', None) + if (in_response_to is None and irt is not None and + security.get('rejectUnsolicitedResponsesWithInResponseTo', False)) \ + or in_response_to and irt and irt != in_response_to: + continue + recipient = sc_data.get('Recipient', None) + if recipient and current_url not in recipient: + continue + nooa = sc_data.get('NotOnOrAfter', None) + if nooa: + parsed_nooa = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa) + if parsed_nooa <= OneLogin_Saml2_Utils.now(): + continue + nb = sc_data.get('NotBefore', None) + if nb: + parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time(nb) + if parsed_nb > OneLogin_Saml2_Utils.now(): + continue + + if nooa: + self.valid_scd_not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa) + + any_subject_confirmation = True + break + + if not any_subject_confirmation: + raise OneLogin_Saml2_ValidationError( + 'A valid SubjectConfirmation was not found on this Response', + OneLogin_Saml2_ValidationError.WRONG_SUBJECTCONFIRMATION + ) + + if security.get('wantAssertionsSigned', False) 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_ASSERTION + ) + + if security.get('wantMessagesSigned', False) 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_MESSAGE + ) + + if not signed_elements or (not has_signed_response and not has_signed_assertion): + 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) + fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) + + multicerts = None + if 'x509certMulti' in idp_data and 'signing' in idp_data['x509certMulti'] and idp_data['x509certMulti']['signing']: + multicerts = idp_data['x509certMulti']['signing'] + + # 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, multicerts=multicerts, raise_exceptions=False): + 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, multicerts=multicerts, raise_exceptions=False): + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. SAML Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) + + return True + except Exception as err: + self.__error = err.__str__() + debug = self.__settings.is_debug_active() + if debug: + print(err.__str__()) + if raise_exceptions: + raise err + return False
    + +
    [docs] def check_status(self): + """ + Check if the status of the response is success or not + + :raises: Exception. If the status is not success + """ + status = OneLogin_Saml2_Utils.get_status(self.document) + code = status.get('code', None) + if code and code != OneLogin_Saml2_Constants.STATUS_SUCCESS: + splited_code = code.split(':') + printable_code = splited_code.pop() + status_exception_msg = 'The status code of the Response was not Success, was %s' % printable_code + status_msg = status.get('msg', None) + if status_msg: + status_exception_msg += ' -> ' + status_msg + raise OneLogin_Saml2_ValidationError( + status_exception_msg, + OneLogin_Saml2_ValidationError.STATUS_CODE_IS_NOT_SUCCESS + )
    + +
    [docs] def check_one_condition(self): + """ + Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique. + """ + condition_nodes = self.__query_assertion('/saml:Conditions') + if len(condition_nodes) == 1: + return True + else: + return False
    + +
    [docs] def check_one_authnstatement(self): + """ + Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique. + """ + authnstatement_nodes = self.__query_assertion('/saml:AuthnStatement') + if len(authnstatement_nodes) == 1: + return True + else: + return False
    + +
    [docs] def get_audiences(self): + """ + Gets the audiences + + :returns: The valid audiences for the SAML Response + :rtype: list + """ + audience_nodes = self.__query_assertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience') + return [OneLogin_Saml2_Utils.element_text(node) for node in audience_nodes if OneLogin_Saml2_Utils.element_text(node) is not None]
    + +
    [docs] def get_authn_contexts(self): + """ + Gets the authentication contexts + :returns: The authentication classes for the SAML Response + :rtype: list + """ + authn_context_nodes = self.__query_assertion('/saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef') + return [OneLogin_Saml2_Utils.element_text(node) for node in authn_context_nodes]
    + +
    [docs] def get_in_response_to(self): + """ + Gets the ID of the request which this response is in response to + :returns: ID of AuthNRequest this Response is in response to or None if it is not present + :rtype: str + """ + return self.document.get('InResponseTo')
    + +
    [docs] def get_issuers(self): + """ + Gets the issuers (from message and from assertion) + + :returns: The issuers + :rtype: list + """ + issuers = [] + + message_issuer_nodes = OneLogin_Saml2_Utils.query(self.document, '/samlp:Response/saml:Issuer') + if len(message_issuer_nodes) > 0: + if len(message_issuer_nodes) == 1: + issuers.append(OneLogin_Saml2_Utils.element_text(message_issuer_nodes[0])) + else: + raise OneLogin_Saml2_ValidationError( + 'Issuer of the Response is multiple.', + OneLogin_Saml2_ValidationError.ISSUER_MULTIPLE_IN_RESPONSE + ) + + assertion_issuer_nodes = self.__query_assertion('/saml:Issuer') + if len(assertion_issuer_nodes) == 1: + issuers.append(OneLogin_Saml2_Utils.element_text(assertion_issuer_nodes[0])) + else: + raise OneLogin_Saml2_ValidationError( + 'Issuer of the Assertion not found or multiple.', + OneLogin_Saml2_ValidationError.ISSUER_NOT_FOUND_IN_ASSERTION + ) + + return list(set(issuers))
    + +
    [docs] def get_nameid_data(self): + """ + Gets the NameID Data provided by the SAML Response from the IdP + + :returns: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) + :rtype: dict + """ + nameid = None + nameid_data = {} + + encrypted_id_data_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') + if encrypted_id_data_nodes: + encrypted_data = encrypted_id_data_nodes[0] + key = self.__settings.get_sp_key() + nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) + else: + nameid_nodes = self.__query_assertion('/saml:Subject/saml:NameID') + if nameid_nodes: + nameid = nameid_nodes[0] + + is_strict = self.__settings.is_strict() + want_nameid = self.__settings.get_security_data().get('wantNameId', True) + if nameid is None: + if is_strict and want_nameid: + raise OneLogin_Saml2_ValidationError( + 'NameID not found in the assertion of the Response', + OneLogin_Saml2_ValidationError.NO_NAMEID + ) + else: + if is_strict and want_nameid and not OneLogin_Saml2_Utils.element_text(nameid): + raise OneLogin_Saml2_ValidationError( + 'An empty NameID value found', + OneLogin_Saml2_ValidationError.EMPTY_NAMEID + ) + + nameid_data = {'Value': OneLogin_Saml2_Utils.element_text(nameid)} + for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: + value = nameid.get(attr, None) + if value: + if is_strict and attr == 'SPNameQualifier': + sp_data = self.__settings.get_sp_data() + sp_entity_id = sp_data.get('entityId', '') + if sp_entity_id != 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
    + +
    [docs] def get_nameid(self): + """ + Gets the NameID provided by the SAML Response from the IdP + + :returns: NameID (value) + :rtype: string|None + """ + nameid_value = None + nameid_data = self.get_nameid_data() + if nameid_data and 'Value' in nameid_data.keys(): + nameid_value = nameid_data['Value'] + return nameid_value
    + +
    [docs] def get_nameid_format(self): + """ + Gets the NameID Format provided by the SAML Response from the IdP + + :returns: NameID Format + :rtype: string|None + """ + nameid_format = None + nameid_data = self.get_nameid_data() + if nameid_data and 'Format' in nameid_data.keys(): + nameid_format = nameid_data['Format'] + return nameid_format
    + +
    [docs] def get_nameid_nq(self): + """ + Gets the NameID NameQualifier provided by the SAML Response from the IdP + + :returns: NameID NameQualifier + :rtype: string|None + """ + nameid_nq = None + nameid_data = self.get_nameid_data() + if nameid_data and 'NameQualifier' in nameid_data.keys(): + nameid_nq = nameid_data['NameQualifier'] + return nameid_nq
    + +
    [docs] def get_nameid_spnq(self): + """ + Gets the NameID SP NameQualifier provided by the SAML response from the IdP. + + :returns: NameID SP NameQualifier + :rtype: string|None + """ + nameid_spnq = None + nameid_data = self.get_nameid_data() + if nameid_data and 'SPNameQualifier' in nameid_data.keys(): + nameid_spnq = nameid_data['SPNameQualifier'] + return nameid_spnq
    + +
    [docs] def get_session_not_on_or_after(self): + """ + Gets the SessionNotOnOrAfter from the AuthnStatement + Could be used to set the local session expiration + + :returns: The SessionNotOnOrAfter value + :rtype: time|None + """ + not_on_or_after = None + authn_statement_nodes = self.__query_assertion('/saml:AuthnStatement[@SessionNotOnOrAfter]') + if authn_statement_nodes: + not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(authn_statement_nodes[0].get('SessionNotOnOrAfter')) + return not_on_or_after
    + +
    [docs] def get_assertion_not_on_or_after(self): + """ + Returns the NotOnOrAfter value of the valid SubjectConfirmationData node if any + """ + return self.valid_scd_not_on_or_after
    + +
    [docs] def get_session_index(self): + """ + Gets the SessionIndex from the AuthnStatement + Could be used to be stored in the local session in order + to be used in a future Logout Request that the SP could + send to the SP, to set what specific session must be deleted + + :returns: The SessionIndex value + :rtype: string|None + """ + session_index = None + authn_statement_nodes = self.__query_assertion('/saml:AuthnStatement[@SessionIndex]') + if authn_statement_nodes: + session_index = authn_statement_nodes[0].get('SessionIndex') + return session_index
    + +
    [docs] def get_attributes(self): + """ + Gets the Attributes from the AttributeStatement element. + EncryptedAttributes are not supported + """ + attributes = {} + attribute_nodes = self.__query_assertion('/saml:AttributeStatement/saml:Attribute') + for attribute_node in attribute_nodes: + attr_name = attribute_node.get('Name') + if attr_name in attributes.keys(): + 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[OneLogin_Saml2_Constants.NS_PREFIX_SAML]): + # Remove any whitespace (which may be present where attributes are + # nested inside NameID children). + attr_text = OneLogin_Saml2_Utils.element_text(attr) + if attr_text: + attr_text = attr_text.strip() + if attr_text: + values.append(attr_text) + + # Parse any nested NameID children + for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]): + values.append({ + 'NameID': { + 'Format': nameid.get('Format'), + 'NameQualifier': nameid.get('NameQualifier'), + 'value': OneLogin_Saml2_Utils.element_text(nameid) + } + }) + + attributes[attr_name] = values + return attributes
    + +
    [docs] def get_friendlyname_attributes(self): + """ + Gets the Attributes from the AttributeStatement element indexed by FiendlyName. + EncryptedAttributes are not supported + """ + attributes = {} + attribute_nodes = self.__query_assertion('/saml:AttributeStatement/saml:Attribute') + for attribute_node in attribute_nodes: + attr_friendlyname = attribute_node.get('FriendlyName') + if attr_friendlyname: + if attr_friendlyname in attributes.keys(): + raise OneLogin_Saml2_ValidationError( + 'Found an Attribute element with duplicated FriendlyName', + OneLogin_Saml2_ValidationError.DUPLICATED_ATTRIBUTE_NAME_FOUND + ) + + values = [] + for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]): + # Remove any whitespace (which may be present where attributes are + # nested inside NameID children). + attr_text = OneLogin_Saml2_Utils.element_text(attr) + if attr_text: + attr_text = attr_text.strip() + if attr_text: + values.append(attr_text) + + # Parse any nested NameID children + for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]): + values.append({ + 'NameID': { + 'Format': nameid.get('Format'), + 'NameQualifier': nameid.get('NameQualifier'), + 'value': OneLogin_Saml2_Utils.element_text(nameid) + } + }) + + attributes[attr_friendlyname] = values + return attributes
    + +
    [docs] def validate_num_assertions(self): + """ + Verifies that the document only contains a single Assertion (encrypted or not) + + :returns: True if only 1 assertion encrypted or not + :rtype: bool + """ + encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:EncryptedAssertion') + assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:Assertion') + + valid = len(encrypted_assertion_nodes) + len(assertion_nodes) == 1 + + if (self.encrypted): + assertion_nodes = OneLogin_Saml2_Utils.query(self.decrypted_document, '//saml:Assertion') + valid = valid and len(assertion_nodes) == 1 + + return valid
    + +
    [docs] def process_signed_elements(self): + """ + Verifies the signature nodes: + - Checks that are Response or Assertion + - Check that IDs and reference URI are unique and consistent. + + :returns: The signed elements tag names + :rtype: list + """ + sign_nodes = self.__query('//ds:Signature') + + security = self.__settings.get_security_data() + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + + signed_elements = [] + verified_seis = [] + verified_ids = [] + response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP + assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML + + for sign_node in sign_nodes: + signed_element = sign_node.getparent().tag + if signed_element != response_tag and signed_element != assertion_tag: + 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 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 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 + ref = OneLogin_Saml2_Utils.query(sign_node, './/ds:Reference') + if ref: + ref = ref[0] + if ref.get('URI'): + sei = ref.get('URI')[1:] + + if sei != id_value: + raise OneLogin_Saml2_ValidationError( + 'Found an invalid Signed Element. SAML Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNED_ELEMENT + ) + + if sei in verified_seis: + raise OneLogin_Saml2_ValidationError( + 'Duplicated Reference URI. SAML Response rejected', + OneLogin_Saml2_ValidationError.DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS + ) + verified_seis.append(sei) + + # Check the signature and digest algorithm + if reject_deprecated_alg: + sig_method_node = OneLogin_Saml2_Utils.query(sign_node, './/ds:SignatureMethod') + if sig_method_node: + sig_method = sig_method_node[0].get("Algorithm") + if sig_method in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sig_method, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + + dig_method_node = OneLogin_Saml2_Utils.query(sign_node, './/ds:DigestMethod') + if dig_method_node: + dig_method = dig_method_node[0].get("Algorithm") + if dig_method in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated digest algorithm found: %s' % dig_method, + OneLogin_Saml2_ValidationError.DEPRECATED_DIGEST_METHOD + ) + + signed_elements.append(signed_element) + + if signed_elements: + 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_ELEMENTS + ) + return signed_elements
    + +
    [docs] @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 + + response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP + assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML + + if (response_tag in signed_elements and signed_elements.count(response_tag) > 1) or \ + (assertion_tag in signed_elements and signed_elements.count(assertion_tag) > 1) or \ + (response_tag not in signed_elements and assertion_tag not in signed_elements): + return False + + # Check that the signed elements found here, are the ones that will be verified + # by OneLogin_Saml2_Utils.validate_sign + if response_tag in signed_elements: + expected_signature_nodes = OneLogin_Saml2_Utils.query(self.document, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH) + if len(expected_signature_nodes) != 1: + 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 OneLogin_Saml2_ValidationError( + 'Unexpected number of Assertion signatures found. SAML Response rejected.', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION + ) + + return True
    + +
    [docs] @return_false_on_exception + def validate_timestamps(self): + """ + Verifies that the document is valid according to Conditions Element + + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean + + :returns: True if the condition is valid, False otherwise + :rtype: bool + """ + conditions_nodes = self.__query_assertion('/saml:Conditions') + + for conditions_node in conditions_nodes: + 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 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 OneLogin_Saml2_ValidationError( + 'Could not validate timestamp: expired. Check system clock.', + OneLogin_Saml2_ValidationError.ASSERTION_EXPIRED + ) + return True
    + + def __query_assertion(self, xpath_expr): + """ + Extracts nodes that match the query from the Assertion + + :param query: Xpath Expresion + :type query: String + + :returns: The queried nodes + :rtype: list + """ + assertion_expr = '/saml:Assertion' + signature_expr = '/ds:Signature/ds:SignedInfo/ds:Reference' + signed_assertion_query = '/samlp:Response' + assertion_expr + signature_expr + assertion_reference_nodes = self.__query(signed_assertion_query) + tagid = None + + if not assertion_reference_nodes: + # Check if the message is signed + signed_message_query = '/samlp:Response' + signature_expr + message_reference_nodes = self.__query(signed_message_query) + if message_reference_nodes: + message_id = message_reference_nodes[0].get('URI') + final_query = "/samlp:Response[@ID=$tagid]/" + tagid = message_id[1:] + else: + final_query = "/samlp:Response" + final_query += assertion_expr + else: + assertion_id = assertion_reference_nodes[0].get('URI') + final_query = '/samlp:Response' + assertion_expr + "[@ID=$tagid]" + tagid = assertion_id[1:] + final_query += xpath_expr + return self.__query(final_query, tagid) + + def __query(self, query, tagid=None): + """ + Extracts nodes that match the query from the Response + + :param query: Xpath Expresion + :type query: String + + :param tagid: Tag ID + :type query: String + + :returns: The queried nodes + :rtype: list + """ + if self.encrypted: + document = self.decrypted_document + else: + document = self.document + return OneLogin_Saml2_Utils.query(document, query, None, tagid) + + def __decrypt_assertion(self, dom): + """ + Decrypts the Assertion + + :raises: Exception if no private key available + + :param dom: Encrypted Assertion + :type dom: Element + + :returns: Decrypted Assertion + :rtype: Element + """ + key = self.__settings.get_sp_key() + debug = self.__settings.is_debug_active() + + if not key: + 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_Utils.query(dom, '/samlp:Response/saml:EncryptedAssertion') + if encrypted_assertion_nodes: + encrypted_data_nodes = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData') + if encrypted_data_nodes: + keyinfo = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData/ds:KeyInfo') + if not keyinfo: + 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 OneLogin_Saml2_ValidationError( + 'KeyInfo has no children nodes, invalid Assertion', + OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOUND_IN_KEYINFO + ) + for child in children: + if 'RetrievalMethod' in child.tag: + if child.attrib['Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey': + raise OneLogin_Saml2_ValidationError( + 'Unsupported Retrieval Method found', + OneLogin_Saml2_ValidationError.UNSUPPORTED_RETRIEVAL_METHOD + ) + uri = child.attrib['URI'] + if not uri.startswith('#'): + break + uri = uri.split('#')[1] + encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id=$tagid]', None, uri) + if encrypted_key: + keyinfo.append(encrypted_key[0]) + + encrypted_data = encrypted_data_nodes[0] + decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug=debug, inplace=True) + dom.replace(encrypted_assertion_nodes[0], decrypted) + + return dom + +
    [docs] def get_error(self): + """ + After executing a validation process, if it fails this method returns the cause + """ + return self.__error
    + +
    [docs] def get_xml_document(self): + """ + Returns the SAML Response document (If contains an encrypted assertion, decrypts it) + + :return: Decrypted XML response document + :rtype: DOMDocument + """ + if self.encrypted: + return self.decrypted_document + else: + return self.document
    + +
    [docs] def get_id(self): + """ + :returns: the ID of the response + :rtype: string + """ + return self.document.get('ID', None)
    + +
    [docs] def get_assertion_id(self): + """ + :returns: the ID of the assertion in the response + :rtype: string + """ + if not self.validate_num_assertions(): + raise OneLogin_Saml2_ValidationError( + 'SAML Response must contain 1 assertion', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS + ) + return self.__query_assertion('')[0].get('ID', None)
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/settings.html b/docs/saml2/_modules/onelogin/saml2/settings.html new file mode 100644 index 00000000..43aabe0c --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/settings.html @@ -0,0 +1,960 @@ + + + + + + onelogin.saml2.settings — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    + +

    Source code for onelogin.saml2.settings

    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_Settings class
    +
    +MIT License
    +
    +Setting class of Python Toolkit.
    +
    +"""
    +
    +import json
    +import re
    +from time import time
    +from os.path import dirname, exists, join, sep, abspath
    +from xml.dom.minidom import Document
    +
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +from onelogin.saml2.errors import OneLogin_Saml2_Error
    +from onelogin.saml2.metadata import OneLogin_Saml2_Metadata
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +
    +
    +# Regex from Django Software Foundation and individual contributors.
    +# Released under a BSD 3-Clause License
    +url_regex = re.compile(
    +    r'^(?:[a-z0-9\.\-]*)://'  # scheme is validated separately
    +    r'(?:(?:[A-Z0-9_](?:[A-Z0-9-_]{0,61}[A-Z0-9_])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # domain...
    +    r'localhost|'  # localhost...
    +    r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|'  # ...or ipv4
    +    r'\[?[A-F0-9]*:[A-F0-9:]+\]?)'  # ...or ipv6
    +    r'(?::\d+)?'  # optional port
    +    r'(?:/?|[/?]\S+)$', re.IGNORECASE)
    +url_regex_single_label_domain = re.compile(
    +    r'^(?:[a-z0-9\.\-]*)://'  # scheme is validated separately
    +    r'(?:(?:[A-Z0-9_](?:[A-Z0-9-_]{0,61}[A-Z0-9_])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # domain...
    +    r'(?:[A-Z0-9_](?:[A-Z0-9-_]{0,61}[A-Z0-9_]))|'  # single-label-domain
    +    r'localhost|'  # localhost...
    +    r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|'  # ...or ipv4
    +    r'\[?[A-F0-9]*:[A-F0-9:]+\]?)'  # ...or ipv6
    +    r'(?::\d+)?'  # optional port
    +    r'(?:/?|[/?]\S+)$', re.IGNORECASE)
    +url_schemes = ['http', 'https', 'ftp', 'ftps']
    +
    +
    +
    [docs]def validate_url(url, allow_single_label_domain=False): + """ + Auxiliary method to validate an urllib + :param url: An url to be validated + :type url: string + :param allow_single_label_domain: In order to allow or not single label domain + :type url: bool + :returns: True if the url is valid + :rtype: bool + """ + + scheme = url.split('://')[0].lower() + if scheme not in url_schemes: + return False + if allow_single_label_domain: + if not bool(url_regex_single_label_domain.search(url)): + return False + else: + if not bool(url_regex.search(url)): + return False + return True
    + + +
    [docs]class OneLogin_Saml2_Settings(object): + """ + + Handles the settings of the Python toolkits. + + """ + + def __init__(self, settings=None, custom_base_path=None, sp_validation_only=False): + """ + Initializes the settings: + - Sets the paths of the different folders + - Loads settings info from settings file or array/object provided + + :param settings: SAML Toolkit Settings + :type settings: dict + + :param custom_base_path: Path where are stored the settings file and the cert folder + :type custom_base_path: string + """ + self.__sp_validation_only = sp_validation_only + self.__paths = {} + self.__strict = True + self.__debug = False + self.__sp = {} + self.__idp = {} + self.__security = {} + self.__contacts = {} + self.__organization = {} + self.__errors = [] + + self.__load_paths(base_path=custom_base_path) + self.__update_paths(settings) + + if settings is None: + try: + valid = self.__load_settings_from_file() + except Exception as e: + raise e + if not valid: + raise OneLogin_Saml2_Error( + 'Invalid dict settings at the file: %s', + OneLogin_Saml2_Error.SETTINGS_INVALID, + ','.join(self.__errors) + ) + self.__add_default_values() + elif isinstance(settings, dict): + if not self.__load_settings_from_dict(settings): + raise OneLogin_Saml2_Error( + 'Invalid dict settings: %s', + OneLogin_Saml2_Error.SETTINGS_INVALID, + ','.join(self.__errors) + ) + else: + raise OneLogin_Saml2_Error( + 'Unsupported settings object', + OneLogin_Saml2_Error.UNSUPPORTED_SETTINGS_OBJECT + ) + + self.format_idp_cert() + self.format_sp_cert() + if 'x509certNew' in self.__sp: + self.format_sp_cert_new() + self.format_sp_key() + if 'x509certMulti' in self.__idp: + self.format_idp_cert_multi() + + def __load_paths(self, base_path=None): + """ + Set the paths of the different folders + """ + if base_path is None: + base_path = dirname(dirname(dirname(abspath(__file__)))) + if not base_path.endswith(sep): + base_path += sep + self.__paths = { + 'base': base_path, + 'cert': base_path + 'certs' + sep, + 'lib': dirname(__file__) + sep + } + + def __update_paths(self, settings): + """ + Set custom paths if necessary + """ + if not isinstance(settings, dict): + return + + if 'custom_base_path' in settings: + base_path = settings['custom_base_path'] + base_path = join(dirname(abspath(__file__)), base_path) + self.__load_paths(base_path) + +
    [docs] def get_base_path(self): + """ + Returns base path + + :return: The base toolkit folder path + :rtype: string + """ + return self.__paths['base']
    + +
    [docs] def set_cert_path(self, path): + """ + Set a new cert path + """ + self.__paths['cert'] = path
    + +
    [docs] def get_cert_path(self): + """ + Returns cert path + + :return: The cert folder path + :rtype: string + """ + return self.__paths['cert']
    + +
    [docs] def get_lib_path(self): + """ + Returns lib path + + :return: The library folder path + :rtype: string + """ + return self.__paths['lib']
    + +
    [docs] def get_schemas_path(self): + """ + Returns schema path + + :return: The schema folder path + :rtype: string + """ + return self.__paths['lib'] + 'schemas/'
    + + def __load_settings_from_dict(self, settings): + """ + Loads settings info from a settings Dict + + :param settings: SAML Toolkit Settings + :type settings: dict + + :returns: True if the settings info is valid + :rtype: boolean + """ + errors = self.check_settings(settings) + if len(errors) == 0: + self.__errors = [] + self.__sp = settings['sp'] + + self.__idp = settings.get('idp', {}) + self.__strict = settings.get('strict', True) + self.__debug = settings.get('debug', False) + self.__security = settings.get('security', {}) + self.__contacts = settings.get('contactPerson', {}) + self.__organization = settings.get('organization', {}) + + self.__add_default_values() + return True + + self.__errors = errors + return False + + def __load_settings_from_file(self): + """ + Loads settings info from the settings json file + + :returns: True if the settings info is valid + :rtype: boolean + """ + filename = self.get_base_path() + 'settings.json' + + if not exists(filename): + raise OneLogin_Saml2_Error( + 'Settings file not found: %s', + OneLogin_Saml2_Error.SETTINGS_FILE_NOT_FOUND, + filename + ) + + # In the php toolkit instead of being a json file it is a php file and + # it is directly included + json_data = open(filename, 'r') + settings = json.load(json_data) + json_data.close() + + advanced_filename = self.get_base_path() + 'advanced_settings.json' + if exists(advanced_filename): + json_data = open(advanced_filename, 'r') + settings.update(json.load(json_data)) # Merge settings + json_data.close() + + return self.__load_settings_from_dict(settings) + + def __add_default_values(self): + """ + Add default values if the settings info is not complete + """ + self.__sp.setdefault('assertionConsumerService', {}) + self.__sp['assertionConsumerService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_POST) + + self.__sp.setdefault('attributeConsumingService', {}) + + self.__sp.setdefault('singleLogoutService', {}) + self.__sp['singleLogoutService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT) + + self.__idp.setdefault('singleLogoutService', {}) + + # Related to nameID + self.__sp.setdefault('NameIDFormat', OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED) + self.__security.setdefault('nameIdEncrypted', False) + + # Metadata format + self.__security.setdefault('metadataValidUntil', None) # None means use default + self.__security.setdefault('metadataCacheDuration', None) # None means use default + + # Sign provided + self.__security.setdefault('authnRequestsSigned', False) + self.__security.setdefault('logoutRequestSigned', False) + self.__security.setdefault('logoutResponseSigned', False) + self.__security.setdefault('signMetadata', False) + + # Sign expected + self.__security.setdefault('wantMessagesSigned', False) + self.__security.setdefault('wantAssertionsSigned', False) + + # NameID element expected + self.__security.setdefault('wantNameId', True) + + # SAML responses with a InResponseTo attribute not rejected when requestId not passed + self.__security.setdefault('rejectUnsolicitedResponsesWithInResponseTo', False) + + # Encrypt expected + self.__security.setdefault('wantAssertionsEncrypted', False) + self.__security.setdefault('wantNameIdEncrypted', False) + + # Signature Algorithm + self.__security.setdefault('signatureAlgorithm', OneLogin_Saml2_Constants.RSA_SHA256) + + # Digest Algorithm + self.__security.setdefault('digestAlgorithm', OneLogin_Saml2_Constants.SHA256) + + # Reject Deprecated Algorithms + self.__security.setdefault('rejectDeprecatedAlgorithm', False) + + # AttributeStatement required by default + self.__security.setdefault('wantAttributeStatement', True) + + self.__idp.setdefault('x509cert', '') + self.__idp.setdefault('certFingerprint', '') + self.__idp.setdefault('certFingerprintAlgorithm', 'sha1') + + self.__sp.setdefault('x509cert', '') + self.__sp.setdefault('privateKey', '') + + self.__security.setdefault('requestedAuthnContext', True) + self.__security.setdefault('requestedAuthnContextComparison', 'exact') + self.__security.setdefault('failOnAuthnContextMismatch', False) + +
    [docs] def check_settings(self, settings): + """ + Checks the settings info. + + :param settings: Dict with settings data + :type settings: dict + + :returns: Errors found on the settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or len(settings) == 0: + errors.append('invalid_syntax') + else: + if not self.__sp_validation_only: + errors += self.check_idp_settings(settings) + sp_errors = self.check_sp_settings(settings) + errors += sp_errors + + return errors
    + +
    [docs] def check_idp_settings(self, settings): + """ + Checks the IdP settings info. + + :param settings: Dict with settings data + :type settings: dict + + :returns: Errors found on the IdP settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or len(settings) == 0: + errors.append('invalid_syntax') + else: + if not settings.get('idp'): + errors.append('idp_not_found') + else: + allow_single_domain_urls = self._get_allow_single_label_domain(settings) + idp = settings['idp'] + if not idp.get('entityId'): + errors.append('idp_entityId_not_found') + + if not idp.get('singleSignOnService', {}).get('url'): + errors.append('idp_sso_not_found') + elif not validate_url(idp['singleSignOnService']['url'], allow_single_domain_urls): + errors.append('idp_sso_url_invalid') + + slo_url = idp.get('singleLogoutService', {}).get('url') + if slo_url and not validate_url(slo_url, allow_single_domain_urls): + errors.append('idp_slo_url_invalid') + + if 'security' in settings: + security = settings['security'] + + exists_x509 = bool(idp.get('x509cert')) + exists_fingerprint = bool(idp.get('certFingerprint')) + + exists_multix509sign = 'x509certMulti' in idp and \ + 'signing' in idp['x509certMulti'] and \ + idp['x509certMulti']['signing'] + exists_multix509enc = 'x509certMulti' in idp and \ + 'encryption' in idp['x509certMulti'] and \ + idp['x509certMulti']['encryption'] + + want_assert_sign = bool(security.get('wantAssertionsSigned')) + want_mes_signed = bool(security.get('wantMessagesSigned')) + nameid_enc = bool(security.get('nameIdEncrypted')) + + if (want_assert_sign or want_mes_signed) and \ + not(exists_x509 or exists_fingerprint or exists_multix509sign): + errors.append('idp_cert_or_fingerprint_not_found_and_required') + if nameid_enc and not (exists_x509 or exists_multix509enc): + errors.append('idp_cert_not_found_and_required') + + return errors
    + +
    [docs] def check_sp_settings(self, settings): + """ + Checks the SP settings info. + + :param settings: Dict with settings data + :type settings: dict + + :returns: Errors found on the SP settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or not settings: + errors.append('invalid_syntax') + else: + if not settings.get('sp'): + errors.append('sp_not_found') + else: + allow_single_domain_urls = self._get_allow_single_label_domain(settings) + # check_sp_certs uses self.__sp so I add it + old_sp = self.__sp + self.__sp = settings['sp'] + + sp = settings['sp'] + security = settings.get('security', {}) + + if not sp.get('entityId'): + errors.append('sp_entityId_not_found') + + if not sp.get('assertionConsumerService', {}).get('url'): + errors.append('sp_acs_not_found') + elif not validate_url(sp['assertionConsumerService']['url'], allow_single_domain_urls): + errors.append('sp_acs_url_invalid') + + if sp.get('attributeConsumingService'): + attributeConsumingService = sp['attributeConsumingService'] + if 'serviceName' not in attributeConsumingService: + errors.append('sp_attributeConsumingService_serviceName_not_found') + elif not isinstance(attributeConsumingService['serviceName'], basestring): + errors.append('sp_attributeConsumingService_serviceName_type_invalid') + + if 'requestedAttributes' not in attributeConsumingService: + errors.append('sp_attributeConsumingService_requestedAttributes_not_found') + elif not isinstance(attributeConsumingService['requestedAttributes'], list): + errors.append('sp_attributeConsumingService_serviceName_type_invalid') + else: + for req_attrib in attributeConsumingService['requestedAttributes']: + if 'name' not in req_attrib: + errors.append('sp_attributeConsumingService_requestedAttributes_name_not_found') + if 'name' in req_attrib and not req_attrib['name'].strip(): + errors.append('sp_attributeConsumingService_requestedAttributes_name_invalid') + if 'attributeValue' in req_attrib and type(req_attrib['attributeValue']) != list: + errors.append('sp_attributeConsumingService_requestedAttributes_attributeValue_type_invalid') + if 'isRequired' in req_attrib and type(req_attrib['isRequired']) != bool: + errors.append('sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid') + + if "serviceDescription" in attributeConsumingService and not isinstance(attributeConsumingService['serviceDescription'], basestring): + errors.append('sp_attributeConsumingService_serviceDescription_type_invalid') + + slo_url = sp.get('singleLogoutService', {}).get('url') + if slo_url and not validate_url(slo_url, allow_single_domain_urls): + errors.append('sp_sls_url_invalid') + + if 'signMetadata' in security and isinstance(security['signMetadata'], dict): + if 'keyFileName' not in security['signMetadata'] or \ + 'certFileName' not in security['signMetadata']: + errors.append('sp_signMetadata_invalid') + + authn_sign = bool(security.get('authnRequestsSigned')) + logout_req_sign = bool(security.get('logoutRequestSigned')) + logout_res_sign = bool(security.get('logoutResponseSigned')) + want_assert_enc = bool(security.get('wantAssertionsEncrypted')) + want_nameid_enc = bool(security.get('wantNameIdEncrypted')) + + if not self.check_sp_certs(): + if authn_sign or logout_req_sign or logout_res_sign or \ + want_assert_enc or want_nameid_enc: + errors.append('sp_cert_not_found_and_required') + + if 'contactPerson' in settings: + types = settings['contactPerson'].keys() + valid_types = ['technical', 'support', 'administrative', 'billing', 'other'] + for c_type in types: + if c_type not in valid_types: + errors.append('contact_type_invalid') + break + + for c_type in settings['contactPerson']: + contact = settings['contactPerson'][c_type] + if ('givenName' not in contact or len(contact['givenName']) == 0) or \ + ('emailAddress' not in contact or len(contact['emailAddress']) == 0): + errors.append('contact_not_enough_data') + break + + if 'organization' in settings: + for org in settings['organization']: + organization = settings['organization'][org] + if ('name' not in organization or len(organization['name']) == 0) or \ + ('displayname' not in organization or len(organization['displayname']) == 0) or \ + ('url' not in organization or len(organization['url']) == 0): + errors.append('organization_not_enough_data') + break + # Restores the value that had the self.__sp + if 'old_sp' in locals(): + self.__sp = old_sp + + return errors
    + +
    [docs] def check_sp_certs(self): + """ + Checks if the x509 certs of the SP exists and are valid. + + :returns: If the x509 certs of the SP exists and are valid + :rtype: boolean + """ + key = self.get_sp_key() + cert = self.get_sp_cert() + return key is not None and cert is not None
    + +
    [docs] def get_idp_sso_url(self): + """ + Gets the IdP SSO URL. + + :returns: An URL, the SSO endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + return idp_data['singleSignOnService']['url']
    + +
    [docs] def get_idp_slo_url(self): + """ + Gets the IdP SLO URL. + + :returns: An URL, the SLO endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + if 'url' in idp_data['singleLogoutService']: + return idp_data['singleLogoutService']['url']
    + +
    [docs] def get_idp_slo_response_url(self): + """ + Gets the IdP SLO return URL for IdP-initiated logout. + + :returns: an URL, the SLO return endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + if 'url' in idp_data['singleLogoutService']: + return idp_data['singleLogoutService'].get('responseUrl', self.get_idp_slo_url())
    + +
    [docs] def get_sp_key(self): + """ + Returns the x509 private key of the SP. + + :returns: SP private key + :rtype: string or None + """ + key = self.__sp.get('privateKey') + key_file_name = self.__paths['cert'] + 'sp.key' + + if not key and exists(key_file_name): + with open(key_file_name) as f: + key = f.read() + + return key or None
    + +
    [docs] def get_sp_cert(self): + """ + Returns the x509 public cert of the SP. + + :returns: SP public cert + :rtype: string or None + """ + cert = self.__sp.get('x509cert') + cert_file_name = self.__paths['cert'] + 'sp.crt' + + if not cert and exists(cert_file_name): + with open(cert_file_name) as f: + cert = f.read() + + return cert or None
    + +
    [docs] def get_sp_cert_new(self): + """ + Returns the x509 public of the SP planned + to be used soon instead the other public cert + + :returns: SP public cert new + :rtype: string or None + """ + cert = self.__sp.get('x509certNew') + cert_file_name = self.__paths['cert'] + 'sp_new.crt' + + if not cert and exists(cert_file_name): + with open(cert_file_name) as f: + cert = f.read() + + return cert or None
    + +
    [docs] def get_idp_cert(self): + """ + Returns the x509 public cert of the IdP. + + :returns: IdP public cert + :rtype: string + """ + return self.__idp.get('x509cert')
    + +
    [docs] def get_idp_data(self): + """ + Gets the IdP data. + + :returns: IdP info + :rtype: dict + """ + return self.__idp
    + +
    [docs] def get_sp_data(self): + """ + Gets the SP data. + + :returns: SP info + :rtype: dict + """ + return self.__sp
    + +
    [docs] def get_security_data(self): + """ + Gets security data. + + :returns: Security info + :rtype: dict + """ + return self.__security
    + +
    [docs] def get_contacts(self): + """ + Gets contact data. + + :returns: Contacts info + :rtype: dict + """ + return self.__contacts
    + +
    [docs] def get_organization(self): + """ + Gets organization data. + + :returns: Organization info + :rtype: dict + """ + return self.__organization
    + +
    [docs] def get_sp_metadata(self): + """ + Gets the SP metadata. The XML representation. + + :returns: SP metadata (xml) + :rtype: string + """ + metadata = OneLogin_Saml2_Metadata.builder( + self.__sp, self.__security['authnRequestsSigned'], + self.__security['wantAssertionsSigned'], + self.__security['metadataValidUntil'], + self.__security['metadataCacheDuration'], + self.get_contacts(), self.get_organization() + ) + + add_encryption = self.__security['wantNameIdEncrypted'] or self.__security['wantAssertionsEncrypted'] + + cert_new = self.get_sp_cert_new() + metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert_new, add_encryption) + + cert = self.get_sp_cert() + metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert, add_encryption) + + # Sign metadata + if 'signMetadata' in self.__security and self.__security['signMetadata'] is not False: + if self.__security['signMetadata'] is True: + # Use the SP's normal key to sign the metadata: + if not cert: + raise OneLogin_Saml2_Error( + 'Cannot sign metadata: missing SP public key certificate.', + OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND + ) + cert_metadata = cert + key_metadata = self.get_sp_key() + if not key_metadata: + raise OneLogin_Saml2_Error( + 'Cannot sign metadata: missing SP private key.', + OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND + ) + else: + # Use a custom key to sign the metadata: + if ('keyFileName' not in self.__security['signMetadata'] or + 'certFileName' not in self.__security['signMetadata']): + raise OneLogin_Saml2_Error( + 'Invalid Setting: signMetadata value of the sp is not valid', + OneLogin_Saml2_Error.SETTINGS_INVALID_SYNTAX + ) + key_file_name = self.__security['signMetadata']['keyFileName'] + cert_file_name = self.__security['signMetadata']['certFileName'] + key_metadata_file = self.__paths['cert'] + key_file_name + cert_metadata_file = self.__paths['cert'] + cert_file_name + + try: + with open(key_metadata_file, 'r') as f_metadata_key: + key_metadata = f_metadata_key.read() + except IOError: + raise OneLogin_Saml2_Error( + 'Private key file not readable: %s', + OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND, + key_metadata_file + ) + + try: + with open(cert_metadata_file, 'r') as f_metadata_cert: + cert_metadata = f_metadata_cert.read() + except IOError: + raise OneLogin_Saml2_Error( + 'Public cert file not readable: %s', + OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND, + cert_metadata_file + ) + + signature_algorithm = self.__security['signatureAlgorithm'] + digest_algorithm = self.__security['digestAlgorithm'] + + metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key_metadata, cert_metadata, signature_algorithm, digest_algorithm) + + return metadata
    + +
    [docs] def validate_metadata(self, xml): + """ + Validates an XML SP Metadata. + + :param xml: Metadata's XML that will be validate + :type xml: string + + :returns: The list of found errors + :rtype: list + """ + + assert isinstance(xml, basestring) + + if len(xml) == 0: + raise Exception('Empty string supplied as input') + + errors = [] + res = OneLogin_Saml2_Utils.validate_xml(xml, 'saml-schema-metadata-2.0.xsd', self.__debug) + if not isinstance(res, Document): + errors.append(res) + else: + dom = res + element = dom.documentElement + if element.tagName not in 'md:EntityDescriptor': + errors.append('noEntityDescriptor_xml') + else: + if len(element.getElementsByTagName('md:SPSSODescriptor')) != 1: + errors.append('onlySPSSODescriptor_allowed_xml') + else: + valid_until = cache_duration = expire_time = None + + if element.hasAttribute('validUntil'): + valid_until = OneLogin_Saml2_Utils.parse_SAML_to_time(element.getAttribute('validUntil')) + if element.hasAttribute('cacheDuration'): + cache_duration = element.getAttribute('cacheDuration') + + expire_time = OneLogin_Saml2_Utils.get_expire_time(cache_duration, valid_until) + if expire_time is not None and int(time()) > int(expire_time): + errors.append('expired_xml') + + # TODO: Validate Sign + + return errors
    + +
    [docs] def format_idp_cert(self): + """ + Formats the IdP cert. + """ + self.__idp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509cert'])
    + +
    [docs] def format_idp_cert_multi(self): + """ + Formats the Multple IdP certs. + """ + if 'x509certMulti' in self.__idp: + if 'signing' in self.__idp['x509certMulti']: + for idx in range(len(self.__idp['x509certMulti']['signing'])): + self.__idp['x509certMulti']['signing'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['signing'][idx]) + + if 'encryption' in self.__idp['x509certMulti']: + for idx in range(len(self.__idp['x509certMulti']['encryption'])): + self.__idp['x509certMulti']['encryption'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['encryption'][idx])
    + +
    [docs] def format_sp_cert(self): + """ + Formats the SP cert. + """ + self.__sp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__sp['x509cert'])
    + +
    [docs] def format_sp_cert_new(self): + """ + Formats the SP cert. + """ + self.__sp['x509certNew'] = OneLogin_Saml2_Utils.format_cert(self.__sp['x509certNew'])
    + +
    [docs] def format_sp_key(self): + """ + Formats the private key. + """ + self.__sp['privateKey'] = OneLogin_Saml2_Utils.format_private_key(self.__sp['privateKey'])
    + +
    [docs] def get_errors(self): + """ + Returns an array with the errors, the array is empty when the settings is ok. + + :returns: Errors + :rtype: list + """ + return self.__errors
    + +
    [docs] def set_strict(self, value): + """ + Activates or deactivates the strict mode. + + :param value: Strict parameter + :type xml: boolean + """ + assert isinstance(value, bool) + + self.__strict = value
    + +
    [docs] def is_strict(self): + """ + Returns if the 'strict' mode is active. + + :returns: Strict parameter + :rtype: boolean + """ + return self.__strict
    + +
    [docs] def is_debug_active(self): + """ + Returns if the debug is active. + + :returns: Debug parameter + :rtype: boolean + """ + return self.__debug
    + + def _get_allow_single_label_domain(self, settings): + security = settings.get('security', {}) + return 'allowSingleLabelDomains' in security.keys() and security['allowSingleLabelDomains']
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/utils.html b/docs/saml2/_modules/onelogin/saml2/utils.html new file mode 100644 index 00000000..d9b7f442 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/utils.html @@ -0,0 +1,1402 @@ + + + + + + onelogin.saml2.utils — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    + +

    Source code for onelogin.saml2.utils

    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_Utils class
    +
    +MIT License
    +
    +Auxiliary class of Python Toolkit.
    +
    +"""
    +
    +import base64
    +from copy import deepcopy
    +from datetime import datetime
    +import calendar
    +from hashlib import sha1, sha256, sha384, sha512
    +from isodate import parse_duration as duration_parser
    +from lxml import etree
    +from os.path import basename, dirname, join
    +import re
    +from sys import stderr
    +from tempfile import NamedTemporaryFile
    +from textwrap import wrap
    +from urllib import quote_plus
    +from urlparse import urlsplit, urlunsplit
    +from uuid import uuid4
    +from xml.dom.minidom import Document, Element
    +from defusedxml.minidom import parseString
    +from functools import wraps
    +
    +import zlib
    +
    +import dm.xmlsec.binding as xmlsec
    +from dm.xmlsec.binding.tmpl import EncData, Signature
    +
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
    +from onelogin.saml2.xmlparser import tostring, fromstring
    +
    +
    +if not globals().get('xmlsec_setup', False):
    +    xmlsec.initialize()
    +    globals()['xmlsec_setup'] = True
    +
    +
    +
    [docs]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
    + + + + + +
    [docs]class OneLogin_Saml2_Utils(object): + """ + + Auxiliary class that contains several utility methods to parse time, + urls, add sign, encrypt, decrypt, sign validation, handle xml ... + + """ + + RESPONSE_SIGNATURE_XPATH = '/samlp:Response/ds:Signature' + ASSERTION_SIGNATURE_XPATH = '/samlp:Response/saml:Assertion/ds:Signature' + + TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" + TIME_FORMAT_2 = "%Y-%m-%dT%H:%M:%S.%fZ" + TIME_FORMAT_WITH_FRAGMENT = re.compile(r'^(\d{4,4}-\d{2,2}-\d{2,2}T\d{2,2}:\d{2,2}:\d{2,2})(\.\d*)?Z?$') + +
    [docs] @staticmethod + def decode_base64_and_inflate(value): + """ + base64 decodes and then inflates according to RFC1951 + :param value: a deflated and encoded string + :type value: string + :returns: the string after decoding and inflating + :rtype: string + """ + decoded = base64.b64decode(value) + # We try to inflate + try: + result = zlib.decompress(decoded, -15) + except Exception: + result = decoded + + return result.decode('utf-8')
    + +
    [docs] @staticmethod + def deflate_and_base64_encode(value): + """ + Deflates and then base64 encodes a string + :param value: The string to deflate and encode + :type value: string + :returns: The deflated and encoded string + :rtype: string + """ + return base64.b64encode(zlib.compress(value.encode('utf-8'))[2:-4])
    + +
    [docs] @staticmethod + def validate_xml(xml, schema, debug=False): + """ + Validates a xml against a schema + :param xml: The xml that will be validated + :type: string|DomDocument + :param schema: The schema + :type: string + :param debug: If debug is active, the parse-errors will be showed + :type: bool + :returns: Error code or the DomDocument of the xml + :rtype: string + """ + assert isinstance(xml, basestring) or isinstance(xml, Document) or isinstance(xml, etree._Element) + assert isinstance(schema, basestring) + + if isinstance(xml, Document): + xml = xml.toxml() + elif isinstance(xml, etree._Element): + xml = tostring(xml, encoding='unicode') + + # Switch to lxml for schema validation + try: + dom = fromstring(xml.encode('utf-8'), forbid_dtd=True) + except Exception: + return 'unloaded_xml' + + schema_file = join(dirname(__file__), 'schemas', schema) + f_schema = open(schema_file, 'r') + schema_doc = etree.parse(f_schema) + f_schema.close() + xmlschema = etree.XMLSchema(schema_doc) + + if not xmlschema.validate(dom): + if debug: + stderr.write('Errors validating the metadata') + stderr.write(':\n\n') + for error in xmlschema.error_log: + stderr.write('%s\n' % error.message) + + return 'invalid_xml' + + return parseString(tostring(dom, encoding='unicode').encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True)
    + +
    [docs] @staticmethod + def element_text(node): + # Double check, the LXML Parser already removes comments + etree.strip_tags(node, etree.Comment) + return node.text
    + +
    [docs] @staticmethod + def format_cert(cert, heads=True): + """ + Returns a x509 cert (adding header & footer if required). + + :param cert: A x509 unformatted cert + :type: string + + :param heads: True if we want to include head and footer + :type: boolean + + :returns: Formatted cert + :rtype: string + """ + x509_cert = cert.replace('\x0D', '') + x509_cert = x509_cert.replace('\r', '') + x509_cert = x509_cert.replace('\n', '') + if len(x509_cert) > 0: + x509_cert = x509_cert.replace('-----BEGIN CERTIFICATE-----', '') + x509_cert = x509_cert.replace('-----END CERTIFICATE-----', '') + x509_cert = x509_cert.replace(' ', '') + + if heads: + x509_cert = "-----BEGIN CERTIFICATE-----\n" + "\n".join(wrap(x509_cert, 64)) + "\n-----END CERTIFICATE-----\n" + + return x509_cert
    + +
    [docs] @staticmethod + def format_private_key(key, heads=True): + """ + Returns a private key (adding header & footer if required). + + :param key A private key + :type: string + + :param heads: True if we want to include head and footer + :type: boolean + + :returns: Formatted private key + :rtype: string + """ + private_key = key.replace('\x0D', '') + private_key = private_key.replace('\r', '') + private_key = private_key.replace('\n', '') + if len(private_key) > 0: + if private_key.find('-----BEGIN PRIVATE KEY-----') != -1: + private_key = private_key.replace('-----BEGIN PRIVATE KEY-----', '') + private_key = private_key.replace('-----END PRIVATE KEY-----', '') + private_key = private_key.replace(' ', '') + if heads: + private_key = "-----BEGIN PRIVATE KEY-----\n" + "\n".join(wrap(private_key, 64)) + "\n-----END PRIVATE KEY-----\n" + else: + private_key = private_key.replace('-----BEGIN RSA PRIVATE KEY-----', '') + private_key = private_key.replace('-----END RSA PRIVATE KEY-----', '') + private_key = private_key.replace(' ', '') + if heads: + private_key = "-----BEGIN RSA PRIVATE KEY-----\n" + "\n".join(wrap(private_key, 64)) + "\n-----END RSA PRIVATE KEY-----\n" + return private_key
    + +
    [docs] @staticmethod + def redirect(url, parameters={}, request_data={}): + """ + Executes a redirection to the provided url (or return the target url). + + :param url: The target url + :type: string + + :param parameters: Extra parameters to be passed as part of the url + :type: dict + + :param request_data: The request as a dict + :type: dict + + :returns: Url + :rtype: string + """ + assert isinstance(url, basestring) + assert isinstance(parameters, dict) + + if url.startswith('/'): + url = '%s%s' % (OneLogin_Saml2_Utils.get_self_url_host(request_data), url) + + # Verify that the URL is to a http or https site. + if re.search('^https?://', url) is None: + raise OneLogin_Saml2_Error( + 'Redirect to invalid URL: ' + url, + OneLogin_Saml2_Error.REDIRECT_INVALID_URL + ) + + # Add encoded parameters + if url.find('?') < 0: + param_prefix = '?' + else: + param_prefix = '&' + + for name, value in parameters.items(): + + if value is None: + param = quote_plus(name) + elif isinstance(value, list): + param = '' + for val in value: + param += quote_plus(name) + '[]=' + quote_plus(val) + '&' + if len(param) > 0: + param = param[0:-1] + else: + param = quote_plus(name) + '=' + quote_plus(value) + + if param: + url += param_prefix + param + param_prefix = '&' + + return url
    + +
    [docs] @staticmethod + def get_self_url_host(request_data): + """ + Returns the protocol + the current host + the port (if different than + common ports). + + :param request_data: The request as a dict + :type: dict + + :return: Url + :rtype: string + """ + current_host = OneLogin_Saml2_Utils.get_self_host(request_data) + port = '' + if OneLogin_Saml2_Utils.is_https(request_data): + protocol = 'https' + else: + protocol = 'http' + + if 'server_port' in request_data and request_data['server_port'] is not None: + port_number = str(request_data['server_port']) + port = ':' + port_number + + if protocol == 'http' and port_number == '80': + port = '' + elif protocol == 'https' and port_number == '443': + port = '' + + return '%s://%s%s' % (protocol, current_host, port)
    + +
    [docs] @staticmethod + def get_self_host(request_data): + """ + Returns the current host. + + :param request_data: The request as a dict + :type: dict + + :return: The current host + :rtype: string + """ + if 'http_host' in request_data: + current_host = request_data['http_host'] + elif 'server_name' in request_data: + current_host = request_data['server_name'] + else: + raise Exception('No hostname defined') + + if ':' in current_host: + current_host_data = current_host.split(':') + possible_port = current_host_data[-1] + try: + possible_port = float(possible_port) + current_host = current_host_data[0] + except ValueError: + current_host = ':'.join(current_host_data) + + return current_host
    + +
    [docs] @staticmethod + def is_https(request_data): + """ + Checks if https or http. + + :param request_data: The request as a dict + :type: dict + + :return: False if https is not active + :rtype: boolean + """ + is_https = 'https' in request_data and request_data['https'] != 'off' + is_https = is_https or ('server_port' in request_data and str(request_data['server_port']) == '443') + return is_https
    + +
    [docs] @staticmethod + def get_self_url_no_query(request_data): + """ + Returns the URL of the current host + current view. + + :param request_data: The request as a dict + :type: dict + + :return: The url of current host + current view + :rtype: string + """ + self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) + script_name = request_data['script_name'] + if script_name: + if script_name[0] != '/': + script_name = '/' + script_name + else: + script_name = '' + self_url_no_query = self_url_host + script_name + if 'path_info' in request_data: + self_url_no_query += request_data['path_info'] + + return self_url_no_query
    + +
    [docs] @staticmethod + def get_self_routed_url_no_query(request_data): + """ + Returns the routed URL of the current host + current view. + + :param request_data: The request as a dict + :type: dict + + :return: The url of current host + current view + :rtype: string + """ + self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) + route = '' + if 'request_uri' in request_data.keys() and request_data['request_uri']: + route = request_data['request_uri'] + if 'query_string' in request_data.keys() and request_data['query_string']: + route = route.replace(request_data['query_string'], '') + + return self_url_host + route
    + +
    [docs] @staticmethod + def get_self_url(request_data): + """ + Returns the URL of the current host + current view + query. + + :param request_data: The request as a dict + :type: dict + + :return: The url of current host + current view + query + :rtype: string + """ + self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) + + request_uri = '' + if 'request_uri' in request_data: + request_uri = request_data['request_uri'] + if not request_uri.startswith('/'): + match = re.search('^https?://[^/]*(/.*)', request_uri) + if match is not None: + request_uri = match.groups()[0] + + return self_url_host + request_uri
    + +
    [docs] @staticmethod + def generate_unique_id(): + """ + Generates an unique string (used for example as ID for assertions). + + :return: A unique string + :rtype: string + """ + return 'ONELOGIN_%s' % sha1(uuid4().hex).hexdigest()
    + +
    [docs] @staticmethod + def parse_time_to_SAML(time): + r""" + Converts a UNIX timestamp to SAML2 timestamp on the form + yyyy-mm-ddThh:mm:ss(\.s+)?Z. + + :param time: The time we should convert (DateTime). + :type: string + + :return: SAML2 timestamp. + :rtype: string + """ + data = datetime.utcfromtimestamp(float(time)) + return data.strftime(OneLogin_Saml2_Utils.TIME_FORMAT)
    + +
    [docs] @staticmethod + def parse_SAML_to_time(timestr): + r""" + Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(\.s+)?Z + to a UNIX timestamp. The sub-second part is ignored. + + :param time: The time we should convert (SAML Timestamp). + :type: string + + :return: Converted to a unix timestamp. + :rtype: int + """ + try: + data = datetime.strptime(timestr, OneLogin_Saml2_Utils.TIME_FORMAT) + except ValueError: + try: + data = datetime.strptime(timestr, OneLogin_Saml2_Utils.TIME_FORMAT_2) + except ValueError: + elem = OneLogin_Saml2_Utils.TIME_FORMAT_WITH_FRAGMENT.match(timestr) + if not elem: + raise Exception("time data %s does not match format %s" % (timestr, r'yyyy-mm-ddThh:mm:ss(\.s+)?Z')) + data = datetime.strptime(elem.groups()[0] + "Z", OneLogin_Saml2_Utils.TIME_FORMAT) + + return calendar.timegm(data.utctimetuple())
    + +
    [docs] @staticmethod + def now(): + """ + :return: unix timestamp of actual time. + :rtype: int + """ + return calendar.timegm(datetime.utcnow().utctimetuple())
    + +
    [docs] @staticmethod + def parse_duration(duration, timestamp=None): + """ + Interprets a ISO8601 duration value relative to a given timestamp. + + :param duration: The duration, as a string. + :type: string + + :param timestamp: The unix timestamp we should apply the duration to. + Optional, default to the current time. + :type: string + + :return: The new timestamp, after the duration is applied. + :rtype: int + """ + assert isinstance(duration, basestring) + assert timestamp is None or isinstance(timestamp, int) + + timedelta = duration_parser(duration) + if timestamp is None: + data = datetime.utcnow() + timedelta + else: + data = datetime.utcfromtimestamp(timestamp) + timedelta + return calendar.timegm(data.utctimetuple())
    + +
    [docs] @staticmethod + def get_expire_time(cache_duration=None, valid_until=None): + """ + Compares 2 dates and returns the earliest. + + :param cache_duration: The duration, as a string. + :type: string + + :param valid_until: The valid until date, as a string or as a timestamp + :type: string + + :return: The expiration time. + :rtype: int + """ + expire_time = None + + if cache_duration is not None: + expire_time = OneLogin_Saml2_Utils.parse_duration(cache_duration) + + if valid_until is not None: + if isinstance(valid_until, int): + valid_until_time = valid_until + else: + valid_until_time = OneLogin_Saml2_Utils.parse_SAML_to_time(valid_until) + if expire_time is None or expire_time > valid_until_time: + expire_time = valid_until_time + + if expire_time is not None: + return '%d' % expire_time + return None
    + +
    [docs] @staticmethod + def query(dom, query, context=None, tagid=None): + """ + Extracts nodes that match the query from the Element + + :param dom: The root of the lxml objet + :type: Element + + :param query: Xpath Expresion + :type: string + + :param context: Context Node + :type: DOMElement + + :param tagid: Tag ID + :type: string + + :returns: The queried nodes + :rtype: list + """ + if context is None: + source = dom + else: + source = context + + if tagid is None: + return source.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) + else: + return source.xpath(query, tagid=tagid, namespaces=OneLogin_Saml2_Constants.NSMAP)
    + +
    [docs] @staticmethod + def delete_local_session(callback=None): + """ + Deletes the local session. + """ + + if callback is not None: + callback()
    + +
    [docs] @staticmethod + def calculate_x509_fingerprint(x509_cert, alg='sha1'): + """ + Calculates the fingerprint of a formatted x509cert. + + :param x509_cert: x509 cert formatted + :type: string + + :param alg: The algorithm to build the fingerprint + :type: string + + :returns: fingerprint + :rtype: string + """ + assert isinstance(x509_cert, basestring) + + lines = x509_cert.split('\n') + data = '' + inData = False + + for line in lines: + # Remove '\r' from end of line if present. + line = line.rstrip() + if not inData: + if line == '-----BEGIN CERTIFICATE-----': + inData = True + elif line == '-----BEGIN PUBLIC KEY-----' or line == '-----BEGIN RSA PRIVATE KEY-----': + # This isn't an X509 certificate. + return None + else: + if line == '-----END CERTIFICATE-----': + break + + # Append the current line to the certificate data. + data += line + + if not data: + return None + + decoded_data = base64.b64decode(data) + + if alg == 'sha512': + fingerprint = sha512(decoded_data) + elif alg == 'sha384': + fingerprint = sha384(decoded_data) + elif alg == 'sha256': + fingerprint = sha256(decoded_data) + else: + fingerprint = sha1(decoded_data) + + return fingerprint.hexdigest().lower()
    + +
    [docs] @staticmethod + def format_finger_print(fingerprint): + """ + Formats a fingerprint. + + :param fingerprint: fingerprint + :type: string + + :returns: Formatted fingerprint + :rtype: string + """ + formated_fingerprint = fingerprint.replace(':', '') + return formated_fingerprint.lower()
    + +
    [docs] @staticmethod + def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=None): + """ + Generates a nameID. + + :param value: fingerprint + :type: string + + :param sp_nq: SP Name Qualifier + :type: string + + :param sp_format: SP Format + :type: string + + :param cert: IdP Public Cert to encrypt the nameID + :type: string + + :param debug: Activate the xmlsec debug + :type: bool + + :param nq: IDP Name Qualifier + :type: string + + :returns: DOMElement | XMLSec nameID + :rtype: string + """ + doc = Document() + name_id_container = doc.createElementNS(OneLogin_Saml2_Constants.NS_SAML, 'container') + name_id_container.setAttribute("xmlns:saml", OneLogin_Saml2_Constants.NS_SAML) + + name_id = doc.createElement('saml:NameID') + if sp_nq is not None: + name_id.setAttribute('SPNameQualifier', sp_nq) + if nq is not None: + name_id.setAttribute('NameQualifier', nq) + if sp_format is not None: + name_id.setAttribute('Format', sp_format) + name_id.appendChild(doc.createTextNode(value)) + name_id_container.appendChild(name_id) + + if cert is not None: + xml = name_id_container.toxml() + elem = fromstring(xml, forbid_dtd=True) + + error_callback_method = None + if debug: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + # Load the public cert + mngr = xmlsec.KeysMngr() + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + key_data = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + key_data.name = basename(file_cert.name) + mngr.addKey(key_data) + file_cert.close() + + # Prepare for encryption + enc_data = EncData(xmlsec.TransformAes128Cbc, type=xmlsec.TypeEncElement) + enc_data.ensureCipherValue() + key_info = enc_data.ensureKeyInfo() + # enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaPkcs1) + enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaOaep) + enc_key.ensureCipherValue() + + # Encrypt! + enc_ctx = xmlsec.EncCtx(mngr) + enc_ctx.encKey = xmlsec.Key.generate(xmlsec.KeyDataAes, 128, xmlsec.KeyDataTypeSession) + + edata = enc_ctx.encryptXml(enc_data, elem[0]) + + newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True) + + if newdoc.hasChildNodes(): + child = newdoc.firstChild + child.removeAttribute('xmlns') + child.removeAttribute('xmlns:saml') + child.setAttribute('xmlns:xenc', OneLogin_Saml2_Constants.NS_XENC) + child.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS) + + nodes = newdoc.getElementsByTagName("*") + for node in nodes: + if node.tagName == 'ns0:KeyInfo': + node.tagName = 'dsig:KeyInfo' + node.removeAttribute('xmlns:ns0') + node.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS) + else: + node.tagName = 'xenc:' + node.tagName + + encrypted_id = newdoc.createElement('saml:EncryptedID') + encrypted_data = newdoc.replaceChild(encrypted_id, newdoc.firstChild) + encrypted_id.appendChild(encrypted_data) + return newdoc.saveXML(encrypted_id) + else: + return doc.saveXML(name_id)
    + +
    [docs] @staticmethod + def get_status(dom): + """ + Gets Status from a Response. + + :param dom: The Response as XML + :type: Document + + :returns: The Status, an array with the code and a message. + :rtype: dict + """ + status = {} + + status_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status') + if len(status_entry) != 1: + raise OneLogin_Saml2_ValidationError( + 'Missing Status on response', + OneLogin_Saml2_ValidationError.MISSING_STATUS + ) + + code_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode', status_entry[0]) + if len(code_entry) != 1: + raise OneLogin_Saml2_ValidationError( + 'Missing Status Code on response', + OneLogin_Saml2_ValidationError.MISSING_STATUS_CODE + ) + code = code_entry[0].values()[0] + status['code'] = code + + status['msg'] = '' + message_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusMessage', status_entry[0]) + if len(message_entry) == 0: + subcode_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode', status_entry[0]) + if len(subcode_entry) == 1: + status['msg'] = subcode_entry[0].values()[0] + elif len(message_entry) == 1: + status['msg'] = OneLogin_Saml2_Utils.element_text(message_entry[0]) + + return status
    + +
    [docs] @staticmethod + def decrypt_element(encrypted_data, key, debug=False, inplace=False): + """ + Decrypts an encrypted element. + + :param encrypted_data: The encrypted data. + :type: lxml.etree.Element | DOMElement | basestring + + :param key: The key. + :type: string + + :param debug: Activate the xmlsec debug + :type: bool + + :param inplace: update passed data with decrypted result + :type: bool + + :returns: The decrypted element. + :rtype: lxml.etree.Element + """ + if isinstance(encrypted_data, Element): + encrypted_data = fromstring(str(encrypted_data.toxml()), forbid_dtd=True) + elif isinstance(encrypted_data, basestring): + encrypted_data = fromstring(str(encrypted_data), forbid_dtd=True) + elif not inplace and isinstance(encrypted_data, etree._Element): + encrypted_data = deepcopy(encrypted_data) + + error_callback_method = None + if debug: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + mngr = xmlsec.KeysMngr() + + key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + mngr.addKey(key) + enc_ctx = xmlsec.EncCtx(mngr) + + return enc_ctx.decrypt(encrypted_data)
    + +
    [docs] @staticmethod + def write_temp_file(content): + """ + Writes some content into a temporary file and returns it. + + :param content: The file content + :type: string + + :returns: The temporary file + :rtype: file-like object + """ + f_temp = NamedTemporaryFile(delete=True) + f_temp.file.write(content) + f_temp.file.flush() + return f_temp
    + +
    [docs] @staticmethod + def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA256, digest_algorithm=OneLogin_Saml2_Constants.SHA256): + """ + Adds signature key and senders certificate to an element (Message or + Assertion). + + :param xml: The element we should sign + :type: string | Document + + :param key: The private key + :type: string + + :param cert: The public + :type: string + + :param debug: Activate the xmlsec debug + :type: bool + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + + :param digest_algorithm: Digest algorithm method + :type digest_algorithm: string + + :returns: Signed XML + :rtype: string + """ + if xml is None or xml == '': + raise Exception('Empty string supplied as input') + elif isinstance(xml, etree._Element): + elem = xml + elif isinstance(xml, Document): + xml = xml.toxml() + elem = fromstring(xml.encode('utf-8'), forbid_dtd=True) + elif isinstance(xml, Element): + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_SAMLP), + 'xmlns:samlp', + unicode(OneLogin_Saml2_Constants.NS_SAMLP) + ) + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_SAML), + 'xmlns:saml', + unicode(OneLogin_Saml2_Constants.NS_SAML) + ) + xml = xml.toxml() + elem = fromstring(xml.encode('utf-8'), forbid_dtd=True) + elif isinstance(xml, basestring): + elem = fromstring(xml.encode('utf-8'), forbid_dtd=True) + else: + raise Exception('Error parsing xml string') + + error_callback_method = None + if debug: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + sign_algorithm_transform_map = { + OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1, + OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1, + OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256, + OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384, + OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512 + } + sign_algorithm_transform = sign_algorithm_transform_map.get(sign_algorithm, xmlsec.TransformRsaSha1) + + signature = Signature(xmlsec.TransformExclC14N, sign_algorithm_transform, nsPrefix='ds') + + issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer') + if len(issuer) > 0: + issuer = issuer[0] + issuer.addnext(signature) + elem_to_sign = issuer.getparent() + else: + entity_descriptor = OneLogin_Saml2_Utils.query(elem, '//md:EntityDescriptor') + if len(entity_descriptor) > 0: + elem.insert(0, signature) + else: + elem[0].insert(0, signature) + elem_to_sign = elem + + elem_id = elem_to_sign.get('ID', None) + if elem_id is not None: + if elem_id: + elem_id = '#' + elem_id + else: + generated_id = generated_id = OneLogin_Saml2_Utils.generate_unique_id() + elem_id = '#' + generated_id + elem_to_sign.attrib['ID'] = generated_id + + xmlsec.addIDs(elem_to_sign, ["ID"]) + + digest_algorithm_transform_map = { + OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1, + OneLogin_Saml2_Constants.SHA256: xmlsec.TransformSha256, + OneLogin_Saml2_Constants.SHA384: xmlsec.TransformSha384, + OneLogin_Saml2_Constants.SHA512: xmlsec.TransformSha512 + } + digest_algorithm_transform = digest_algorithm_transform_map.get(digest_algorithm, xmlsec.TransformSha1) + + ref = signature.addReference(digest_algorithm_transform) + if elem_id: + ref.attrib['URI'] = elem_id + + ref.addTransform(xmlsec.TransformEnveloped) + ref.addTransform(xmlsec.TransformExclC14N) + + key_info = signature.ensureKeyInfo() + key_info.addX509Data() + + dsig_ctx = xmlsec.DSigCtx() + sign_key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + sign_key.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem) + file_cert.close() + + dsig_ctx.signKey = sign_key + dsig_ctx.sign(signature) + + return tostring(elem, encoding='unicode').encode('utf-8')
    + +
    [docs] @staticmethod + @return_false_on_exception + def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None, multicerts=None): + """ + Validates a signature (Message or Assertion). + + :param xml: The element we should validate + :type: string | Document + + :param cert: The pubic cert + :type: string + + :param fingerprint: The fingerprint of the public cert + :type: string + + :param fingerprintalg: The algorithm used to build the fingerprint + :type: string + + :param validatecert: If true, will verify the signature and if the cert is valid. + :type: bool + + :param debug: Activate the xmlsec debug + :type: bool + + :param xpath: The xpath of the signed element + :type: string + + :param multicerts: Multiple public certs + :type: list + + :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') + elif isinstance(xml, etree._Element): + elem = xml + elif isinstance(xml, Document): + xml = xml.toxml() + elem = fromstring(str(xml), forbid_dtd=True) + elif isinstance(xml, Element): + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_SAMLP), + 'xmlns:samlp', + unicode(OneLogin_Saml2_Constants.NS_SAMLP) + ) + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_SAML), + 'xmlns:saml', + unicode(OneLogin_Saml2_Constants.NS_SAML) + ) + xml = xml.toxml() + elem = fromstring(str(xml), forbid_dtd=True) + elif isinstance(xml, basestring): + elem = fromstring(str(xml), forbid_dtd=True) + else: + raise Exception('Error parsing xml string') + + error_callback_method = None + if debug: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + xmlsec.addIDs(elem, ["ID"]) + + if xpath: + signature_nodes = OneLogin_Saml2_Utils.query(elem, xpath) + else: + signature_nodes = OneLogin_Saml2_Utils.query(elem, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH) + + if len(signature_nodes) == 0: + signature_nodes = OneLogin_Saml2_Utils.query(elem, OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH) + + if len(signature_nodes) == 1: + signature_node = signature_nodes[0] + + if not multicerts: + return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) + else: + # If multiple certs are provided, I may ignore cert and + # fingerprint provided by the method and just check the + # certs multicerts + fingerprint = fingerprintalg = None + for cert in multicerts: + if OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, False, raise_exceptions=False): + return True + raise OneLogin_Saml2_ValidationError('Signature validation failed. SAML Response rejected.') + else: + raise OneLogin_Saml2_ValidationError('Expected exactly one signature node; got {}.'.format(len(signature_nodes)), OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES)
    + +
    [docs] @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. + + :param xml: The element we should validate + :type: string | Document + + :param cert: The pubic cert + :type: string + + :param fingerprint: The fingerprint of the public cert + :type: string + + :param fingerprintalg: The algorithm used to build the fingerprint + :type: string + + :param validatecert: If true, will verify the signature and if the cert is valid. + :type: bool + + :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 + """ + if xml is None or xml == '': + raise Exception('Empty string supplied as input') + elif isinstance(xml, etree._Element): + elem = xml + elif isinstance(xml, Document): + xml = xml.toxml() + elem = fromstring(str(xml), forbid_dtd=True) + elif isinstance(xml, Element): + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_MD), + 'xmlns:md', + unicode(OneLogin_Saml2_Constants.NS_MD) + ) + xml = xml.toxml() + elem = fromstring(str(xml), forbid_dtd=True) + elif isinstance(xml, basestring): + elem = fromstring(str(xml), forbid_dtd=True) + else: + raise Exception('Error parsing xml string') + + error_callback_method = None + if debug: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + xmlsec.addIDs(elem, ["ID"]) + + signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature') + + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature') + + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature') + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature') + + if len(signature_nodes) > 0: + for signature_node in signature_nodes: + 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.')
    + +
    [docs] @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. + + :param signature_node: The signature node + :type: Node + + :param xml: The element we should validate + :type: Document + + :param cert: The public cert + :type: string + + :param fingerprint: The fingerprint of the public cert + :type: string + + :param fingerprintalg: The algorithm used to build the fingerprint + :type: string + + :param validatecert: If true, will verify the signature and if the cert is valid. + :type: bool + + :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 + """ + error_callback_method = None + if debug: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + xmlsec.addIDs(elem, ["ID"]) + + if (cert is None or cert == '') and fingerprint: + x509_certificate_nodes = OneLogin_Saml2_Utils.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 = OneLogin_Saml2_Utils.element_text(x509_certificate_node) + x509_cert_value_formatted = OneLogin_Saml2_Utils.format_cert(x509_cert_value) + x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value_formatted, fingerprintalg) + + if fingerprint == x509_fingerprint_value: + cert = x509_cert_value_formatted + + # Check if Reference URI is empty + # reference_elem = OneLogin_Saml2_Utils.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 OneLogin_Saml2_Error( + 'Could not validate node signature: No certificate provided.', + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) + + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + + if validatecert: + mngr = xmlsec.KeysMngr() + mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted) + dsig_ctx = xmlsec.DSigCtx(mngr) + else: + dsig_ctx = xmlsec.DSigCtx() + dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + + file_cert.close() + + dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) + + try: + dsig_ctx.verify(signature_node) + except Exception as err: + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. SAML Response rejected. %s', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE, + err.__str__() + ) + + return True
    + +
    [docs] @staticmethod + @return_false_on_exception + def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False): + """ + Validates signed binary data (Used to validate GET Signature). + + :param signed_query: The element we should validate + :type: string + + :param signature: The signature that will be validate + :type: string + + :param cert: The public cert + :type: string + + :param algorithm: Signature algorithm + :type: string + + :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 + """ + error_callback_method = None + if debug: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + dsig_ctx = xmlsec.DSigCtx() + + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + file_cert.close() + + # Sign the metadata with our private key. + sign_algorithm_transform_map = { + OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1, + OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1, + OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256, + OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384, + OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512 + } + sign_algorithm_transform = sign_algorithm_transform_map.get(algorithm, xmlsec.TransformRsaSha1) + + dsig_ctx.verifyBinary(signed_query, sign_algorithm_transform, signature) + return True
    + +
    [docs] @staticmethod + def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False): + """Return a URL encoded get parameter value + Prefer to extract the original encoded value directly from query_string since URL + encoding is not canonical. The encoding used by ADFS 3.0 is not compatible with + python's quote_plus (ADFS produces lower case hex numbers and quote_plus produces + upper case hex numbers) + """ + + if name not in get_data: + return OneLogin_Saml2_Utils.case_sensitive_urlencode(default, lowercase_urlencoding) + if 'query_string' in get_data: + return OneLogin_Saml2_Utils.extract_raw_query_parameter(get_data['query_string'], name) + return OneLogin_Saml2_Utils.case_sensitive_urlencode(get_data[name], lowercase_urlencoding)
    + +
    [docs] @staticmethod + def extract_raw_query_parameter(query_string, parameter, default=''): + m = re.search('%s=([^&]+)' % parameter, query_string) + if m: + return m.group(1) + else: + return default
    + +
    [docs] @staticmethod + def case_sensitive_urlencode(to_encode, lowercase=False): + encoded = quote_plus(to_encode) + return re.sub(r"%[A-F0-9]{2}", lambda m: m.group(0).lower(), encoded) if lowercase else encoded
    + +
    [docs] @staticmethod + def normalize_url(url): + """ + Returns normalized URL for comparison. + This method converts the netloc to lowercase, as it should be case-insensitive (per RFC 4343, RFC 7617) + If standardization fails, the original URL is returned + Python documentation indicates that URL split also normalizes query strings if empty query fields are present + + :param url: URL + :type url: String + + :returns: A normalized URL, or the given URL string if parsing fails + :rtype: String + """ + try: + scheme, netloc, path, query, fragment = urlsplit(url) + normalized_url = urlunsplit((scheme.lower(), netloc.lower(), path, query, fragment)) + return normalized_url + except Exception: + return url
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/xmlparser.html b/docs/saml2/_modules/onelogin/saml2/xmlparser.html new file mode 100644 index 00000000..d8585182 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/xmlparser.html @@ -0,0 +1,244 @@ + + + + + + onelogin.saml2.xmlparser — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    + +

    Source code for onelogin.saml2.xmlparser

    +# Based on the lxml example from defusedxml
    +#
    +# Copyright (c) 2013 by Christian Heimes <christian@python.org>
    +# Licensed to PSF under a Contributor Agreement.
    +# See https://www.python.org/psf/license for licensing details.
    +"""lxml.etree protection"""
    +
    +from __future__ import print_function, absolute_import
    +
    +import threading
    +
    +from lxml import etree as _etree
    +
    +from defusedxml.lxml import DTDForbidden, EntitiesForbidden, NotSupportedError
    +
    +LXML3 = _etree.LXML_VERSION[0] >= 3
    +
    +__origin__ = "lxml.etree"
    +
    +tostring = _etree.tostring
    +
    +
    +
    [docs]class RestrictedElement(_etree.ElementBase): + """A restricted Element class that filters out instances of some classes + """ + + __slots__ = () + blacklist = (_etree._Entity, _etree._ProcessingInstruction, _etree._Comment) + + def _filter(self, iterator): + blacklist = self.blacklist + for child in iterator: + if isinstance(child, blacklist): + continue + yield child + + def __iter__(self): + iterator = super(RestrictedElement, self).__iter__() + return self._filter(iterator) + +
    [docs] def iterchildren(self, tag=None, reversed=False): + iterator = super(RestrictedElement, self).iterchildren(tag=tag, reversed=reversed) + return self._filter(iterator)
    + +
    [docs] def iter(self, tag=None, *tags): + iterator = super(RestrictedElement, self).iter(tag=tag, *tags) + return self._filter(iterator)
    + +
    [docs] def iterdescendants(self, tag=None, *tags): + iterator = super(RestrictedElement, self).iterdescendants(tag=tag, *tags) + return self._filter(iterator)
    + +
    [docs] def itersiblings(self, tag=None, preceding=False): + iterator = super(RestrictedElement, self).itersiblings(tag=tag, preceding=preceding) + return self._filter(iterator)
    + +
    [docs] def getchildren(self): + iterator = super(RestrictedElement, self).__iter__() + return list(self._filter(iterator))
    + +
    [docs] def getiterator(self, tag=None): + iterator = super(RestrictedElement, self).getiterator(tag) + return self._filter(iterator)
    + + +
    [docs]class GlobalParserTLS(threading.local): + """Thread local context for custom parser instances + """ + + parser_config = { + "resolve_entities": False, + 'remove_comments': True, + 'no_network': True, + 'remove_pis': True, + 'huge_tree': False + } + + element_class = RestrictedElement + +
    [docs] def createDefaultParser(self): + parser = _etree.XMLParser(**self.parser_config) + element_class = self.element_class + if self.element_class is not None: + lookup = _etree.ElementDefaultClassLookup(element=element_class) + parser.set_element_class_lookup(lookup) + return parser
    + +
    [docs] def setDefaultParser(self, parser): + self._default_parser = parser
    + +
    [docs] def getDefaultParser(self): + parser = getattr(self, "_default_parser", None) + if parser is None: + parser = self.createDefaultParser() + self.setDefaultParser(parser) + return parser
    + + +_parser_tls = GlobalParserTLS() +getDefaultParser = _parser_tls.getDefaultParser + + +
    [docs]def check_docinfo(elementtree, forbid_dtd=False, forbid_entities=True): + """Check docinfo of an element tree for DTD and entity declarations + The check for entity declarations needs lxml 3 or newer. lxml 2.x does + not support dtd.iterentities(). + """ + docinfo = elementtree.docinfo + if docinfo.doctype: + if forbid_dtd: + raise DTDForbidden(docinfo.doctype, docinfo.system_url, docinfo.public_id) + if forbid_entities and not LXML3: + # lxml < 3 has no iterentities() + raise NotSupportedError("Unable to check for entity declarations " "in lxml 2.x") + + if forbid_entities: + for dtd in docinfo.internalDTD, docinfo.externalDTD: + if dtd is None: + continue + for entity in dtd.iterentities(): + raise EntitiesForbidden(entity.name, entity.content, None, None, None, None)
    + + +
    [docs]def parse(source, parser=None, base_url=None, forbid_dtd=True, forbid_entities=True): + if parser is None: + parser = getDefaultParser() + elementtree = _etree.parse(source, parser, base_url=base_url) + check_docinfo(elementtree, forbid_dtd, forbid_entities) + return elementtree
    + + +
    [docs]def fromstring(text, parser=None, base_url=None, forbid_dtd=True, forbid_entities=True): + if parser is None: + parser = getDefaultParser() + rootelement = _etree.fromstring(text, parser, base_url=base_url) + elementtree = rootelement.getroottree() + check_docinfo(elementtree, forbid_dtd, forbid_entities) + return rootelement
    + + +XML = fromstring + + +
    [docs]def iterparse(*args, **kwargs): + raise NotSupportedError("iterparse not available")
    +
    + +
    +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/saml2/_sources/index.rst.txt b/docs/saml2/_sources/index.rst.txt new file mode 100644 index 00000000..4057b939 --- /dev/null +++ b/docs/saml2/_sources/index.rst.txt @@ -0,0 +1,14 @@ +.. SAML Python Toolkit documentation master file, created by + sphinx-quickstart on Sun Oct 1 01:56:19 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to SAML Python Toolkit's documentation! +=============================================== + +.. toctree:: + :maxdepth: 4 + :caption: Contents: + + onelogin + diff --git a/docs/saml2/_sources/modules.rst.txt b/docs/saml2/_sources/modules.rst.txt new file mode 100644 index 00000000..529aa5bd --- /dev/null +++ b/docs/saml2/_sources/modules.rst.txt @@ -0,0 +1,7 @@ +onelogin +======== + +.. toctree:: + :maxdepth: 4 + + onelogin diff --git a/docs/saml2/_sources/onelogin.rst.txt b/docs/saml2/_sources/onelogin.rst.txt new file mode 100644 index 00000000..87b2801a --- /dev/null +++ b/docs/saml2/_sources/onelogin.rst.txt @@ -0,0 +1,17 @@ +onelogin package +================ + +Subpackages +----------- + +.. toctree:: + + onelogin.saml2 + +Module contents +--------------- + +.. automodule:: onelogin + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/saml2/_sources/onelogin.saml2.rst.txt b/docs/saml2/_sources/onelogin.saml2.rst.txt new file mode 100644 index 00000000..0c0e62f8 --- /dev/null +++ b/docs/saml2/_sources/onelogin.saml2.rst.txt @@ -0,0 +1,110 @@ +onelogin.saml2 package +====================== + +Submodules +---------- + +onelogin.saml2.auth module +-------------------------- + +.. automodule:: onelogin.saml2.auth + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.authn\_request module +------------------------------------ + +.. automodule:: onelogin.saml2.authn_request + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.constants module +------------------------------- + +.. automodule:: onelogin.saml2.constants + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.errors module +---------------------------- + +.. automodule:: onelogin.saml2.errors + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.idp\_metadata\_parser module +------------------------------------------- + +.. automodule:: onelogin.saml2.idp_metadata_parser + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.logout\_request module +------------------------------------- + +.. automodule:: onelogin.saml2.logout_request + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.logout\_response module +-------------------------------------- + +.. automodule:: onelogin.saml2.logout_response + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.metadata module +------------------------------ + +.. automodule:: onelogin.saml2.metadata + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.response module +------------------------------ + +.. automodule:: onelogin.saml2.response + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.settings module +------------------------------ + +.. automodule:: onelogin.saml2.settings + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.utils module +--------------------------- + +.. automodule:: onelogin.saml2.utils + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.xmlparser module +------------------------------- + +.. automodule:: onelogin.saml2.xmlparser + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: onelogin.saml2 + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/saml2/_static/ajax-loader.gif b/docs/saml2/_static/ajax-loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..61faf8cab23993bd3e1560bff0668bd628642330 GIT binary patch literal 673 zcmZ?wbhEHb6krfw_{6~Q|Nno%(3)e{?)x>&1u}A`t?OF7Z|1gRivOgXi&7IyQd1Pl zGfOfQ60;I3a`F>X^fL3(@);C=vM_KlFfb_o=k{|A33hf2a5d61U}gjg=>Rd%XaNQW zW@Cw{|b%Y*pl8F?4B9 zlo4Fz*0kZGJabY|>}Okf0}CCg{u4`zEPY^pV?j2@h+|igy0+Kz6p;@SpM4s6)XEMg z#3Y4GX>Hjlml5ftdH$4x0JGdn8~MX(U~_^d!Hi)=HU{V%g+mi8#UGbE-*ao8f#h+S z2a0-5+vc7MU$e-NhmBjLIC1v|)9+Im8x1yacJ7{^tLX(ZhYi^rpmXm0`@ku9b53aN zEXH@Y3JaztblgpxbJt{AtE1ad1Ca>{v$rwwvK(>{m~Gf_=-Ro7Fk{#;i~+{{>QtvI yb2P8Zac~?~=sRA>$6{!(^3;ZP0TPFR(G_-UDU(8Jl0?(IXu$~#4A!880|o%~Al1tN literal 0 HcmV?d00001 diff --git a/docs/saml2/_static/basic.css b/docs/saml2/_static/basic.css new file mode 100644 index 00000000..0807176e --- /dev/null +++ b/docs/saml2/_static/basic.css @@ -0,0 +1,676 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 450px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist td { + vertical-align: top; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +div.code-block-caption { + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +div.code-block-caption + div > div.highlight > pre { + margin-top: 0; +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + padding: 1em 1em 0; +} + +div.literal-block-wrapper div.highlight { + margin: 0; +} + +code.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +code.descclassname { + background-color: transparent; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: relative; + left: 0px; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/saml2/_static/comment-bright.png b/docs/saml2/_static/comment-bright.png new file mode 100644 index 0000000000000000000000000000000000000000..15e27edb12ac25701ac0ac21b97b52bb4e45415e GIT binary patch literal 756 zcmVgfIX78 z$8Pzv({A~p%??+>KickCb#0FM1rYN=mBmQ&Nwp<#JXUhU;{|)}%&s>suq6lXw*~s{ zvHx}3C%<;wE5CH!BR{p5@ml9ws}y)=QN-kL2?#`S5d*6j zk`h<}j1>tD$b?4D^N9w}-k)bxXxFg>+#kme^xx#qg6FI-%iv2U{0h(Y)cs%5a|m%Pn_K3X_bDJ>EH#(Fb73Z zfUt2Q3B>N+ot3qb*DqbTZpFIn4a!#_R-}{?-~Hs=xSS6p&$sZ-k1zDdtqU`Y@`#qL z&zv-~)Q#JCU(dI)Hf;$CEnK=6CK50}q7~wdbI->?E07bJ0R;!GSQTs5Am`#;*WHjvHRvY?&$Lm-vq1a_BzocI^ULXV!lbMd%|^B#fY;XX)n<&R^L z=84u1e_3ziq;Hz-*k5~zwY3*oDKt0;bM@M@@89;@m*4RFgvvM_4;5LB!@OB@^WbVT zjl{t;a8_>od-~P4 m{5|DvB&z#xT;*OnJqG}gk~_7HcNkCr0000W zanA~u9RIXo;n7c96&U)YLgs-FGlx~*_c{Jgvesu1E5(8YEf&5wF=YFPcRe@1=MJmi zag(L*xc2r0(slpcN!vC5CUju;vHJkHc*&70_n2OZsK%O~A=!+YIw z7zLLl7~Z+~RgWOQ=MI6$#0pvpu$Q43 zP@36QAmu6!_9NPM?o<1_!+stoVRRZbW9#SPe!n;#A_6m8f}|xN1;H{`0RoXQ2LM47 zt(g;iZ6|pCb@h2xk&(}S3=EVBUO0e90m2Lp5CB<(SPIaB;n4))3JB87Or#XPOPcum z?<^(g+m9}VNn4Y&B`g8h{t_$+RB1%HKRY6fjtd-<7&EsU;vs0GM(Lmbhi%Gwcfs0FTF}T zL{_M6Go&E0Eg8FuB*(Yn+Z*RVTBE@10eIOb3El^MhO`GabDll(V0&FlJi2k^;q8af zkENdk2}x2)_KVp`5OAwXZM;dG0?M-S)xE1IKDi6BY@5%Or?#aZ9$gcX)dPZ&wA1a< z$rFXHPn|TBf`e?>Are8sKtKrKcjF$i^lp!zkL?C|y^vlHr1HXeVJd;1I~g&Ob-q)& z(fn7s-KI}G{wnKzg_U5G(V%bX6uk zIa+<@>rdmZYd!9Y=C0cuchrbIjuRB_Wq{-RXlic?flu1*_ux}x%(HDH&nT`k^xCeC ziHi1!ChH*sQ6|UqJpTTzX$aw8e(UfcS^f;6yBWd+(1-70zU(rtxtqR%j z-lsH|CKQJXqD{+F7V0OTv8@{~(wp(`oIP^ZykMWgR>&|RsklFMCnOo&Bd{le} zV5F6424Qzl;o2G%oVvmHgRDP9!=rK8fy^!yV8y*4p=??uIRrrr0?>O!(z*g5AvL2!4z0{sq%vhG*Po}`a<6%kTK5TNhtC8}rXNu&h^QH4A&Sk~Autm*s~45(H7+0bi^MraaRVzr05hQ3iK?j` zR#U@^i0WhkIHTg29u~|ypU?sXCQEQgXfObPW;+0YAF;|5XyaMAEM0sQ@4-xCZe=0e z7r$ofiAxn@O5#RodD8rh5D@nKQ;?lcf@tg4o+Wp44aMl~c47azN_(im0N)7OqdPBC zGw;353_o$DqGRDhuhU$Eaj!@m000000NkvXXu0mjfjZ7Z_ literal 0 HcmV?d00001 diff --git a/docs/saml2/_static/css/badge_only.css b/docs/saml2/_static/css/badge_only.css new file mode 100644 index 00000000..c718cee4 --- /dev/null +++ b/docs/saml2/_static/css/badge_only.css @@ -0,0 +1 @@ +.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} \ No newline at end of file diff --git a/docs/saml2/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/saml2/_static/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..6cb60000181dbd348963953ac8ac54afb46c63d5 GIT binary patch literal 87624 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

    !pma|lSQ?8FBe{DRl|@FzhU7Cb>&@ataZS{g zrCQo@Lushkm71KyL3$%QD?KeSADp!x{f z=8g-xggQyFIyp&U(+DO4!2ygCg$vPu1pqEc^*Qa)IPstXB!i@fDLx<-```)5UsmT$Yl%a2onC zAFmUqUVZ7M=U;sB~=#ZhJ zS2lbGii8+5IZ#kxQUi*iaRi9X

    #R4x5a04mzf2YJ>#U<-Nc81}`=EFMdUjIP>d| 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

    b(7>T&1{%W+D%3_gwxcBR7_myM-&X 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 DnuPx& literal 0 HcmV?d00001 diff --git a/docs/saml2/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/saml2/_static/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..f815f63f99da80ad2be69e4021023ec2981eaea0 GIT binary patch literal 86288 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 z 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

    msZ%O|+*Z+LS;;x4v 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%0AU9xwl*jMUcB+ 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 NDMp1hFB${m{{V&DCTjoy literal 0 HcmV?d00001 diff --git a/docs/saml2/_static/css/fonts/lato-bold-italic.woff2 b/docs/saml2/_static/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c4e3d804b57b625b16a36d767bfca6bbf63d414e GIT binary patch literal 193308 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

    l?s7}*4vp!2W|88=L^r| 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

    nR1?qM=|aYy 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

    F}woT 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"bt0}@AlIHaMn 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 literal 0 HcmV?d00001 diff --git a/docs/saml2/_static/css/fonts/lato-bold.woff2 b/docs/saml2/_static/css/fonts/lato-bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..bb195043cfc07fa52741c6144d7378b5ba8be4c5 GIT binary patch literal 184912 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

    x{o9f|7gEmo2*wv z?7&s&_B`qFUY>GLIw{v9To*&gMh;QhE+?bNPT0R?7!`WA618s`CAQKeiO`;^lCE7l zbz3bz(~KSUi0*_@EX5E7Q;9_AOU#0)g9SQJMW>@d zY+IpgICVmz?J?Un@WIW;+paH$Ls6Yy9uVS(MLkv>KF4lK$zKvam&_6Or)9Bl>emu$ z428RkeA!X%k@vI69SslFI?qfJmKfenq(ge{M5n_The>b{Z^oS#cE~}Sgke5ja@QI9G z8|1I34v{mkaQWgI?n>CBQb7qfL_bflb>64hs0?jpaU)4;6KgCw{nY?l&!%o58h!zb z<1V8idvq$r0=jqd5KoQUPec-UMPga8R!HR)41iOG-E)<*`G9nnqE z;EX_&2+!w_gjL^*7V~Npl$dke48^=V09mo-!N|(Nfcd_~zMcrcI{L5#e%v_eTZkc~ z$j)BqhI!a%a(rJY%i|sAz9WX^uPR)1(y^v`H4QaOad5AMcG6my*LWFHHl^0NDE$cH zQ3iPx<{C7trrDrJxg@YqAkD2nCF@+5RH9NT6x`HqXM{+aXFze7kR29dS4;#BDR=aA zM``BZjj@FhtZB+iQY?l$A3TrtI_hq@E-Fzu6IsmxjXg~;eP+6!y-~qJO1d+B9irr|Anq{O&h2Fw zUj_S7xKJj~9Or++bbEV;xlZx`nWx={GcO*l&My%%GIZek^2$Vm%hInpzySYatsGL5 z@?zK!hpSn2DI>%IVS*I`8YJtw4aeKeuhZe{QXEGom};$;NZ|^cP8vmwT<~UJXqqb& zNPx$eo*Qc*%`Mu|(Zk3ffHv_4`pGbH@l1rUH<562Cg5g)@L8|VE3iOnVCD>4z(hwDq@Aab5vNFQoWCs9pgJRajQ zdfvPHC4PZa5L-4-gkhM+>#2Yyzqk!&BMOrJ96>wEZ*|xMqVFow%+o7_w;TbOsth8- z{xIc!i92<|ksx-j-^N12(SVf$Ernf1P{?wlc*zbHDV&v~bdO|z`D$CTGE_Diy)nW#YC|7$+eEyl z>mI-D*z?ez!b8|uKhqTGIcXh4jOJmIjsSalr>4}xeO#3-?6=V)@tJWicKfXBt1vr~ z@-M(+P0r@Mos3Y{BxQv8m7{X!NrcfE508imh%Z6hH`(+$q8JV91;qG)HU-0p?zhSH zR}&EX0p#?<>0Tmtqaat9naqi2=n&13pMw+5I4-jGET+?&eZFSQTwZ?c+~$>b6&RZk zy9#|~%pPblsnJgPSPFw~j%;|uZWYV<^YfmS1=P1T^7nk+?-Hs;NsigEP8=a-7spzj zll(w4386N}<7S6^GdM2d{4&zVV?Ewu!=0uqtk3v#T*JHhgz{b!IwbTmF|U<+mC?!l zNJKfwoQ>u|JK7+ka$HMJv1vl|cz7ri6jj9;yIni#he!EK3xBui%ns8=vpj*zpNhQ9 zJVv;-@WVNp@@&uNv?TVaV;v5SDdioD_SzjB7sLi>50Kbpj%vYgXbr(qSBWXqUDZ^i z$^~_CQ+%+?Tyn* zMaz1gquS}L15xs!g4J6_m*qW3m0powcuGF&mW;Ph!)4+$$wCq5!q^; zSwJWdtwi*vGUObe%!ehjr4M*a6i(DLFsYaR9r~D4A|W4@p6(g;9J=4$^OX^l|&{(g=Fc?IuTy z$i4=SNQnvgvFndhq`&o;3WnTEh(JoiUL`@+x#rcRxbuhp&b=y`*4t!t+V6Kf)B^1x z7t4|gaC*stWQ)DISIIQAJ-SbOGerwh_0`zFtRN( zp~U`RnqFNz8W5myIOoV;%Def28=V!S!2FYjwPFmyC)RZz`B~}C5UVuL1Rx;`u%IZA z4+7IePhn@{LrEP8ozz3FPCg1niYGPfi*L)h1oj6pIO77hMaQPzBpjB3fxJ|Nxc5C8 zM2Pqhfh9KgX4M-IEyE+@ct--0l^d589|z}>GuGIwE9d$vZL8?S#foiHsqkV*{kEEj zZId((k|uCz*Bqvh^Gn3sC?*AL8;RmoNyp}AC9wo;p?g=AkJ{TEDI-~Za;N?rDGCiJ80 z#Q=;RF;pJSeXzC<(Z0@JFGvIu*ECJB8Dp4KX+iOR;4Q6*eS?Hrl~U;`eeMO?d@Yj2 z8p|YdN_nDs==*8vrL2}G=ylVAucdB&&W8Nz`|!5?>2@+De~1R6>H5f#n4^QuP~zP- z{`%bILg*W7{X;9Gq0z;P%J@Fkfk!L!YoG$WJ-RzC6wHl_tF)?56H4Y9BB+Mlp3l;- zlq?H#42oHQmH2`!W3dePNmZlaPD$HEP|3XDLBgERniw}gE}4h2iO=%LF(4k3o~Fnl znL5WM7Pe}R%2xYK8H#meF^H}SQ~Ev8z6pXTk&(Vs7QPUZN1=j|BMBu*sw)4O7o)}7 zQHqURjnDE3LY`3v7s^G^02wFDkz$7`vEQ=QW?r>ar!N`lfi`iBz5)(nBEfJ;!MBEB z|HwB^6Su@P-=;AorwA~}Pjo%dQiiLZwnfIAXXfnTtbSXR=Mvd0_Mw&Qk#@HF44$iB z!kA0ae>qFo4Y&cfpz_ksHhxOLR_6S$u`L>}{1(J7w8N9a7MD+2<6S*umhOx?mco<5 z4580~N@)OM5ofifNER}VD)-`K1kKFkBePhaA-G1uH3%=7q1zS*`@2i_B5U34o3twx zm8x=|r=?7gC`@riaVq?`k@ORg{x9B0N=a%BnHO8nO6#E%&@$hqYEEWIbU=cvuFzJXsYOD-+OL@XH|I+OIi@f}NJrQ=&3j#K^#3 zLRpY}?JvdfMFxq+i1y4vJ*L?wq3G1HPqh5)V!^I`;obY=gI`eI2uWSk%6}OKv!q^9 z78=4@?$G%RpOyofDXlu$y30(iq6AhfuH zqd!mm^CS{qWTg+>#alta zyot0tZtf}SpofZ03_wn!#xBQU=#Lgv$F&uqITMPVI0qqkQpxQ9q}kPD2Fwcs>mL7~-;ug&rgneG^R8MTpePS(MkW4&;k;Y3onw@O>NvmmGkD4#9!xO~yjE1E(s=V2{}{8{QzhxQfk8FpqCmd|DKzV^2GZ`d zuFl18AP=!}@^QLkyCTy(CdYf^UgmuYOE3GNk&jMrkOIEw`QRH@ue zS*@Bpd+E0hnw&4NcpR>8;#b)_F%3lEk+d_^qg%HPTu znB8W#o&$vTlSRP}g6-XTuAsZbcYs`7SluI-^LU4XtkaA3vbn=ikaaIpd z7o?%rQb02g2)w^!SzmHqYNJ*?%VEQ8MKr2QHLCzUK*GOAqcnfMel(8SlSV8doJ-Qd zk9Rq4h?x!$k}3iV>9CLisr|B$NXqP5fq8%AY!t1{0NuK^4bGAsByNqoWNC-}OI9Fk zJ1C_)rMxxC{Wm+3zcPzNa*rg2Bp7ntKPd+9g+*c+$;t#Su9Msa7R;KLoGcAxG?5&^K=#kG&Z3&0i5G3@7ItzLhX{Y8h3_?HOL;tl3XBUXH_e; zw6^_((WG@MFMC9iyV|s@Q7-oW%K{j0lf1$z5k3I)0Xe4`f`Q!CKrxE*x+sm)3Hw(DK`%sELtU~(k1^VILd{S0fOp{g&rwR~U?9XpY$YrTS zLS>A57Ge}@_Oyk$Tlw5EDqN_mq+2(O=18$1CPc1lt+z++U|}!eGoaAmc7Ly8H+oYo zE|RMOP)F!}R%EB{FB~=(}Rw#*h^`UTwxCL4_Y__bMx=!E;N>r#Skwy|P z=}MdZZJo*Kmjs2aEXuP>jsy~W#pU@em5x&9aLPRy^b(1Mp~@-E%5c)0-~(m9NoR%I z`^)>3U4oMCoxR+_jcc?h3)$NHxHOct3{JFVWr16tIA$lQ>XZ8{3EX%gu^R)w}iPHg_Htle)QSfBt4Tpozs#^r(m{cSIsiRBXz`m!RH z>c$_~oIiUi*#s-I#znN(aW7Y(p%s4)dT5i};#5^w!y8Rd(bSILS&|@aQoRqRtEc$= zIpk`90Db-p1y%uxRI7)#NOK2dkHni1wALRpGA%j&^+0Ixp=xWvX2JDa1({Ue`yxCB< z-ikxOd0%)gp|rPytHg!~%ouJs8+%qoJ29>PpypG?k5AvXhMce{Is z=(4eIuf?*Xz*`kWQciTnO*K$#k??D@shtoH!K^?RCxc@0R(LSqoV*#l7prr#2YF=u zb&l|*Ia3g!PE|n~EfAU)?dI(>F?TD;`TUdoihaP_f>^$RHoGwM_6|L&Tm53uY&$Yu zZMV^!gO@ROFK3f#&dcfTLP0mB_O&$?PB6KF()eK}QB5k@8MW6FYc?-q|HccuPh{~PbOZw6KyNL38jKQb(b_&dXfSc#|$YTf7Ogu~o&)Gs2GnX@reRoRtV zv{bXkLiR`;rg1m3}6h|!S()J`e^$apYizZT<$96gT?#X`Osv2dXn z!iSI2S7~_(yc+eex0%2zAhQM5ibUzSXdY2 z7@F*uSbHqFDW?(~-r!^nh<&AqCqNfp)1CrB*6WYcVWD>ZNhQ#_XWZ1sH*_kJ))Q(Z zwa?YeqOe|(?jpUdO*}=?amon3Hx%8`v%nHBVeG*gJo-_i)NF0>DAvWeP|Z#Ik4I}^ zoG$5gt+Fy)gYfyv72~_BWoL|t%Iv-r+cXSOjIURXoThjiN!aDk#_t#P!CuX;cZx+v zQ}KE@T(v;Cz>@A$|3A$eq}C|n`uGlXaN=q%c02)@zr>N|)~kxPZ}w)n8y~MclEzdB ziL+}t1fn*1XI?2IHtZrxR_?%HB_?k64#S>?l)W7Wh?Te6Q0PYVXv~qcWRh3@Iuy^5 zd=NfE+IGMje!_=>%Jb^2029_k2ns!YD|eSk47lvuRoXkBvZ+Suk`VvM9?Y=!p^BqC zLZEE#B4)Ozz6&V_K}Eeq5N)2m^`(i&61LmKo=$}1EPagmsDI-J?2J)UN|u3)59oYJ zPzaaD&rZLTNLC@15h53pFMc$CH`5oIe=YKxU-MVZUlw^U|2fS!izkljytt?D`fcLd zzg;EoRi$glN|dk?`ezXzH`SqM^$00>rf>wqX{-Z%t!&c}OE!1^+k3tBKtk3{pG4$> z2ugNt1XYV*>ORMhH5>V&nyy3OpO#e0YRV`eNwSW}`8n-iij5!JyPh%*gvYIt0Ef&t zhwv>rTTls4!X+EGF3!j~L^pv3X??}(Y%4H_5B*b$qNtS9z+#f8r&G8-M!#SRGUheE4*i*`)JwH5 zWvR#7V+BYvIH5P$ag{+sB94S`Al>dm=wwIcC*RJC)kp!*L zF;`6b%3$P8d&v5AMA0$E5=`eu_Zhbv2qoM$d*gZXL8@>hkIGq#yBK)v5B$g!Vf6C- zrPGM{hnUw+FB*D?>y2QPM-d8JfTY>B7rPitXLRagGDwCRF3U3*OZr(%QVw@D2SotMY=jVA3yrQ2I6hu6Zdd z@^8gA8HE+MCpgOH@7QTG^Dy}+y`LhCkfQ-;`4FUETpNXkgHH%tljxPHmHM;Rob~_n z)nEQ+s#@k4pxZ>G$$X&}s<4`3a;mng`Ty59|CiBi>Ob%;tR#^P{uSXj<`Vji!};FW ziAHE-MQ(QAvrIQXtN#SDdG|L%S371BR*yqS6zfB|tsJ)8iJbR21mXFt!v!aiJ-}N> z1(}!}xz-dxNs5JtWUPNwejhN`<#79-_dU<5y-09WZXT)5d-8y`9NTrg7GB-#N=KBsI@7w6`MLbMjt$ zcMzFgfRL>ghB063VVk@*#9{Jf?8W}?)p4+|9^A*C3ES55MmBUz+wKe*QjNwV&=)d# zID4|VrGELxlOh0hN@RjX(p(Q?g}~!bhdH*s$8^Knrzb`@CryvD(_Y*mtDXX3xd&E@<^NbF zvwMxPj3R-;+Q}vF0C?60n3TxuuAts)?Kw@KH)C(b7uBI+#p38bBSjsm*`kuo^c}RM zCw-D0{&Kup|1%)gvn(9WqUmgCaew%){m{dQQLutsuoLvEZHjXK*i|Y&KHS7raiCaf z%-1zut$YelsC=QQ5^4bO#;pUP$yJM}EwdF*kRu15U)5^_)F_-yfMbJlYg&7>ilb0O zZYZZ^8&w`-9Wi{v>EdsMfup}rc``esH&Ur&I) zNcGNf=*vjR=1gBs!g5{0Qtmz9+sRWRW83X>sj@5H8z?BqZ^DG`QQ0!exV+sb6qc>t zSCF?RnnDe1`ze7?9MwpRPLu>mb<^CW!=sA{)|D(i#GQ z%s~>V+F(x)1NjF9@AmkF&i_gY{8nn^jt}oELI>mxR1g~!$Usj#q}^-B`Nr~xALPi! z-E$1xd9`>)7zWusa1ma>COzPN2x;t({fS{y)0C;D@MZ*W+YJia&~+RcBjq{wdXjh= zwvA`1w&0hz6(yCX8m zTN<&DL_T1veO-oSl*BYd9HNbM^gbkmg)yN<17{n4=7xxpRN-8*4Ee=?p>S-byxU2q zC{k}QdrlEph++7-V^|`!P>zvhkfKXQ3&^Q&oYGxqFVZBQ(v^V#j*ZHGpy2{B-s zu9inTzVxh2ky5cEd3r}p)%4q|g8#i`(82X`Kt8B7-ncW=%sabiv8QjUp6kq@Jv?uNBY4RNX<9VUhnc}90vcZpc6HD!vN-D=HKR}`+W^iM(G>MTXTVcSVSbZf?3)d21Bu?Mp0y~0ZFa|(_B;-_1$YARFU!Uf? z2#6_%mty;LKU?pssg>g#|HTu;g}1je2^r0?bA}TKgPmB@33>xV^#loPq~Sve>xEZE zDlhg>Nun;OoK4`Z`F3JsZB~rM^8_sSmti1|3H?6@ z#`*JvxVus=KwwIs0jgf+9A70NJdt6~C|dmP^bHCGN{rX>)|J9W2*XdX^?`5sKasez z!gB51m(lCC@i)hU(_^T?{I!nE%K_P|ge=F0q7vFoHca|bKonw1mlg3IY7}W7_$*xPQ&qQ!0g!W> z`WS+L)7^vjJ~6(jmH!NPYC-&1tOrs09>D%dQy;v7e5g6QFkMz+oIa|_u;j1i>ebyh zX-}P8JJNHBiC+{vG6E$qBzT%u;0?ND&ha~cK?w{CAjd>B?x)xJK+ zT$$`*M+*BPZitWO%OUz0_VIXDRwP_17>60V zCg?Pd#yoD-vpC^n9=D@jtsh1-wFzM=1Cl8aCN&i3tlu5O$?KO1dKBs(R;Mo^MnB z^!MM|f289;dhFpG;QL@_qBR3?;MZvOPK;MDKg>~cJu~;zYB9&3TOjpE>m5~S zr12w(+oOa`KCf%sl4jtV=cp|j-YClunnPAm)YgTFoSqd@Z@!!bQ?FVv*(Es=vVo;W zf#pFZ9a1*aV~@Cpg$IoGvhg33N5jfGt42-Bxea_mP2*tJZj z;;#?MUUL%*feY=)ivBI-0`?I3^LVNHW42G^3~=DzSPmLmy*yOP|Dt1Uqz>KsJDnH)m{0l zwcTE%t5tVUHyi(@li{@3#Z-4cn@Fu)r_H?Q$bOSdMmUQPgUmy@Y%0H^)4^BBOLD*r z(RniIt4SUviv4zUKr;Uh-}HCLn>Lu3`05rIA4~cM>>n|rU}~Br(E4fXHP|EHnI=wM zRE!t`bO}QS8TuwNQadZHtNRSCA*|8x(R#{09+aC`i0^iPfvq@~rz{pW6nN;I`lkD= zQBk9%jYa5?cQNZFDrGzv#QNYDo82HB7#UZ}JvGQqf8o_&RUE0ylij?lfNf+zfnm4r ztPJtTV$|jFD$=DtP?OGWd(+38-Qqp0ia|GRGtTlm8ce5d*s#@^n>OtUOEJ*SQ`fiI z-WJ~h0ex7eI{d^Gv1EHTrjhIKCO&W26?E=UcYj2Q5xf!%(R)29TK!yHgpp*5;^b?I zo2Y#xyL48N@R`X&*l}*n9$hecq{tNd$;H^lrJc8id8@apt@_m2jHQBdRxh^lg(B_r zy2O&;?5nRWsF|Kpa2`iJH zoZO5o(90?U1_Gn(vO78&L?Mbmf<$mmD_*aJj|eMBsk8axLnuo_8^=+UFyY*tNHLHS zq!Z#yYhfb$vRuCxW&po&62B>ChH`q%Is z2u$IY(H4LvI?FLYC5?Cg?&4upAguEGx4nBu~A|RFhm`tj1=+H)pxR0xy zaZWjjGk3Bo=j@5`?fFY9RVtEP>G;_D;`;*Z+J@&c>y9YCW|}JS2n9zLG7V-uz66uB}TAuCW8?op%LS0cp_)`u8j zNb@Dt2V`@%me2>DBbDnnus6d{=UZ2ep!Ydv@~)DDf!1<)SxZUF(->AUE1XdGHq%_# zsvSi3?3~W>q6T86+egrJOatW-yozcb`uXbpk$nSrQ39dCHfdHTgdbKBF^;#gJ{7Fq z#yjIPifBD&?L=hZP=|WCk}t9hPmv0~Jm8X(pZOk2p_8kj0-AZD)^xS0#B>mDRCh!) zPgvORhcs1sa{)SHx>Sl!BSm>?oC!MV)HFjiGE#Qz1Xe|t8lt-%w5q^|Ax8griRyR_6|YCBpDySt+uy`m{=cc+Dd=CvqYfvDP$m8jcn4kQ%VbIO<~eG%;d2-(X@s$UwVm1O4@~Iki)yEcH*fJ6wkq~ z+#DETg}6tk9_l?nkQMRRChP8)F{%7|;(}&!@p&z%7@nwya5-HfEZMvRj*_>^ZVNNi z9i~|Q(8<<9FO$%D_+yW<`GApAffvPFV|9KV}So3y&>O9S$=4 zTx`yBAPfKcf)0Jm_TIbfp9!-q8y7F}UpDX}{+;gwxQD-by`EbQmxJb^X3Q|Pi)U~AvO!Iw>~%3v zrrQM(85{<)aB@WXaGC*LLIN@jjJxVM_C$XT3>=h3gKB`qx+$a<17pF5 z^HCnv9yEy*iMg{<(AoRN3urZ@0?v?^73`omf3{+MH$4e0N0+9iQ|*`lU=)JZhh&4WuqU%7+V`P}L< z_9NYRp)*|^VpMdzk~@vff7Y%_qlNx9>o?xJD4OHHYunA@KN*gKg)%{ovaaS&slhrL zI6Xh*C?BBm4B%*q9t{K;>ohfuL+-;usE+WV#?puy)b^_eb*VI1<`bX}iJ2ufB6{~+ z8;UZ9@^wvA-!0u%b>T?CAG_e;?UW=MW?LT<-ag6?O_Bs0#9iz}@d*PE3`1U^XgeCT z^hL?=D8Jm|%Oy8#ifz}CQ%`YPpysAnK>3LJli3gUL}DUia5M~x%d&_5BVqUq|4Ro53*y1L8o-ZWQv4C=Hx3BSZv#4z^dhwfyiMf`zLmi#f@C(J2jK&XuL+D) zqq4ZzuAL7tVShwN`Ov5NTQ)PPfol;b->6K@2qJQl`{s9)m|}hzX#| zL8DIMsMA?ybqJPRvBwD#petK1s2O$vVes++ZJ?nJRfP?IR|v!PSC1kz_>w_eRJ)Bd!9JT7*S^MDDD*UY=kGPyLknG0C&YJs)o)Y2(TW9 zVkVpD9{+1#-iWisF+gZ{)@#i>&@guqpnqQ?^g{wf>^|v< zv%WjoH}_%gdvx(p?pCxoRlz=KjvrcI$(g}a1mv#_A+>(Em}sc6aHnUC6r`QHbCd$! zLJ1*%#Iq7}kd6--{)`qt=u2vguq%Tb4!z(JYQ-kvMW{(0 zP=DAnFh;p~_2IZ1iyp)!2G6UN#Q>0DmD-JR(JjZ!>rFhzO`>~Sq<_bd&N-cfKwu%C zf+B2aB;A|l855P&{mwaFK$@v5*VnuoS8D7cp<9ONr&m^iFw*MvE4&?qYzmOKFSvI= zETlp$Goq=zNRMo%o(T64>EPZiIC6O2AmFeXT^$&R@ZF0vtH-UxJwbBcYBLDId2XH| zdHDiY1>SF~adJ3!z<&v~&TG{0OdyXd&Y+LyTF)Yym*(MwO*CBFp)OJhQrMIx%l})c zQ~7{pZS-bhV9pr9@p{p^7-D%8hB&5ZB+BG`ZDsL7RYT30y2H7?hmKQYw*c1#A!83> zJs`undC*eA(>(-INIcHG0zpK0_G*QBSYfSQ?~}owognmx3hU;WjtkxIY2&`!J9C!e zx6A4VB7ks#XCPm}h*cm#ByP$+@cP!j5Jsl^gK{2W3nQ267RX6RdgPivItQS4^a2BW zx{6cQ!cG$+b*dUtty^1q$dk-N>-M(V=C;qRNou4Zf`vO*A9bFnt3}isni$Yy>mG|{ zhw?^AL3d9EO{UrP$kBSmS7|(e0s2fkJ4dqR4c13xaxxEVO?E^BdhFG1!oCXP4}8Q@ zr!`R=0+ulEr=LjKw*yd9#?Y;C-~(~SCsHTuDlK<`qZ!iW&Fx1k0WBIqTiD)lF}8H> z9nBH@Pl4D8v8ccVtaQP9$g9RRDM&C{inOu6m4N<)a%z@88{yU8`T)~#zNSQUbvvXR zdkr#WJ!SjdqI59YlEI9Gq`NP9oVns)UuPh!2a-Ac$0sS5^ikz@RP6+3yX|eY65xenTL{s-hDhISVgVhv zCH0ShZe&T)06*ou0V(cTSSr#;+WX3im0mKu(fGQn(+uK#SOz_@7P?J|qZIYV%RLI& znjaxej-Yp$@G9%~*yDISg~b5934#P{>8d?)=KW4?LJ6}~;OqFHaa-@j;T=bM8(J}= zMZl?~>dshhR=NP>ID-e`mkEJ^L{=|BxUZ`~rAB(n2uw=!)e_atlgI<>hZeP!Z1rGO;@|R7b-{?2ERKWinmb&{SwS z5q@I;fwM?j5MEsxHJ%7|;6xH`jfR@fMZ?Em9GXE!i;8dq=76lJ9;J6= zpay5Gde=KVH=`1$c&8Larl+VMX=zO@KJw(ty6g-@R>e(9U&}Ge9f5uSjSsu7v%3IoSB~m@ir~RIWB>|v@RxvB!vNZPwo-1rV@pC|G zgil!Tm~2)kM!Lo^Y~4$c_z2~2BNA=^n5{Yjk%pTDCam{iSVZIsXh%kxq2pO2=$qgH zbtF0%j8Eqb@M9&0!(DxeNvP<^vk{7O7GE%yO9ZIc(Kl4J2qNEH;;%kP*Z`~V)8!8< zoK$$}Dwq;lEYY^Iqx@1rH#rxvWuPO}p55{N;wb^n^4Rd3#5Q+wG!wBzz@?HVDn z0rZv3>$hM^%+BSkX?oOk0h+=17YmM5=%HS#uS#=?W#6n9o{1r9Eb~6Q{vmC(i&}GI zfWX^*m}2fzgO#d-Gt;e;tpca4P#E=2Axrx+;kJgSwng72Zd3^Qh@y{&s(`wPsb2}&ff@`1kM!rLLTI5rFg6c>ULFIp7yPS*aM0&3Jg?C z2X`snCjrDU8-5JsZn|02bGi{#QXc3eqUn*=JWPiKFmEIv^~v?3GPD{#0Y8u~gy2U9 zGQT@1(Uq3=<^ySsg~*~LU||9Oz^~BZ6abzhXLC2m$QtHX(uGGZy}BYRL+G)hVdU_I z{;pM=GTqgsCMUO<9Y@2-1eknN!G(HU@@lH$m}B#ln%c?Nh`e6LuUbk>Fco5c$}3=V zJPPLLtVQ^6WBN@wgx7b!^Insi-@ovE3oKlRg%DGpp8GkD)G`HA;g9(!D?l08ltSx5 zO1?DYoAt<0P+Kk_lIEQ8scbm+d=2INRv~ldK530vV)_>Ttn1Vuku9XgF2VMJHRy`@ ziPS@ZQjf&J8Gk-XgQ!(?H=BJ_wua4?^JCv*B|Y*6b62X8HF?1GuDWKu6~BFlApb>( z{fEddL%7r@2QV%H9(4EPp#F3>NePr8Wob_y1iOD|i6$%1hB8_@2Pn!kSyAc6(ctAC z3;JI|#95D1Y;ob&_@=^WM5qYVBdPSL(#3D<%V8KeZ#7bi_GsuqO&Z9QrzP?Z+C-ND+SXZjAl zh6P2OS`t>lUQ#len$R+-G_ap-!yK~op%ylEblHE2Zsas)oyLQoKFIxkqWOJZ zq~#Z(h@KxOp)dt=cV38aOAGJqvK}wg)ue6JpOlvGTix^sWsz4>;m!(tvQITZGtCiN zR_e7LjE;rx4Pw^;`-JJXRGqb+D4p*)e)6t}SCRa5tkaH5< z_EBIB&;}sj6eLth9ldI$j;!hW7i7!01u#~wo2VPJkkKzSkR>mXceM5$#JLg5@oyH@ z6i4aE`&H7S5?)aS|IYcCpWeOXkR;mTFSg?;+o3HdNPk2(u`QvwFhH9%rMC+bOl*Y2&&kB;MCU)X3DFHgG#!x`|^PFW_HD`5n0_H zOieZuD9Bamxc0n>3o?G1U?r;_ga84q8Dq>2bvOpB17)fR`#hu z+#-&xk1s*&WjDXVZf~AQ!aAzs7Tj?29~qH(K{7+JU$$uA%#4v^%cBCejy4xVZo=bC zi%a*0$Cl>>E=zDp6IHnK1SPcm>1H^ro80O>P8G>8yFMDtgS*sBb>9>+?t5@IK2Ex4 z-8-=l_6O-l)fawN!s;$Q>Ys)C6K!ox)rp1A4cG{se;qLrM~I5cS?X!qy?H-Ap=qKc zTsBCk+F;fR^50CnBA9*8Z>ilBreN zM|w5x&}z^QPf)AG-LUB0zZd#(u|*r4i}aW88?%ype!J}Y#-MIiswmQs?2VG2JrR^| z%OB&C%*~N_dG!$a&zUksFm=eErz*Zy9sdeuGw|29QId*8MW(83tUf#VC85uZvvXea z0#-+_J8G*Yt@eFx%R)~Ov?A|sHX?So7WeqN*MQ4=#P#oiNOMs)w*XNO zmG|Ud#`3A1Ph!wspj2+V{wkubHNT#QIB%5AnQYv#nQ~BSc?OEse6tI@XpYM)-bi4i zM!c`36#2#2qFm9Jsk44Z)9Gt-CB=YsjbFxW|s%b>NZ%AmjG6;|&B z%RZXcbQH_x69u|uJy`h8&xMv0`pdXkl6lqMkM$c8u5H2!mZN~9wweG#>*Olo*>n)9Z(Bg8BfL8iFse0=;Qo94&S z6tB~$n4r2C$2F-~&FmNtu@llRs$q<}n_m~THY*>%3N%$;NE^y@5J|+Z%hV*q;JF z2=s;x5Tk27#M8Uru-Pqb5#fQB2}aTB8x=X9T5-!#(8w@Ng43XJV^+-k!q{^SH57@J z@Z5b~T=}E#AOFXnYd9|ohgga)=$7=>tD2UPt47_t8^)JYw>yb8YTci3E})S&D8YX1 z6$Bnthl^9xe8)tVV(nv;;P%{Zy8b$Lwyjk3F(_lN0ogswd7^3F=xhTh0vO^#ZF@UapFLym9qT@bBvzKX0f17XT#-rVTMlu#(55!CxF;pnr?3vYD5_{s>IE-yBjv08V_tH?N73TsChV{2&x3Zvzk^`-1S8 zmF9P0kHYWIm-S(Z(fOIw&Me%+f)kSO_YPDUwA_GTNIcN06c>L2}vBP~m@XpiVFLcu-t74Q@ z{YaPS9-+D3R}R$x-;f|{?2*LCkLia%PH*xd)Ws&eyVl{i}U z((+=@Hj4=RACuE0eF}8%M13dbxzdff&f*?3wQ}9t+;O4;%{wdU>2|48B@81KmQ+Zg zZnp*#c*{a+?5eyr*YYb(SCT2`sR5HD04UXnWzWaO;P0hUe>866a7?e9#AY1K?0UMR zlun3JpXq<(F*RxY^%Zj6uABO5|B6GeN(B{&{aP|EeY|SM{8{|25)WG-Y1s|a_Y@t3 zML!wszHmMbylXAtN)tkNLf7Sz))vbV4!>$3#Fip2uyDFR5(|t;A7+_DR)QYVt3tOB zUD<@szr+~fmB}<#a?0|kh=iJRQhTN&zU}5uA~FOS#s?od6xWXieHYX_H4d2JxELW= zmlmas_GGn*h{+|tOsAX8F>$T8S0-ZW_WkTGXC^jW$A_=x%se|@uYlt`D8?%hiyu!m z(|oT*XVeRH>0X#Eac!^-hSbgbjf*69#Jm#yYcbm;j_k15Ne35YV*#ixJf`qzhPo#7kqzF=yQdY=| z>6FKB9q_ngWs^gJJY2#bYAnTE+C=6V!TdIDF?}zKv|X4rq!VQ;Pz*P+W6>?PFj~Hm z;MZSJnl@*L!8;6ayB3FFOi!&0nI4}m#rmLwgrDQOeU_L9_Nnx+Y6Mpoty z<6b!g39$ejH9blL?lKK`o`im4X;QR=-^sQKoeJHk?Evh8tcI_y{a?R>z$*e>Aw|YBxOw^-l`pW`?W5-2jpKAGLQ7K^_ciktP8?~Dk4Z(cG zH#wVp@!sP+SnR32+J7ee$}(WHl!y{nI@ZZzTp#+}Fpdd)lw`rj4;b?X`=!q2h51=G z?L_Qs^mS@#KXZy3-Z@nY;c?Za-!!Bw`G*p3?lT64&Mc**xK!QmN>DFq+q@p<`>Vx3X-Nt)!ef;4?{sRE_H|Bw#!J}gI> zxz8{!y@>%D*FJkI1F$nKP{3E}HK)fSmy;Hl$sYtPSIS67GDkn*8`o#~AfX|j$E6y4 zFL9WS&iOusOXP3U4{5vwR)=H%RX2$18kXL)5p-RaA(G7b6CUJJ9U^i(D0BouDg#w0 z)Z8L@;y2K64I`dR7)%tVJ()ORNS$;_tf}W8AMz&8_|40MGH9`>h1QTi;TD>!kQyuv zc|~uR8x3v~gfcl&`J6%|T7msB3L?VCO8L(rngBo}so$1<;^pkg=ZqF%t3g1BQV3Uo z3x&?Lq|ATf3DM~G8W$~58SiPUnoPLxxeMafO7mqxm7!b8D5$8p`QPNYth;IVgiONW z%wtTE`{8y_DUB9^7~N%( z1eCghiVMFeMrf2LES~dEPFI;0rHBIU5Yj^3Fi1sIA&U^x3E2{vAs~`7hfG#|v?8^D zeq{Pdp6y4?{G%bvWThO5lf)_=2LQbtwFkxrI!l8HKjA@uWEsI4!`FRb+ zBpgthvh~^{B#;{vK@$p?c@UvFACQJNmk2;a)ysmQ71bONFQI(NVMpD?QxzQ2Fs!sn zOFEwh{G>p%h@UGXDn-+lqSKI(m|AH@MR;oGAp-goi-Ib=idd-ps&93-PI3!!`uvJh z%w{(Jo{q4JE+#;w<7M6f){j19O~|qVk@se2B`g_3#>{1S{>bVyrgju19GQU}s}S=_ zpr{k5;1U#B5$co=q3Ub~%8qRTH@3i`qLW&{>0qc@!{dY@6l=tOGC>DWGMSu#eU1{) zK^h~ab;tv8ay$hGt_zbC;99VkwJW>#JL9qGFWg?uzqgTXWS2X+C7Hvlnq11k-dCj) z^zH<&b@b?9x{^(i8(Inh1V7DnrjGPc;O946Q!xuU!c6*G^M z_6Hox%U4~=F58KV;&N7zD1kUgcMk9=Ur!W%p5C+N8c&;SU>TdC(GltHR`_^p7{#5f zstPw!)IlWZ22X`==;jhu1qOiM`oYcwr4z>>3B497L@3$?Nj$cYpro}U z$N!kDd^{*I_kX>_MShXb7I6E|cbpAl%*iPxyJVIC73N<%Nt7q})wr<3+ z&2!W%gl>?5UH~gW=5EYYeQhUoV=9P-s8s(kHnP&Ca>z}kU}e{D)!$=}wl1&@qdC|S z$jyWvpzP&U;(>7%k{`KPU$>7aJzLnSjb11C& zdrQ3aizl~7Z!43={yy^7@zs|?ZEJ0Z@UBFv&Iq6n+`DJ-ZoL4aRvlD3kbT5>Z6WnA z1YLG&)(Vc@KMrbOdWd^9>bVI`W3pe_&C%Z(411yi3`XA;ZTUsbw%`rLG%XV~IQGF! zL0q!_0i)v7;B}h%2kJzPcsjV83BZKsqHd&1quf$`%0cw1u{@TMR{fDBCKKDriXZnD zvcu9g>L+&H)KIfAA{%@Woc-q$wRj7S9_)-)J#iY6+B;JA<7a(T!-7>kx!uP4)Kkub zQKgj;!}cdM6M_g&8ju9$EAnnOloLl8U&=`%=fi?gH!S7PdSKX# zof1)f#wfF`5%062V#(#N&VLiH&WM7IQQ?70ni+Nv&svsMwzjye*~{~@8h<5TL?8NLc+8K+M7ARX;*62c=d zrrj=!+uj59W26)mL4_2Gy-j#pc}+g~HH8beb5^i*d4UJT3F>1FMK^+)F8=oE%7gk@ zM9HSBLVO4LlP2+(W3opn9V-6Bk&^pqWm6$B(c9R)mKcMbA@wAc;v7Nx2_+p(P#`0@ zscW;h?Rn^S`u`*1sl&C9DzTK!^q1=oY`x)Iof6O;MI@9l5?ZastR068fJuGl!6B%& zDptUS`g=0-S_}elE}qyBH3wZp$l>waydr;*;!tVvVM4BYNI##Rq~1y^?f;~y05L$$ zzt>H7O2m`$__?=9UN*)zv$zLw)IS`wXYOO!;Rl9>2kLI={ zd&l(WZz|nNqkPFw}OElX@@Ll@{s)mm(>S)dO|N1bL7S&NZno(eIBK~JlGLiO6NGwPc+ zc+5;Z!$G&yvr&=KEjgVISDn#m0QE}ig;rd?A18GqhWQh9ofW|qvcCrW(3?y#zG-!A zKl^Z2k@)qob9T{1G;^995uJ+1Wxu314C2Pt0sFbt6djt*0jsi@@-Y(VZzCBfDNS>mog`>4+Yu0tB1pl=MRe?ur{c zf+6LgcZ4=RFEbIkuf%?)@(d$kP&6+kUNV`F{|`q&n#=5bOHhonkH;z{O|zPwHSxns zy|Oxf>f<30cgGihGoH?Iz$-u&ih{lRtL4}ZKp(aGpfg>*NSGO*=~>;e;0C5ZRhHv` zD01L{v6`jRQ)$Lg5zDrL6}g`UZU8Cvxo`aQ)BCB|@C$1pxXrTsarnFYjP^U5DsQhb zJ>S*u&wSgr>U;ZuiUDKP1erWM-C5Dv3HBD#GFs z>rbuyV2)i6|v&}IJkxkXJ~LLNfzMWffN$W=lSqbC!#-s3+`fD z%=t8PmGm#QtW{|epDvXvc%fJ`?@4k_sAC5?i!Ectw`ynaVu;-x?0*r!f5v%k7NqGY zi51bE*2dPFpzTykz9UOTGfCa_D>$KK8Dd-(+snC9EEQkPtlLd#p#2%_&ZDKvmp)&b z4Uj0_F?el|I4hU%Q7M1oGhoRI@p3()kgvzoDvW+ix-+dWvQV}ue_z~I#j~=~6U@T$ zlZvu#(wU|x*}m4o1N!)D9aR|#MVnoow3V;)A%ics`Hwf zaq5*CAMAym+H-Vi;T~~ztKvwVXKOx{*OO4aLm!$*Aog|#R2GeUcFV*%n`{51_*RoZ{LCcbLb33(z=tyFe=tx5--DL7da zE7icGL|H@B-y2m?ylwgEHv;M%n9E$j;lQK|o8D$zX^>9S4G-U5+3aY*V7Zkc}Y=+t`@a=blywY4&N` zdQ&4gqRGPOtIUche$M+V7#k7x>GiEy`dE&P4RHtx9D`uk!f=C*rTB!HBcNDXU{6uF z;#RKmD=XoV>d>k*fcK8`OIH9Ch^v}s6)H`7&CDpCSOsr(rsbHU9jHrDl+(ZJ++@>47-A@M zV?yRGRjO-KJPEYS#*S}w|G*ek8i%ZJ>1rbWHuJi(%Y?Yfz@Akhoy*Mb?B&&3dJ^q- z%k1waPH-H~)En%ed>sDnqi0yU%ClLeSj`irLNpAbgU>3UCz@2JujJV!SW^zKlQ@*= z2Momj0QW zY6K5D1o5+hWWE$L-a8Sl`%A^u?pZQN(UkWm_;;{fXvwc1*fTno*;P&`mHho}ZjK-H zLVxBlS&p(HJ%lt-J_zT{-`CL@OQGZnlqDF29N}eHo*Z|vEJ?J;CQbYRZy`5Dd|BGC zi!P>FQWUU>z6)10AlqlwLCv3R**-0$32T(8&qhi-J@Lv=X6sz*;`$<`M-OD<o>__~|4;hldXM7c(-7|)3&tIE?jD3axjdGhE-=N6-y8=y28I;1Va;Upet8!}H&F#=`KT0*ovd0PUnreQ$~j4cRzBR`<_<=ri~Zr#GQI87yxj$~7W5CwK_! zxbagbLNlf>$*bzRp(%m;8z$rBk4tNOQC~R{yqMQm#>$janAvnmzY7VA`3>DzncQ!E z1!nP@&dG(*{HZnNg?snyD%=B{9i6r)9Xl}UqZWA_J-)#)Fn(|}2W2jjNtCLqZ4g_a z>j4KS4`k7s(z$c@FfR4=Jh1T6wY6Rx!%2PfAFxhU! zfcCA$hEhFjpc~(GAn(Rz|BzG*38UBJTVuQR1!F2{#i(x;PCrIW=(-g13UhqtD@#1H zrzK7)o<_j);U%saRjS#9qz97y1>fn9-|0zqjvq(i(=(x>TVrSM&Md6|M!c`HGLw&l zH#)G?Yc6q)t6~Q@r`BoScF9+JEXxI-&E#H)auNIOI_0$;`rVA4vr1-hj%c6U;g=yLurI9wk!KG964lx691aIWa z3&MOjjfCKl7+76p{w<7Hc1cAv9`95K%)`T9{&$uzhMi-u^Q|`_)QWqslyUE_a}Mpl^BoPIb)5k+M?+kf5%f zC{Rb_U5@{j~{AK?5csFvV1Gb)o<17*4to%b)s1g z8i?2I;{~EICua36$g;D>fkY8pBv z^Hs_ekV{qrR%~UPWa-lNal$m0owi(ljh*ADOEm$UHz(`XD49lt?CZ9?LFz9+<;ePp zz#_JMonF|i`d6o&tNSZy%S!x9UapNFuxHK@X5Qk7Keg;yT%Qg+qdc2+;#>nH*5c1X zDD-3;2km>)l~^(9%FnDIO;>aoR~5lp@KT?E-YVyl8@!lukwc7<7J5S$QvyHu;=4rm zq`qYGOUiZausr~Eo>vl)3rf@%<4d7uxEJ>&cYT+T_e0Y`+9CW=d>ZsLEjx8RvJah} zb0{bM&V(RUk~@N`Pu+{YM9kQd5JvJXN_&(=?$KatZkF2LEIfwm`Ob5<0BwK!nzm}r z?esOYK13MrQSxKZeN~S8iN0CN-+!!6@57$E4A_`kT3a3upP9AgH*BpZ8!)u2(8s)a z()y)Ow*vFuM;&jDgaVDzxyA@TMMJY=d6+Gid}WP8l*9-hPhw|#^lMVdQ@sbJM;~}N z1ScVWK*Hg;Qn^!t;SfsLO9l4`<}Lv@wF!tM_It(?UCESYkAc#f?obH5r=93dp|*PU z)aDGE@#&sp?M|OhR-n{4_6uZLl<%ILYI^?Iju7~2F{L4H|oHLT97#ONzu}e~h_fEn2HQjHRZ!TkZt27-AQX;i4DYMC`IiT?(GAn%^X1n9;Rg z`v84}G2ylcSeVk|P&4)Fyh};)Wc&bY<Jd@*7%Rqzl~4iNnCuoJ14@Vzq>F*Zr?KCa=))SQS^UV zb-tlu=mmR9+`UN@tXs8m{o)2%co9z@O{j=rq4J&DC|(?v$^MUrKjs1T$0+ht`_YI%=##}CL0?ZaWyj_B}@kC2Y#5I7j7DaXQ zC(OiT*1wG7IHH3R#ifqd9?n?RxI0O%@gWdQXaCjkKTwW5{Lmk&sw;s5!$kDFg7U)h z^qflq(Xo^It(==&9P;J({?r;~52$Jz@Ga$Qqp`&{nW!X2Q-T(m8L2KBTVj`DOJX%8 zDB%i?!s*eI6&i+7Ua=CC*~(4=Vk`Pks5)G7$wQiqFLv=6ZhFZLH9v2bz@JDPA3bGGj3A)HL|G9GHJzZ)5LM}F#C1ic}5$_}3VctX2j_g~s4?PF8bxb+U>^_pJ2XRk?fi!&E9y;pq(q09Ln zrjDLJ)#3iCh&BQQt*ZXlgp}c~&5p71hCos3T))PKpLosSoXm}a`Z#XM2GFVv-+;tp zLrTY{a-Fua8%uYV;e2tr>x}s_#~FLfWz*yoz(5p;UO4ybj$RUM$NRU?8&g*RT-aWG z&D-xv>m;L+{Fk~TUkI^wf7q&q@*wC`n1_72J~ibC4_FI@4AXy zsUCj<3Y6+t{xo~UF4XR7U&Rh>%nlc-Jd`~7mi+-p#coI-BjT;7iTbKq)e(2}5mjYv z%Zo`M?H2n9_Nn#pE>u?lr%qLcLJfC$nwD)u0l&vp_&IpULoYhW&|WHGdo4cD)Rx3d zku3^m<``t&d@}!5-ZLlzDqZRW#&{t~{AYQ$Kwx$q5#pVbvRF&a(QD%I44s$tj*3Ql z7daQbR6`r~00=iUj&;Tc2v9|*bi;TE{rh8bQm`X{D*P0K0%tmy`j4)F21?d{MWqv5 z$>;NENzNrSx}PqI2D0np=(GTr9pKmvbHCtTM-kKQ-fk}tL1tu(C20~MWu~LdZO1Vp z5$5kV+1gHjrO1$q~iEVf!?<|}q(NxG&31;AhmB`@wYok3Fg z7!UbzN;2Q`M+luPPom%Ed8UzskL2R_Lg`zv`~hI8>tvkhRy_?bji>c-qGbe*v(-c; zNIcX_WoJDe`@3BHPDF>V@|5Z!J%OI=y{@>$^O{8u0sP{=xeNK9C99f&7sCm|3zB81#Tt_*x)-C6xA)dUU^QL~VNgk3T?ziKS)b z_vj%$d|&$K7ytiNxf}Ms9zN{;$877H=J&rp&-#NKEgygFJHM6PhIdY7h|0M&4__<5 zJ#j*HfByk$lWKdvS=rqUxZ(52%I+LP1bZ|KX($_IBMfu4RD%=d(~&j!OxFWOm!f!0 zLT0rYU$ScN#0u_bH@)aBt{D&=AC{olbSuEhZ@HDM-v^wWt8J?~ao?K{gwrsnh;oA& z^WI${!9&|&m?6z-c76upufjs$&=Z<~Gwgt^68%f#^|9=kF2K_^mtnNAP4R}3_h6X_ zcBYH8U5W0Zeu@C_*eyIDKEQQcaDuM;vL3TQh5>+_8!Kl&8U&pDRS)*^vCkCGPq%3~ z>)!;csOmi2$5Hb8awugNVWVN=5-}EX$#CrN8#UKRP^tbcU@?S6E!yTzXyNbRa{1d6 zWE+E`7X1xlr~4#3FRLnfFQ%ryiMux(`^BraQH9ckYdzbZvO70NA|n?mJZ5)qqh2dfI4FB8oC-58 zr*r#U`Egmutb(|59mAhr9zJFMSOyA9*D8J<)JgoYPN%6>wWs;}m}7!8&EEE;c{bNi zV@{QNs!qV%&n;^YQ>~(<6aF(Bd*%?c;*aYLH)JHt363PRR)0FSPfoOG6F5gtrhJ-{ z5IiQ>y&;nW`y*PB3eXSSy*Domn}wzMuqqdBB$}PyAvBl~B%%=}P-APCn|Ba}Ddx5v zD05BvtBC*W+hi`BHzpkN~cS1uwCux)nFaFvara-He#vntnt`yL4}5wN6tJ*%&OTY z5ydEd*u>ZiMP{nBYPCL$X;pfqJ7u~1w33|>!I5XcB3{zUUkHfvPG$B^oWiglVm3pHeEm8Ejt>v#U!`gOn`mi(?f>@JU@~BH71z z>)mI<1R8n|gBrD#e3~k?WBaN*%6N>G7!&zhIjbE^MPo z0rS%q4L!Io#g_3cKQV6ub!>$vx=<9->Huh#gyy93-$$3py%Xw`{kMel= z;NGVbhTVEMwhU%r@JV66Y8)f@+&$xpLyM53fYZ~{5*-jj$CAm;&G;;fs>$dKx0n$9 zTfJ3bd{$|>X)8Id^`Ls3%YRsiNnz{snQWh5&f=CS3-|5Xl_pe6;DgU`1d_-EF50dS z^RJIiX_r@zER&`Ay}lA>y}T1M5abPUycwlulA1hX6Ld}Ag#6a6sd<0? zb(5PJ)WP70Lh>3`s_u>Q(fy=^vRG&8Yh zISNke{q_55Z6J?)(c|woovXP{fTd|kk?0IIGr|#z!W>1TB9Jcv`=K)Np}>?NQy>g6 zl{NNR(Iy#ejr2&U@?x;o*|}io;E^MIY15@})r$n^xmkkN(BY#zfbg9q4k_oBTr}r# z6&fT-^xe>zB36W8&C7uvMr-$-Iaf8Ue7iWlk-0}a&ECk+6~XNcBhv?#E6_Fe<1K=L zLf;aBb`X1W8PZ~E!PYPTIonH;{ zd7e7At=@yM4m?|N{o;Wvb}_gwN~0})>IXG`?nv{V`wuZ}j7U@+Va;01sxhOda{*(e zENjtzTg{Llj8KYDPNu8RmREAUN1qGXXF#S0-0zsIm_A2yD8(|2mF_;5;l%aMaz!{ z61_#+W)?Do9=j~hhH{No@)hekV~#SC-jEdZ-T>E($wH8Kn8C#I{%~&WI*5~w!;>(* z(vi)lAQjrQ*L-2*qKxj@O2eU|eMjsR3*pW133;}8$zhlr`l4%|Tx$hUlTjaPRQ743 z_zKFb;k$szrFi7kJXBT|P^^E4ejSc=Rtrl5{+SVlVyYAA>cGtR!y;ql&l3E1=jNQm zWEP*mS5{L`7O00=$9S7265qeXEkS$xCib9zm*wvocZ0++IaExt{*i3uas3tSohN4F z(sa$GirOP&Y2%Cds!HNQZh`G)SM?oy9Wcb}ez7F<5v;H%0+;GC47f%>XG5IpiXn)e z#3;=sUpqClhOTYRjd9izez;kjh-iou$xanl9j58cIos^8Icf!m%A+hQ8B-jOe47MV zi!}x?PeXTLWr@;YnQd#x&6?U7G{1C*dr}Fnh5HT0(ZIogfC$dBv1TGLPJC0c%N8L@ zh!Ht57o59PfTqFzak+!X_meEMqcK~ z{1&_b*3vyf{T2)!jwq#cO(3GohNz}f$6KeZsj`Vau-wy3xQ^=7souo1WEinzlPN5QEd!1`kfPJN$XQp;>Nj?8; zbxSvcFW~1=NPC&0W4QyOa=rMQ8HqDqXX{Hei@h>WLheNbUuiLCVp1W6f<$DsCshSD zvhFeqP7w(|um6Jh6sexrY;A-Cds30riQb4pOe&f=b3B;%oeJP|Z|9|MG08ujZfL{R zOL%GEOvtzy9oXB+Iy$9mFdbdp2d%rX?47RRCBB4or}9#81MH?Hk^KX+FN*qs$xGo7$6VR%Ig1?& z?igkcW!_9tU$pE+*D9FC0K0rb8Fd!dwqi_NdJ2CR_BaP3`}(Jt8BAyIgL$!}P_6vL zo2#LAwOU?0JDEWx$N$r=WcQiz*H{S`9e?sS2o(i`(8`63m>4*h=5tCW&Tfuy?e zfAP5j3zSlBEan&D<=Favnzn>iWr5&8kOc&;#g>~)w3m9o>CO{ zk6-e(ySf@lsPK>UB5+|(HMGUwLxAOYMJ#Q7avt*R%&m|yBNrW)`z5t~yZpUFApp{_ z%e1H@GO~nGO{35lDOOp^qQ41J9C=enucpvEvdGvTA$5`3(L%NSBi>Mdpl&GzOREY+nyWGqR202%w ztiT&~+f@{{4t$$-M5#xgTp=8iuBoksd8H1z+zS~H4ugrt?y|_Ab`){4+}tEZ~>_HH`T*ZpY5e$%hx?REO8Zs2v>KkkWHN zPMTR=+4uwo6D0s-@f`dJ8n`GtPv*tqS|r1c)m&TP}B6PBX8g5x{cz<6CWE*l0_;qm234b?BFC*JT{ z`+ueGJ1cl3Y20^8t@gLPiP}4s^jx(*FhP!8fC+3l^?5mxP}=@qERqbDw}h}3=w4`- z+5c(Z>>Qex<9_-Gf;f7HOlyv!WvY;7TXotQ|6%|LZEV78!TmOCNxH?7ixJn0Z`s{i zTLz@}^^Ks5wB1{(_52F@ko2_bBGV$Znt3|7hhI;z(jXkZFzvd$mw4tRU7I*NaRKC3 zmKF`~=+7T6E*|LZ9WLChQc2>)Rhsv_KdX>uwG+^ON@A{`WKVGBfbXI7EZxP*@}ohS zbuR|j43g!JuJnRbT)6dgyK*v3r6l}&889}uve9S*D&3?ZB^a#5!iYUoZy!c}qFM$@ z6EZ{84Y63?jR@qBz%id$iq0>H)Cig))3s+Riw|QnTh9jP_ey3G`}>xf{AQ^xUKn@? z+rvQ^Gz{Ca@9s3MFah)$8rP@*KTJiZJ0I3M1R3HVoXbM#{0e>Iuz8@U(JWF<dW}gM6L5E;kQ!oImys`^oA$pmjO1?Uzz1;~M=X z7Y*9y9AcrCzhoQ-+sXlHKdwSDR zpO08u{bu^uXpK^#2V_w6>oM2aU^rPnkPV2&(-*kLg?2QmwClmz$h^(>$JL zg8kJ9maJ?on8fEsHjrXXtRfC1sp|d1R95NgfKk=dU|Wx;kxG(d!a`ZE#`hShvt39A zpZR_36i6jS>bmA#%`s(49@8)PvGticc1mj3LlA{qym;tZ9gR@jn9AT;wIxb-XU*1k z&NI5gM%W^Y=s@LOyY=C11|avH+DZTN*y-2J(`i)bU)$f0)?kEFa8pUJHs zU|3UN00Rm71v}=XuDasPj7Vb{EN*`4Rv#mI`p3ph%4HLcl~)&lW#D93NPz;JKJv}( zoY}#f$@7_!9$ceT$80ZhPgA3gPu~ZH!2ZR5<9%O~Wu9?lyq}gw^V3PO6+XV`Q~xXa zX235czYP>8PwHd;d9#fTng>YJD{6UR&S(+c=eJe?BRFXrnmK`N`?B+9vp16x7}+Pt zB3~~A>}p6&ZLKt8cob3t=2=Ut%2Lxu`f5oO59!A^z;lG$+E~OD0|}O#YJ3)#a{g@~ zPYbeVm_H(!Aok$q{D!z1r1kpSn(}#{Q7YxG8W4+LGTD$Fo=J;>6a%dP56*5G+Sk`N zw14p3AqF&RZ6185+04jn?nZ__F3GR5aoHW(oDurb=4|U_a4*ptml>XtLLfc;SWA}m z5}>(nd;#JZ=U+0;kxw&saeDScAcaTZ7R!S-!e zNBkZb+PP?=y!CjlzLKoH{i2ywvGMaAX?umSb*J?uBWfuzmZeBAseD14+ho|cGVbC& z8SC5KV6B?nu*1FTPgHH#YKy%o_jGx3h;bV zRbjy#BN@o{;a$+^>++>jZ=Y7N@0+h?oN|wRnWXTj4^#K+EF_RXWem9%LOgfOnOOJS zdt^i{a*pMQA9$laS0l?J-w|BANnrNCdCkv5KUH332`J;231IIWQE1n%*LNX1 zT)Z}@!9a(LM|Lp%F}2$rzo`buuK&m{h-_FtEGP?YP~d`@Ty7w9cP$Q`<;vlJ zHBm(DLj*H>QVi3?2me(Ik`!Ww9x@npHB&aGgSzlLfFDkop()&R38Tnuvh_1i7ZLNI+HH{$Xfqn+Sy3!%l*r`!t3o z9pS7_hMcrK3H+)qC)#32KL|u8SdhpN5Hz&;+GfzS*LvC-)Hj7KgKa1>z#eEHfDVA! z46bTNwrmHM>)(kjchwGVglc9R~n3s2L4(wK5DWdjaYsHoHE{^0tpH@j5)ye5uQDZZxHToy zSdWRjB^JRC&Gy-&-d=U=mdGH9WU4M@2$7oTlSc6a{`4Mwdt-6jj$vLRMy>z6|D??! z**rAJcJ$Vk{#$2* z0NQEk_9#awsZQ!^YFp@|rxWvD&I1n-s5W+oruC4mNtXF=t$ zpjwbwxq2H2sJZ#2d*S_LFgSicT2?FDz>Jh_lqK!=iEjWL)(2;GR$Gsk7^0UJR(^V* zB;%RhLgTLi@M5~BU$FcYs%4?cTSq0Zqd_b<8H5Vq2e^|_ly+Z%zqMKMYWyb;6u7&N z?n3A%dzi6hA{N|5BO1~QAB?VG+yEb&*CS$!_AuYmK;k>wi$>j2l$^p78ier zBrVy6&*&15izMoU1^EkDVckwrgv{%)rf95e{OsZzg+c2OadK6DoCxnkxj4P=HS>OW z%hUCkV7R(^NO06uQhM2SSWsOp7U<*?w`&`YjEKaD%&dbCHmK!5G z?Y9UPTbo+yT5ar-ZHZQPMs9_9F1P;K*lJ%Xt^BjBr43jkqFY2|?p>}PA|XdBYZneS zZh>LjB1og5hHmhqRdz;}WF1i%(gfYXF#DjVo3jfC9k;;oP2qvXHmIwY7^ULMU_@@T zR~9a{%VqdBu@X<&u49@0;Zn?h>7lMbfxm5umQ@%G^stC}3PuIkg<3VcAQ zf~f>P2QgwSNrJ)vwo`Ci`g&() zPYm4Mm7mwu-IZUkXI(5poG(BX8 zMs#;=!%C>KDg#q6s2@mxB##YY#+5Owl)^C?6>gY>xZFeEd~(|A5S-cMnqHvp3Wvl) zIfX}QY0H#z1M&38)LV6GR|UZuWBm(qS}^4I5UV`Y0$!`$E!C?Wkx+!KSs4e3DNDCR zk=ziG$%_fdFOpyP8o=Sfc5xYqFMy9X zF11ejf$uq7V8c{9*avdtrSg*mA_ z?dj1>-Gx4d`)_)zHZ6em7opQC32~cCV-DHn_gUcZmblCSBdW)-Cto9pA%0C`tM2*l z?@yC0_;@Q^rT|GTOKUWr0WT(K)dvWqbpZLE~j`QuJo32?tqaa{k_1i7GP=y7H|c@Uj+sgc!BUDjG^$FNG89c3S-7u(#a} z;;Tv)JWvO)W@RUE`iJzl_CYR@7_^nA{eruS&x_Hk+n34d7~;(Qo1n2&D|bvRnHF$7GXilbcZ^Qwj*G>zvAxr{;7M9}tT7xe zZ7eM-^+>k?dbf-A2sRjwcseL=qwMqAe|Rfu)Xe1dt`qVVXE_Dq`+MGGzuj*08T6#Q z5PsTJ`!B7S8R4hrX|(iwBW0&qdRX{L`5Fy9FS0@5$n*%1P32#8*1Gy&giX@&)GhX@ zcbd#qWN_*&a#yCtzmb(VFQ+Rezmh*KQjMFX=D)I4Xh)s3Ze!_q(nXlIZ5A1(jaQ>{ zspiXRz5&3q>`@bbyJOoqdV2j!^!9CGw`*#{R;$ceT6+DC^B77RJ&z<-vhl=zv^(#a8s{s;1gWwys&v)`1N8{ME02x1_1^ z{`)!5l>=XvS~!`lVvn9|h}$j@dS_VT5Vw!!kE&2a0-nlcZ$wokVu9E?Jj6X&AGb{) z3~gY?2f5w)s><0)iw<VKuec%f^3f-yJvK^wtD$aU#lK$90(X1+2+K^jJbn;;8XwF-M+%&pM62{52@DfpV ze`_wKFzvrKtCZ>8`IcX5`apiMPZUn7K?K=?{D4gAgLCn_g-ns}q4D4mi8_E;bnbZM zlp`XYXlcgv1f#h71X)Ntn8aTb(!fbDcuOYY85)EOqP zl!lEj}t+ zSIAYq(2;XU6<59>&1#bD5(Z?o5bI@iquEtHE7F@FVgG~U&anB}WzLv}w>YcM@?lpN z_}airFOl8j^qduF|Np)hE?2V}nrU)*6V1~hyb{QPn?Tu2^V)$(x#{3GhV_jGS>XN6 z|3oE2uMpVtr9drwaNH6$J$sss<2tA7ES;^9u3h*fWW34gKFiSlPuT8id_ysf!!Dv3 zJ4ja~`QjRiXlza~&D23gAKGUw9!4EDBQsEM^$VJR)skhdywH02J+SP1Z23me@i3I4 ziJdzi$BOZbp{0Bl#1b>*(MX1z?FDCdzKw|bV5(*5+tf56F35Mu}*I9t17tn_X;sAFgD7Z?bjYNcw($Y|SouPGP|mB&wRU zi=>{MMo^xyp?J~|P9lfe@T^RyTG(%tdcqId3^hEEl~&qN5h3Ywj+Eguf)o<)3hJ&i z*mgYGJ(ICrw=!lmrZ_jO$?YOMn7>sG>S8Q3p*g*{E85LzmS`&dF`P%iZ&I6NOpDo6OW&;3_dSUGToafX>z zuG(52qdnMHGOJImJE1P@RvfRuX0_pKg=@zd{-=Ot6CZ)9j6%{BC8~@p5|xsTLBdo_ z3}KqgD+0A?FnCNsNp$B&+X22w8=<)FS~=eB<%PhVnp@=t2eV zhk;}G=G4LvkoOt>mXZMBr@r*-0;P{W0b~bOBN6p?Z(+<@7#(Y{57Tl(c|3t1@hyu+ z!RIKad{;|a&vSGDopQU-1hYykviDH+l5YB9pkD8Qu7&tD!a)usJz2D?W6)S< zZve!Y`1a&SfYEVX(gj}lMI?hJ!WS8}6jr_-r9pt1_E zY^aZ9@~LqcIEqLw+PPxs^bR`3Z<*5Z$2WX4L+Z0nnTHD4n2<(Oj2FvFuxc%;P$

      <&@$xJ=1*qibF8}O9UNvYAY5E=Ynx4^B-^`#_rU< zE((dyI8^kHU_k5bEB5FeznIW*U~9IzLEAN-1LCLJIIb;1$&@E|&+0RmxC;JO4pPan z&4DfM&B|K9CQdv|4(}M`+v7Xxwoy)5<{X2i+&=9C8K`tR5jj#(Jsf??CFQhA-mc40 zTQ+8=0d4Ca*F<}Gnsh=lHHsbLNh@RxLU(xg?IiVjRLB!UV|4z66E*L$+JGJ&E{NIW z5vFNihjfq@LD%ZlHNl}ShDJhyIQ&fNOiD*t(l*~vWeEvmh9`(bkz>CY$jDpk#mI;libY%(`y#hRd?P1bjVE=iEEQ2dc>ZA3QaOniKAzNZc@BySxyEN_X_BcN}O|3 zT=cMg$gR>?MIw0$9lH!9;C35J4lTcVmS>*Kp+~{Qf*9v6nrXBr;pXd_UgvMvC9?(%Yrwk~;Crb}9S zZT7;rw?1>M#~_qHzgurnwg-^A3qe5=k>K*ob;y=(e3BQ+m$!rxr?1tJ+LSQCnF+5i zPbF3LgtLJ0pej$W%RSeTvvmEFe5QPPYX}LabqglwYxOm#jF=6i#;>x&=D4!EH$F~E zDxgDKVjCYe54e4RIzx+ z&w-38H=hE)zHMY}DTlRG@RArVE*|}Tb`;o}knTZ{yp^T;g6W2QkQ+D?7T%dxHQ*Cs9RbEEmdD3D3afGnrL&NLWmwWT^5Tuf)a%&q*YP4K_A3upnIz80%0VnUi@ zJ3e`Y3uRAXhXF9V^*?VuSVhGbnMH0v7LIm!>n3*>ZSA8*nyWKBl#~||08H}F`U(MqJ;9Ac+gO!v&CYp*A8N`&=UOziAwe?V7UDx-uB9h{wVpQ{l zy#BurT}RH;)sm>X36u?z5Zxjw_WW`HD4cUt`G#SI&c7L72n0zkQ4n&Ci;I2-K9uSs zkACO(aD=NRE=~KQ&^6f7^rs-3?WzNP#Zb-hWGktBOep5U`rTbTFd>ef z@W#Vx{kEJN&;zcbAuEO+5i8i?8z~kR`Rm_zhqrO8O+RSG@$5CJW&XFlK{OX-3~xv{^9XVyNFv=`V9;YS#&&uLZVMA_(a+M67#1Z1T84BBvT8EITjvp3 z_!nh972r=Z-6T*lgOgx~ex8MpP+*vaBvqL6m#xsw$G@lqzR!+X=f}cnT<1c(WPn1w zsd8q$^|{ozuDQtz=)3e77aI=O4zf>0s48>axP=t~+SuC{q}6m9d7tF_v{+mP_cctG zc?uQsR5v#7T74&!?*fjiq!*S1%1~ODzYp=aVpu`I2uaWI)o=>4A})HnXFNdaYt^H- z)YTEJwFGQ?3G%WffhUKbt55!3aE=2{p|5=x*-w9dP zwVT0!)YYBS9pr)dESIO>J!0ug!{ix<8UhutR?C+dGu>D81Y0lO%#E?~$Q&HnJCNBL zPbibVA}#(=RF!lYj2^~`q=Y$^*f)b0ukn~m%a$zGHV#w8?pF4nwcVn(mrg$bV=x)v zuXJ>r56@^IS)}}B;C%N2gP6HqUmFvr9>xft7pm3FysFT~3R1N(IetV52sjg;f*b`$ z(UEe$atG$KppbrgY$ps6m}85BcSAw5)@u7itnUs_s7s1D)lIfT)2p@jsp-LhL^qwl zq&%bJ@Vas^jguBxKdgfI?>;s_&42N)KckYAlwB5LXT>>7B0h5kn$4A=biwT9#exDOimvi_x z93_R)svfB*^LUV`Nmxc>QCW~i)w3L`|EX~i;QA%gF-052`-P3pcfLuHYIV~9llXM_ zW9LVmc4cMzq3+I}_m>6YkkOLV`5mPC(2iJN4a#2v-akw0~S3cT&GR%q487j05T(I=6i zsWUH&A4-mCDn`zD>y@dmzcr>m`-Bf@36rp6Fy*CDy>nv)S10VVTOkwGi>a@4#(_w~ zRjr{Je~;1`Ku)S{#J}y;X$yps2hPl&0l1{_nd37cH2m6RbZl&*&KTELE&e1%(0)4y znLPaXuK4y(*>HQUpBn++TehXm_qw%gxTfRP*E2G{9&>9|@G_?j?{+!c;xdj9wX%^2 zGf6Q7Au7cnknXZ#fct0BGC~Fvnri^$`O+gdDChtc&nQlZXyfpYAX;RQwQ?k{;Ii`s+oPb1X z8u@~0_AA@_u>IIRFinEnHoY?4hXZ2wwiE8y9iKs0Y`=W}X7?F)Fzxb?iwA>crb&kS zX8JZBbR0uq-o?*ox9HV6Byln1%XP&h90s4dszp;}q>&998NWbVbxdX)!Pv*N855mW z=ymu;s@8F~4$kQR;Wf;0#UzXl#%D?{fNcl^SN{ydSqWpIVOwKZlaXVdMgF>AtTm@~ z9B~f3cEG$4p>BPyts@n>cQ3;^+7feFkWfBs28N;T3veCi>kgPKA`!OdNXEZ%+HnlY&$=H!l&(2m90=nbY4pUoWpU}3}4S>t^0<`=^1wlGq z#)t#~bEmC$+D+|@L7Ru^tQCNg-8M7nu&58O9Xd1p!Y6KKDf?{*n-c{%+XZQBf=h$& zmu330XNF9akh)udiOXNQ)^ICbhixenDGohW`r{ZgCNjXXj>bccDZO@OgNU}WPCLLy z$A0-ZCfv}xwcMcr#`HXQ4iwEd4q>_&Gh{q;$h`>p$I0op3E;FY!$X{G1Y8?;=mOLc zA>6_puoX z>$^~7k3PkRGR|22EJOVA#F^_K=18){+pEF#wA@OTvY=T;rJ9wv>X@{UBUNgwg8XMA5M&$rPawj5@_$DzL84q&7enL)Ye}Y$EMmd~P27VC zh}pKH;{i}`(MBIfW<%yg@Oi%+ap!Q2IUKnSnTsgIC$&5 zBIJ*`g$I68iVn@uc8I9URgB|wd=erFG_(z7o>zhKnwAnYGN)OpcF1zJD!w7*u5;CF zx`8ZDV&CQEMlzY%W<@qy7?B}B0*9d4?|x5AuWtY^<_jn zaSm$)mBsYjIeip7;_uXDpRnyWexNMub6Oq}mJ~8hn-M%1%7E{uFTXd8l%fL^^{J<# zh?p3URq>4(jD@nrqrp{KAp5kY5l!!-n5c`0pp9_Av$vkR+Z6+>6*E%C**_gZa;=KN zS|ct{Cn156U$`LEmXk1!F9;J?d8NsnE5GBOEJXj8oiyy6$IM-j-L>dn03_BoHO)*F zH7zTq+e%SjntxjI(pot^jz$-hr7V(NYL@|D5wv@)emnJVNIZA6^oLTF2?kx&u2KeXF)MViNOyGlo+h{~Z(=m*Um3`a9dTacf z3G=yBU)5Cpm!v>GUJ2A(_MYgiS$f5<_pkpw3UmItC;Su1y7t@sd~@K}e-;1#bL5Ua zY2W<^LznUIwNp69wm@Kr6~^;}{xvEw0J&#k4MWo$U|6=LUn6y_ z37iOv)klYm&k|b68P7KOH=SKK_$aZGA-3j*$yBgkrYR*F^Ado5TZn&&kqCcaUX|Ua zI^LNly(}$1*)jGZoPvlhXTk4=CbCUKqt(vJ&wyU6Xas=XLEB}99fr_qelUN~>Q9ex z9cH74eCly5UL_0#N_|}ueK57pj7NYcE9#!;WaX9CvKIy!H}NDf(^I{uY4`kwl-;*t zY*4`F^2W;SQ6T#;@`MtD#2pd-%N5aBMw-4Jmpmewl52h2h*I#*WoW9ds<+3i-qKq#P%^F$+Dws(Rh zE|N|thuhZI_~NE~-RvFO)J=RpKXw8BCN`H;Mz{^Hf+ z-!fZp5xNeC$b2#dbc_$(vAy0&cDFV?e1X%R%H3D%i>gxi!rFrdj+Jf9b`s6HsfnZu z=vHfWmXr_`4*J9=N|G{3=KKb*QxS&Tqz9rylZBN}P6m-{uxbh1ie+Lw7FiS8tcvES zQ7fri5>r&;Rw-I@4}KjdaYVq!-uC|NFscib_fk$h5#^8-h>LN!wD`(DT`C1HQr|3^ zb_5?69EM*B`pbiUNUUfT44L(4M!XQ8a(uCdn(1n{MWrMr5F*Tltt56nu;Gl8B7M!+WnTri&70* z0^%xYlMW*rJ^z0?I6}{5!=Or>?|{%0LiV93PEZ)*H{&(ueyilkQ z#M$2;;?XsFnog}=uqS&4*%2k=nFtVW{rMYRdK$!)k0hi4W> zI)Y`!N}QEGs;~9M%A2KITVHh2hxCKmFG@=@1Luo4{O*viP25dwBZfG99UGggu$nUmrrV5-GvW^RbS(sYq93j-F+jGd zslA(~U%51h<(1C}BFk}W*%urJt?wYf9F_7)PIT-4waLWh9!|0Px8!{8Nq}F^oI&;qgjL4a^s0_iaBfAe~=x(Ta*xfx_eT7sL+$uUxa?+=Qt;(|UcwRu;3+QIC|(b@C`SlI zeRr=zq#U!+JNeX59n4-%xfaW1Br@Q?WxciJ+){=AxLm_KXL+OwKv$)--)OxHaej-! zZsH$j(`&Hv>o}^(f1Y{pTZdZ|4q%h27!!>A=#}~MCEVy4ppoBF-u$U3IBACT@3SRi zf40EXKr$N9#aueVZVqWyC7bnT$Ryui#;B05WwwpF1pxgY#^BgzHDXwv%e=hNs6^sK-4a= zO2CiV4gUOZjMZmxZS4X3Vi<%`a%#-g%3p&(d)9*s`IYYPqN?u_L$ivT#k`p*)<(h z&XL=OhN4(js8Np}c=qSrBUOEfT;EpzA=W*HwSXd%Kd1Og%s`t+6$1_6r~LA>lPAJs zJ#!RkrSeeMIs4w9tOGb#S!kX`P{h+WX4b88y~fA5xfn{*_fKt&GQgf$?9>XE8`6K;V|_kfWiPpxYqeRXDb|{kZ0;kY*4iXS)XMEvLE(n=Y{f9tPt&1ct`j2ZhK4 z+i3j1Um;)f{+0E{_t0d{Kv(?e;pfy===n=^^(m`_&##6_Lf;YYu9 zgKIp!Fx`R1u%&8ML47rNwgM5W8{SaMYFRyE zl|cp6LVNfU!A7875l0>=`)Ejfl62-xO26$N@3|pyj!R>^Iai7^se6Q}=Cog8n&N{z z5A}wUpx!AlJVoSFr@+-H1W%`2C9G``uHV? z)}{{C^Xw#VeOad~5bk)L%30D9d~)bedq*+FM%zJpv2dmM93tp#cHQ7 zNq>X5@;v;JVrqpDprmqfBg92mq>6~Em z0{+p1Q>6HU6azuLrLrpur?p2h8n-C5Ve4BFfVS*uFVD>=9eeou11OlSDuIkYfXZbo zIb}g575i33$#FrDWW3IwV{c`fOVBeBYwK%5CDra^bCeAHKS1}U#YLR!ZqGbk`vF}M zMbxH=gVRKjg;}eT5kopvkQPA);X3p+V@IdRNKvNi_N1$sR&zO9P6lQB#|V5gtx6#$ z1$BxbC5j5dbeNlb9nv}sl)};Gn&)yX6u@$Du8eQ)vZBZe1uC|4^+y}5T^+0-57m~w zB(1pX7nIsegfm%gM2LO@@5{4(mna*nNoSdp`!r&@7A2uzY!2CI>4Ml6BKHwj9SK@a zAzbSFq_vvDu3tI;ek};WQXV9jTsW~^U;fNpZuDsJJ|HMO zh>A2>_f=hKH)bJBCHL`!Gt!t#s7KTy^ua~A~D(@efLySNP|jiKop9$ zT-Y}PGn1vN--Doj_rb016Yf>W+K#&y@d$G#%q5|OrR@td{py*kDw0%Ek1B8yXepBq zWeFxpJR7-A;ZqGZu-6l4rMDT$5sm4pPHB~A{J-~>ny6!_%A7l~Y-6Q4fN>@Ky}~@Z zIGVVeoM(>BC8qwt)O_T94(Pe0W2tr&uX38yy1gH_xVtKoUVzm}y>)53#j;lUZ4Ou6 z=3u(wcXVvc^Ywsa2v8REF;FKz9phTt-)?t!Ln)P<*N`x}DKN;k^nj{M(4k2-*~!Uw zh;ZbOtoi1HVZ>0-HKM{ID&J}{F1o40~XmV1;idXi~YVuINnw<%9xlmNG%6ot;9c}3(tDqnVBP4R-hSXPndMQ9QCyLbbp zyNN`|;z$aSDiyLw!UlQn3b@LPX3G_X`n&n^KJAPP(1AD1vmkj~0@O1I&B^@jLNonc zjcZ7Ff>vbXIZ$1Brsc~h)}0YcwV{+2oJuUFbp9Uaa?OkL$5Tqg4Q0Gl^Mc#)-M#x6 z0F!M)^1cy7NEEnImBTM5Zzp0nHfXU#fewBjLItb-s)5$fb)*`jyGT_#Uri|}qmM>O z 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≶M9Zv|5`G?V+t`dH%WEMXqpk;_3Hk#c)5bow}^>GHdOg=@5pYx=49b13Ly zrJynzDRebu8rnRSaDCBo@|9g53n?-Toj%;)7@=UY|Noy}Ma{<^CTZW`I9PGat{6t2 zaDw4J3oUfqrNwG{*q4kXjQO#)d%E?I`6unq0@g?R`EPa45Y|xpiRX?D^@^QqfPjrp z`1!tA9;wNGs_VH%HtUBo8*1_;v~g)avtfdTTKYQ+w|_T@R1|@!z4MWbhw@XQ#{r8n zFBZwGL~*S&Epds65DN=S9LDmC%OD``N?Ja3mq@-KYHmwkg<5GJXlu`C1U~hF;}GUt zEV_svDDRB0pxY;h0iVB6!+t9MoPjoUN2{tnNvmp71!_;3|F6OefCDX3EgVYSHa&Y~ zU}M_~aPglu-?Am;VW9ar%IVbGkq@`{Tq5wAt!;iU^4x0?c1udN{X&%ux4V&|KJ4W8 z^-G%6%-Mk6r|NGgPt?6Gz6|&!zRxhaV4(F`GZ8rK4j__!PX?Q7^;*CQ1mh5%DvR6C z?e!IxGMT8~xw^^9NiPO6=pQ@SRODn~Fsq8jD|VmScW#=CFmzog>dWa9ISWyD>bX6* zxnZ#2TwN0;af`pj^X|5h-UR+^#wtJ!B@tKiK!pf5VjK5!fZLt zzX!?qwZ#;g@k9fI%Zc#zkaZg@VAO57d!5O3Ue&0UhOx8iALeb!OI zRvYFgz{G^aMMxIXA9Q8%Ks(ns{Ng8LwsDMRXV9_!HbeTZg#-D*^A&&kWc>8s7s|rI z=ynSoO>Z_is~m>%blt}gX#It_>e2=$ntV|mjceXzzfgYM^uWLH4|qn@T1Vx$y+(mB zr_oTl_0pJ8ZR}4L)pN;gX0*r7gHMr7oprWF%6?*dA?da-61JtJNHWwP^#36TIN#Kl zHX65H`VqI9GB?_ee-?H!Yb?FpyYadu!+UaImNKBnpF_sZf6ay)B7}^R1hodXNP?eK z4UhCNG=>^Ra(w)t-DekO(;#W^pCZ0-I00f84I3?O?7O zB%x)J$UIIl{lRxlFHDbkes}4Xh27aeZ@lufb-e26$_f|NxU&YtVUJG#2x>BxrM_VU z1LCx!(BL?8gDF{Vq$J)xNc>yL)$=)7I*g|%)(&0-;j8CsUSvGa_^S6DVMpH zb8v0T>G{!ohHICj*>1fftNM+zZtws0KCZLv(IK>C_QT1EH@fD(8$2iPv~!>Y4cV!P zSZxtr!>L43HPE+4<>ftO;-5eywKK$aj&E__@NZF$Ub2O545&GURXRcHZf+@4Q*sxY zWp376swn?@b?YH5czxG41HZOjt)n5ymnfWVn}~l0v!W9f09Qb$zl@4>j!2BD$PIWy zG%tHq1co&qIS~ddbB3;u@9Dn~xdsYnr5s8^`W*i3m@VDEnOB&F?o#)Rs5v1eUah$w>#9 z(>0sd(bH1S1!0KMO_Fm9-(30B*tAvEi!I8bj@kN6Z10~`=5`eh*Oh@TSgc~GARo=W z<9@k$y19A1xpq3jbDRG4LD38!4XW&4?>FThiiaoIiVSTRo?|YzPPb-muv!>myEIOW zt#-bt4&U((ZdDWewL`sy(~|m2`BN@QF+Q{|G9ZCrd6ta$KC*E*TQ_G)_3NXw*H*CY zbEL^@$wN>XGaLNe4bu&0*GJMUg5zS=CN)LQyqHShcMq=mcw_4h9+%ozHC=V~`$%?N z`DWW#&TppMgR}4%!g!nK9J8-_y1Mk=F&oUrC@E68`W@5@I(HpHbiw(p1ld9~Keub^ z`qq-^lE(GMES@?`16xSt_yAA?b$A0;omip zd;&xiYAZP+=TTDO@|)ZHN*1!_{_wQ@i@zy5-UG{`HCbB7dqN5AgmRL@R~5 z-KAmZe7J3gow^U;9@e_R+QBj&j{H=={FCM3!&hwmwI9#NDh>OjBmRdo%0A!wRKaXY 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