authentik.providers.saml.tests.test_metadata

Test Service-Provider Metadata Parser

  1"""Test Service-Provider Metadata Parser"""
  2
  3import xmlsec
  4from defusedxml.lxml import fromstring
  5from django.test import RequestFactory, TestCase
  6from lxml import etree  # nosec
  7
  8from authentik.common.saml.constants import ECDSA_SHA256, NS_MAP, NS_SAML_METADATA
  9from authentik.core.models import Application
 10from authentik.core.tests.utils import create_test_cert, create_test_flow
 11from authentik.crypto.builder import PrivateKeyAlg
 12from authentik.lib.generators import generate_id
 13from authentik.lib.tests.utils import load_fixture
 14from authentik.lib.xml import lxml_from_string
 15from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
 16from authentik.providers.saml.processors.metadata import MetadataProcessor
 17from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser
 18from authentik.sources.saml.models import SAMLNameIDPolicy
 19
 20
 21class TestServiceProviderMetadataParser(TestCase):
 22    """Test ServiceProviderMetadataParser parsing and creation of SAML Provider"""
 23
 24    def setUp(self) -> None:
 25        self.flow = create_test_flow()
 26        self.factory = RequestFactory()
 27
 28    def test_consistent(self):
 29        """Test that metadata generation is consistent"""
 30        provider = SAMLProvider.objects.create(
 31            name=generate_id(),
 32            authorization_flow=self.flow,
 33        )
 34        Application.objects.create(
 35            name=generate_id(),
 36            slug=generate_id(),
 37            provider=provider,
 38        )
 39        request = self.factory.get("/")
 40        metadata_a = MetadataProcessor(provider, request).build_entity_descriptor()
 41        metadata_b = MetadataProcessor(provider, request).build_entity_descriptor()
 42        self.assertEqual(metadata_a, metadata_b)
 43
 44    def test_schema(self):
 45        """Test that metadata generation is consistent"""
 46        provider = SAMLProvider.objects.create(
 47            name=generate_id(),
 48            authorization_flow=self.flow,
 49        )
 50        Application.objects.create(
 51            name=generate_id(),
 52            slug=generate_id(),
 53            provider=provider,
 54        )
 55        request = self.factory.get("/")
 56        metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
 57
 58        schema = etree.XMLSchema(
 59            etree.parse(
 60                source="schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()
 61            )  # nosec
 62        )
 63        self.assertTrue(schema.validate(metadata))
 64
 65    def test_schema_want_authn_requests_signed(self):
 66        """Test metadata generation with WantAuthnRequestsSigned"""
 67        cert = create_test_cert()
 68        provider = SAMLProvider.objects.create(
 69            name=generate_id(),
 70            authorization_flow=self.flow,
 71            verification_kp=cert,
 72        )
 73        Application.objects.create(
 74            name=generate_id(),
 75            slug=generate_id(),
 76            provider=provider,
 77        )
 78        request = self.factory.get("/")
 79        metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
 80        idp_sso_descriptor = metadata.findall(f"{{{NS_SAML_METADATA}}}IDPSSODescriptor")[0]
 81        self.assertEqual(idp_sso_descriptor.attrib["WantAuthnRequestsSigned"], "true")
 82
 83    def test_simple(self):
 84        """Test simple metadata without Signing"""
 85        metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/simple.xml"))
 86        provider = metadata.to_provider("test", self.flow, self.flow)
 87        self.assertEqual(provider.acs_url, "http://localhost:8080/saml/acs")
 88        self.assertEqual(provider.issuer, "http://localhost:8080/saml/metadata")
 89        self.assertEqual(provider.sp_binding, SAMLBindings.POST)
 90        self.assertEqual(provider.default_name_id_policy, SAMLNameIDPolicy.EMAIL)
 91        self.assertEqual(
 92            len(provider.property_mappings.all()),
 93            len(SAMLPropertyMapping.objects.exclude(managed__isnull=True)),
 94        )
 95
 96    def test_with_signing_cert(self):
 97        """Test Metadata with signing cert"""
 98        create_test_cert()
 99        metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/cert.xml"))
