From 728d51f5ef07b8f8e6ec16c1eaeb878739c5f44b Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 13 May 2017 10:31:37 +0200 Subject: [PATCH 1/5] Be able to register future SP x509cert on the settings and publish it on SP metadata --- README.md | 19 +++++++ demo-django/saml/certs/README | 6 ++- demo-flask/saml/certs/README | 6 ++- demo_pyramid/demo_pyramid/saml/certs/README | 6 ++- src/onelogin/saml2/settings.py | 28 +++++++++++ tests/settings/settings7.json | 50 +++++++++++++++++++ .../saml2_tests/logout_request_test.py | 7 +-- .../saml2_tests/logout_response_test.py | 7 +-- .../src/OneLogin/saml2_tests/response_test.py | 10 ++-- .../src/OneLogin/saml2_tests/settings_test.py | 40 +++++++++++++-- 10 files changed, 157 insertions(+), 22 deletions(-) create mode 100644 tests/settings/settings7.json diff --git a/README.md b/README.md index 1e68f8f3..3769538a 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,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 @@ -253,6 +256,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. @@ -776,6 +788,11 @@ 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. + ### Main classes and methods ### @@ -884,6 +901,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. @@ -892,6 +910,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-django/saml/certs/README b/demo-django/saml/certs/README index 03c13737..7e837fb9 100644 --- a/demo-django/saml/certs/README +++ b/demo-django/saml/certs/README @@ -2,8 +2,10 @@ 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..7e837fb9 100644 --- a/demo-flask/saml/certs/README +++ b/demo-flask/saml/certs/README @@ -2,8 +2,10 @@ 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..7e837fb9 100644 --- a/demo_pyramid/demo_pyramid/saml/certs/README +++ b/demo_pyramid/demo_pyramid/saml/certs/README @@ -2,8 +2,10 @@ 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 307a2229..eda30cee 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -121,6 +121,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): @@ -523,6 +525,22 @@ 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. @@ -589,6 +607,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) @@ -699,6 +721,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/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index b23050a1..a9afaae6 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -20,7 +20,8 @@ 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') # assertRegexpMatches deprecated on python3 def assertRegex(self, text, regexp, msg=None): @@ -29,8 +30,8 @@ def assertRegex(self, text, regexp, msg=None): else: return self.assertRegexpMatches(text, regexp, msg) - def loadSettingsJSON(self): - filename = join(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) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 9689d688..50620587 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -22,7 +22,8 @@ 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') # assertRegexpMatches deprecated on python3 def assertRegex(self, text, regexp, msg=None): @@ -31,8 +32,8 @@ def assertRegex(self, text, regexp, msg=None): else: return self.assertRegexpMatches(text, regexp, msg) - def loadSettingsJSON(self): - filename = join(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) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index d5bb4e08..4272953e 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -20,13 +20,11 @@ class OneLogin_Saml2_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=None): - if filename: - filename = join(dirname(__file__), '..', '..', '..', 'settings', filename) - else: - filename = join(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) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index ed5d7b0a..05d1a85a 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -14,11 +14,11 @@ 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') + def loadSettingsJSON(self, name='settings1.json'): + filename = join(self.settings_path, name) if exists(filename): stream = open(filename, 'r') settings = json.load(stream) @@ -186,6 +186,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 @@ -395,6 +410,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: Mon, 15 May 2017 12:53:25 +0200 Subject: [PATCH 2/5] Be able to register more than 1 Identity Provider x509cert, linked with an specific use (signing or encryption --- README.md | 32 +++++++ src/onelogin/saml2/auth.py | 37 ++++++-- src/onelogin/saml2/logout_request.py | 8 +- src/onelogin/saml2/settings.py | 27 +++++- src/onelogin/saml2/utils.py | 22 ++++- tests/settings/settings8.json | 58 ++++++++++++ tests/src/OneLogin/saml2_tests/auth_test.py | 25 +++-- .../saml2_tests/authn_request_test.py | 9 +- .../saml2_tests/logout_request_test.py | 21 +++-- .../saml2_tests/logout_response_test.py | 15 ++- .../src/OneLogin/saml2_tests/response_test.py | 91 ++++++++++--------- .../src/OneLogin/saml2_tests/settings_test.py | 14 +-- tests/src/OneLogin/saml2_tests/utils_test.py | 19 ++-- 13 files changed, 282 insertions(+), 96 deletions(-) create mode 100644 tests/settings/settings8.json diff --git a/README.md b/README.md index 3769538a..71e43104 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,22 @@ This is the settings.json file: */ // "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': [ + // '' + // ] + // } } } ``` @@ -788,12 +804,27 @@ 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 ### Described below are the main classes and methods that can be invoked from the SAML2 library. @@ -909,6 +940,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/auth.py b/src/onelogin/saml2/auth.py index ad78b662..ea77ebd4 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -534,10 +534,15 @@ def __validate_signature(self, data, saml_type, raise_exceptions=False): ) return True - x509cert = self.get_settings().get_idp_cert() + idp_data = self.get_settings().get_idp_data() - if not x509cert: - error_msg = "In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type + 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): + error_msg = 'In order to validate the sign on the %s, the x509cert of the IdP is required' % saml_type self.__errors.append(error_msg) raise OneLogin_Saml2_Error( error_msg, @@ -559,15 +564,29 @@ def __validate_signature(self, data, saml_type, raise_exceptions=False): lowercase_urlencoding ) - if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, - OneLogin_Saml2_Utils.b64decode(signature), - x509cert, - sign_alg, - self.__settings.is_debug_active()): + if exists_multix509sign: + for cert in idp_data['x509certMulti']['signing']: + if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, + OneLogin_Saml2_Utils.b64decode(signature), + cert, + sign_alg): + return True raise OneLogin_Saml2_ValidationError( - 'Signature validation failed. %s rejected.' % saml_type, + 'Signature validation failed. %s rejected' % saml_type, OneLogin_Saml2_ValidationError.INVALID_SIGNATURE ) + else: + cert = idp_data['x509cert'] + + if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, + OneLogin_Saml2_Utils.b64decode(signature), + cert, + sign_alg, + self.__settings.is_debug_active()): + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. %s rejected' % saml_type, + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True except Exception as e: self.__error_reason = str(e) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 507f88db..7c9bf4c9 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -63,7 +63,13 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq= cert = None if 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 None: diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index eda30cee..f57ff5c8 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -120,11 +120,14 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals ) self.format_idp_cert() + if 'x509certMulti' in self.__idp: + self.format_idp_cert_multi() 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): """ Set the paths of the different folders @@ -368,14 +371,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 @@ -715,6 +725,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 f6f33051..d649d19a 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -760,7 +760,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). @@ -785,6 +785,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 """ @@ -805,8 +808,21 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid if len(signature_nodes) == 1: signature_node = signature_nodes[0] - # Raises expection if invalid - return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) + + 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.', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) else: raise OneLogin_Saml2_ValidationError( 'Expected exactly one signature node; got {}.'.format(len(signature_nodes)), 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/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 84c4728f..dff71ddd 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -22,13 +22,18 @@ 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=None): - if filename: - filename = join(dirname(__file__), '..', '..', '..', 'settings', filename) + # assertRaisesRegexp deprecated on python3 + def assertRaisesRegex(self, exception, regexp, msg=None): + if hasattr(unittest.TestCase, 'assertRaisesRegex'): + return super(OneLogin_Saml2_Auth_Test, self).assertRaisesRegex(exception, regexp, msg=msg) else: - filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json') + return self.assertRaisesRegexp(exception, regexp) + + def loadSettingsJSON(self, name='settings1.json'): + filename = join(self.settings_path, name) if exists(filename): stream = open(filename, 'r') settings = json.load(stream) @@ -144,7 +149,7 @@ def testProcessNoResponse(self): Case No Response, An exception is throw """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) - with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML Response not found'): + with self.assertRaisesRegex(OneLogin_Saml2_Error, 'SAML Response not found'): auth.process_response() self.assertEqual(auth.get_errors(), ['invalid_binding']) @@ -258,7 +263,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(OneLogin_Saml2_Error, 'SAML LogoutRequest/LogoutResponse not found'): + with self.assertRaisesRegex(OneLogin_Saml2_Error, 'SAML LogoutRequest/LogoutResponse not found'): auth.process_slo(True) self.assertEqual(auth.get_errors(), ['invalid_binding']) @@ -770,7 +775,7 @@ def testLogoutNoSLO(self): del settings_info['idp']['singleLogoutService'] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) # The Header of the redirect produces an Exception - with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'The IdP does not support Single Log Out'): + with self.assertRaisesRegex(OneLogin_Saml2_Error, 'The IdP does not support Single Log Out'): auth.logout('http://example.com/returnto') def testLogoutNameIDandSessionIndex(self): @@ -965,7 +970,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(OneLogin_Saml2_Error, "Trying to sign the SAMLRequest but can't load the SP private key"): + with self.assertRaisesRegex(OneLogin_Saml2_Error, "Trying to sign the SAMLRequest but can't load the SP private key"): auth2.add_request_signature(parameters) def testBuildResponseSignature(self): @@ -986,7 +991,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(OneLogin_Saml2_Error, "Trying to sign the SAMLResponse but can't load the SP private key"): + with self.assertRaisesRegex(OneLogin_Saml2_Error, "Trying to sign the SAMLResponse but can't load the SP private key"): auth2.add_response_signature(parameters) def testIsInValidLogoutResponseSign(self): diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 598ed115..abbded20 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -22,6 +22,7 @@ class OneLogin_Saml2_Authn_Request_Test(unittest.TestCase): + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') # assertRegexpMatches deprecated on python3 def assertRegex(self, text, regexp, msg=None): @@ -30,8 +31,8 @@ def assertRegex(self, text, regexp, msg=None): else: return self.assertRegexpMatches(text, regexp, msg) - def loadSettingsJSON(self, filename='settings1.json'): - filename = join(dirname(__file__), '..', '..', '..', 'settings', filename) + def loadSettingsJSON(self, name='settings1.json'): + filename = join(self.settings_path, name) if exists(filename): stream = open(filename, 'r') settings = json.load(stream) @@ -90,7 +91,7 @@ def testGetXML(self): authn_request = OneLogin_Saml2_Authn_Request(settings) inflated = authn_request.get_xml() - self.assertRegexpMatches(inflated, '^ something_is_wrong'): + with self.assertRaisesRegex(Exception, 'The status code of the Response was not Success, was Responder -> something_is_wrong'): response_3.check_status() def testCheckOneCondition(self): @@ -414,7 +421,7 @@ def testGetIssuers(self): 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.assertRaisesRegex(Exception, 'Issuer of the Assertion not found or multiple.'): response_5.get_issuers() def testGetSessionIndex(self): @@ -462,7 +469,7 @@ def testOnlyRetrieveAssertionWithIDThatMatchesSignatureReference(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'wrapped_response_2.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'Invalid Signature Element {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor SAML Response rejected'): + with self.assertRaisesRegex(Exception, 'Invalid Signature Element {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor SAML Response rejected'): response.is_valid(self.get_request_data(), raise_exceptions=True) nameid = response.get_nameid() self.assertEqual('root@example.com', nameid) @@ -564,7 +571,7 @@ def testValidateVersion(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_saml2.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'Unsupported SAML version'): + with self.assertRaisesRegex(Exception, 'Unsupported SAML version'): response.is_valid(self.get_request_data(), raise_exceptions=True) def testValidateID(self): @@ -575,7 +582,7 @@ def testValidateID(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_id.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'Missing ID attribute on SAML Response'): + with self.assertRaisesRegex(Exception, 'Missing ID attribute on SAML Response'): response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidReference(self): @@ -589,7 +596,7 @@ def testIsInValidReference(self): self.assertFalse(response.is_valid(self.get_request_data())) self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error()) - with self.assertRaisesRegexp(Exception, 'Signature validation failed. SAML Response rejected'): + with self.assertRaisesRegex(Exception, 'Signature validation failed. SAML Response rejected'): response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidExpired(self): @@ -605,7 +612,7 @@ def testIsInValidExpired(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'Could not validate timestamp: expired. Check system clock.'): + with self.assertRaisesRegex(Exception, 'Could not validate timestamp: expired. Check system clock.'): response_2.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidNoStatement(self): @@ -663,7 +670,7 @@ def testIsInValidNoKey(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'Signature validation failed. SAML Response rejected'): + with self.assertRaisesRegex(Exception, 'Signature validation failed. SAML Response rejected'): response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidMultipleAssertions(self): @@ -675,7 +682,7 @@ def testIsInValidMultipleAssertions(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'multiple_assertions.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'SAML Response must contain 1 assertion'): + with self.assertRaisesRegex(Exception, 'SAML Response must contain 1 assertion'): response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidEncAttrs(self): @@ -691,7 +698,7 @@ def testIsInValidEncAttrs(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'There is an EncryptedAttribute in the Response and this SP not support them'): + with self.assertRaisesRegex(Exception, 'There is an EncryptedAttribute in the Response and this SP not support them'): response_2.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidDuplicatedAttrs(self): @@ -702,7 +709,7 @@ def testIsInValidDuplicatedAttrs(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'duplicated_attributes.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'Found an Attribute element with duplicated Name'): + with self.assertRaisesRegex(Exception, 'Found an Attribute element with duplicated Name'): response.get_attributes() def testIsInValidDestination(self): @@ -794,11 +801,11 @@ def testIsInValidIssuer(self): settings.set_strict(True) response_3 = OneLogin_Saml2_Response(settings, message) - with self.assertRaisesRegexp(Exception, 'Invalid issuer in the Assertion/Response'): + with self.assertRaisesRegex(Exception, 'Invalid issuer in the Assertion/Response'): response_3.is_valid(request_data, raise_exceptions=True) response_4 = OneLogin_Saml2_Response(settings, message_2) - with self.assertRaisesRegexp(Exception, 'Invalid issuer in the Assertion/Response'): + with self.assertRaisesRegex(Exception, 'Invalid issuer in the Assertion/Response'): response_4.is_valid(request_data, raise_exceptions=True) def testIsInValidSessionIndex(self): @@ -823,7 +830,7 @@ def testIsInValidSessionIndex(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Response(settings, message) - with self.assertRaisesRegexp(Exception, 'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response'): + with self.assertRaisesRegex(Exception, 'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response'): response_2.is_valid(request_data, raise_exceptions=True) def testDatetimeWithMiliseconds(self): @@ -914,27 +921,27 @@ def testIsInValidSubjectConfirmation(self): settings.set_strict(True) response = OneLogin_Saml2_Response(settings, message) - with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + with self.assertRaisesRegex(Exception, 'A valid SubjectConfirmation was not found on this Response'): response.is_valid(request_data, raise_exceptions=True) response_2 = OneLogin_Saml2_Response(settings, message_2) - with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + with self.assertRaisesRegex(Exception, 'A valid SubjectConfirmation was not found on this Response'): response_2.is_valid(request_data, raise_exceptions=True) response_3 = OneLogin_Saml2_Response(settings, message_3) - with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + with self.assertRaisesRegex(Exception, 'A valid SubjectConfirmation was not found on this Response'): response_3.is_valid(request_data, raise_exceptions=True) response_4 = OneLogin_Saml2_Response(settings, message_4) - with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + with self.assertRaisesRegex(Exception, 'A valid SubjectConfirmation was not found on this Response'): response_4.is_valid(request_data, raise_exceptions=True) response_5 = OneLogin_Saml2_Response(settings, message_5) - with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + with self.assertRaisesRegex(Exception, 'A valid SubjectConfirmation was not found on this Response'): response_5.is_valid(request_data, raise_exceptions=True) response_6 = OneLogin_Saml2_Response(settings, message_6) - with self.assertRaisesRegexp(Exception, 'A valid SubjectConfirmation was not found on this Response'): + with self.assertRaisesRegex(Exception, 'A valid SubjectConfirmation was not found on this Response'): response_6.is_valid(request_data, raise_exceptions=True) def testIsInValidRequestId(self): @@ -960,7 +967,7 @@ def testIsInValidRequestId(self): settings.set_strict(True) response = OneLogin_Saml2_Response(settings, message) - with self.assertRaisesRegexp(Exception, 'The InResponseTo of the Response'): + with self.assertRaisesRegex(Exception, 'The InResponseTo of the Response'): response.is_valid(request_data, request_id, raise_exceptions=True) valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38' @@ -1005,7 +1012,7 @@ def testIsInValidSignIssues(self): settings_info['security']['wantAssertionsSigned'] = True settings_4 = OneLogin_Saml2_Settings(settings_info) response_4 = OneLogin_Saml2_Response(settings_4, message) - with self.assertRaisesRegexp(Exception, 'The Assertion of the Response is not signed and the SP require it'): + with self.assertRaisesRegex(Exception, 'The Assertion of the Response is not signed and the SP require it'): response_4.is_valid(request_data, raise_exceptions=True) settings_info['security']['wantAssertionsSigned'] = False @@ -1033,7 +1040,7 @@ def testIsInValidSignIssues(self): settings_info['security']['wantMessagesSigned'] = True settings_8 = OneLogin_Saml2_Settings(settings_info) response_8 = OneLogin_Saml2_Response(settings_8, message) - with self.assertRaisesRegexp(Exception, 'The Message of the Response is not signed and the SP require it'): + with self.assertRaisesRegex(Exception, 'The Message of the Response is not signed and the SP require it'): response_8.is_valid(request_data, raise_exceptions=True) def testIsInValidEncIssues(self): @@ -1115,7 +1122,7 @@ def testIsInValidCert(self): xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'Signature validation failed. SAML Response rejected'): + with self.assertRaisesRegex(Exception, 'Signature validation failed. SAML Response rejected'): response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidCert2(self): @@ -1408,5 +1415,5 @@ def testStatusCheckBeforeAssertionCheck(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - with self.assertRaisesRegexp(Exception, 'The status code of the Response was not Success, was Responder'): + with self.assertRaisesRegex(Exception, 'The status code of the Response was not Success, was Responder'): response.is_valid(self.get_request_data(), raise_exceptions=True) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 05d1a85a..fbd9a554 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -410,9 +410,9 @@ 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: Mon, 15 May 2017 13:18:34 +0200 Subject: [PATCH 3/5] pep8 --- src/onelogin/saml2/auth.py | 8 ++++---- src/onelogin/saml2/settings.py | 1 - src/onelogin/saml2/utils.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index ea77ebd4..dfd10eb2 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -579,10 +579,10 @@ def __validate_signature(self, data, saml_type, raise_exceptions=False): cert = idp_data['x509cert'] if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, - OneLogin_Saml2_Utils.b64decode(signature), - cert, - sign_alg, - self.__settings.is_debug_active()): + OneLogin_Saml2_Utils.b64decode(signature), + cert, + sign_alg, + self.__settings.is_debug_active()): raise OneLogin_Saml2_ValidationError( 'Signature validation failed. %s rejected' % saml_type, OneLogin_Saml2_ValidationError.INVALID_SIGNATURE diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index f57ff5c8..369b78f2 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -127,7 +127,6 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals self.format_sp_cert_new() self.format_sp_key() - def __load_paths(self, base_path=None): """ Set the paths of the different folders diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index d649d19a..1b60ee2a 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -808,7 +808,7 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid 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: From 701d65ec467465b89db490fd602850cbf56401a3 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 15 May 2017 13:23:21 +0200 Subject: [PATCH 4/5] Allow metadata to be retrieved from source containing data of multiple entities --- README.md | 17 ++++++ src/onelogin/saml2/idp_metadata_parser.py | 28 ++++++++-- .../metadata/idp_multiple_descriptors.xml | 53 +++++++++++++++++++ .../saml2_tests/idp_metadata_parser_test.py | 38 +++++++++++++ 4 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 tests/data/metadata/idp_multiple_descriptors.xml diff --git a/README.md b/README.md index 71e43104..e074f739 100644 --- a/README.md +++ b/README.md @@ -477,6 +477,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 diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py index a3776d7e..9f557b33 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -32,6 +32,10 @@ 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 """ @@ -61,22 +65,31 @@ 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 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, **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, + entity_id=None, index=0): """ Parses the Identity Provider metadata and return a dict with extracted data. @@ -104,6 +117,10 @@ 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 + :param index: If the metadata contains more than 1 certificate, use index to get the right certificate. :type index: number @@ -113,10 +130,13 @@ def parse( data = {} dom = OneLogin_Saml2_XML.to_etree(idp_metadata) - entity_descriptor_nodes = OneLogin_Saml2_XML.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 + entity_desc_path = '//md:EntityDescriptor' + if entity_id: + entity_desc_path += "[@entityID='%s']" % entity_id + entity_descriptor_nodes = OneLogin_Saml2_XML.query(dom, entity_desc_path) + if len(entity_descriptor_nodes) > 0: for entity_descriptor_node in entity_descriptor_nodes: idp_descriptor_nodes = OneLogin_Saml2_XML.query(entity_descriptor_node, './md:IDPSSODescriptor') diff --git a/tests/data/metadata/idp_multiple_descriptors.xml b/tests/data/metadata/idp_multiple_descriptors.xml new file mode 100644 index 00000000..a74f8e4a --- /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 + + + + + + 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 d0c6a2de..08a0ab70 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -281,6 +281,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 650d2b9efa1b8b6d6b155d420e46fb607803ec60 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 15 May 2017 13:52:13 +0200 Subject: [PATCH 5/5] 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 | 170 +++++++++--------- .../metadata/idp_metadata_multi_certs.xml | 75 ++++++++ .../saml2_tests/idp_metadata_parser_test.py | 104 +++++++++++ 3 files changed, 266 insertions(+), 83 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 9f557b33..01d3f8c6 100644 --- a/src/onelogin/saml2/idp_metadata_parser.py +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -18,7 +18,6 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.xml_utils import OneLogin_Saml2_XML -from onelogin.saml2.utils import OneLogin_Saml2_Utils class OneLogin_Saml2_IdPMetadataParser(object): @@ -89,8 +88,7 @@ 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, - index=0): + entity_id=None): """ Parses the Identity Provider metadata and return a dict with extracted data. @@ -121,16 +119,13 @@ def parse( that contains multiple EntityDescriptor. :type entity_id: string - :param index: If the metadata contains more than 1 certificate, use index to get the right certificate. - :type index: number - :returns: settings dict with extracted data :rtype: dict """ data = {} dom = OneLogin_Saml2_XML.to_etree(idp_metadata) - 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 entity_desc_path = '//md:EntityDescriptor' if entity_id: @@ -138,82 +133,83 @@ def parse( entity_descriptor_nodes = OneLogin_Saml2_XML.query(dom, entity_desc_path) if len(entity_descriptor_nodes) > 0: - for entity_descriptor_node in entity_descriptor_nodes: - idp_descriptor_nodes = OneLogin_Saml2_XML.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_XML.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_XML.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_XML.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) - - # Attempt to extract the cert/public key to be used for - # verifying signatures (as opposed to extracing a key to be - # used for encryption), by specifying `use=signing` in the - # XPath expression. If that does not yield a cert, retry - # using a more relaxed XPath expression (the `use` attribute - # is optional according to the saml-metadata-2.0-os spec). - cert_nodes = OneLogin_Saml2_XML.query( - idp_descriptor_node, - "./md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate" - ) - - if not cert_nodes: - cert_nodes = OneLogin_Saml2_XML.query( - idp_descriptor_node, - "./md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate" - ) - - if len(cert_nodes) > 0: - idp_x509_cert = OneLogin_Saml2_Utils.format_cert(cert_nodes[index].text, False) - - 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 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 + entity_descriptor_node = entity_descriptor_nodes[0] + idp_descriptor_nodes = OneLogin_Saml2_XML.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_XML.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_XML.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_XML.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_XML.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate") + encryption_nodes = OneLogin_Saml2_XML.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 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 + + 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 return data @staticmethod @@ -234,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 08a0ab70..2ab5aa27 100644 --- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -319,6 +319,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": { + "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": [ + "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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 @@ -453,3 +491,69 @@ 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": { + "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": [ + "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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)