44# All rights reserved.
55
66
7+ from copy import deepcopy
78import json
89from os .path import dirname , join , exists
910from lxml .etree import XMLSyntaxError
1011import unittest
1112
1213from onelogin .saml2 .idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
14+ from onelogin .saml2 .constants import OneLogin_Saml2_Constants
1315
1416
1517class OneLogin_Saml2_IdPMetadataParser_Test (unittest .TestCase ):
18+ # Instruct unittest to not hide diffs upon test failure, even for complex
19+ # dictionaries. This prevents the message "Diff is 907 characters long.
20+ # Set self.maxDiff to None to see it." from showing up.
21+ maxDiff = None
22+
1623 data_path = join (dirname (dirname (dirname (dirname (__file__ )))), 'data' )
1724 settings_path = join (dirname (dirname (dirname (dirname (__file__ )))), 'settings' )
1825
@@ -51,8 +58,22 @@ def testParseRemote(self):
5158
5259 data = OneLogin_Saml2_IdPMetadataParser .parse_remote ('https://www.testshib.org/metadata/testshib-providers.xml' )
5360 self .assertTrue (data is not None and data is not {})
54- 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' }}
55- self .assertEqual (expected_data , data )
61+ expected_settings_json = """
62+ {
63+ "sp": {
64+ "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
65+ },
66+ "idp": {
67+ "entityId": "https://idp.testshib.org/idp/shibboleth",
68+ "singleSignOnService": {
69+ "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
70+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
71+ }
72+ }
73+ }
74+ """
75+ expected_settings = json .loads (expected_settings_json )
76+ self .assertEqual (expected_settings , data )
5677
5778 def testParse (self ):
5879 """
@@ -67,13 +88,181 @@ def testParse(self):
6788
6889 xml_idp_metadata = self .file_contents (join (self .data_path , 'metadata' , 'idp_metadata.xml' ))
6990 data = OneLogin_Saml2_IdPMetadataParser .parse (xml_idp_metadata )
70- 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\n MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\n VQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\n MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\n DApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\n ZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\n AQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n 0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\n gf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\n Tr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\n zRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\n UAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\n A1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\n HSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\n UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\n DwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\n BgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\n AuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\n gG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\n sTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\n TbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\n QOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n 1sE=' }}
71- self .assertEqual (expected_data , data )
91+
92+ # W/o further specification, expect to get the redirect binding SSO
93+ # URL extracted.
94+ expected_settings_json = """
95+ {
96+ "idp": {
97+ "singleSignOnService": {
98+ "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123",
99+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
100+ },
101+ "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=",
102+ "entityId": "https://app.onelogin.com/saml/metadata/383123"
103+ },
104+ "sp": {
105+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
106+ }
107+ }
108+ """
109+ expected_settings = json .loads (expected_settings_json )
110+ self .assertEqual (expected_settings , data )
111+
112+ def test_parse_testshib_required_binding_sso_redirect (self ):
113+ """
114+ Test with testshib metadata.
115+
116+ Especially test extracting SSO with REDIRECT binding.
117+
118+ Note that the testshib metadata does not contain an SLO specification
119+ in the first <IDPSSODescriptor> tag.
120+ """
121+ expected_settings_json = """
122+ {
123+ "sp": {
124+ "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
125+ },
126+ "idp": {
127+ "entityId": "https://idp.testshib.org/idp/shibboleth",
128+ "singleSignOnService": {
129+ "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
130+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
131+ }
132+ }
133+ }
134+ """
135+ xmldoc = OneLogin_Saml2_IdPMetadataParser .get_metadata (
136+ 'https://www.testshib.org/metadata/testshib-providers.xml' )
137+ # Parse, require SSO REDIRECT binding, implicitly.
138+ settings1 = OneLogin_Saml2_IdPMetadataParser .parse (xmldoc )
139+ # Parse, require SSO REDIRECT binding, explicitly.
140+ settings2 = OneLogin_Saml2_IdPMetadataParser .parse (
141+ xmldoc ,
142+ required_sso_binding = OneLogin_Saml2_Constants .BINDING_HTTP_REDIRECT
143+ )
144+ expected_settings = json .loads (expected_settings_json )
145+ self .assertEqual (expected_settings , settings1 )
146+ self .assertEqual (expected_settings , settings2 )
147+
148+ def test_parse_testshib_required_binding_sso_post (self ):
149+ """
150+ Test with testshib metadata.
151+
152+ Especially test extracting SSO with POST binding.
153+ """
154+ expected_settings_json = """
155+ {
156+ "sp": {
157+ "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
158+ },
159+ "idp": {
160+ "entityId": "https://idp.testshib.org/idp/shibboleth",
161+ "singleSignOnService": {
162+ "url": "https://idp.testshib.org/idp/profile/SAML2/POST/SSO",
163+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
164+ }
165+ }
166+ }
167+ """
168+ xmldoc = OneLogin_Saml2_IdPMetadataParser .get_metadata (
169+ 'https://www.testshib.org/metadata/testshib-providers.xml' )
170+ # Parse, require POST binding.
171+ settings = OneLogin_Saml2_IdPMetadataParser .parse (
172+ xmldoc ,
173+ required_sso_binding = OneLogin_Saml2_Constants .BINDING_HTTP_POST
174+ )
175+ expected_settings = json .loads (expected_settings_json )
176+ self .assertEqual (expected_settings , settings )
177+
178+ def test_parse_oktadev_required_binding_all (self ):
179+ """
180+ Test all combinations of the `require_slo_binding` and
181+ `require_sso_binding` parameters with the oktadev IdP metadata.
182+
183+ Note: Oktadev's dev IdP metadata contains a single logout (SLO)
184+ service and does not specify any endpoint for the POST binding.
185+ """
186+ expected_settings_json = """
187+ {
188+ "sp": {
189+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
190+ },
191+ "idp": {
192+ "entityId": "urn:example:idp",
193+ "x509cert": "MIIDPDCCAiQCCQDydJgOlszqbzANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEQMA4GA1UEChMHSmFua3lDbzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDMxMjE5NDYzM1oXDTI3MTExOTE5NDYzM1owYDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEDAOBgNVBAoTB0phbmt5Q28xEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMGvJpRTTasRUSPqcbqCG+ZnTAurnu0vVpIG9lzExnh11o/BGmzu7lB+yLHcEdwrKBBmpepDBPCYxpVajvuEhZdKFx/Fdy6j5mH3rrW0Bh/zd36CoUNjbbhHyTjeM7FN2yF3u9lcyubuvOzr3B3gX66IwJlU46+wzcQVhSOlMk2tXR+fIKQExFrOuK9tbX3JIBUqItpI+HnAow509CnM134svw8PTFLkR6/CcMqnDfDK1m993PyoC1Y+N4X9XkhSmEQoAlAHPI5LHrvuujM13nvtoVYvKYoj7ScgumkpWNEvX652LfXOnKYlkB8ZybuxmFfIkzedQrbJsyOhfL03cMECAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAeHwzqwnzGEkxjzSD47imXaTqtYyETZow7XwBc0ZaFS50qRFJUgKTAmKS1xQBP/qHpStsROT35DUxJAE6NY1Kbq3ZbCuhGoSlY0L7VzVT5tpu4EY8+Dq/u2EjRmmhoL7UkskvIZ2n1DdERtd+YUMTeqYl9co43csZwDno/IKomeN5qaPc39IZjikJ+nUC6kPFKeu/3j9rgHNlRtocI6S1FdtFz9OZMQlpr0JbUt2T3xS/YoQJn6coDmJL5GTiiKM6cOe+Ur1VwzS1JEDbSS2TWWhzq8ojLdrotYLGd9JOsoQhElmz+tMfCFQUFLExinPAyy7YHlSiVX13QH2XTu/iQQ==",
194+ "singleSignOnService": {
195+ "url": "http://idp.oktadev.com",
196+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
197+ },
198+ "singleLogoutService": {
199+ "url": "http://idp.oktadev.com/logout",
200+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
201+ }
202+ }
203+ }
204+ """
205+ xmldoc = OneLogin_Saml2_IdPMetadataParser .get_metadata (
206+ 'http://idp.oktadev.com/metadata' )
207+ expected_settings = json .loads (expected_settings_json )
208+
209+ # Parse, require SLO and SSO REDIRECT binding, implicitly.
210+ settings1 = OneLogin_Saml2_IdPMetadataParser .parse (xmldoc )
211+
212+ # Parse, require SLO and SSO REDIRECT binding, explicitly.
213+ settings2 = OneLogin_Saml2_IdPMetadataParser .parse (
214+ xmldoc ,
215+ required_sso_binding = OneLogin_Saml2_Constants .BINDING_HTTP_REDIRECT ,
216+ required_slo_binding = OneLogin_Saml2_Constants .BINDING_HTTP_REDIRECT
217+ )
218+ expected_settings1_2 = deepcopy (expected_settings )
219+ self .assertEqual (expected_settings1_2 , settings1 )
220+ self .assertEqual (expected_settings1_2 , settings2 )
221+
222+ settings3 = OneLogin_Saml2_IdPMetadataParser .parse (
223+ xmldoc ,
224+ required_sso_binding = OneLogin_Saml2_Constants .BINDING_HTTP_POST ,
225+ required_slo_binding = OneLogin_Saml2_Constants .BINDING_HTTP_POST
226+ )
227+ # Oktadev does not specify any POST binding endpoint.
228+ expected_settings3 = deepcopy (expected_settings )
229+ del expected_settings3 ['idp' ]['singleLogoutService' ]
230+ del expected_settings3 ['idp' ]['singleSignOnService' ]
231+ self .assertEqual (expected_settings3 , settings3 )
232+
233+ settings4 = OneLogin_Saml2_IdPMetadataParser .parse (
234+ xmldoc ,
235+ required_sso_binding = OneLogin_Saml2_Constants .BINDING_HTTP_POST ,
236+ required_slo_binding = OneLogin_Saml2_Constants .BINDING_HTTP_REDIRECT
237+ )
238+ settings5 = OneLogin_Saml2_IdPMetadataParser .parse (
239+ xmldoc ,
240+ required_sso_binding = OneLogin_Saml2_Constants .BINDING_HTTP_POST
241+ )
242+ expected_settings4_5 = deepcopy (expected_settings )
243+ del expected_settings4_5 ['idp' ]['singleSignOnService' ]
244+ self .assertEqual (expected_settings4_5 , settings4 )
245+ self .assertEqual (expected_settings4_5 , settings5 )
246+
247+ settings6 = OneLogin_Saml2_IdPMetadataParser .parse (
248+ xmldoc ,
249+ required_sso_binding = OneLogin_Saml2_Constants .BINDING_HTTP_REDIRECT ,
250+ required_slo_binding = OneLogin_Saml2_Constants .BINDING_HTTP_POST
251+ )
252+ settings7 = OneLogin_Saml2_IdPMetadataParser .parse (
253+ xmldoc ,
254+ required_slo_binding = OneLogin_Saml2_Constants .BINDING_HTTP_POST
255+ )
256+ expected_settings6_7 = deepcopy (expected_settings )
257+ del expected_settings6_7 ['idp' ]['singleLogoutService' ]
258+ self .assertEqual (expected_settings6_7 , settings6 )
259+ self .assertEqual (expected_settings6_7 , settings7 )
72260
73261 def testMergeSettings (self ):
74262 """
75263 Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser
76264 """
265+
77266 with self .assertRaises (AttributeError ):
78267 settings_result = OneLogin_Saml2_IdPMetadataParser .merge_settings (None , {})
79268
0 commit comments