100        provider = metadata.to_provider("test", self.flow, self.flow)
101        self.assertEqual(provider.acs_url, "http://localhost:8080/apps/user_saml/saml/acs")
102        self.assertEqual(provider.issuer, "http://localhost:8080/apps/user_saml/saml/metadata")
103        self.assertEqual(provider.sp_binding, SAMLBindings.POST)
104        self.assertEqual(
105            provider.verification_kp.certificate_data, load_fixture("fixtures/cert.pem")
106        )
107        self.assertIsNotNone(provider.signing_kp)
108        self.assertEqual(provider.audience, "")
109
110    def test_with_signing_cert_invalid_signature(self):
111        """Test Metadata with signing cert (invalid signature)"""
112        with self.assertRaises(ValueError):
113            ServiceProviderMetadataParser().parse(
114                load_fixture("fixtures/cert.xml").replace("/apps/user_saml", "")
115            )
116
117    def test_signature_rsa(self):
118        """Test signature validation (RSA)"""
119        provider = SAMLProvider.objects.create(
120            name=generate_id(),
121            authorization_flow=self.flow,
122            signing_kp=create_test_cert(PrivateKeyAlg.RSA),
123        )
124        Application.objects.create(
125            name=generate_id(),
126            slug=generate_id(),
127            provider=provider,
128        )
129        request = self.factory.get("/")
130        metadata = MetadataProcessor(provider, request).build_entity_descriptor()
131
132        root = fromstring(metadata.encode())
133        xmlsec.tree.add_ids(root, ["ID"])
134        signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
135        signature_node = signature_nodes[0]
136        ctx = xmlsec.SignatureContext()
137        key = xmlsec.Key.from_memory(
138            provider.signing_kp.certificate_data,
139            xmlsec.constants.KeyDataFormatCertPem,
140            None,
141        )
142        ctx.key = key
143        ctx.verify(signature_node)
144
145    def test_signature_ecdsa(self):
146        """Test signature validation (ECDSA)"""
147        provider = SAMLProvider.objects.create(
148            name=generate_id(),
149            authorization_flow=self.flow,
150            signing_kp=create_test_cert(PrivateKeyAlg.ECDSA),
151            signature_algorithm=ECDSA_SHA256,
152        )
153        Application.objects.create(
154            name=generate_id(),
155            slug=generate_id(),
156            provider=provider,
157        )
158        request = self.factory.get("/")
159        metadata = MetadataProcessor(provider, request).build_entity_descriptor()
160
161        root = fromstring(metadata.encode())
162        xmlsec.tree.add_ids(root, ["ID"])
163        signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
164        signature_node = signature_nodes[0]
165        ctx = xmlsec.SignatureContext()
166        key = xmlsec.Key.from_memory(
167            provider.signing_kp.certificate_data,
168            xmlsec.constants.KeyDataFormatCertPem,
169            None,
170        )
171        ctx.key = key
172        ctx.verify(signature_node)
class TestServiceProviderMetadataParser(django.test.testcases.TestCase):
 22class TestServiceProviderMetadataParser(TestCase):
 23    """Test ServiceProviderMetadataParser parsing and creation of SAML Provider"""
 24
 25    def setUp(self) -> None:
 26        self.flow = create_test_flow()
 27        self.factory = RequestFactory()
 28
 29    def test_consistent(self):
 30        """Test that metadata generation is consistent"""
 31        provider = SAMLProvider.objects.create(
 32            name=generate_id(),
 33            authorization_flow=self.flow,
 34        )
 35        Application.objects.create(
 36            name=generate_id(),
 37            slug=generate_id(),
 38            provider=provider,
 39        )
 40        request = self.factory.get("/")
 41        metadata_a = MetadataProcessor(provider, request).build_entity_descriptor()
 42        metadata_b = MetadataProcessor(provider, request).build_entity_descriptor()
 43        self.assertEqual(metadata_a, metadata_b)
 44
 45    def test_schema(self):
 46        """Test that metadata generation is consistent"""
 47        provider = SAMLProvider.objects.create(
 48            name=generate_id(),
 49            authorization_flow=self.flow,
 50        )
 51        Application.objects.create(
 52            name=generate_id(),
 53            slug=generate_id(),
 54            provider=provider,
 55        )
 56        request = self.factory.get("/")
 57        metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
 58
 59        schema = etree.XMLSchema(
 60            etree.parse(
 61                source="schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()
 62            )  # nosec
 63        )
 64        self.assertTrue(schema.validate(metadata))
 65
 66    def test_schema_want_authn_requests_signed(self):
 67        """Test metadata generation with WantAuthnRequestsSigned"""
 68        cert = create_test_cert()
 69        provider = SAMLProvider.objects.create(
 70            name=generate_id(),
 71            authorization_flow=self.flow,
 72            verification_kp=cert,
 73        )
 74        Application.objects.create(
 75            name=generate_id(),
 76            slug=generate_id(),
 77            provider=provider,
 78        )
 79        request = self.factory.get("/")
 80        metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
 81        idp_sso_descriptor = metadata.findall(f"{{{NS_SAML_METADATA}}}IDPSSODescriptor")[0]
 82        self.assertEqual(idp_sso_descriptor.attrib["WantAuthnRequestsSigned"], "true")
 83
 84    def test_simple(self):
 85        """Test simple metadata without Signing"""
 86        metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/simple.xml"))
 87        provider = metadata.to_provider("test", self.flow, self.flow)
 88        self.assertEqual(provider.acs_url, "http://localhost:8080/saml/acs")
 89        self.assertEqual(provider.issuer, "http://localhost:8080/saml/metadata")
 90        self.assertEqual(provider.sp_binding, SAMLBindings.POST)
 91        self.assertEqual(provider.default_name_id_policy, SAMLNameIDPolicy.EMAIL)
 92        self.assertEqual(
 93            len(provider.property_mappings.all()),
 94            len(SAMLPropertyMapping.objects.exclude(managed__isnull=True)),
 95        )
 96
 97    def test_with_signing_cert(self):
 98        """Test Metadata with signing cert"""
 99        create_test_cert()
