authentik.providers.saml.tests.test_logout_response_processor

logout response tests

  1"""logout response tests"""
  2
  3from defusedxml import ElementTree
  4from django.test import RequestFactory, TestCase
  5
  6from authentik.blueprints.tests import apply_blueprint
  7from authentik.common.saml.constants import (
  8    NS_SAML_ASSERTION,
  9    NS_SAML_PROTOCOL,
 10    NS_SIGNATURE,
 11)
 12from authentik.core.models import Application
 13from authentik.core.tests.utils import create_test_cert, create_test_flow
 14from authentik.lib.generators import generate_id
 15from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
 16from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
 17from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
 18from authentik.providers.saml.processors.metadata import MetadataProcessor
 19
 20
 21class TestLogoutResponse(TestCase):
 22    """Test LogoutResponse processor"""
 23
 24    @apply_blueprint("system/providers-saml.yaml")
 25    def setUp(self):
 26        cert = create_test_cert()
 27        self.factory = RequestFactory()
 28        self.provider: SAMLProvider = SAMLProvider.objects.create(
 29            authorization_flow=create_test_flow(),
 30            acs_url="http://testserver/source/saml/provider/acs/",
 31            sls_url="http://testserver/source/saml/provider/sls/",
 32            signing_kp=cert,
 33            verification_kp=cert,
 34        )
 35        self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
 36        self.provider.save()
 37        self.application = Application.objects.create(
 38            name=generate_id(),
 39            slug=generate_id(),
 40            provider=self.provider,
 41        )
 42
 43    def test_build_response(self):
 44        """Test building a LogoutResponse uses the generated issuer from the assertion"""
 45        # Generate the issuer the same way the assertion/metadata processors would
 46        request = self.factory.get("/")
 47        metadata_processor = MetadataProcessor(self.provider, request)
 48        generated_issuer = metadata_processor._get_issuer_value()
 49
 50        logout_request = LogoutRequest(
 51            id="test-request-id",
 52            issuer="test-sp",
 53            relay_state="test-relay-state",
 54        )
 55
 56        # Pass the generated issuer as if it came from SAMLSession.issuer
 57        processor = LogoutResponseProcessor(
 58            self.provider,
 59            logout_request,
 60            destination=self.provider.sls_url,
 61            issuer=generated_issuer,
 62        )
 63        response_xml = processor.build_response(status="Success")
 64
 65        # Parse and verify
 66        root = ElementTree.fromstring(response_xml)
 67        self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
 68        self.assertEqual(root.attrib["Version"], "2.0")
 69        self.assertEqual(root.attrib["Destination"], self.provider.sls_url)
 70        self.assertEqual(root.attrib["InResponseTo"], "test-request-id")
 71
 72        # Check Issuer matches the generated issuer from the assertion processor
 73        issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer")
 74        self.assertEqual(issuer.text, generated_issuer)
 75
 76        # Check Status
 77        status = root.find(f".//{{{NS_SAML_PROTOCOL}}}StatusCode")
 78        self.assertEqual(status.attrib["Value"], "urn:oasis:names:tc:SAML:2.0:status:Success")
 79
 80    def test_build_response_signed(self):
 81        """Test building a signed LogoutResponse"""
 82        self.provider.sign_logout_response = True
 83        self.provider.save()
 84
 85        logout_request = LogoutRequest(
 86            id="test-request-id",
 87            issuer="test-sp",
 88            relay_state="test-relay-state",
 89        )
 90
 91        processor = LogoutResponseProcessor(
 92            self.provider, logout_request, destination=self.provider.sls_url
 93        )
 94        response_xml = processor.build_response(status="Success")
 95
 96        # Parse and verify signature is present
 97        root = ElementTree.fromstring(response_xml)
 98        signature = root.find(f".//{{{NS_SIGNATURE}}}Signature")
 99        self.assertIsNotNone(signature)
