PageRenderTime 52ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/djangosaml2/tests/__init__.py

https://bitbucket.org/Lundberg/djangosaml2
Python | 337 lines | 305 code | 17 blank | 15 comment | 0 complexity | 870b1c1e9e078878088a1df3515f607e MD5 | raw file
Possible License(s): Apache-2.0
  1. # Copyright (C) 2011 Yaco Sistemas (http://www.yaco.es)
  2. # Copyright (C) 2010 Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import datetime
  16. import base64
  17. import re
  18. import urlparse
  19. from django.conf import settings
  20. from django.contrib.auth import SESSION_KEY
  21. from django.contrib.auth.models import User
  22. from django.test import TestCase
  23. from saml2.s_utils import decode_base64_and_inflate, deflate_and_base64_encode
  24. from djangosaml2 import views
  25. from djangosaml2.cache import OutstandingQueriesCache
  26. from djangosaml2.tests import conf
  27. from djangosaml2.tests.auth_response import auth_response
  28. from djangosaml2.signals import post_authenticated
  29. class SAML2Tests(TestCase):
  30. urls = 'djangosaml2.urls'
  31. def assertSAMLRequestsEquals(self, xml1, xml2):
  32. def remove_variable_attributes(xml_string):
  33. xml_string = re.sub(r' ID=".*?" ', ' ', xml_string)
  34. xml_string = re.sub(r' IssueInstant=".*?" ', ' ', xml_string)
  35. xml_string = re.sub(
  36. r'<saml:NameID>.*</saml:NameID>',
  37. '<saml:NameID></saml:NameID>',
  38. xml_string)
  39. return xml_string
  40. self.assertEquals(remove_variable_attributes(xml1),
  41. remove_variable_attributes(xml2))
  42. def init_cookies(self):
  43. self.client.cookies[settings.SESSION_COOKIE_NAME] = 'testing'
  44. def add_outstanding_query(self, session_id, came_from):
  45. session = self.client.session
  46. oq_cache = OutstandingQueriesCache(session)
  47. oq_cache.set(session_id, came_from)
  48. session.save()
  49. self.client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key
  50. def test_login_one_idp(self):
  51. # monkey patch SAML configuration
  52. settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
  53. idp_hosts=['idp.example.com'])
  54. response = self.client.get('/login/')
  55. self.assertEquals(response.status_code, 302)
  56. location = response['Location']
  57. url = urlparse.urlparse(location)
  58. self.assertEquals(url.hostname, 'idp.example.com')
  59. self.assertEquals(url.path, '/simplesaml/saml2/idp/SSOService.php')
  60. params = urlparse.parse_qs(url.query)
  61. self.assert_('SAMLRequest' in params)
  62. self.assert_('RelayState' in params)
  63. saml_request = params['SAMLRequest'][0]
  64. expected_request = """<?xml version='1.0' encoding='UTF-8'?>
  65. <samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" ProviderName="Test SP" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="true" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" /></samlp:AuthnRequest>"""
  66. xml = decode_base64_and_inflate(saml_request)
  67. self.assertSAMLRequestsEquals(expected_request, xml)
  68. # if we set a next arg in the login view, it is preserverd
  69. # in the RelayState argument
  70. next = '/another-view/'
  71. response = self.client.get('/login/', {'next': next})
  72. self.assertEquals(response.status_code, 302)
  73. location = response['Location']
  74. url = urlparse.urlparse(location)
  75. self.assertEquals(url.hostname, 'idp.example.com')
  76. self.assertEquals(url.path, '/simplesaml/saml2/idp/SSOService.php')
  77. params = urlparse.parse_qs(url.query)
  78. self.assert_('SAMLRequest' in params)
  79. self.assert_('RelayState' in params)
  80. self.assertEquals(params['RelayState'][0], next)
  81. def test_login_several_idps(self):
  82. settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
  83. idp_hosts=['idp1.example.com',
  84. 'idp2.example.com',
  85. 'idp3.example.com'])
  86. response = self.client.get('/login/')
  87. # a WAYF page should be displayed
  88. self.assertContains(response, 'Where are you from?', status_code=200)
  89. for i in range(1, 4):
  90. link = '/login/?idp=https://idp%d.example.com/simplesaml/saml2/idp/metadata.php&next=/'
  91. self.assertContains(response, link % i)
  92. # click on the second idp
  93. response = self.client.get('/login/', {
  94. 'idp': 'https://idp2.example.com/simplesaml/saml2/idp/metadata.php',
  95. 'next': '/',
  96. })
  97. self.assertEquals(response.status_code, 302)
  98. location = response['Location']
  99. url = urlparse.urlparse(location)
  100. self.assertEquals(url.hostname, 'idp2.example.com')
  101. self.assertEquals(url.path, '/simplesaml/saml2/idp/SSOService.php')
  102. params = urlparse.parse_qs(url.query)
  103. self.assert_('SAMLRequest' in params)
  104. self.assert_('RelayState' in params)
  105. saml_request = params['SAMLRequest'][0]
  106. expected_request = """<?xml version='1.0' encoding='UTF-8'?>
  107. <samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" ProviderName="Test SP" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="true" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" /></samlp:AuthnRequest>"""
  108. xml = decode_base64_and_inflate(saml_request)
  109. self.assertSAMLRequestsEquals(expected_request, xml)
  110. def test_assertion_consumer_service(self):
  111. # there are no users in the database
  112. self.assertEquals(User.objects.count(), 0)
  113. settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
  114. idp_hosts=['idp.example.com'])
  115. config = views.config_settings_loader()
  116. # session_id should start with a letter since it is a NCName
  117. session_id = "a0123456789abcdef0123456789abcdef"
  118. came_from = '/another-view/'
  119. saml_response = auth_response({'uid': 'student'}, session_id, config)
  120. self.init_cookies()
  121. self.add_outstanding_query(session_id, came_from)
  122. # this will create a user
  123. response = self.client.post('/acs/', {
  124. 'SAMLResponse': base64.b64encode(str(saml_response)),
  125. 'RelayState': came_from,
  126. })
  127. self.assertEquals(response.status_code, 302)
  128. location = response['Location']
  129. url = urlparse.urlparse(location)
  130. self.assertEquals(url.hostname, 'testserver')
  131. self.assertEquals(url.path, came_from)
  132. self.assertEquals(User.objects.count(), 1)
  133. user_id = self.client.session[SESSION_KEY]
  134. user = User.objects.get(id=user_id)
  135. self.assertEquals(user.username, 'student')
  136. # let's create another user and log in with that one
  137. new_user = User.objects.create(username='teacher', password='not-used')
  138. session_id = "a1111111111111111111111111111111"
  139. came_from = '/'
  140. saml_response = auth_response({'uid': 'teacher'}, session_id, config)
  141. self.add_outstanding_query(session_id, came_from)
  142. response = self.client.post('/acs/', {
  143. 'SAMLResponse': base64.b64encode(str(saml_response)),
  144. 'RelayState': came_from,
  145. })
  146. self.assertEquals(response.status_code, 302)
  147. self.assertEquals(new_user.id, self.client.session[SESSION_KEY])
  148. def do_login(self):
  149. """Auxiliary method used in several tests (mainly logout tests)"""
  150. config = views.config_settings_loader()
  151. session_id = "a0123456789abcdef0123456789abcdef"
  152. came_from = '/another-view/'
  153. saml_response = auth_response({'uid': 'student'}, session_id, config)
  154. self.init_cookies()
  155. self.add_outstanding_query(session_id, came_from)
  156. # this will create a user
  157. response = self.client.post('/acs/', {
  158. 'SAMLResponse': base64.b64encode(str(saml_response)),
  159. 'RelayState': came_from,
  160. })
  161. self.assertEquals(response.status_code, 302)
  162. def test_logout(self):
  163. settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
  164. idp_hosts=['idp.example.com'])
  165. self.do_login()
  166. response = self.client.get('/logout/')
  167. self.assertEquals(response.status_code, 302)
  168. location = response['Location']
  169. url = urlparse.urlparse(location)
  170. self.assertEquals(url.hostname, 'idp.example.com')
  171. self.assertEquals(url.path,
  172. '/simplesaml/saml2/idp/SingleLogoutService.php')
  173. params = urlparse.parse_qs(url.query)
  174. self.assert_('SAMLRequest' in params)
  175. saml_request = params['SAMLRequest'][0]
  176. expected_request = """<?xml version='1.0' encoding='UTF-8'?>
  177. <samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID>58bcc81ea14700f66aeb707a0eff1360</saml:NameID></samlp:LogoutRequest>"""
  178. xml = decode_base64_and_inflate(saml_request)
  179. self.assertSAMLRequestsEquals(expected_request, xml)
  180. def test_logout_service_local(self):
  181. settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
  182. idp_hosts=['idp.example.com'])
  183. self.do_login()
  184. response = self.client.get('/logout/')
  185. self.assertEquals(response.status_code, 302)
  186. location = response['Location']
  187. url = urlparse.urlparse(location)
  188. self.assertEquals(url.hostname, 'idp.example.com')
  189. self.assertEquals(url.path,
  190. '/simplesaml/saml2/idp/SingleLogoutService.php')
  191. params = urlparse.parse_qs(url.query)
  192. self.assert_('SAMLRequest' in params)
  193. saml_request = params['SAMLRequest'][0]
  194. expected_request = """<?xml version='1.0' encoding='UTF-8'?>
  195. <samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID>58bcc81ea14700f66aeb707a0eff1360</saml:NameID></samlp:LogoutRequest>"""
  196. xml = decode_base64_and_inflate(saml_request)
  197. self.assertSAMLRequestsEquals(expected_request, xml)
  198. # now simulate a logout response sent by the idp
  199. request_id = re.findall(r' ID="(.*?)" ', xml)[0]
  200. instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
  201. saml_response = """<?xml version='1.0' encoding='UTF-8'?>
  202. <samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="http://sp.example.com/saml2/ls/" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="%s" IssueInstant="%s" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>""" % (
  203. request_id, instant)
  204. response = self.client.get('/ls/', {
  205. 'SAMLResponse': deflate_and_base64_encode(saml_response),
  206. })
  207. self.assertContains(response, "Logged out", status_code=200)
  208. self.assertEquals(self.client.session.keys(), [])
  209. def test_logout_service_global(self):
  210. settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
  211. idp_hosts=['idp.example.com'])
  212. self.do_login()
  213. # now simulate a global logout process initiated by another SP
  214. subject_id = views._get_subject_id(self.client.session)
  215. instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
  216. saml_request = '<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_9961abbaae6d06d251226cb25e38bf8f468036e57e" Version="2.0" IssueInstant="%s" Destination="http://sp.example.com/saml2/ls/"><saml:Issuer>https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">%s</saml:NameID><samlp:SessionIndex>_1837687b7bc9faad85839dbeb319627889f3021757</samlp:SessionIndex></samlp:LogoutRequest>' % (
  217. instant, subject_id)
  218. response = self.client.get('/ls/', {
  219. 'SAMLRequest': deflate_and_base64_encode(saml_request),
  220. })
  221. self.assertEquals(response.status_code, 302)
  222. location = response['Location']
  223. url = urlparse.urlparse(location)
  224. self.assertEquals(url.hostname, 'idp.example.com')
  225. self.assertEquals(url.path,
  226. '/simplesaml/saml2/idp/SingleLogoutService.php')
  227. params = urlparse.parse_qs(url.query)
  228. self.assert_('SAMLResponse' in params)
  229. saml_response = params['SAMLResponse'][0]
  230. expected_response = """<?xml version='1.0' encoding='UTF-8'?>
  231. <samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="_9961abbaae6d06d251226cb25e38bf8f468036e57e" IssueInstant="2010-09-05T09:10:12Z" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>"""
  232. xml = decode_base64_and_inflate(saml_response)
  233. self.assertSAMLRequestsEquals(expected_response, xml)
  234. def test_metadata(self):
  235. settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
  236. idp_hosts=['idp.example.com'])
  237. valid_until = datetime.datetime.utcnow() + datetime.timedelta(hours=24)
  238. valid_until = valid_until.strftime("%Y-%m-%dT%H:%M:%SZ")
  239. expected_metadata = """<?xml version='1.0' encoding='UTF-8'?>
  240. <md:EntityDescriptor xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://sp.example.com/saml2/metadata/" validUntil="%s"><md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIDPjCCAiYCCQCkHjPQlll+mzANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJF
  241. UzEQMA4GA1UECBMHU2V2aWxsYTEbMBkGA1UEChMSWWFjbyBTaXN0ZW1hcyBTLkwu
  242. MRAwDgYDVQQHEwdTZXZpbGxhMREwDwYDVQQDEwh0aWNvdGljbzAeFw0wOTEyMDQx
  243. OTQzNTJaFw0xMDEyMDQxOTQzNTJaMGExCzAJBgNVBAYTAkVTMRAwDgYDVQQIEwdT
  244. ZXZpbGxhMRswGQYDVQQKExJZYWNvIFNpc3RlbWFzIFMuTC4xEDAOBgNVBAcTB1Nl
  245. dmlsbGExETAPBgNVBAMTCHRpY290aWNvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
  246. MIIBCgKCAQEA7rMOMOaIZ/YYD5hYS6Hpjpovcu4k8gaIY+om9zCxLV5F8BLEfkxo
  247. Pk9IA3cRQNRxf7AXCFxEOH3nKy56AIi1gU7X6fCT30JBT8NQlYdgOVMLlR+tjy1b
  248. YV07tDa9U8gzjTyKQHgVwH0436+rmSPnacGj3fMwfySTMhtmrJmax0bIa8EB+gY1
  249. 77DBtvf8dIZIXLlGMQFloZeUspvHOrgNoEA9xU4E9AanGnV9HeV37zv3mLDUOQLx
  250. 4tk9sMQmylCpij7WZmcOV07DyJ/cEmnvHSalBTcyIgkcwlhmjtSgfCy6o5zuWxYd
  251. T9ia80SZbWzn8N6B0q+nq23+Oee9H0lvcwIDAQABMA0GCSqGSIb3DQEBBQUAA4IB
  252. AQCQBhKOqucJZAqGHx4ybDXNzpPethszonLNVg5deISSpWagy55KlGCi5laio/xq
  253. hHRx18eTzeCeLHQYvTQxw0IjZOezJ1X30DD9lEqPr6C+IrmZc6bn/pF76xsvdaRS
  254. gduNQPT1B25SV2HrEmbf8wafSlRARmBsyUHh860TqX7yFVjhYIAUF/El9rLca51j
  255. ljCIqqvT+klPdjQoZwODWPFHgute2oNRmoIcMjSnoy1+mxOC2Q/j7kcD8/etulg2
  256. XDxB3zD81gfdtT8VBFP+G4UrBa+5zFk6fT6U8a7ZqVsyH+rCXAdCyVlEC4Y5fZri
  257. ID4zT0FcZASGuthM56rRJJSx
  258. </ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://sp.example.com/saml2/ls/" /><md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://sp.example.com/saml2/acs/" index="1" /><md:AttributeConsumingService index="1"><md:ServiceName xml:lang="en">Test SP</md:ServiceName><md:RequestedAttribute FriendlyName="uid" Name="urn:oid:0.9.2342.19200300.100.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true" /><md:RequestedAttribute FriendlyName="eduPersonAffiliation" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="false" /></md:AttributeConsumingService></md:SPSSODescriptor><md:Organization><md:OrganizationName xml:lang="es">Ejemplo S.A.</md:OrganizationName><md:OrganizationName xml:lang="en">Example Inc.</md:OrganizationName><md:OrganizationDisplayName xml:lang="es">Ejemplo</md:OrganizationDisplayName><md:OrganizationDisplayName xml:lang="en">Example</md:OrganizationDisplayName><md:OrganizationURL xml:lang="es">http://www.example.es</md:OrganizationURL><md:OrganizationURL xml:lang="en">http://www.example.com</md:OrganizationURL></md:Organization><md:ContactPerson contactType="technical"><md:Company>Example Inc.</md:Company><md:GivenName>Technical givenname</md:GivenName><md:SurName>Technical surname</md:SurName><md:EmailAddress>technical@sp.example.com</md:EmailAddress></md:ContactPerson><md:ContactPerson contactType="administrative"><md:Company>Example Inc.</md:Company><md:GivenName>Administrative givenname</md:GivenName><md:SurName>Administrative surname</md:SurName><md:EmailAddress>administrative@sp.example.ccom</md:EmailAddress></md:ContactPerson></md:EntityDescriptor>"""
  259. expected_metadata = expected_metadata % valid_until
  260. response = self.client.get('/metadata/')
  261. self.assertEquals(response['Content-type'], 'text/xml; charset=utf8')
  262. self.assertEquals(response.status_code, 200)
  263. self.assertEquals(response.content, expected_metadata)
  264. def test_post_authenticated_signal(self):
  265. def signal_handler(signal, user, session_info):
  266. self.assertEquals(isinstance(user, User), True)
  267. post_authenticated.connect(signal_handler, dispatch_uid='test_signal')
  268. self.do_login()
  269. post_authenticated.disconnect(dispatch_uid='test_signal')