100        metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/cert.xml"))
101        provider = metadata.to_provider("test", self.flow, self.flow)
102        self.assertEqual(provider.acs_url, "http://localhost:8080/apps/user_saml/saml/acs")
103        self.assertEqual(provider.issuer, "http://localhost:8080/apps/user_saml/saml/metadata")
104        self.assertEqual(provider.sp_binding, SAMLBindings.POST)
105        self.assertEqual(
106            provider.verification_kp.certificate_data, load_fixture("fixtures/cert.pem")
107        )
108        self.assertIsNotNone(provider.signing_kp)
109        self.assertEqual(provider.audience, "")
110
111    def test_with_signing_cert_invalid_signature(self):
112        """Test Metadata with signing cert (invalid signature)"""
113        with self.assertRaises(ValueError):
114            ServiceProviderMetadataParser().parse(
115                load_fixture("fixtures/cert.xml").replace("/apps/user_saml", "")
116            )
117
118    def test_signature_rsa(self):
119        """Test signature validation (RSA)"""
120        provider = SAMLProvider.objects.create(
121            name=generate_id(),
122            authorization_flow=self.flow,
123            signing_kp=create_test_cert(PrivateKeyAlg.RSA),
124        )
125        Application.objects.create(
126            name=generate_id(),
127            slug=generate_id(),
128            provider=provider,
129        )
130        request = self.factory.get("/")
131        metadata = MetadataProcessor(provider, request).build_entity_descriptor()
132
133        root = fromstring(metadata.encode())
134        xmlsec.tree.add_ids(root, ["ID"])
135        signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
136        signature_node = signature_nodes[0]
137        ctx = xmlsec.SignatureContext()
138        key = xmlsec.Key.from_memory(
139            provider.signing_kp.certificate_data,
140            xmlsec.constants.KeyDataFormatCertPem,
141            None,
142        )
143        ctx.key = key
144        ctx.verify(signature_node)
145
146    def test_signature_ecdsa(self):
147        """Test signature validation (ECDSA)"""
148        provider = SAMLProvider.objects.create(
149            name=generate_id(),
150            authorization_flow=self.flow,
151            signing_kp=create_test_cert(PrivateKeyAlg.ECDSA),
152            signature_algorithm=ECDSA_SHA256,
153        )
154        Application.objects.create(
155            name=generate_id(),
156            slug=generate_id(),
157            provider=provider,
158        )
159        request = self.factory.get("/")
160        metadata = MetadataProcessor(provider, request).build_entity_descriptor()
161
162        root = fromstring(metadata.encode())
163        xmlsec.tree.add_ids(root, ["ID"])
164        signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
165        signature_node = signature_nodes[0]
166        ctx = xmlsec.SignatureContext()
167        key = xmlsec.Key.from_memory(
168            provider.signing_kp.certificate_data,
169            xmlsec.constants.KeyDataFormatCertPem,
170            None,
171        )
172        ctx.key = key
173        ctx.verify(signature_node)