100
101        # Verify signature structure
102        signed_info = signature.find(f"{{{NS_SIGNATURE}}}SignedInfo")
103        self.assertIsNotNone(signed_info)
104        signature_value = signature.find(f"{{{NS_SIGNATURE}}}SignatureValue")
105        self.assertIsNotNone(signature_value)
106        self.assertIsNotNone(signature_value.text)
107
108    def test_no_inresponseto(self):
109        """Test building response without a logout request omits InResponseTo attribute"""
110        processor = LogoutResponseProcessor(self.provider, None, destination=self.provider.sls_url)
111        response_xml = processor.build_response(status="Success")
112
113        root = ElementTree.fromstring(response_xml)
114        self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
115        self.assertNotIn("InResponseTo", root.attrib)
116
117    def test_no_destination(self):
118        """Test building response without destination"""
119        logout_request = LogoutRequest(
120            id="test-request-id",
121            issuer="test-sp",
122        )
123
124        processor = LogoutResponseProcessor(self.provider, logout_request, destination=None)
125        response_xml = processor.build_response(status="Success")
126
127        root = ElementTree.fromstring(response_xml)
128        self.assertNotIn("Destination", root.attrib)
129
130    def test_relay_state_from_logout_request(self):
131        """Test that relay_state is taken from logout_request if not provided"""
132        logout_request = LogoutRequest(
133            id="test-request-id",
134            issuer="test-sp",
135            relay_state="request-relay-state",
136        )
137
138        processor = LogoutResponseProcessor(
139            self.provider, logout_request, destination=self.provider.sls_url
140        )
141        self.assertEqual(processor.relay_state, "request-relay-state")
142
143    def test_relay_state_override(self):
144        """Test that explicit relay_state overrides logout_request relay_state"""
145        logout_request = LogoutRequest(
146            id="test-request-id",
147            issuer="test-sp",
148            relay_state="request-relay-state",
149        )
150
151        processor = LogoutResponseProcessor(
152            self.provider,
153            logout_request,
154            destination=self.provider.sls_url,
155            relay_state="explicit-relay-state",
156        )
157        self.assertEqual(processor.relay_state, "explicit-relay-state")
class TestLogoutResponse(django.test.testcases.TestCase):
 22class TestLogoutResponse(TestCase):
 23    """Test LogoutResponse processor"""
 24
 25    @apply_blueprint("system/providers-saml.yaml")
 26    def setUp(self):
 27        cert = create_test_cert()
 28        self.factory = RequestFactory()
 29        self.provider: SAMLProvider = SAMLProvider.objects.create(
 30            authorization_flow=create_test_flow(),
 31            acs_url="http://testserver/source/saml/provider/acs/",
 32            sls_url="http://testserver/source/saml/provider/sls/",
 33            signing_kp=cert,
 34            verification_kp=cert,
 35        )
 36        self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
 37        self.provider.save()
 38        self.application = Application.objects.create(
 39            name=generate_id(),
 40            slug=generate_id(),
 41            provider=self.provider,
 42        )
 43
 44    def test_build_response(self):
 45        """Test building a LogoutResponse uses the generated issuer from the assertion"""
 46        # Generate the issuer the same way the assertion/metadata processors would
 47        request = self.factory.get("/")
 48        metadata_processor = MetadataProcessor(self.provider, request)
 49        generated_issuer = metadata_processor._get_issuer_value()
 50
 51        logout_request = LogoutRequest(
 52            id="test-request-id",
 53            issuer="test-sp",
 54            relay_state="test-relay-state",
 55        )
 56
 57        # Pass the generated issuer as if it came from SAMLSession.issuer
 58        processor = LogoutResponseProcessor(
 59            self.provider,
 60            logout_request,
 61            destination=self.provider.sls_url,
 62            issuer=generated_issuer,
 63        )
 64        response_xml = processor.build_response(status="Success")
 65
 66        # Parse and verify
 67        root = ElementTree.fromstring(response_xml)
 68        self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
 69        self.assertEqual(root.attrib["Version"], "2.0")
 70        self.assertEqual(root.attrib["Destination"], self.provider.sls_url)
 71        self.assertEqual(root.attrib["InResponseTo"], "test-request-id")
 72
 73        # Check Issuer matches the generated issuer from the assertion processor
 74        issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer")
 75        self.assertEqual(issuer.text, generated_issuer)
 76
 77        # Check Status
 78        status = root.find(f".//{{{NS_SAML_PROTOCOL}}}StatusCode")
 79        self.assertEqual(status.attrib["Value"], "urn:oasis:names:tc:SAML:2.0:status:Success")
 80
 81    def test_build_response_signed(self):
 82        """Test building a signed LogoutResponse"""
 83        self.provider.sign_logout_response = True
 84        self.provider.save()
 85
 86        logout_request = LogoutRequest(
 87            id="test-request-id",
 88            issuer="test-sp",
 89            relay_state="test-relay-state",
 90        )
 91
 92        processor = LogoutResponseProcessor(
 93            self.provider, logout_request, destination=self.provider.sls_url
 94        )
 95        response_xml = processor.build_response(status="Success")
 96
 97        # Parse and verify signature is present
 98        root = ElementTree.fromstring(response_xml)
 99        signature = root.find(f".//{{{NS_SIGNATURE}}}Signature")
