authentik.providers.saml.tests.test_auth_n_request
Test AuthN Request generator and parser
1"""Test AuthN Request generator and parser""" 2 3from base64 import b64encode 4 5from defusedxml.lxml import fromstring 6from django.http.request import QueryDict 7from django.test import TestCase 8from guardian.utils import get_anonymous_user 9from lxml import etree # nosec 10 11from authentik.blueprints.tests import apply_blueprint 12from authentik.common.saml.constants import ( 13 NS_MAP, 14 SAML_BINDING_POST, 15 SAML_NAME_ID_FORMAT_EMAIL, 16 SAML_NAME_ID_FORMAT_UNSPECIFIED, 17) 18from authentik.core.tests.utils import ( 19 RequestFactory, 20 create_test_admin_user, 21 create_test_cert, 22 create_test_flow, 23) 24from authentik.crypto.models import CertificateKeyPair 25from authentik.events.models import Event, EventAction 26from authentik.lib.generators import generate_id 27from authentik.lib.xml import lxml_from_string 28from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider 29from authentik.providers.saml.processors.assertion import AssertionProcessor 30from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser 31from authentik.sources.saml.exceptions import MismatchedRequestID 32from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource 33from authentik.sources.saml.processors.request import SESSION_KEY_REQUEST_ID, RequestProcessor 34from authentik.sources.saml.processors.response import ResponseProcessor 35 36POST_REQUEST = ( 37 "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1sMn" 38 "A9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgQXNzZXJ0aW9uQ29uc3VtZXJTZXJ2aWNlVVJMPSJo" 39 "dHRwczovL2V1LWNlbnRyYWwtMS5zaWduaW4uYXdzLmFtYXpvbi5jb20vcGxhdGZvcm0vc2FtbC9hY3MvMmQ3MzdmOTYtNT" 40 "VmYi00MDM1LTk1M2UtNWUyNDEzNGViNzc4IiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9pZC5iZXJ5anUub3JnL2FwcGxpY2F0" 41 "aW9uL3NhbWwvYXdzLXNzby9zc28vYmluZGluZy9wb3N0LyIgSUQ9ImF3c19MRHhMR2V1YnBjNWx4MTJneENnUzZ1UGJpeD" 42 "F5ZDVyZSIgSXNzdWVJbnN0YW50PSIyMDIxLTA3LTA2VDE0OjIzOjA2LjM4OFoiIFByb3RvY29sQmluZGluZz0idXJuOm9h" 43 "c2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIH" 44 "htbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwczovL2V1LWNlbnRyYWwt" 45 "MS5zaWduaW4uYXdzLmFtYXpvbi5jb20vcGxhdGZvcm0vc2FtbC9kLTk5NjcyZjgyNzg8L3NhbWwyOklzc3Vlcj48c2FtbD" 46 "JwOk5hbWVJRFBvbGljeSBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWls" 47 "QWRkcmVzcyIvPjwvc2FtbDJwOkF1dGhuUmVxdWVzdD4=" 48) 49REDIRECT_REQUEST = ( 50 "fZLNbsIwEIRfJfIdbKeFgEUipXAoEm0jSHvopTLJplhK7NTr9Oft6yRUKhekPdk73+yOdoWyqVuRdu6k9/DRAbrgu6k1iu" 51 "EjJp3VwkhUKLRsAIUrxCF92IlwykRrjTOFqUmQIoJ1yui10dg1YA9gP1UBz/tdTE7OtSgo5WzKQzYditGeP8GW9rSQZk+H" 52 "nAQbb6+07EGj7EI1j8SCeaVs21oVQ9dAoRqcf6OIhh6VLpV+pxZKZaFwlATbTUzeyqKazaqiDCO5WEQwZzKCagkwr8obWc" 53 "qjb0PsYKvRSe1iErKQTTj3lYdc3HLBl68kyM4L340u19M5j4LiPs+zybjgC1gclvMNJFn104vB2P5L/TpW/kVNkqvBrug/" 54 "+mjVikeP224y4/P7CdK6Nl9rC9JBTDihySi5vIbkFw==" 55) 56REDIRECT_SIGNATURE = ( 57 "UlOe1BItHVHM+io6rUZAenIqfibm7hM6wr9I1rcP5kPJ4N8cbkyqmAMh5LD2lUq3PDERJfjdO/oOKnvJmbD2y9MOObyR2d" 58 "7Udv62KERrA0qM917Q+w8wrLX7w2nHY96EDvkXD4iAomR5EE9dHRuubDy7uRv2syEevc0gfoLi7W/5vp96vJgsaSqxnTp+" 59 "QiYq49KyWyMtxRULF2yd+vYDnHCDME73mNSULEHfwCU71dvbKpnFaej78q7wS20gUk6ysOOXXtvDHbiVcpUb/9oyDgNAxU" 60 "jVvPdh96AhBFj2HCuGZhP0CGotafTciu6YlsiwUpuBkIYgZmNWYa3FR9LS4Q==" 61) 62REDIRECT_SIG_ALG = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" 63REDIRECT_RELAY_STATE = "ss:mem:7a054b4af44f34f89dd2d973f383c250b6b076e7f06cfa8276008a6504eaf3c7" 64REDIRECT_CERT = """-----BEGIN CERTIFICATE----- 65MIIDCDCCAfCgAwIBAgIRAM5s+bhOHk4ChSpPkGSh0NswDQYJKoZIhvcNAQELBQAw 66KzEpMCcGA1UEAwwgcGFzc2Jvb2sgU2VsZi1zaWduZWQgQ2VydGlmaWNhdGUwHhcN 67MjAxMTA3MjAzNDIxWhcNMjExMTA4MjAzNDIxWjBUMSkwJwYDVQQDDCBwYXNzYm9v 68ayBTZWxmLXNpZ25lZCBDZXJ0aWZpY2F0ZTERMA8GA1UECgwIcGFzc2Jvb2sxFDAS 69BgNVBAsMC1NlbGYtc2lnbmVkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC 70AQEAuh+Bv6a/ogpic72X/sq86YiLzVjixnGqjc4wpsPPP00GX8jUAZJL4Tjo+sYK 71IU2DF2/azlVqjkbLho4rGuuc8YkbFXBEXPYc5h3bseO2vk6sbbbWKV0mro1VFhBh 72T59hBORuMMefmQdhFzsRNOGklIptQdg0quD8ET3+/uNfIT98S2ruZdYteFls46Sa 73MokZFYVD6pWEYV4P2MKVAFqJX9bqBW0LfCCfFqHAOJjUZj9dtleg86d2WfedUOG2 74LK0iLrydjhThbI0GUDhv0jWYkRlv04fdJ1WSRANYA3gBOnyw+Iigh2xNnYbVZMXT 75I0BupIJ4UoODMc4QpD2GYJ6oGwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCCEF3e 76Y99KxEBSR4H4/TvKbnh4QtHswOf7MaGdjtrld7l4u4Hc4NEklNdDn1XLKhZwnq3Z 77LRsRlJutDzZ18SRmAJPXPbka7z7D+LA1mbNQElOgiKyQHD9rIJSBr6X5SM9As3CR 787QUsb8dg7kc+Jn7WuLZIEVxxMtekt0buWEdMJiklF0tCS3LNsP083FaQk/H1K0z6 793PWP26EFdwir3RyTKLY5CBLjKrUAo9O1l/WBVFYbdetnipbGGu5f6nk6nnxbwLLI 80Dm52Vkq+xFDDUq9IqIoYvLaE86MDvtpMQEx65tIGU19vUf3fL/+sSfdRZ1HDzP4d 81qNAZMq1DqpibfCBg 82-----END CERTIFICATE-----""" 83 84 85class TestAuthNRequest(TestCase): 86 """Test AuthN Request generator and parser""" 87 88 @apply_blueprint("system/providers-saml.yaml") 89 def setUp(self): 90 self.request_factory = RequestFactory() 91 self.cert = create_test_cert() 92 self.provider: SAMLProvider = SAMLProvider.objects.create( 93 authorization_flow=create_test_flow(), 94 acs_url="http://testserver/source/saml/provider/acs/", 95 signing_kp=self.cert, 96 verification_kp=self.cert, 97 ) 98 self.provider.property_mappings.set(SAMLPropertyMapping.objects.all()) 99 self.provider.save() 100 self.source = SAMLSource.objects.create( 101 slug="provider", 102 issuer="authentik", 103 pre_authentication_flow=create_test_flow(), 104 signing_kp=self.cert, 105 verification_kp=self.cert, 106 signed_assertion=True, 107 binding_type=SAMLBindingTypes.POST, 108 ) 109 110 def test_signed_valid(self): 111 """Test generated AuthNRequest with valid signature""" 112 http_request = self.request_factory.get("/") 113 114 # First create an AuthNRequest 115 request_proc = RequestProcessor(self.source, http_request, "test_state") 116 auth_n = request_proc.get_auth_n() 117 self.assertEqual(auth_n.attrib["ProtocolBinding"], SAML_BINDING_POST) 118 119 request = request_proc.build_auth_n() 120 # Now we check the ID and signature 121 parsed_request = AuthNRequestParser(self.provider).parse( 122 b64encode(request.encode()).decode(), "test_state" 123 ) 124 self.assertEqual(parsed_request.id, request_proc.request_id) 125 self.assertEqual(parsed_request.relay_state, "test_state") 126 127 def test_request_encrypt(self): 128 """Test full SAML Request/Response flow, fully encrypted""" 129 self.provider.encryption_kp = self.cert 130 self.provider.save() 131 self.source.encryption_kp = self.cert 132 self.source.save() 133 http_request = self.request_factory.get("/", user=get_anonymous_user()) 134 135 # First create an AuthNRequest 136 request_proc = RequestProcessor(self.source, http_request, "test_state") 137 request = request_proc.build_auth_n() 138 139 # To get an assertion we need a parsed request (parsed by provider) 140 parsed_request = AuthNRequestParser(self.provider).parse( 141 b64encode(request.encode()).decode(), "test_state" 142 ) 143 # Now create a response and convert it to string (provider) 144 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 145 response = response_proc.build_response() 146 147 # Now parse the response (source) 148 http_request.POST = QueryDict(mutable=True) 149 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 150 151 response_parser = ResponseProcessor(self.source, http_request) 152 response_parser.parse() 153 154 def test_request_encrypt_cert_only(self): 155 """Test SAML encryption with certificate-only keypair (no private key). 156 157 This tests the scenario where the IdP (provider) only has the SP's public 158 certificate for encryption, without a private key. This is the expected 159 real-world scenario since the SP would never share their private key. 160 """ 161 # Create a full keypair for the source (SP) - it needs the private key to decrypt 162 full_keypair = create_test_cert() 163 164 # Create a certificate-only keypair for the provider (IdP) 165 # This simulates having only the SP's public certificate 166 cert_only = CertificateKeyPair.objects.create( 167 name=generate_id(), 168 certificate_data=full_keypair.certificate_data, 169 key_data="", # No private key 170 ) 171 172 self.provider.encryption_kp = cert_only 173 self.provider.save() 174 self.source.encryption_kp = full_keypair 175 self.source.save() 176 http_request = self.request_factory.get("/", user=get_anonymous_user()) 177 178 # First create an AuthNRequest 179 request_proc = RequestProcessor(self.source, http_request, "test_state") 180 request = request_proc.build_auth_n() 181 182 # To get an assertion we need a parsed request (parsed by provider) 183 parsed_request = AuthNRequestParser(self.provider).parse( 184 b64encode(request.encode()).decode(), "test_state" 185 ) 186 # Now create a response and convert it to string (provider) 187 # This should work with only the certificate (public key) for encryption 188 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 189 response = response_proc.build_response() 190 191 # Now parse the response (source) - decryption requires the private key 192 http_request.POST = QueryDict(mutable=True) 193 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 194 195 response_parser = ResponseProcessor(self.source, http_request) 196 response_parser.parse() 197 198 def test_request_sign_response_and_encrypt(self): 199 """Test SAML with sign_response enabled AND encryption. 200 201 This tests the fix for signature invalidation when encryption is enabled. 202 The response must be signed AFTER encryption, not before, because encryption 203 replaces the Assertion with EncryptedAssertion which changes the response content. 204 """ 205 self.provider.sign_response = True 206 self.provider.sign_assertion = False 207 self.provider.encryption_kp = self.cert 208 self.provider.save() 209 self.source.encryption_kp = self.cert 210 self.source.signed_response = True 211 self.source.signed_assertion = False # Only response is signed, not assertion 212 self.source.save() 213 http_request = self.request_factory.get("/", user=get_anonymous_user()) 214 215 # First create an AuthNRequest 216 request_proc = RequestProcessor(self.source, http_request, "test_state") 217 request = request_proc.build_auth_n() 218 219 # To get an assertion we need a parsed request (parsed by provider) 220 parsed_request = AuthNRequestParser(self.provider).parse( 221 b64encode(request.encode()).decode(), "test_state" 222 ) 223 # Now create a response and convert it to string (provider) 224 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 225 response = response_proc.build_response() 226 227 # Verify the response contains EncryptedAssertion and a signature 228 response_xml = fromstring(response) 229 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 230 self.assertEqual( 231 len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1 232 ) 233 234 # Now parse the response (source) - this will verify the signature and decrypt 235 http_request.POST = QueryDict(mutable=True) 236 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 237 238 response_parser = ResponseProcessor(self.source, http_request) 239 response_parser.parse() 240 241 def test_request_sign_assertion_and_encrypt(self): 242 """Test SAML with sign_assertion enabled AND encryption. 243 244 The assertion signature should be inside the encrypted content and 245 remain valid after decryption. 246 """ 247 self.provider.sign_response = False 248 self.provider.sign_assertion = True 249 self.provider.encryption_kp = self.cert 250 self.provider.save() 251 self.source.encryption_kp = self.cert 252 self.source.signed_assertion = True 253 self.source.save() 254 http_request = self.request_factory.get("/", user=get_anonymous_user()) 255 256 # First create an AuthNRequest 257 request_proc = RequestProcessor(self.source, http_request, "test_state") 258 request = request_proc.build_auth_n() 259 260 # To get an assertion we need a parsed request (parsed by provider) 261 parsed_request = AuthNRequestParser(self.provider).parse( 262 b64encode(request.encode()).decode(), "test_state" 263 ) 264 # Now create a response and convert it to string (provider) 265 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 266 response = response_proc.build_response() 267 268 # Verify the response contains EncryptedAssertion 269 response_xml = fromstring(response) 270 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 271 272 # Now parse the response (source) - this will decrypt and verify assertion signature 273 http_request.POST = QueryDict(mutable=True) 274 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 275 276 response_parser = ResponseProcessor(self.source, http_request) 277 response_parser.parse() 278 279 def test_request_sign_both_and_encrypt(self): 280 """Test SAML with both sign_assertion and sign_response enabled AND encryption. 281 282 This is the most complex scenario: assertion is signed, then encrypted, 283 then the response is signed. Both signatures should be valid. 284 """ 285 self.provider.sign_response = True 286 self.provider.sign_assertion = True 287 self.provider.encryption_kp = self.cert 288 self.provider.save() 289 self.source.encryption_kp = self.cert 290 self.source.signed_assertion = True 291 self.source.signed_response = True 292 self.source.save() 293 http_request = self.request_factory.get("/", user=get_anonymous_user()) 294 295 # First create an AuthNRequest 296 request_proc = RequestProcessor(self.source, http_request, "test_state") 297 request = request_proc.build_auth_n() 298 299 # To get an assertion we need a parsed request (parsed by provider) 300 parsed_request = AuthNRequestParser(self.provider).parse( 301 b64encode(request.encode()).decode(), "test_state" 302 ) 303 # Now create a response and convert it to string (provider) 304 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 305 response = response_proc.build_response() 306 307 # Verify the response contains EncryptedAssertion and response signature 308 response_xml = fromstring(response) 309 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 310 self.assertEqual( 311 len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1 312 ) 313 314 # Now parse the response (source) - this will verify response signature, 315 # decrypt, then verify assertion signature 316 http_request.POST = QueryDict(mutable=True) 317 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 318 319 response_parser = ResponseProcessor(self.source, http_request) 320 response_parser.parse() 321 322 def test_encrypted_assertion_namespace_preservation(self): 323 """Test that encrypted assertions include namespace declarations. 324 325 When an assertion is encrypted, the resulting decrypted XML must include 326 the necessary namespace declarations (xmlns:saml) since it's now a standalone 327 document fragment, no longer inheriting namespaces from the parent Response. 328 """ 329 self.provider.encryption_kp = self.cert 330 self.provider.save() 331 self.source.encryption_kp = self.cert 332 self.source.save() 333 http_request = self.request_factory.get("/", user=get_anonymous_user()) 334 335 # First create an AuthNRequest 336 request_proc = RequestProcessor(self.source, http_request, "test_state") 337 request = request_proc.build_auth_n() 338 339 # To get an assertion we need a parsed request (parsed by provider) 340 parsed_request = AuthNRequestParser(self.provider).parse( 341 b64encode(request.encode()).decode(), "test_state" 342 ) 343 # Now create a response and convert it to string (provider) 344 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 345 response = response_proc.build_response() 346 347 # Parse the encrypted response 348 response_xml = fromstring(response) 349 encrypted_assertion = response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)[0] 350 encrypted_data = encrypted_assertion.xpath("//xenc:EncryptedData", namespaces=NS_MAP)[0] 351 352 # Decrypt the assertion manually to verify namespace is present 353 import xmlsec 354 355 manager = xmlsec.KeysManager() 356 key = xmlsec.Key.from_memory(self.cert.key_data, xmlsec.constants.KeyDataFormatPem, None) 357 manager.add_key(key) 358 enc_ctx = xmlsec.EncryptionContext(manager) 359 decrypted = enc_ctx.decrypt(encrypted_data) 360 361 # The decrypted assertion should have xmlns:saml namespace declaration 362 decrypted_str = etree.tostring(decrypted).decode() 363 self.assertIn("xmlns:saml", decrypted_str) 364 365 # Also verify full round-trip works (source can parse it) 366 http_request.POST = QueryDict(mutable=True) 367 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 368 369 response_parser = ResponseProcessor(self.source, http_request) 370 response_parser.parse() 371 372 def test_encrypted_response_schema_validation(self): 373 """Test that encrypted SAML responses validate against the SAML schema. 374 375 The response with EncryptedAssertion must be valid per saml-schema-protocol-2.0.xsd. 376 This ensures we don't have invalid elements like EncryptedData inside Assertion. 377 """ 378 self.provider.encryption_kp = self.cert 379 self.provider.save() 380 http_request = self.request_factory.get("/", user=get_anonymous_user()) 381 382 # First create an AuthNRequest 383 request_proc = RequestProcessor(self.source, http_request, "test_state") 384 request = request_proc.build_auth_n() 385 386 # To get an assertion we need a parsed request (parsed by provider) 387 parsed_request = AuthNRequestParser(self.provider).parse( 388 b64encode(request.encode()).decode(), "test_state" 389 ) 390 # Now create a response and convert it to string (provider) 391 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 392 response = response_proc.build_response() 393 394 # Validate against SAML schema 395 schema = etree.XMLSchema( 396 etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec 397 ) 398 self.assertTrue(schema.validate(lxml_from_string(response))) 399 400 # Verify structure: should have EncryptedAssertion, not Assertion with EncryptedData inside 401 response_xml = fromstring(response) 402 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 403 self.assertEqual(len(response_xml.xpath("//saml:Assertion", namespaces=NS_MAP)), 0) 404 405 def test_request_signed(self): 406 """Test full SAML Request/Response flow, fully signed""" 407 http_request = self.request_factory.get("/", user=get_anonymous_user()) 408 409 # First create an AuthNRequest 410 request_proc = RequestProcessor(self.source, http_request, "test_state") 411 request = request_proc.build_auth_n() 412 413 # To get an assertion we need a parsed request (parsed by provider) 414 parsed_request = AuthNRequestParser(self.provider).parse( 415 b64encode(request.encode()).decode(), "test_state" 416 ) 417 # Now create a response and convert it to string (provider) 418 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 419 response = response_proc.build_response() 420 421 # Now parse the response (source) 422 http_request.POST = QueryDict(mutable=True) 423 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 424 425 response_parser = ResponseProcessor(self.source, http_request) 426 response_parser.parse() 427 428 def test_request_signed_both(self): 429 """Test full SAML Request/Response flow, fully signed""" 430 self.provider.sign_assertion = True 431 self.provider.sign_response = True 432 self.provider.save() 433 self.source.signed_response = True 434 http_request = self.request_factory.get("/", user=get_anonymous_user()) 435 436 # First create an AuthNRequest 437 request_proc = RequestProcessor(self.source, http_request, "test_state") 438 request = request_proc.build_auth_n() 439 440 # To get an assertion we need a parsed request (parsed by provider) 441 parsed_request = AuthNRequestParser(self.provider).parse( 442 b64encode(request.encode()).decode(), "test_state" 443 ) 444 # Now create a response and convert it to string (provider) 445 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 446 response = response_proc.build_response() 447 # Ensure both response and assertion ID are in the response twice (once as ID attribute, 448 # once as ds:Reference URI) 449 self.assertEqual(response.count(response_proc._assertion_id), 2) 450 self.assertEqual(response.count(response_proc._response_id), 2) 451 452 schema = etree.XMLSchema( 453 etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec 454 ) 455 self.assertTrue(schema.validate(lxml_from_string(response))) 456 457 response_xml = fromstring(response) 458 self.assertEqual( 459 len(response_xml.xpath("//saml:Assertion/ds:Signature", namespaces=NS_MAP)), 1 460 ) 461 self.assertEqual( 462 len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1 463 ) 464 465 # Now parse the response (source) 466 http_request.POST = QueryDict(mutable=True) 467 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 468 469 response_parser = ResponseProcessor(self.source, http_request) 470 response_parser.parse() 471 472 def test_request_id_invalid(self): 473 """Test generated AuthNRequest with invalid request ID""" 474 http_request = self.request_factory.get("/", user=get_anonymous_user()) 475 476 # First create an AuthNRequest 477 request_proc = RequestProcessor(self.source, http_request, "test_state") 478 request = request_proc.build_auth_n() 479 480 # change the request ID 481 http_request.session[SESSION_KEY_REQUEST_ID] = "test" 482 http_request.session.save() 483 484 # To get an assertion we need a parsed request (parsed by provider) 485 parsed_request = AuthNRequestParser(self.provider).parse( 486 b64encode(request.encode()).decode(), "test_state" 487 ) 488 # Now create a response and convert it to string (provider) 489 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 490 response = response_proc.build_response() 491 492 # Now parse the response (source) 493 http_request.POST = QueryDict(mutable=True) 494 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 495 496 response_parser = ResponseProcessor(self.source, http_request) 497 498 with self.assertRaises(MismatchedRequestID): 499 response_parser.parse() 500 501 def test_signed_valid_detached(self): 502 """Test generated AuthNRequest with valid signature (detached)""" 503 http_request = self.request_factory.get("/") 504 505 # First create an AuthNRequest 506 request_proc = RequestProcessor(self.source, http_request, "test_state") 507 params = request_proc.build_auth_n_detached() 508 # Now we check the ID and signature 509 parsed_request = AuthNRequestParser(self.provider).parse_detached( 510 params["SAMLRequest"], 511 params["RelayState"], 512 params["Signature"], 513 params["SigAlg"], 514 ) 515 self.assertEqual(parsed_request.id, request_proc.request_id) 516 self.assertEqual(parsed_request.relay_state, "test_state") 517 518 def test_signed_detached_static(self): 519 """Test request with detached signature, 520 taken from https://www.samltool.com/generic_sso_req.php""" 521 static_keypair = CertificateKeyPair.objects.create( 522 name="samltool", certificate_data=REDIRECT_CERT 523 ) 524 provider = SAMLProvider( 525 name="samltool", 526 authorization_flow=create_test_flow(), 527 acs_url="https://10.120.20.200/saml-sp/SAML2/POST", 528 audience="https://10.120.20.200/saml-sp/SAML2/POST", 529 issuer="https://10.120.20.200/saml-sp/SAML2/POST", 530 signing_kp=static_keypair, 531 verification_kp=static_keypair, 532 ) 533 parsed_request = AuthNRequestParser(provider).parse_detached( 534 REDIRECT_REQUEST, REDIRECT_RELAY_STATE, REDIRECT_SIGNATURE, REDIRECT_SIG_ALG 535 ) 536 self.assertEqual(parsed_request.id, "_dcf55fcd27a887e60a7ef9ee6fd3adab") 537 self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_UNSPECIFIED) 538 self.assertEqual(parsed_request.relay_state, REDIRECT_RELAY_STATE) 539 540 def test_signed_static(self): 541 """Test post request with static request""" 542 provider = SAMLProvider( 543 name="aws", 544 authorization_flow=create_test_flow(), 545 acs_url=( 546 "https://eu-central-1.signin.aws.amazon.com/platform/" 547 "saml/acs/2d737f96-55fb-4035-953e-5e24134eb778" 548 ), 549 audience="https://10.120.20.200/saml-sp/SAML2/POST", 550 issuer="https://10.120.20.200/saml-sp/SAML2/POST", 551 signing_kp=create_test_cert(), 552 ) 553 parsed_request = AuthNRequestParser(provider).parse(POST_REQUEST) 554 self.assertEqual(parsed_request.id, "aws_LDxLGeubpc5lx12gxCgS6uPbix1yd5re") 555 self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_EMAIL) 556 557 def test_authn_context_class_ref_mapping(self): 558 """Test custom authn_context_class_ref""" 559 authn_context_class_ref = generate_id() 560 mapping = SAMLPropertyMapping.objects.create( 561 name=generate_id(), expression=f"""return '{authn_context_class_ref}'""" 562 ) 563 self.provider.authn_context_class_ref_mapping = mapping 564 self.provider.save() 565 user = create_test_admin_user() 566 http_request = self.request_factory.get("/", user=user) 567 568 # First create an AuthNRequest 569 request_proc = RequestProcessor(self.source, http_request, "test_state") 570 request = request_proc.build_auth_n() 571 572 # To get an assertion we need a parsed request (parsed by provider) 573 parsed_request = AuthNRequestParser(self.provider).parse( 574 b64encode(request.encode()).decode(), "test_state" 575 ) 576 # Now create a response and convert it to string (provider) 577 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 578 response = response_proc.build_response() 579 self.assertIn(user.username, response) 580 self.assertIn(authn_context_class_ref, response) 581 582 def test_authn_context_class_ref_mapping_invalid(self): 583 """Test custom authn_context_class_ref (invalid)""" 584 mapping = SAMLPropertyMapping.objects.create(name=generate_id(), expression="q") 585 self.provider.authn_context_class_ref_mapping = mapping 586 self.provider.save() 587 user = create_test_admin_user() 588 http_request = self.request_factory.get("/", user=user) 589 590 # First create an AuthNRequest 591 request_proc = RequestProcessor(self.source, http_request, "test_state") 592 request = request_proc.build_auth_n() 593 594 # To get an assertion we need a parsed request (parsed by provider) 595 parsed_request = AuthNRequestParser(self.provider).parse( 596 b64encode(request.encode()).decode(), "test_state" 597 ) 598 # Now create a response and convert it to string (provider) 599 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 600 response = response_proc.build_response() 601 self.assertIn(user.username, response) 602 603 events = Event.objects.filter( 604 action=EventAction.CONFIGURATION_ERROR, 605 ) 606 self.assertTrue(events.exists()) 607 self.assertEqual( 608 events.first().context["message"], 609 f"Failed to evaluate property-mapping: '{mapping.name}'", 610 ) 611 612 def test_request_attributes(self): 613 """Test full SAML Request/Response flow, fully signed""" 614 user = create_test_admin_user() 615 http_request = self.request_factory.get("/", user=user) 616 617 # First create an AuthNRequest 618 request_proc = RequestProcessor(self.source, http_request, "test_state") 619 request = request_proc.build_auth_n() 620 621 # To get an assertion we need a parsed request (parsed by provider) 622 parsed_request = AuthNRequestParser(self.provider).parse( 623 b64encode(request.encode()).decode(), "test_state" 624 ) 625 # Now create a response and convert it to string (provider) 626 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 627 self.assertIn(user.username, response_proc.build_response()) 628 629 def test_request_attributes_invalid(self): 630 """Test full SAML Request/Response flow, fully signed""" 631 user = create_test_admin_user() 632 http_request = self.request_factory.get("/", user=user) 633 634 # First create an AuthNRequest 635 request_proc = RequestProcessor(self.source, http_request, "test_state") 636 request = request_proc.build_auth_n() 637 638 # Create invalid PropertyMapping 639 mapping = SAMLPropertyMapping.objects.create( 640 name=generate_id(), saml_name="test", expression="q" 641 ) 642 self.provider.property_mappings.add(mapping) 643 644 # To get an assertion we need a parsed request (parsed by provider) 645 parsed_request = AuthNRequestParser(self.provider).parse( 646 b64encode(request.encode()).decode(), "test_state" 647 ) 648 # Now create a response and convert it to string (provider) 649 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 650 self.assertIn(user.username, response_proc.build_response()) 651 652 events = Event.objects.filter( 653 action=EventAction.CONFIGURATION_ERROR, 654 ) 655 self.assertTrue(events.exists()) 656 self.assertEqual( 657 events.first().context["message"], 658 f"Failed to evaluate property-mapping: '{mapping.name}'", 659 ) 660 661 def test_idp_initiated(self): 662 """Test IDP-initiated login""" 663 self.provider.default_relay_state = generate_id() 664 request = AuthNRequestParser(self.provider).idp_initiated() 665 self.assertEqual(request.id, None) 666 self.assertEqual(request.relay_state, self.provider.default_relay_state)
86class TestAuthNRequest(TestCase): 87 """Test AuthN Request generator and parser""" 88 89 @apply_blueprint("system/providers-saml.yaml") 90 def setUp(self): 91 self.request_factory = RequestFactory() 92 self.cert = create_test_cert() 93 self.provider: SAMLProvider = SAMLProvider.objects.create( 94 authorization_flow=create_test_flow(), 95 acs_url="http://testserver/source/saml/provider/acs/", 96 signing_kp=self.cert, 97 verification_kp=self.cert, 98 ) 99 self.provider.property_mappings.set(SAMLPropertyMapping.objects.all()) 100 self.provider.save() 101 self.source = SAMLSource.objects.create( 102 slug="provider", 103 issuer="authentik", 104 pre_authentication_flow=create_test_flow(), 105 signing_kp=self.cert, 106 verification_kp=self.cert, 107 signed_assertion=True, 108 binding_type=SAMLBindingTypes.POST, 109 ) 110 111 def test_signed_valid(self): 112 """Test generated AuthNRequest with valid signature""" 113 http_request = self.request_factory.get("/") 114 115 # First create an AuthNRequest 116 request_proc = RequestProcessor(self.source, http_request, "test_state") 117 auth_n = request_proc.get_auth_n() 118 self.assertEqual(auth_n.attrib["ProtocolBinding"], SAML_BINDING_POST) 119 120 request = request_proc.build_auth_n() 121 # Now we check the ID and signature 122 parsed_request = AuthNRequestParser(self.provider).parse( 123 b64encode(request.encode()).decode(), "test_state" 124 ) 125 self.assertEqual(parsed_request.id, request_proc.request_id) 126 self.assertEqual(parsed_request.relay_state, "test_state") 127 128 def test_request_encrypt(self): 129 """Test full SAML Request/Response flow, fully encrypted""" 130 self.provider.encryption_kp = self.cert 131 self.provider.save() 132 self.source.encryption_kp = self.cert 133 self.source.save() 134 http_request = self.request_factory.get("/", user=get_anonymous_user()) 135 136 # First create an AuthNRequest 137 request_proc = RequestProcessor(self.source, http_request, "test_state") 138 request = request_proc.build_auth_n() 139 140 # To get an assertion we need a parsed request (parsed by provider) 141 parsed_request = AuthNRequestParser(self.provider).parse( 142 b64encode(request.encode()).decode(), "test_state" 143 ) 144 # Now create a response and convert it to string (provider) 145 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 146 response = response_proc.build_response() 147 148 # Now parse the response (source) 149 http_request.POST = QueryDict(mutable=True) 150 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 151 152 response_parser = ResponseProcessor(self.source, http_request) 153 response_parser.parse() 154 155 def test_request_encrypt_cert_only(self): 156 """Test SAML encryption with certificate-only keypair (no private key). 157 158 This tests the scenario where the IdP (provider) only has the SP's public 159 certificate for encryption, without a private key. This is the expected 160 real-world scenario since the SP would never share their private key. 161 """ 162 # Create a full keypair for the source (SP) - it needs the private key to decrypt 163 full_keypair = create_test_cert() 164 165 # Create a certificate-only keypair for the provider (IdP) 166 # This simulates having only the SP's public certificate 167 cert_only = CertificateKeyPair.objects.create( 168 name=generate_id(), 169 certificate_data=full_keypair.certificate_data, 170 key_data="", # No private key 171 ) 172 173 self.provider.encryption_kp = cert_only 174 self.provider.save() 175 self.source.encryption_kp = full_keypair 176 self.source.save() 177 http_request = self.request_factory.get("/", user=get_anonymous_user()) 178 179 # First create an AuthNRequest 180 request_proc = RequestProcessor(self.source, http_request, "test_state") 181 request = request_proc.build_auth_n() 182 183 # To get an assertion we need a parsed request (parsed by provider) 184 parsed_request = AuthNRequestParser(self.provider).parse( 185 b64encode(request.encode()).decode(), "test_state" 186 ) 187 # Now create a response and convert it to string (provider) 188 # This should work with only the certificate (public key) for encryption 189 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 190 response = response_proc.build_response() 191 192 # Now parse the response (source) - decryption requires the private key 193 http_request.POST = QueryDict(mutable=True) 194 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 195 196 response_parser = ResponseProcessor(self.source, http_request) 197 response_parser.parse() 198 199 def test_request_sign_response_and_encrypt(self): 200 """Test SAML with sign_response enabled AND encryption. 201 202 This tests the fix for signature invalidation when encryption is enabled. 203 The response must be signed AFTER encryption, not before, because encryption 204 replaces the Assertion with EncryptedAssertion which changes the response content. 205 """ 206 self.provider.sign_response = True 207 self.provider.sign_assertion = False 208 self.provider.encryption_kp = self.cert 209 self.provider.save() 210 self.source.encryption_kp = self.cert 211 self.source.signed_response = True 212 self.source.signed_assertion = False # Only response is signed, not assertion 213 self.source.save() 214 http_request = self.request_factory.get("/", user=get_anonymous_user()) 215 216 # First create an AuthNRequest 217 request_proc = RequestProcessor(self.source, http_request, "test_state") 218 request = request_proc.build_auth_n() 219 220 # To get an assertion we need a parsed request (parsed by provider) 221 parsed_request = AuthNRequestParser(self.provider).parse( 222 b64encode(request.encode()).decode(), "test_state" 223 ) 224 # Now create a response and convert it to string (provider) 225 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 226 response = response_proc.build_response() 227 228 # Verify the response contains EncryptedAssertion and a signature 229 response_xml = fromstring(response) 230 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 231 self.assertEqual( 232 len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1 233 ) 234 235 # Now parse the response (source) - this will verify the signature and decrypt 236 http_request.POST = QueryDict(mutable=True) 237 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 238 239 response_parser = ResponseProcessor(self.source, http_request) 240 response_parser.parse() 241 242 def test_request_sign_assertion_and_encrypt(self): 243 """Test SAML with sign_assertion enabled AND encryption. 244 245 The assertion signature should be inside the encrypted content and 246 remain valid after decryption. 247 """ 248 self.provider.sign_response = False 249 self.provider.sign_assertion = True 250 self.provider.encryption_kp = self.cert 251 self.provider.save() 252 self.source.encryption_kp = self.cert 253 self.source.signed_assertion = True 254 self.source.save() 255 http_request = self.request_factory.get("/", user=get_anonymous_user()) 256 257 # First create an AuthNRequest 258 request_proc = RequestProcessor(self.source, http_request, "test_state") 259 request = request_proc.build_auth_n() 260 261 # To get an assertion we need a parsed request (parsed by provider) 262 parsed_request = AuthNRequestParser(self.provider).parse( 263 b64encode(request.encode()).decode(), "test_state" 264 ) 265 # Now create a response and convert it to string (provider) 266 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 267 response = response_proc.build_response() 268 269 # Verify the response contains EncryptedAssertion 270 response_xml = fromstring(response) 271 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 272 273 # Now parse the response (source) - this will decrypt and verify assertion signature 274 http_request.POST = QueryDict(mutable=True) 275 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 276 277 response_parser = ResponseProcessor(self.source, http_request) 278 response_parser.parse() 279 280 def test_request_sign_both_and_encrypt(self): 281 """Test SAML with both sign_assertion and sign_response enabled AND encryption. 282 283 This is the most complex scenario: assertion is signed, then encrypted, 284 then the response is signed. Both signatures should be valid. 285 """ 286 self.provider.sign_response = True 287 self.provider.sign_assertion = True 288 self.provider.encryption_kp = self.cert 289 self.provider.save() 290 self.source.encryption_kp = self.cert 291 self.source.signed_assertion = True 292 self.source.signed_response = True 293 self.source.save() 294 http_request = self.request_factory.get("/", user=get_anonymous_user()) 295 296 # First create an AuthNRequest 297 request_proc = RequestProcessor(self.source, http_request, "test_state") 298 request = request_proc.build_auth_n() 299 300 # To get an assertion we need a parsed request (parsed by provider) 301 parsed_request = AuthNRequestParser(self.provider).parse( 302 b64encode(request.encode()).decode(), "test_state" 303 ) 304 # Now create a response and convert it to string (provider) 305 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 306 response = response_proc.build_response() 307 308 # Verify the response contains EncryptedAssertion and response signature 309 response_xml = fromstring(response) 310 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 311 self.assertEqual( 312 len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1 313 ) 314 315 # Now parse the response (source) - this will verify response signature, 316 # decrypt, then verify assertion signature 317 http_request.POST = QueryDict(mutable=True) 318 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 319 320 response_parser = ResponseProcessor(self.source, http_request) 321 response_parser.parse() 322 323 def test_encrypted_assertion_namespace_preservation(self): 324 """Test that encrypted assertions include namespace declarations. 325 326 When an assertion is encrypted, the resulting decrypted XML must include 327 the necessary namespace declarations (xmlns:saml) since it's now a standalone 328 document fragment, no longer inheriting namespaces from the parent Response. 329 """ 330 self.provider.encryption_kp = self.cert 331 self.provider.save() 332 self.source.encryption_kp = self.cert 333 self.source.save() 334 http_request = self.request_factory.get("/", user=get_anonymous_user()) 335 336 # First create an AuthNRequest 337 request_proc = RequestProcessor(self.source, http_request, "test_state") 338 request = request_proc.build_auth_n() 339 340 # To get an assertion we need a parsed request (parsed by provider) 341 parsed_request = AuthNRequestParser(self.provider).parse( 342 b64encode(request.encode()).decode(), "test_state" 343 ) 344 # Now create a response and convert it to string (provider) 345 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 346 response = response_proc.build_response() 347 348 # Parse the encrypted response 349 response_xml = fromstring(response) 350 encrypted_assertion = response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)[0] 351 encrypted_data = encrypted_assertion.xpath("//xenc:EncryptedData", namespaces=NS_MAP)[0] 352 353 # Decrypt the assertion manually to verify namespace is present 354 import xmlsec 355 356 manager = xmlsec.KeysManager() 357 key = xmlsec.Key.from_memory(self.cert.key_data, xmlsec.constants.KeyDataFormatPem, None) 358 manager.add_key(key) 359 enc_ctx = xmlsec.EncryptionContext(manager) 360 decrypted = enc_ctx.decrypt(encrypted_data) 361 362 # The decrypted assertion should have xmlns:saml namespace declaration 363 decrypted_str = etree.tostring(decrypted).decode() 364 self.assertIn("xmlns:saml", decrypted_str) 365 366 # Also verify full round-trip works (source can parse it) 367 http_request.POST = QueryDict(mutable=True) 368 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 369 370 response_parser = ResponseProcessor(self.source, http_request) 371 response_parser.parse() 372 373 def test_encrypted_response_schema_validation(self): 374 """Test that encrypted SAML responses validate against the SAML schema. 375 376 The response with EncryptedAssertion must be valid per saml-schema-protocol-2.0.xsd. 377 This ensures we don't have invalid elements like EncryptedData inside Assertion. 378 """ 379 self.provider.encryption_kp = self.cert 380 self.provider.save() 381 http_request = self.request_factory.get("/", user=get_anonymous_user()) 382 383 # First create an AuthNRequest 384 request_proc = RequestProcessor(self.source, http_request, "test_state") 385 request = request_proc.build_auth_n() 386 387 # To get an assertion we need a parsed request (parsed by provider) 388 parsed_request = AuthNRequestParser(self.provider).parse( 389 b64encode(request.encode()).decode(), "test_state" 390 ) 391 # Now create a response and convert it to string (provider) 392 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 393 response = response_proc.build_response() 394 395 # Validate against SAML schema 396 schema = etree.XMLSchema( 397 etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec 398 ) 399 self.assertTrue(schema.validate(lxml_from_string(response))) 400 401 # Verify structure: should have EncryptedAssertion, not Assertion with EncryptedData inside 402 response_xml = fromstring(response) 403 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 404 self.assertEqual(len(response_xml.xpath("//saml:Assertion", namespaces=NS_MAP)), 0) 405 406 def test_request_signed(self): 407 """Test full SAML Request/Response flow, fully signed""" 408 http_request = self.request_factory.get("/", user=get_anonymous_user()) 409 410 # First create an AuthNRequest 411 request_proc = RequestProcessor(self.source, http_request, "test_state") 412 request = request_proc.build_auth_n() 413 414 # To get an assertion we need a parsed request (parsed by provider) 415 parsed_request = AuthNRequestParser(self.provider).parse( 416 b64encode(request.encode()).decode(), "test_state" 417 ) 418 # Now create a response and convert it to string (provider) 419 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 420 response = response_proc.build_response() 421 422 # Now parse the response (source) 423 http_request.POST = QueryDict(mutable=True) 424 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 425 426 response_parser = ResponseProcessor(self.source, http_request) 427 response_parser.parse() 428 429 def test_request_signed_both(self): 430 """Test full SAML Request/Response flow, fully signed""" 431 self.provider.sign_assertion = True 432 self.provider.sign_response = True 433 self.provider.save() 434 self.source.signed_response = True 435 http_request = self.request_factory.get("/", user=get_anonymous_user()) 436 437 # First create an AuthNRequest 438 request_proc = RequestProcessor(self.source, http_request, "test_state") 439 request = request_proc.build_auth_n() 440 441 # To get an assertion we need a parsed request (parsed by provider) 442 parsed_request = AuthNRequestParser(self.provider).parse( 443 b64encode(request.encode()).decode(), "test_state" 444 ) 445 # Now create a response and convert it to string (provider) 446 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 447 response = response_proc.build_response() 448 # Ensure both response and assertion ID are in the response twice (once as ID attribute, 449 # once as ds:Reference URI) 450 self.assertEqual(response.count(response_proc._assertion_id), 2) 451 self.assertEqual(response.count(response_proc._response_id), 2) 452 453 schema = etree.XMLSchema( 454 etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec 455 ) 456 self.assertTrue(schema.validate(lxml_from_string(response))) 457 458 response_xml = fromstring(response) 459 self.assertEqual( 460 len(response_xml.xpath("//saml:Assertion/ds:Signature", namespaces=NS_MAP)), 1 461 ) 462 self.assertEqual( 463 len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1 464 ) 465 466 # Now parse the response (source) 467 http_request.POST = QueryDict(mutable=True) 468 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 469 470 response_parser = ResponseProcessor(self.source, http_request) 471 response_parser.parse() 472 473 def test_request_id_invalid(self): 474 """Test generated AuthNRequest with invalid request ID""" 475 http_request = self.request_factory.get("/", user=get_anonymous_user()) 476 477 # First create an AuthNRequest 478 request_proc = RequestProcessor(self.source, http_request, "test_state") 479 request = request_proc.build_auth_n() 480 481 # change the request ID 482 http_request.session[SESSION_KEY_REQUEST_ID] = "test" 483 http_request.session.save() 484 485 # To get an assertion we need a parsed request (parsed by provider) 486 parsed_request = AuthNRequestParser(self.provider).parse( 487 b64encode(request.encode()).decode(), "test_state" 488 ) 489 # Now create a response and convert it to string (provider) 490 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 491 response = response_proc.build_response() 492 493 # Now parse the response (source) 494 http_request.POST = QueryDict(mutable=True) 495 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 496 497 response_parser = ResponseProcessor(self.source, http_request) 498 499 with self.assertRaises(MismatchedRequestID): 500 response_parser.parse() 501 502 def test_signed_valid_detached(self): 503 """Test generated AuthNRequest with valid signature (detached)""" 504 http_request = self.request_factory.get("/") 505 506 # First create an AuthNRequest 507 request_proc = RequestProcessor(self.source, http_request, "test_state") 508 params = request_proc.build_auth_n_detached() 509 # Now we check the ID and signature 510 parsed_request = AuthNRequestParser(self.provider).parse_detached( 511 params["SAMLRequest"], 512 params["RelayState"], 513 params["Signature"], 514 params["SigAlg"], 515 ) 516 self.assertEqual(parsed_request.id, request_proc.request_id) 517 self.assertEqual(parsed_request.relay_state, "test_state") 518 519 def test_signed_detached_static(self): 520 """Test request with detached signature, 521 taken from https://www.samltool.com/generic_sso_req.php""" 522 static_keypair = CertificateKeyPair.objects.create( 523 name="samltool", certificate_data=REDIRECT_CERT 524 ) 525 provider = SAMLProvider( 526 name="samltool", 527 authorization_flow=create_test_flow(), 528 acs_url="https://10.120.20.200/saml-sp/SAML2/POST", 529 audience="https://10.120.20.200/saml-sp/SAML2/POST", 530 issuer="https://10.120.20.200/saml-sp/SAML2/POST", 531 signing_kp=static_keypair, 532 verification_kp=static_keypair, 533 ) 534 parsed_request = AuthNRequestParser(provider).parse_detached( 535 REDIRECT_REQUEST, REDIRECT_RELAY_STATE, REDIRECT_SIGNATURE, REDIRECT_SIG_ALG 536 ) 537 self.assertEqual(parsed_request.id, "_dcf55fcd27a887e60a7ef9ee6fd3adab") 538 self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_UNSPECIFIED) 539 self.assertEqual(parsed_request.relay_state, REDIRECT_RELAY_STATE) 540 541 def test_signed_static(self): 542 """Test post request with static request""" 543 provider = SAMLProvider( 544 name="aws", 545 authorization_flow=create_test_flow(), 546 acs_url=( 547 "https://eu-central-1.signin.aws.amazon.com/platform/" 548 "saml/acs/2d737f96-55fb-4035-953e-5e24134eb778" 549 ), 550 audience="https://10.120.20.200/saml-sp/SAML2/POST", 551 issuer="https://10.120.20.200/saml-sp/SAML2/POST", 552 signing_kp=create_test_cert(), 553 ) 554 parsed_request = AuthNRequestParser(provider).parse(POST_REQUEST) 555 self.assertEqual(parsed_request.id, "aws_LDxLGeubpc5lx12gxCgS6uPbix1yd5re") 556 self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_EMAIL) 557 558 def test_authn_context_class_ref_mapping(self): 559 """Test custom authn_context_class_ref""" 560 authn_context_class_ref = generate_id() 561 mapping = SAMLPropertyMapping.objects.create( 562 name=generate_id(), expression=f"""return '{authn_context_class_ref}'""" 563 ) 564 self.provider.authn_context_class_ref_mapping = mapping 565 self.provider.save() 566 user = create_test_admin_user() 567 http_request = self.request_factory.get("/", user=user) 568 569 # First create an AuthNRequest 570 request_proc = RequestProcessor(self.source, http_request, "test_state") 571 request = request_proc.build_auth_n() 572 573 # To get an assertion we need a parsed request (parsed by provider) 574 parsed_request = AuthNRequestParser(self.provider).parse( 575 b64encode(request.encode()).decode(), "test_state" 576 ) 577 # Now create a response and convert it to string (provider) 578 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 579 response = response_proc.build_response() 580 self.assertIn(user.username, response) 581 self.assertIn(authn_context_class_ref, response) 582 583 def test_authn_context_class_ref_mapping_invalid(self): 584 """Test custom authn_context_class_ref (invalid)""" 585 mapping = SAMLPropertyMapping.objects.create(name=generate_id(), expression="q") 586 self.provider.authn_context_class_ref_mapping = mapping 587 self.provider.save() 588 user = create_test_admin_user() 589 http_request = self.request_factory.get("/", user=user) 590 591 # First create an AuthNRequest 592 request_proc = RequestProcessor(self.source, http_request, "test_state") 593 request = request_proc.build_auth_n() 594 595 # To get an assertion we need a parsed request (parsed by provider) 596 parsed_request = AuthNRequestParser(self.provider).parse( 597 b64encode(request.encode()).decode(), "test_state" 598 ) 599 # Now create a response and convert it to string (provider) 600 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 601 response = response_proc.build_response() 602 self.assertIn(user.username, response) 603 604 events = Event.objects.filter( 605 action=EventAction.CONFIGURATION_ERROR, 606 ) 607 self.assertTrue(events.exists()) 608 self.assertEqual( 609 events.first().context["message"], 610 f"Failed to evaluate property-mapping: '{mapping.name}'", 611 ) 612 613 def test_request_attributes(self): 614 """Test full SAML Request/Response flow, fully signed""" 615 user = create_test_admin_user() 616 http_request = self.request_factory.get("/", user=user) 617 618 # First create an AuthNRequest 619 request_proc = RequestProcessor(self.source, http_request, "test_state") 620 request = request_proc.build_auth_n() 621 622 # To get an assertion we need a parsed request (parsed by provider) 623 parsed_request = AuthNRequestParser(self.provider).parse( 624 b64encode(request.encode()).decode(), "test_state" 625 ) 626 # Now create a response and convert it to string (provider) 627 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 628 self.assertIn(user.username, response_proc.build_response()) 629 630 def test_request_attributes_invalid(self): 631 """Test full SAML Request/Response flow, fully signed""" 632 user = create_test_admin_user() 633 http_request = self.request_factory.get("/", user=user) 634 635 # First create an AuthNRequest 636 request_proc = RequestProcessor(self.source, http_request, "test_state") 637 request = request_proc.build_auth_n() 638 639 # Create invalid PropertyMapping 640 mapping = SAMLPropertyMapping.objects.create( 641 name=generate_id(), saml_name="test", expression="q" 642 ) 643 self.provider.property_mappings.add(mapping) 644 645 # To get an assertion we need a parsed request (parsed by provider) 646 parsed_request = AuthNRequestParser(self.provider).parse( 647 b64encode(request.encode()).decode(), "test_state" 648 ) 649 # Now create a response and convert it to string (provider) 650 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 651 self.assertIn(user.username, response_proc.build_response()) 652 653 events = Event.objects.filter( 654 action=EventAction.CONFIGURATION_ERROR, 655 ) 656 self.assertTrue(events.exists()) 657 self.assertEqual( 658 events.first().context["message"], 659 f"Failed to evaluate property-mapping: '{mapping.name}'", 660 ) 661 662 def test_idp_initiated(self): 663 """Test IDP-initiated login""" 664 self.provider.default_relay_state = generate_id() 665 request = AuthNRequestParser(self.provider).idp_initiated() 666 self.assertEqual(request.id, None) 667 self.assertEqual(request.relay_state, self.provider.default_relay_state)
Test AuthN Request generator and parser
89 @apply_blueprint("system/providers-saml.yaml") 90 def setUp(self): 91 self.request_factory = RequestFactory() 92 self.cert = create_test_cert() 93 self.provider: SAMLProvider = SAMLProvider.objects.create( 94 authorization_flow=create_test_flow(), 95 acs_url="http://testserver/source/saml/provider/acs/", 96 signing_kp=self.cert, 97 verification_kp=self.cert, 98 ) 99 self.provider.property_mappings.set(SAMLPropertyMapping.objects.all()) 100 self.provider.save() 101 self.source = SAMLSource.objects.create( 102 slug="provider", 103 issuer="authentik", 104 pre_authentication_flow=create_test_flow(), 105 signing_kp=self.cert, 106 verification_kp=self.cert, 107 signed_assertion=True, 108 binding_type=SAMLBindingTypes.POST, 109 )
Hook method for setting up the test fixture before exercising it.
111 def test_signed_valid(self): 112 """Test generated AuthNRequest with valid signature""" 113 http_request = self.request_factory.get("/") 114 115 # First create an AuthNRequest 116 request_proc = RequestProcessor(self.source, http_request, "test_state") 117 auth_n = request_proc.get_auth_n() 118 self.assertEqual(auth_n.attrib["ProtocolBinding"], SAML_BINDING_POST) 119 120 request = request_proc.build_auth_n() 121 # Now we check the ID and signature 122 parsed_request = AuthNRequestParser(self.provider).parse( 123 b64encode(request.encode()).decode(), "test_state" 124 ) 125 self.assertEqual(parsed_request.id, request_proc.request_id) 126 self.assertEqual(parsed_request.relay_state, "test_state")
Test generated AuthNRequest with valid signature
128 def test_request_encrypt(self): 129 """Test full SAML Request/Response flow, fully encrypted""" 130 self.provider.encryption_kp = self.cert 131 self.provider.save() 132 self.source.encryption_kp = self.cert 133 self.source.save() 134 http_request = self.request_factory.get("/", user=get_anonymous_user()) 135 136 # First create an AuthNRequest 137 request_proc = RequestProcessor(self.source, http_request, "test_state") 138 request = request_proc.build_auth_n() 139 140 # To get an assertion we need a parsed request (parsed by provider) 141 parsed_request = AuthNRequestParser(self.provider).parse( 142 b64encode(request.encode()).decode(), "test_state" 143 ) 144 # Now create a response and convert it to string (provider) 145 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 146 response = response_proc.build_response() 147 148 # Now parse the response (source) 149 http_request.POST = QueryDict(mutable=True) 150 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 151 152 response_parser = ResponseProcessor(self.source, http_request) 153 response_parser.parse()
Test full SAML Request/Response flow, fully encrypted
155 def test_request_encrypt_cert_only(self): 156 """Test SAML encryption with certificate-only keypair (no private key). 157 158 This tests the scenario where the IdP (provider) only has the SP's public 159 certificate for encryption, without a private key. This is the expected 160 real-world scenario since the SP would never share their private key. 161 """ 162 # Create a full keypair for the source (SP) - it needs the private key to decrypt 163 full_keypair = create_test_cert() 164 165 # Create a certificate-only keypair for the provider (IdP) 166 # This simulates having only the SP's public certificate 167 cert_only = CertificateKeyPair.objects.create( 168 name=generate_id(), 169 certificate_data=full_keypair.certificate_data, 170 key_data="", # No private key 171 ) 172 173 self.provider.encryption_kp = cert_only 174 self.provider.save() 175 self.source.encryption_kp = full_keypair 176 self.source.save() 177 http_request = self.request_factory.get("/", user=get_anonymous_user()) 178 179 # First create an AuthNRequest 180 request_proc = RequestProcessor(self.source, http_request, "test_state") 181 request = request_proc.build_auth_n() 182 183 # To get an assertion we need a parsed request (parsed by provider) 184 parsed_request = AuthNRequestParser(self.provider).parse( 185 b64encode(request.encode()).decode(), "test_state" 186 ) 187 # Now create a response and convert it to string (provider) 188 # This should work with only the certificate (public key) for encryption 189 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 190 response = response_proc.build_response() 191 192 # Now parse the response (source) - decryption requires the private key 193 http_request.POST = QueryDict(mutable=True) 194 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 195 196 response_parser = ResponseProcessor(self.source, http_request) 197 response_parser.parse()
Test SAML encryption with certificate-only keypair (no private key).
This tests the scenario where the IdP (provider) only has the SP's public certificate for encryption, without a private key. This is the expected real-world scenario since the SP would never share their private key.
199 def test_request_sign_response_and_encrypt(self): 200 """Test SAML with sign_response enabled AND encryption. 201 202 This tests the fix for signature invalidation when encryption is enabled. 203 The response must be signed AFTER encryption, not before, because encryption 204 replaces the Assertion with EncryptedAssertion which changes the response content. 205 """ 206 self.provider.sign_response = True 207 self.provider.sign_assertion = False 208 self.provider.encryption_kp = self.cert 209 self.provider.save() 210 self.source.encryption_kp = self.cert 211 self.source.signed_response = True 212 self.source.signed_assertion = False # Only response is signed, not assertion 213 self.source.save() 214 http_request = self.request_factory.get("/", user=get_anonymous_user()) 215 216 # First create an AuthNRequest 217 request_proc = RequestProcessor(self.source, http_request, "test_state") 218 request = request_proc.build_auth_n() 219 220 # To get an assertion we need a parsed request (parsed by provider) 221 parsed_request = AuthNRequestParser(self.provider).parse( 222 b64encode(request.encode()).decode(), "test_state" 223 ) 224 # Now create a response and convert it to string (provider) 225 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 226 response = response_proc.build_response() 227 228 # Verify the response contains EncryptedAssertion and a signature 229 response_xml = fromstring(response) 230 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 231 self.assertEqual( 232 len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1 233 ) 234 235 # Now parse the response (source) - this will verify the signature and decrypt 236 http_request.POST = QueryDict(mutable=True) 237 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 238 239 response_parser = ResponseProcessor(self.source, http_request) 240 response_parser.parse()
Test SAML with sign_response enabled AND encryption.
This tests the fix for signature invalidation when encryption is enabled. The response must be signed AFTER encryption, not before, because encryption replaces the Assertion with EncryptedAssertion which changes the response content.
242 def test_request_sign_assertion_and_encrypt(self): 243 """Test SAML with sign_assertion enabled AND encryption. 244 245 The assertion signature should be inside the encrypted content and 246 remain valid after decryption. 247 """ 248 self.provider.sign_response = False 249 self.provider.sign_assertion = True 250 self.provider.encryption_kp = self.cert 251 self.provider.save() 252 self.source.encryption_kp = self.cert 253 self.source.signed_assertion = True 254 self.source.save() 255 http_request = self.request_factory.get("/", user=get_anonymous_user()) 256 257 # First create an AuthNRequest 258 request_proc = RequestProcessor(self.source, http_request, "test_state") 259 request = request_proc.build_auth_n() 260 261 # To get an assertion we need a parsed request (parsed by provider) 262 parsed_request = AuthNRequestParser(self.provider).parse( 263 b64encode(request.encode()).decode(), "test_state" 264 ) 265 # Now create a response and convert it to string (provider) 266 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 267 response = response_proc.build_response() 268 269 # Verify the response contains EncryptedAssertion 270 response_xml = fromstring(response) 271 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 272 273 # Now parse the response (source) - this will decrypt and verify assertion signature 274 http_request.POST = QueryDict(mutable=True) 275 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 276 277 response_parser = ResponseProcessor(self.source, http_request) 278 response_parser.parse()
Test SAML with sign_assertion enabled AND encryption.
The assertion signature should be inside the encrypted content and remain valid after decryption.
280 def test_request_sign_both_and_encrypt(self): 281 """Test SAML with both sign_assertion and sign_response enabled AND encryption. 282 283 This is the most complex scenario: assertion is signed, then encrypted, 284 then the response is signed. Both signatures should be valid. 285 """ 286 self.provider.sign_response = True 287 self.provider.sign_assertion = True 288 self.provider.encryption_kp = self.cert 289 self.provider.save() 290 self.source.encryption_kp = self.cert 291 self.source.signed_assertion = True 292 self.source.signed_response = True 293 self.source.save() 294 http_request = self.request_factory.get("/", user=get_anonymous_user()) 295 296 # First create an AuthNRequest 297 request_proc = RequestProcessor(self.source, http_request, "test_state") 298 request = request_proc.build_auth_n() 299 300 # To get an assertion we need a parsed request (parsed by provider) 301 parsed_request = AuthNRequestParser(self.provider).parse( 302 b64encode(request.encode()).decode(), "test_state" 303 ) 304 # Now create a response and convert it to string (provider) 305 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 306 response = response_proc.build_response() 307 308 # Verify the response contains EncryptedAssertion and response signature 309 response_xml = fromstring(response) 310 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 311 self.assertEqual( 312 len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1 313 ) 314 315 # Now parse the response (source) - this will verify response signature, 316 # decrypt, then verify assertion signature 317 http_request.POST = QueryDict(mutable=True) 318 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 319 320 response_parser = ResponseProcessor(self.source, http_request) 321 response_parser.parse()
Test SAML with both sign_assertion and sign_response enabled AND encryption.
This is the most complex scenario: assertion is signed, then encrypted, then the response is signed. Both signatures should be valid.
323 def test_encrypted_assertion_namespace_preservation(self): 324 """Test that encrypted assertions include namespace declarations. 325 326 When an assertion is encrypted, the resulting decrypted XML must include 327 the necessary namespace declarations (xmlns:saml) since it's now a standalone 328 document fragment, no longer inheriting namespaces from the parent Response. 329 """ 330 self.provider.encryption_kp = self.cert 331 self.provider.save() 332 self.source.encryption_kp = self.cert 333 self.source.save() 334 http_request = self.request_factory.get("/", user=get_anonymous_user()) 335 336 # First create an AuthNRequest 337 request_proc = RequestProcessor(self.source, http_request, "test_state") 338 request = request_proc.build_auth_n() 339 340 # To get an assertion we need a parsed request (parsed by provider) 341 parsed_request = AuthNRequestParser(self.provider).parse( 342 b64encode(request.encode()).decode(), "test_state" 343 ) 344 # Now create a response and convert it to string (provider) 345 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 346 response = response_proc.build_response() 347 348 # Parse the encrypted response 349 response_xml = fromstring(response) 350 encrypted_assertion = response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)[0] 351 encrypted_data = encrypted_assertion.xpath("//xenc:EncryptedData", namespaces=NS_MAP)[0] 352 353 # Decrypt the assertion manually to verify namespace is present 354 import xmlsec 355 356 manager = xmlsec.KeysManager() 357 key = xmlsec.Key.from_memory(self.cert.key_data, xmlsec.constants.KeyDataFormatPem, None) 358 manager.add_key(key) 359 enc_ctx = xmlsec.EncryptionContext(manager) 360 decrypted = enc_ctx.decrypt(encrypted_data) 361 362 # The decrypted assertion should have xmlns:saml namespace declaration 363 decrypted_str = etree.tostring(decrypted).decode() 364 self.assertIn("xmlns:saml", decrypted_str) 365 366 # Also verify full round-trip works (source can parse it) 367 http_request.POST = QueryDict(mutable=True) 368 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 369 370 response_parser = ResponseProcessor(self.source, http_request) 371 response_parser.parse()
Test that encrypted assertions include namespace declarations.
When an assertion is encrypted, the resulting decrypted XML must include the necessary namespace declarations (xmlns:saml) since it's now a standalone document fragment, no longer inheriting namespaces from the parent Response.
373 def test_encrypted_response_schema_validation(self): 374 """Test that encrypted SAML responses validate against the SAML schema. 375 376 The response with EncryptedAssertion must be valid per saml-schema-protocol-2.0.xsd. 377 This ensures we don't have invalid elements like EncryptedData inside Assertion. 378 """ 379 self.provider.encryption_kp = self.cert 380 self.provider.save() 381 http_request = self.request_factory.get("/", user=get_anonymous_user()) 382 383 # First create an AuthNRequest 384 request_proc = RequestProcessor(self.source, http_request, "test_state") 385 request = request_proc.build_auth_n() 386 387 # To get an assertion we need a parsed request (parsed by provider) 388 parsed_request = AuthNRequestParser(self.provider).parse( 389 b64encode(request.encode()).decode(), "test_state" 390 ) 391 # Now create a response and convert it to string (provider) 392 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 393 response = response_proc.build_response() 394 395 # Validate against SAML schema 396 schema = etree.XMLSchema( 397 etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec 398 ) 399 self.assertTrue(schema.validate(lxml_from_string(response))) 400 401 # Verify structure: should have EncryptedAssertion, not Assertion with EncryptedData inside 402 response_xml = fromstring(response) 403 self.assertEqual(len(response_xml.xpath("//saml:EncryptedAssertion", namespaces=NS_MAP)), 1) 404 self.assertEqual(len(response_xml.xpath("//saml:Assertion", namespaces=NS_MAP)), 0)
Test that encrypted SAML responses validate against the SAML schema.
The response with EncryptedAssertion must be valid per saml-schema-protocol-2.0.xsd. This ensures we don't have invalid elements like EncryptedData inside Assertion.
406 def test_request_signed(self): 407 """Test full SAML Request/Response flow, fully signed""" 408 http_request = self.request_factory.get("/", user=get_anonymous_user()) 409 410 # First create an AuthNRequest 411 request_proc = RequestProcessor(self.source, http_request, "test_state") 412 request = request_proc.build_auth_n() 413 414 # To get an assertion we need a parsed request (parsed by provider) 415 parsed_request = AuthNRequestParser(self.provider).parse( 416 b64encode(request.encode()).decode(), "test_state" 417 ) 418 # Now create a response and convert it to string (provider) 419 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 420 response = response_proc.build_response() 421 422 # Now parse the response (source) 423 http_request.POST = QueryDict(mutable=True) 424 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 425 426 response_parser = ResponseProcessor(self.source, http_request) 427 response_parser.parse()
Test full SAML Request/Response flow, fully signed
429 def test_request_signed_both(self): 430 """Test full SAML Request/Response flow, fully signed""" 431 self.provider.sign_assertion = True 432 self.provider.sign_response = True 433 self.provider.save() 434 self.source.signed_response = True 435 http_request = self.request_factory.get("/", user=get_anonymous_user()) 436 437 # First create an AuthNRequest 438 request_proc = RequestProcessor(self.source, http_request, "test_state") 439 request = request_proc.build_auth_n() 440 441 # To get an assertion we need a parsed request (parsed by provider) 442 parsed_request = AuthNRequestParser(self.provider).parse( 443 b64encode(request.encode()).decode(), "test_state" 444 ) 445 # Now create a response and convert it to string (provider) 446 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 447 response = response_proc.build_response() 448 # Ensure both response and assertion ID are in the response twice (once as ID attribute, 449 # once as ds:Reference URI) 450 self.assertEqual(response.count(response_proc._assertion_id), 2) 451 self.assertEqual(response.count(response_proc._response_id), 2) 452 453 schema = etree.XMLSchema( 454 etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec 455 ) 456 self.assertTrue(schema.validate(lxml_from_string(response))) 457 458 response_xml = fromstring(response) 459 self.assertEqual( 460 len(response_xml.xpath("//saml:Assertion/ds:Signature", namespaces=NS_MAP)), 1 461 ) 462 self.assertEqual( 463 len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1 464 ) 465 466 # Now parse the response (source) 467 http_request.POST = QueryDict(mutable=True) 468 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 469 470 response_parser = ResponseProcessor(self.source, http_request) 471 response_parser.parse()
Test full SAML Request/Response flow, fully signed
473 def test_request_id_invalid(self): 474 """Test generated AuthNRequest with invalid request ID""" 475 http_request = self.request_factory.get("/", user=get_anonymous_user()) 476 477 # First create an AuthNRequest 478 request_proc = RequestProcessor(self.source, http_request, "test_state") 479 request = request_proc.build_auth_n() 480 481 # change the request ID 482 http_request.session[SESSION_KEY_REQUEST_ID] = "test" 483 http_request.session.save() 484 485 # To get an assertion we need a parsed request (parsed by provider) 486 parsed_request = AuthNRequestParser(self.provider).parse( 487 b64encode(request.encode()).decode(), "test_state" 488 ) 489 # Now create a response and convert it to string (provider) 490 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 491 response = response_proc.build_response() 492 493 # Now parse the response (source) 494 http_request.POST = QueryDict(mutable=True) 495 http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode() 496 497 response_parser = ResponseProcessor(self.source, http_request) 498 499 with self.assertRaises(MismatchedRequestID): 500 response_parser.parse()
Test generated AuthNRequest with invalid request ID
502 def test_signed_valid_detached(self): 503 """Test generated AuthNRequest with valid signature (detached)""" 504 http_request = self.request_factory.get("/") 505 506 # First create an AuthNRequest 507 request_proc = RequestProcessor(self.source, http_request, "test_state") 508 params = request_proc.build_auth_n_detached() 509 # Now we check the ID and signature 510 parsed_request = AuthNRequestParser(self.provider).parse_detached( 511 params["SAMLRequest"], 512 params["RelayState"], 513 params["Signature"], 514 params["SigAlg"], 515 ) 516 self.assertEqual(parsed_request.id, request_proc.request_id) 517 self.assertEqual(parsed_request.relay_state, "test_state")
Test generated AuthNRequest with valid signature (detached)
519 def test_signed_detached_static(self): 520 """Test request with detached signature, 521 taken from https://www.samltool.com/generic_sso_req.php""" 522 static_keypair = CertificateKeyPair.objects.create( 523 name="samltool", certificate_data=REDIRECT_CERT 524 ) 525 provider = SAMLProvider( 526 name="samltool", 527 authorization_flow=create_test_flow(), 528 acs_url="https://10.120.20.200/saml-sp/SAML2/POST", 529 audience="https://10.120.20.200/saml-sp/SAML2/POST", 530 issuer="https://10.120.20.200/saml-sp/SAML2/POST", 531 signing_kp=static_keypair, 532 verification_kp=static_keypair, 533 ) 534 parsed_request = AuthNRequestParser(provider).parse_detached( 535 REDIRECT_REQUEST, REDIRECT_RELAY_STATE, REDIRECT_SIGNATURE, REDIRECT_SIG_ALG 536 ) 537 self.assertEqual(parsed_request.id, "_dcf55fcd27a887e60a7ef9ee6fd3adab") 538 self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_UNSPECIFIED) 539 self.assertEqual(parsed_request.relay_state, REDIRECT_RELAY_STATE)
Test request with detached signature, taken from https://www.samltool.com/generic_sso_req.php
541 def test_signed_static(self): 542 """Test post request with static request""" 543 provider = SAMLProvider( 544 name="aws", 545 authorization_flow=create_test_flow(), 546 acs_url=( 547 "https://eu-central-1.signin.aws.amazon.com/platform/" 548 "saml/acs/2d737f96-55fb-4035-953e-5e24134eb778" 549 ), 550 audience="https://10.120.20.200/saml-sp/SAML2/POST", 551 issuer="https://10.120.20.200/saml-sp/SAML2/POST", 552 signing_kp=create_test_cert(), 553 ) 554 parsed_request = AuthNRequestParser(provider).parse(POST_REQUEST) 555 self.assertEqual(parsed_request.id, "aws_LDxLGeubpc5lx12gxCgS6uPbix1yd5re") 556 self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_EMAIL)
Test post request with static request
558 def test_authn_context_class_ref_mapping(self): 559 """Test custom authn_context_class_ref""" 560 authn_context_class_ref = generate_id() 561 mapping = SAMLPropertyMapping.objects.create( 562 name=generate_id(), expression=f"""return '{authn_context_class_ref}'""" 563 ) 564 self.provider.authn_context_class_ref_mapping = mapping 565 self.provider.save() 566 user = create_test_admin_user() 567 http_request = self.request_factory.get("/", user=user) 568 569 # First create an AuthNRequest 570 request_proc = RequestProcessor(self.source, http_request, "test_state") 571 request = request_proc.build_auth_n() 572 573 # To get an assertion we need a parsed request (parsed by provider) 574 parsed_request = AuthNRequestParser(self.provider).parse( 575 b64encode(request.encode()).decode(), "test_state" 576 ) 577 # Now create a response and convert it to string (provider) 578 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 579 response = response_proc.build_response() 580 self.assertIn(user.username, response) 581 self.assertIn(authn_context_class_ref, response)
Test custom authn_context_class_ref
583 def test_authn_context_class_ref_mapping_invalid(self): 584 """Test custom authn_context_class_ref (invalid)""" 585 mapping = SAMLPropertyMapping.objects.create(name=generate_id(), expression="q") 586 self.provider.authn_context_class_ref_mapping = mapping 587 self.provider.save() 588 user = create_test_admin_user() 589 http_request = self.request_factory.get("/", user=user) 590 591 # First create an AuthNRequest 592 request_proc = RequestProcessor(self.source, http_request, "test_state") 593 request = request_proc.build_auth_n() 594 595 # To get an assertion we need a parsed request (parsed by provider) 596 parsed_request = AuthNRequestParser(self.provider).parse( 597 b64encode(request.encode()).decode(), "test_state" 598 ) 599 # Now create a response and convert it to string (provider) 600 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 601 response = response_proc.build_response() 602 self.assertIn(user.username, response) 603 604 events = Event.objects.filter( 605 action=EventAction.CONFIGURATION_ERROR, 606 ) 607 self.assertTrue(events.exists()) 608 self.assertEqual( 609 events.first().context["message"], 610 f"Failed to evaluate property-mapping: '{mapping.name}'", 611 )
Test custom authn_context_class_ref (invalid)
613 def test_request_attributes(self): 614 """Test full SAML Request/Response flow, fully signed""" 615 user = create_test_admin_user() 616 http_request = self.request_factory.get("/", user=user) 617 618 # First create an AuthNRequest 619 request_proc = RequestProcessor(self.source, http_request, "test_state") 620 request = request_proc.build_auth_n() 621 622 # To get an assertion we need a parsed request (parsed by provider) 623 parsed_request = AuthNRequestParser(self.provider).parse( 624 b64encode(request.encode()).decode(), "test_state" 625 ) 626 # Now create a response and convert it to string (provider) 627 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 628 self.assertIn(user.username, response_proc.build_response())
Test full SAML Request/Response flow, fully signed
630 def test_request_attributes_invalid(self): 631 """Test full SAML Request/Response flow, fully signed""" 632 user = create_test_admin_user() 633 http_request = self.request_factory.get("/", user=user) 634 635 # First create an AuthNRequest 636 request_proc = RequestProcessor(self.source, http_request, "test_state") 637 request = request_proc.build_auth_n() 638 639 # Create invalid PropertyMapping 640 mapping = SAMLPropertyMapping.objects.create( 641 name=generate_id(), saml_name="test", expression="q" 642 ) 643 self.provider.property_mappings.add(mapping) 644 645 # To get an assertion we need a parsed request (parsed by provider) 646 parsed_request = AuthNRequestParser(self.provider).parse( 647 b64encode(request.encode()).decode(), "test_state" 648 ) 649 # Now create a response and convert it to string (provider) 650 response_proc = AssertionProcessor(self.provider, http_request, parsed_request) 651 self.assertIn(user.username, response_proc.build_response()) 652 653 events = Event.objects.filter( 654 action=EventAction.CONFIGURATION_ERROR, 655 ) 656 self.assertTrue(events.exists()) 657 self.assertEqual( 658 events.first().context["message"], 659 f"Failed to evaluate property-mapping: '{mapping.name}'", 660 )
Test full SAML Request/Response flow, fully signed
662 def test_idp_initiated(self): 663 """Test IDP-initiated login""" 664 self.provider.default_relay_state = generate_id() 665 request = AuthNRequestParser(self.provider).idp_initiated() 666 self.assertEqual(request.id, None) 667 self.assertEqual(request.relay_state, self.provider.default_relay_state)
Test IDP-initiated login