Test ServiceProviderMetadataParser parsing and creation of SAML Provider

def setUp(self) -> None:
25    def setUp(self) -> None:
26        self.flow = create_test_flow()
27        self.factory = RequestFactory()

Hook method for setting up the test fixture before exercising it.

def test_consistent(self):
29    def test_consistent(self):
30        """Test that metadata generation is consistent"""
31        provider = SAMLProvider.objects.create(
32            name=generate_id(),
33            authorization_flow=self.flow,
34        )
35        Application.objects.create(
36            name=generate_id(),
37            slug=generate_id(),
38            provider=provider,
39        )
40        request = self.factory.get("/")
41        metadata_a = MetadataProcessor(provider, request).build_entity_descriptor()
42        metadata_b = MetadataProcessor(provider, request).build_entity_descriptor()
43        self.assertEqual(metadata_a, metadata_b)

Test that metadata generation is consistent

def test_schema(self):
45    def test_schema(self):
46        """Test that metadata generation is consistent"""
47        provider = SAMLProvider.objects.create(
48            name=generate_id(),
49            authorization_flow=self.flow,
50        )
51        Application.objects.create(
52            name=generate_id(),
53            slug=generate_id(),
54            provider=provider,
55        )
56        request = self.factory.get("/")
57        metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
58
59        schema = etree.XMLSchema(
60            etree.parse(
61                source="schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()
62            )  # nosec
63        )
64        self.assertTrue(schema.validate(metadata))

Test that metadata generation is consistent

def test_schema_want_authn_requests_signed(self):
66    def test_schema_want_authn_requests_signed(self):
67        """Test metadata generation with WantAuthnRequestsSigned"""
68        cert = create_test_cert()
69        provider = SAMLProvider.objects.create(
70            name=generate_id(),
71            authorization_flow=self.flow,
72            verification_kp=cert,
73        )
74        Application.objects.create(
75            name=generate_id(),
76            slug=generate_id(),
77            provider=provider,
78        )
79        request = self.factory.get("/")
80        metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
81        idp_sso_descriptor = metadata.findall(f"{{{NS_SAML_METADATA}}}IDPSSODescriptor")[0]
82        self.assertEqual(idp_sso_descriptor.attrib["WantAuthnRequestsSigned"], "true")

Test metadata generation with WantAuthnRequestsSigned

