test_views.py 6.33 KB
Newer Older
1 2 3
"""
Test the views served by third_party_auth.
"""
4

5 6
import unittest

7
import ddt
8
from django.conf import settings
9
from lxml import etree
10
from onelogin.saml2.errors import OneLogin_Saml2_Error
11

12 13
# Define some XML namespaces:
from third_party_auth.tasks import SAML_XML_NS
14

15
from .testutil import AUTH_FEATURE_ENABLED, AUTH_FEATURES_KEY, SAMLTestCase
16 17 18 19

XMLDSIG_XML_NS = 'http://www.w3.org/2000/09/xmldsig#'


20
@unittest.skipUnless(AUTH_FEATURE_ENABLED, AUTH_FEATURES_KEY + ' not enabled')
21 22 23 24 25 26 27 28 29 30 31 32 33
@ddt.ddt
class SAMLMetadataTest(SAMLTestCase):
    """
    Test the SAML metadata view
    """
    METADATA_URL = '/auth/saml/metadata.xml'

    def test_saml_disabled(self):
        """ When SAML is not enabled, the metadata view should return 404 """
        self.enable_saml(enabled=False)
        response = self.client.get(self.METADATA_URL)
        self.assertEqual(response.status_code, 404)

34
    def test_metadata(self):
35
        self.enable_saml()
36 37 38 39 40 41
        doc = self._fetch_metadata()
        # Check the ACS URL:
        acs_node = doc.find(".//{}".format(etree.QName(SAML_XML_NS, 'AssertionConsumerService')))
        self.assertIsNotNone(acs_node)
        self.assertEqual(acs_node.attrib['Location'], 'http://example.none/auth/complete/tpa-saml/')

42 43 44 45
    def test_default_contact_info(self):
        self.enable_saml()
        self.check_metadata_contacts(
            xml=self._fetch_metadata(),
46
            tech_name=u"{} Support".format(settings.PLATFORM_NAME),
47
            tech_email="technical@example.com",
48
            support_name=u"{} Support".format(settings.PLATFORM_NAME),
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
            support_email="technical@example.com"
        )

    def test_custom_contact_info(self):
        self.enable_saml(
            other_config_str=(
                '{'
                '"TECHNICAL_CONTACT": {"givenName": "Jane Tech", "emailAddress": "jane@example.com"},'
                '"SUPPORT_CONTACT": {"givenName": "Joe Support", "emailAddress": "joe@example.com"}'
                '}'
            )
        )
        self.check_metadata_contacts(
            xml=self._fetch_metadata(),
            tech_name="Jane Tech",
            tech_email="jane@example.com",
            support_name="Joe Support",
            support_email="joe@example.com"
        )

69 70 71 72 73 74 75
    @ddt.data(
        # Test two slightly different key pair export formats
        ('saml_key', 'MIICsDCCAhmgAw'),
        ('saml_key_alt', 'MIICWDCCAcGgAw'),
    )
    @ddt.unpack
    def test_signed_metadata(self, key_name, pub_key_starts_with):
76
        self.enable_saml(
77 78
            private_key=self._get_private_key(key_name),
            public_key=self._get_public_key(key_name),
79 80
            other_config_str='{"SECURITY_CONFIG": {"signMetadata": true} }',
        )
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
        self._validate_signed_metadata(pub_key_starts_with=pub_key_starts_with)

    def test_secure_key_configuration(self):
        """ Test that the SAML private key can be stored in Django settings and not the DB """
        self.enable_saml(
            public_key='',
            private_key='',
            other_config_str='{"SECURITY_CONFIG": {"signMetadata": true} }',
        )
        with self.assertRaises(OneLogin_Saml2_Error):
            self._fetch_metadata()  # OneLogin_Saml2_Error: Cannot sign metadata: missing SP private key.
        with self.settings(
            SOCIAL_AUTH_SAML_SP_PRIVATE_KEY=self._get_private_key('saml_key'),
            SOCIAL_AUTH_SAML_SP_PUBLIC_CERT=self._get_public_key('saml_key'),
        ):
            self._validate_signed_metadata()

    def _validate_signed_metadata(self, pub_key_starts_with='MIICsDCCAhmgAw'):
        """ Fetch the SAML metadata and do some validation """
100 101 102
        doc = self._fetch_metadata()
        sig_node = doc.find(".//{}".format(etree.QName(XMLDSIG_XML_NS, 'SignatureValue')))
        self.assertIsNotNone(sig_node)
103 104 105 106
        # Check that the right public key was used:
        pub_key_node = doc.find(".//{}".format(etree.QName(XMLDSIG_XML_NS, 'X509Certificate')))
        self.assertIsNotNone(pub_key_node)
        self.assertIn(pub_key_starts_with, pub_key_node.text)
107 108 109 110 111 112 113 114 115 116 117 118 119

    def _fetch_metadata(self):
        """ Fetch and parse the metadata XML at self.METADATA_URL """
        response = self.client.get(self.METADATA_URL)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response['Content-Type'], 'text/xml')
        # The result should be valid XML:
        try:
            metadata_doc = etree.fromstring(response.content)
        except etree.LxmlError:
            self.fail('SAML metadata must be valid XML')
        self.assertEqual(metadata_doc.tag, etree.QName(SAML_XML_NS, 'EntityDescriptor'))
        return metadata_doc
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135

    def check_metadata_contacts(self, xml, tech_name, tech_email, support_name, support_email):
        """ Validate that the contact info in the metadata has the expected values """
        technical_node = xml.find(".//{}[@contactType='technical']".format(etree.QName(SAML_XML_NS, 'ContactPerson')))
        self.assertIsNotNone(technical_node)
        tech_name_node = technical_node.find(etree.QName(SAML_XML_NS, 'GivenName'))
        self.assertEqual(tech_name_node.text, tech_name)
        tech_email_node = technical_node.find(etree.QName(SAML_XML_NS, 'EmailAddress'))
        self.assertEqual(tech_email_node.text, tech_email)

        support_node = xml.find(".//{}[@contactType='support']".format(etree.QName(SAML_XML_NS, 'ContactPerson')))
        self.assertIsNotNone(support_node)
        support_name_node = support_node.find(etree.QName(SAML_XML_NS, 'GivenName'))
        self.assertEqual(support_name_node.text, support_name)
        support_email_node = support_node.find(etree.QName(SAML_XML_NS, 'EmailAddress'))
        self.assertEqual(support_email_node.text, support_email)
136 137


138
@unittest.skipUnless(AUTH_FEATURE_ENABLED, AUTH_FEATURES_KEY + ' not enabled')
139 140 141 142 143 144 145 146 147 148 149
class SAMLAuthTest(SAMLTestCase):
    """
    Test the SAML auth views
    """
    LOGIN_URL = '/auth/login/tpa-saml/'

    def test_login_without_idp(self):
        """ Accessing the login endpoint without an idp query param should return 302 """
        self.enable_saml()
        response = self.client.get(self.LOGIN_URL)
        self.assertEqual(response.status_code, 302)
150 151 152 153 154 155

    def test_login_disabled(self):
        """ When SAML is not enabled, the login view should return 404 """
        self.enable_saml(enabled=False)
        response = self.client.get(self.LOGIN_URL)
        self.assertEqual(response.status_code, 404)