authentik.providers.saml.tests.test_logout_response_processor

logout response tests

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

Test LogoutResponse processor

@apply_blueprint('system/providers-saml.yaml')
def setUp(self):
22    @apply_blueprint("system/providers-saml.yaml")
23    def setUp(self):
24        cert = create_test_cert()
25        self.provider: SAMLProvider = SAMLProvider.objects.create(
26            authorization_flow=create_test_flow(),
27            acs_url="http://testserver/source/saml/provider/acs/",
28            sls_url="http://testserver/source/saml/provider/sls/",
29            signing_kp=cert,
30            verification_kp=cert,
31        )
32        self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
33        self.provider.save()

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

def test_build_response(self):
35    def test_build_response(self):
36        """Test building a LogoutResponse"""
37        logout_request = LogoutRequest(
38            id="test-request-id",
39            issuer="test-sp",
40            relay_state="test-relay-state",
41        )
42
43        processor = LogoutResponseProcessor(
44            self.provider, logout_request, destination=self.provider.sls_url
45        )
46        response_xml = processor.build_response(status="Success")
47
48        # Parse and verify
49        root = ElementTree.fromstring(response_xml)
50        self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
51        self.assertEqual(root.attrib["Version"], "2.0")
52        self.assertEqual(root.attrib["Destination"], self.provider.sls_url)
53        self.assertEqual(root.attrib["InResponseTo"], "test-request-id")
54
55        # Check Issuer
56        issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer")
57        self.assertEqual(issuer.text, self.provider.issuer)
58
59        # Check Status
60        status = root.find(f".//{{{NS_SAML_PROTOCOL}}}StatusCode")
61        self.assertEqual(status.attrib["Value"], "urn:oasis:names:tc:SAML:2.0:status:Success")

Test building a LogoutResponse

def test_build_response_signed(self):
63    def test_build_response_signed(self):
64        """Test building a signed LogoutResponse"""
65        self.provider.sign_logout_response = True
66        self.provider.save()
67
68        logout_request = LogoutRequest(
69            id="test-request-id",
70            issuer="test-sp",
71            relay_state="test-relay-state",
72        )
73
74        processor = LogoutResponseProcessor(
75            self.provider, logout_request, destination=self.provider.sls_url
76        )
77        response_xml = processor.build_response(status="Success")
78
79        # Parse and verify signature is present
80        root = ElementTree.fromstring(response_xml)
81        signature = root.find(f".//{{{NS_SIGNATURE}}}Signature")
82        self.assertIsNotNone(signature)
83
84        # Verify signature structure
85        signed_info = signature.find(f"{{{NS_SIGNATURE}}}SignedInfo")
86        self.assertIsNotNone(signed_info)
87        signature_value = signature.find(f"{{{NS_SIGNATURE}}}SignatureValue")
88        self.assertIsNotNone(signature_value)
89        self.assertIsNotNone(signature_value.text)

Test building a signed LogoutResponse

def test_no_inresponseto(self):
91    def test_no_inresponseto(self):
92        """Test building response without a logout request omits InResponseTo attribute"""
93        processor = LogoutResponseProcessor(self.provider, None, destination=self.provider.sls_url)
94        response_xml = processor.build_response(status="Success")
95
96        root = ElementTree.fromstring(response_xml)
97        self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
98        self.assertNotIn("InResponseTo", root.attrib)

Test building response without a logout request omits InResponseTo attribute

def test_no_destination(self):
100    def test_no_destination(self):
101        """Test building response without destination"""
102        logout_request = LogoutRequest(
103            id="test-request-id",
104            issuer="test-sp",
105        )
106
107        processor = LogoutResponseProcessor(self.provider, logout_request, destination=None)
108        response_xml = processor.build_response(status="Success")
109
110        root = ElementTree.fromstring(response_xml)
111        self.assertNotIn("Destination", root.attrib)

Test building response without destination

def test_relay_state_from_logout_request(self):
113    def test_relay_state_from_logout_request(self):
114        """Test that relay_state is taken from logout_request if not provided"""
115        logout_request = LogoutRequest(
116            id="test-request-id",
117            issuer="test-sp",
118            relay_state="request-relay-state",
119        )
120
121        processor = LogoutResponseProcessor(
122            self.provider, logout_request, destination=self.provider.sls_url
123        )
124        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):
126    def test_relay_state_override(self):
127        """Test that explicit relay_state overrides logout_request relay_state"""
128        logout_request = LogoutRequest(
129            id="test-request-id",
130            issuer="test-sp",
131            relay_state="request-relay-state",
132        )
133
134        processor = LogoutResponseProcessor(
135            self.provider,
136            logout_request,
137            destination=self.provider.sls_url,
138            relay_state="explicit-relay-state",
139        )
140        self.assertEqual(processor.relay_state, "explicit-relay-state")

Test that explicit relay_state overrides logout_request relay_state