def test_simple(self):
84    def test_simple(self):
85        """Test simple metadata without Signing"""
86        metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/simple.xml"))
87        provider = metadata.to_provider("test", self.flow, self.flow)
88        self.assertEqual(provider.acs_url, "http://localhost:8080/saml/acs")
89        self.assertEqual(provider.issuer, "http://localhost:8080/saml/metadata")
90        self.assertEqual(provider.sp_binding, SAMLBindings.POST)
91        self.assertEqual(provider.default_name_id_policy, SAMLNameIDPolicy.EMAIL)
92        self.assertEqual(
93            len(provider.property_mappings.all()),
94            len(SAMLPropertyMapping.objects.exclude(managed__isnull=True)),
95        )

Test simple metadata without Signing

def test_with_signing_cert(self):
 97    def test_with_signing_cert(self):
 98        """Test Metadata with signing cert"""
 99        create_test_cert()
100        metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/cert.xml"))
101        provider = metadata.to_provider("test", self.flow, self.flow)
102        self.assertEqual(provider.acs_url, "http://localhost:8080/apps/user_saml/saml/acs")
103        self.assertEqual(provider.issuer, "http://localhost:8080/apps/user_saml/saml/metadata")
104        self.assertEqual(provider.sp_binding, SAMLBindings.POST)
105        self.assertEqual(
106            provider.verification_kp.certificate_data, load_fixture("fixtures/cert.pem")
107        )
108        self.assertIsNotNone(provider.signing_kp)
109        self.assertEqual(provider.audience, "")

Test Metadata with signing cert

def test_with_signing_cert_invalid_signature(self):
111    def test_with_signing_cert_invalid_signature(self):
112        """Test Metadata with signing cert (invalid signature)"""
113        with self.assertRaises(ValueError):
114            ServiceProviderMetadataParser().parse(
115                load_fixture("fixtures/cert.xml").replace("/apps/user_saml", "")
116            )

Test Metadata with signing cert (invalid signature)

def test_signature_rsa(self):
118    def test_signature_rsa(self):
119        """Test signature validation (RSA)"""
120        provider = SAMLProvider.objects.create(
121            name=generate_id(),
122            authorization_flow=self.flow,
123            signing_kp=create_test_cert(PrivateKeyAlg.RSA),
124        )
125        Application.objects.create(
126            name=generate_id(),
127            slug=generate_id(),
128            provider=provider,
129        )
130        request = self.factory.get("/")
131        metadata = MetadataProcessor(provider, request).build_entity_descriptor()
132
133        root = fromstring(metadata.encode())
134        xmlsec.tree.add_ids(root, ["ID"])
135        signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
136        signature_node = signature_nodes[0]
137        ctx = xmlsec.SignatureContext()
138        key = xmlsec.Key.from_memory(
139            provider.signing_kp.certificate_data,
140            xmlsec.constants.KeyDataFormatCertPem,
141            None,
142        )
143        ctx.key = key
144        ctx.verify(signature_node)

Test signature validation (RSA)

def test_signature_ecdsa(self):
146    def test_signature_ecdsa(self):
147        """Test signature validation (ECDSA)"""
148        provider = SAMLProvider.objects.create(
149            name=generate_id(),
150            authorization_flow=self.flow,
151            signing_kp=create_test_cert(PrivateKeyAlg.ECDSA),
152            signature_algorithm=ECDSA_SHA256,
153        )
154        Application.objects.create(
155            name=generate_id(),
156            slug=generate_id(),
157            provider=provider,
158        )
159        request = self.factory.get("/")
160        metadata = MetadataProcessor(provider, request).build_entity_descriptor()
161
162        root = fromstring(metadata.encode())
163        xmlsec.tree.add_ids(root, ["ID"])
164        signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
165        signature_node = signature_nodes[0]
166        ctx = xmlsec.SignatureContext()
167        key = xmlsec.Key.from_memory(
168            provider.signing_kp.certificate_data,
169            xmlsec.constants.KeyDataFormatCertPem,
170            None,
171        )
172        ctx.key = key
173        ctx.verify(signature_node)

Test signature validation (ECDSA)