100        self.assertIsNotNone(signature)
101
102        # Verify signature structure
103        signed_info = signature.find(f"{{{NS_SIGNATURE}}}SignedInfo")
104        self.assertIsNotNone(signed_info)
105        signature_value = signature.find(f"{{{NS_SIGNATURE}}}SignatureValue")
106        self.assertIsNotNone(signature_value)
107        self.assertIsNotNone(signature_value.text)
108
109    def test_no_inresponseto(self):
110        """Test building response without a logout request omits InResponseTo attribute"""
111        processor = LogoutResponseProcessor(self.provider, None, destination=self.provider.sls_url)
112        response_xml = processor.build_response(status="Success")
113
114        root = ElementTree.fromstring(response_xml)
115        self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
116        self.assertNotIn("InResponseTo", root.attrib)
117
118    def test_no_destination(self):
119        """Test building response without destination"""
120        logout_request = LogoutRequest(
121            id="test-request-id",
122            issuer="test-sp",
123        )
124
125        processor = LogoutResponseProcessor(self.provider, logout_request, destination=None)
126        response_xml = processor.build_response(status="Success")
127
128        root = ElementTree.fromstring(response_xml)
129        self.assertNotIn("Destination", root.attrib)
130
131    def test_relay_state_from_logout_request(self):
132        """Test that relay_state is taken from logout_request if not provided"""
133        logout_request = LogoutRequest(
134            id="test-request-id",
135            issuer="test-sp",
136            relay_state="request-relay-state",
137        )
138
139        processor = LogoutResponseProcessor(
140            self.provider, logout_request, destination=self.provider.sls_url
141        )
142        self.assertEqual(processor.relay_state, "request-relay-state")
143
144    def test_relay_state_override(self):
145        """Test that explicit relay_state overrides logout_request relay_state"""
146        logout_request = LogoutRequest(
147            id="test-request-id",
148            issuer="test-sp",
149            relay_state="request-relay-state",
150        )
151
152        processor = LogoutResponseProcessor(
153            self.provider,
154            logout_request,
155            destination=self.provider.sls_url,
156            relay_state="explicit-relay-state",
157        )
158        self.assertEqual(processor.relay_state, "explicit-relay-state")

Test LogoutResponse processor

@apply_blueprint('system/providers-saml.yaml')
def setUp(self):
25    @apply_blueprint("system/providers-saml.yaml")
26    def setUp(self):
27        cert = create_test_cert()
28        self.factory = RequestFactory()
29        self.provider: SAMLProvider = SAMLProvider.objects.create(
30            authorization_flow=create_test_flow(),
31            acs_url="http://testserver/source/saml/provider/acs/",
32            sls_url="http://testserver/source/saml/provider/sls/",
33            signing_kp=cert,
34            verification_kp=cert,
35        )
36        self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
37        self.provider.save()
38        self.application = Application.objects.create(
39            name=generate_id(),
40            slug=generate_id(),
41            provider=self.provider,
42        )

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

