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