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.sp_binding, SAMLBindings.POST)
 89        self.assertEqual(provider.default_name_id_policy, SAMLNameIDPolicy.EMAIL)
 90        self.assertEqual(
 91            len(provider.property_mappings.all()),
 92            len(SAMLPropertyMapping.objects.exclude(managed__isnull=True)),
 93        )
 94
 95    def test_with_signing_cert(self):
 96        """Test Metadata with signing cert"""
 97        create_test_cert()
 98        metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/cert.xml"))
 99        provider = metadata.to_provider("test", self.flow, self.flow)
100        self.assertEqual(provider.acs_url, "http://localhost:8080/apps/user_saml/saml/acs")
101        self.assertEqual(provider.sp_binding, SAMLBindings.POST)
102        self.assertEqual(
103            provider.verification_kp.certificate_data, load_fixture("fixtures/cert.pem")
104        )
105        self.assertIsNotNone(provider.signing_kp)
106        self.assertEqual(provider.audience, "http://localhost:8080/apps/user_saml/saml/metadata")
107
108    def test_with_signing_cert_invalid_signature(self):
109        """Test Metadata with signing cert (invalid signature)"""
110        with self.assertRaises(ValueError):
111            ServiceProviderMetadataParser().parse(
112                load_fixture("fixtures/cert.xml").replace("/apps/user_saml", "")
113            )
114
115    def test_signature_rsa(self):
116        """Test signature validation (RSA)"""
117        provider = SAMLProvider.objects.create(
118            name=generate_id(),
119            authorization_flow=self.flow,
120            signing_kp=create_test_cert(PrivateKeyAlg.RSA),
121        )
122        Application.objects.create(
123            name=generate_id(),
124            slug=generate_id(),
125            provider=provider,
126        )
127        request = self.factory.get("/")
128        metadata = MetadataProcessor(provider, request).build_entity_descriptor()
129
130        root = fromstring(metadata.encode())
131        xmlsec.tree.add_ids(root, ["ID"])
132        signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
133        signature_node = signature_nodes[0]
134        ctx = xmlsec.SignatureContext()
135        key = xmlsec.Key.from_memory(
136            provider.signing_kp.certificate_data,
137            xmlsec.constants.KeyDataFormatCertPem,
138            None,
139        )
140        ctx.key = key
141        ctx.verify(signature_node)
142
143    def test_signature_ecdsa(self):
144        """Test signature validation (ECDSA)"""
145        provider = SAMLProvider.objects.create(
146            name=generate_id(),
147            authorization_flow=self.flow,
148            signing_kp=create_test_cert(PrivateKeyAlg.ECDSA),
149            signature_algorithm=ECDSA_SHA256,
150        )
151        Application.objects.create(
152            name=generate_id(),
153            slug=generate_id(),
154            provider=provider,
155        )
156        request = self.factory.get("/")
157        metadata = MetadataProcessor(provider, request).build_entity_descriptor()
158
159        root = fromstring(metadata.encode())
160        xmlsec.tree.add_ids(root, ["ID"])
161        signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
162        signature_node = signature_nodes[0]
163        ctx = xmlsec.SignatureContext()
164        key = xmlsec.Key.from_memory(
165            provider.signing_kp.certificate_data,
166            xmlsec.constants.KeyDataFormatCertPem,
167            None,
168        )
169        ctx.key = key
170        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.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.sp_binding, SAMLBindings.POST)
103        self.assertEqual(
104            provider.verification_kp.certificate_data, load_fixture("fixtures/cert.pem")
105        )
106        self.assertIsNotNone(provider.signing_kp)
107        self.assertEqual(provider.audience, "http://localhost:8080/apps/user_saml/saml/metadata")
108
109    def test_with_signing_cert_invalid_signature(self):
110        """Test Metadata with signing cert (invalid signature)"""
111        with self.assertRaises(ValueError):
112            ServiceProviderMetadataParser().parse(
113                load_fixture("fixtures/cert.xml").replace("/apps/user_saml", "")
114            )
115
116    def test_signature_rsa(self):
117        """Test signature validation (RSA)"""
118        provider = SAMLProvider.objects.create(
119            name=generate_id(),
120            authorization_flow=self.flow,
121            signing_kp=create_test_cert(PrivateKeyAlg.RSA),
122        )
123        Application.objects.create(
124            name=generate_id(),
125            slug=generate_id(),
126            provider=provider,
127        )
128        request = self.factory.get("/")
129        metadata = MetadataProcessor(provider, request).build_entity_descriptor()
130
131        root = fromstring(metadata.encode())
132        xmlsec.tree.add_ids(root, ["ID"])
133        signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
134        signature_node = signature_nodes[0]
135        ctx = xmlsec.SignatureContext()
136        key = xmlsec.Key.from_memory(
137            provider.signing_kp.certificate_data,
138            xmlsec.constants.KeyDataFormatCertPem,
139            None,
140        )
141        ctx.key = key
142        ctx.verify(signature_node)
143
144    def test_signature_ecdsa(self):
145        """Test signature validation (ECDSA)"""
146        provider = SAMLProvider.objects.create(
147            name=generate_id(),
148            authorization_flow=self.flow,
149            signing_kp=create_test_cert(PrivateKeyAlg.ECDSA),
150            signature_algorithm=ECDSA_SHA256,
151        )
152        Application.objects.create(
153            name=generate_id(),
154            slug=generate_id(),
155            provider=provider,
156        )
157        request = self.factory.get("/")
158        metadata = MetadataProcessor(provider, request).build_entity_descriptor()
159
160        root = fromstring(metadata.encode())
161        xmlsec.tree.add_ids(root, ["ID"])
162        signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
163        signature_node = signature_nodes[0]
164        ctx = xmlsec.SignatureContext()
165        key = xmlsec.Key.from_memory(
166            provider.signing_kp.certificate_data,
167            xmlsec.constants.KeyDataFormatCertPem,
168            None,
169        )
170        ctx.key = key
171        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.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        )

Test simple metadata without Signing

def test_with_signing_cert(self):
 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.sp_binding, SAMLBindings.POST)
103        self.assertEqual(
104            provider.verification_kp.certificate_data, load_fixture("fixtures/cert.pem")
105        )
106        self.assertIsNotNone(provider.signing_kp)
107        self.assertEqual(provider.audience, "http://localhost:8080/apps/user_saml/saml/metadata")

Test Metadata with signing cert

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

Test Metadata with signing cert (invalid signature)

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

Test signature validation (RSA)

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

Test signature validation (ECDSA)