def test_build_response(self):
44    def test_build_response(self):
45        """Test building a LogoutResponse uses the generated issuer from the assertion"""
46        # Generate the issuer the same way the assertion/metadata processors would
47        request = self.factory.get("/")
48        metadata_processor = MetadataProcessor(self.provider, request)
49        generated_issuer = metadata_processor._get_issuer_value()
50
51        logout_request = LogoutRequest(
52            id="test-request-id",
53            issuer="test-sp",
54            relay_state="test-relay-state",
55        )
56
57        # Pass the generated issuer as if it came from SAMLSession.issuer
58        processor = LogoutResponseProcessor(
59            self.provider,
60            logout_request,
61            destination=self.provider.sls_url,
62            issuer=generated_issuer,
63        )
64        response_xml = processor.build_response(status="Success")
65
66        # Parse and verify
67        root = ElementTree.fromstring(response_xml)
68        self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
69        self.assertEqual(root.attrib["Version"], "2.0")
70        self.assertEqual(root.attrib["Destination"], self.provider.sls_url)
71        self.assertEqual(root.attrib["InResponseTo"], "test-request-id")
72
73        # Check Issuer matches the generated issuer from the assertion processor
74        issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer")
75        self.assertEqual(issuer.text, generated_issuer)
76
77        # Check Status
78        status = root.find(f".//{{{NS_SAML_PROTOCOL}}}StatusCode")
79        self.assertEqual(status.attrib["Value"], "urn:oasis:names:tc:SAML:2.0:status:Success")

Test building a LogoutResponse uses the generated issuer from the assertion

def test_build_response_signed(self):
 81    def test_build_response_signed(self):
 82        """Test building a signed LogoutResponse"""
 83        self.provider.sign_logout_response = True
 84        self.provider.save()
 85
 86        logout_request = LogoutRequest(
 87            id="test-request-id",
 88            issuer="test-sp",
 89            relay_state="test-relay-state",
 90        )
 91
 92        processor = LogoutResponseProcessor(
 93            self.provider, logout_request, destination=self.provider.sls_url
 94        )
 95        response_xml = processor.build_response(status="Success")
 96
 97        # Parse and verify signature is present
 98        root = ElementTree.fromstring(response_xml)
 99        signature = root.find(f".//{{{NS_SIGNATURE}}}Signature")
100        self.assertIsNotNone(signature)
101
102        # Verify signature structure
103        signed_info = signature.find(f"{{{NS_SIGNATURE}}}SignedInfo")
104        self.assertIsNotNone(signed_info)
105        signature_value = signature.find(f"{{{NS_SIGNATURE}}}SignatureValue")
106        self.assertIsNotNone(signature_value)
107        self.assertIsNotNone(signature_value.text)

Test building a signed LogoutResponse

def test_no_inresponseto(self):
109    def test_no_inresponseto(self):
110        """Test building response without a logout request omits InResponseTo attribute"""
111        processor = LogoutResponseProcessor(self.provider, None, destination=self.provider.sls_url)
112        response_xml = processor.build_response(status="Success")
113
114        root = ElementTree.fromstring(response_xml)
115        self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
116        self.assertNotIn("InResponseTo", root.attrib)

Test building response without a logout request omits InResponseTo attribute

def test_no_destination(self):
118    def test_no_destination(self):
119        """Test building response without destination"""
120        logout_request = LogoutRequest(
121            id="test-request-id",
122            issuer="test-sp",
123        )
124
125        processor = LogoutResponseProcessor(self.provider, logout_request, destination=None)
126        response_xml = processor.build_response(status="Success")
127
128        root = ElementTree.fromstring(response_xml)
129        self.assertNotIn("Destination", root.attrib)

Test building response without destination

def test_relay_state_from_logout_request(self):
131    def test_relay_state_from_logout_request(self):
132        """Test that relay_state is taken from logout_request if not provided"""
133        logout_request = LogoutRequest(
134            id="test-request-id",
135            issuer="test-sp",
136            relay_state="request-relay-state",
137        )
138
139        processor = LogoutResponseProcessor(
140            self.provider, logout_request, destination=self.provider.sls_url
141        )
142        self.assertEqual(processor.relay_state, "request-relay-state")

Test that relay_state is taken from logout_request if not provided

def test_relay_state_override(self):
144    def test_relay_state_override(self):
145        """Test that explicit relay_state overrides logout_request relay_state"""
146        logout_request = LogoutRequest(
147            id="test-request-id",
148            issuer="test-sp",
149            relay_state="request-relay-state",
150        )
151
152        processor = LogoutResponseProcessor(
153            self.provider,
154            logout_request,
155            destination=self.provider.sls_url,
156            relay_state="explicit-relay-state",
157        )
158        self.assertEqual(processor.relay_state, "explicit-relay-state")

Test that explicit relay_state overrides logout_request relay_state