diff --git a/djangosaml2/tests/__init__.py b/djangosaml2/tests/__init__.py index 672ffa74..5a987a77 100644 --- a/djangosaml2/tests/__init__.py +++ b/djangosaml2/tests/__init__.py @@ -37,6 +37,7 @@ from djangosaml2.conf import get_config from djangosaml2.signals import post_authenticated from djangosaml2.tests import conf +from djangosaml2.tests.utils import SAMLPostFormParser from djangosaml2.tests.auth_response import auth_response from djangosaml2.views import finish_logout from saml2.config import SPConfig @@ -108,6 +109,35 @@ def render_template(self, text): def b64_for_post(self, xml_text, encoding='utf-8'): return base64.b64encode(xml_text.encode(encoding)).decode('ascii') + def test_unsigned_post_authn_request(self): + """ + Test that unsigned authentication requests via POST binding + does not error. + + https://github.com/knaperek/djangosaml2/issues/168 + """ + settings.SAML_CONFIG = conf.create_conf( + sp_host='sp.example.com', + idp_hosts=['idp.example.com'], + metadata_file='remote_metadata_post_binding.xml', + authn_requests_signed=False + ) + response = self.client.get(reverse('saml2_login')) + + self.assertEqual(response.status_code, 200) + + # Using POST-binding returns a page with form containing the SAMLRequest + response_parser = SAMLPostFormParser() + response_parser.feed(response.content.decode('utf-8')) + saml_request = response_parser.saml_request_value + expected_request = """http://sp.example.com/saml2/metadata/""" + + self.assertIsNotNone(saml_request) + self.assertSAMLRequestsEquals( + base64.b64decode(saml_request).decode('utf-8'), + expected_request + ) + def test_login_evil_redirect(self): """ Make sure that if we give an URL other than our own host as the next diff --git a/djangosaml2/tests/conf.py b/djangosaml2/tests/conf.py index 2c1bd92e..a2f59cc7 100644 --- a/djangosaml2/tests/conf.py +++ b/djangosaml2/tests/conf.py @@ -19,7 +19,7 @@ def create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'], - metadata_file='remote_metadata.xml'): + metadata_file='remote_metadata.xml', authn_requests_signed=None): try: from saml2.sigver import get_xmlsec_binary @@ -90,6 +90,9 @@ def create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'], 'valid_for': 24, } + if authn_requests_signed is not None: + config['service']['sp']['authn_requests_signed'] = authn_requests_signed + for idp in idp_hosts: entity_id = 'https://%s/simplesaml/saml2/idp/metadata.php' % idp config['service']['sp']['idp'][entity_id] = { diff --git a/djangosaml2/tests/remote_metadata_post_binding.xml b/djangosaml2/tests/remote_metadata_post_binding.xml new file mode 100644 index 00000000..941f9580 --- /dev/null +++ b/djangosaml2/tests/remote_metadata_post_binding.xml @@ -0,0 +1,33 @@ + + + + + + + + MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo + + + + + + + MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + Lorenzo's test IdP + idp.example.com IdP + http://idp.example.com/ + + + Administrator + lgs@yaco.es + + + diff --git a/djangosaml2/tests/utils.py b/djangosaml2/tests/utils.py new file mode 100644 index 00000000..58331ca4 --- /dev/null +++ b/djangosaml2/tests/utils.py @@ -0,0 +1,16 @@ +from html.parser import HTMLParser + + +class SAMLPostFormParser(HTMLParser): + """ + Parses the SAML Post binding form page for the SAMLRequest value. + """ + + saml_request_value = None + + def handle_starttag(self, tag, attrs): + attrs_dict = dict(attrs) + + if tag != "input" or attrs_dict.get("name") != "SAMLRequest": + return + self.saml_request_value = attrs_dict.get("value") diff --git a/djangosaml2/views.py b/djangosaml2/views.py index bbb0b5d0..6ef2fab8 100644 --- a/djangosaml2/views.py +++ b/djangosaml2/views.py @@ -44,6 +44,7 @@ ) from saml2.mdstore import SourceNotFound from saml2.sigver import MissingKey +from saml2.samlp import AuthnRequest from saml2.validate import ResponseLifetimeExceed, ToEarly from saml2.xmldsig import ( # support for SHA1 is required by spec SIG_RSA_SHA1, SIG_RSA_SHA256) @@ -228,6 +229,9 @@ def login(request, binding=binding, **kwargs) try: + if isinstance(request_xml, AuthnRequest): + # request_xml will be an instance of AuthnRequest if the message is not signed + request_xml = str(request_xml) saml_request = base64.b64encode(bytes(request_xml, 'UTF-8')).decode('utf-8') http_response = render(request, post_binding_form_template, {