def test_decrypt_xml_with_document_not_encrypted(self): with cast(BinaryIO, (DATA_DIR / 'saml_response.xml').open('rb')) as f: document = parse_xml(f.read()) expected = dump_xml(document).decode('utf-8') self.assertEqual(decrypt_xml(document, KEY_FILE), 0) actual = dump_xml(document).decode('utf-8') self.assertXMLEqual(expected, actual)
def get_context_data(self, **kwargs) -> dict: """Adjust template context data.""" context = super().get_context_data(**kwargs) if self.saml_request: context['connector_endpoint'] = reverse('country-selector') encoded_saml_request = b64encode( dump_xml(self.saml_request.document, pretty_print=False)).decode('ascii') context['saml_request'] = encoded_saml_request context['saml_request_xml'] = dump_xml( self.saml_request.document).decode('ascii') relay_state = self.saml_request.relay_state country = self.saml_request.citizen_country_code else: country = None relay_state = None context['presets'] = [(i, preset.label) for i, preset in enumerate(PRESETS)] context['relay_state'] = relay_state or '' context[ 'country'] = country if country and country != COUNTRY_PLACEHOLDER else '' context['country_parameter'] = CONNECTOR_SETTINGS.service_provider[ 'country_parameter'] return context
def test_encrypt_xml_node(self): supported_ciphers = set(XmlBlockCipher) # type: Set[XmlBlockCipher] if LIBXMLSEC_VERSION < (1, 2, 27): # pragma: no cover supported_ciphers -= { XmlBlockCipher.AES128_GCM, XmlBlockCipher.AES192_GCM, XmlBlockCipher.AES256_GCM } for cipher in supported_ciphers: with cast(BinaryIO, (DATA_DIR / 'saml_response_decrypted.xml').open('rb')) as f: document = parse_xml(f.read()) remove_extra_xml_whitespace(document.getroot()) original = dump_xml(document).decode() # Encrypt <Assertion> assertion = document.find(".//{}".format( Q_NAMES['saml2:Assertion'])) encrypt_xml_node(assertion, CERT_FILE, cipher, XmlKeyTransport.RSA_OAEP_MGF1P) # <Assertion> replaced with <EncryptedData> self.assertIsNone( document.find(".//{}".format(Q_NAMES['saml2:Assertion']))) enc_data = document.find(".//{}/{}".format( Q_NAMES['saml2:EncryptedAssertion'], Q_NAMES['xmlenc:EncryptedData'])) self.assertIsNotNone(enc_data) self.assertEqual(enc_data[0].get('Algorithm'), cipher.value) # Verify that the original and decrypted document match. self.assertEqual(decrypt_xml(document, KEY_FILE), 1) decrypted = dump_xml(document).decode() self.assertEqual(original, decrypted)
def test_get_saml_response_signed_and_encrypted(self): with cast(TextIO, (DATA_DIR / 'nia_test_response.xml').open('r')) as f: tree = parse_xml(f.read()) remove_extra_xml_whitespace(tree) saml_response_encoded = b64encode(dump_xml(tree, pretty_print=False)).decode('ascii') view = IdentityProviderResponseView() view.request = self.factory.post(self.url, {'SAMLResponse': saml_response_encoded, 'RelayState': 'relay123'}) saml_response = view.get_saml_response(KEY_FILE, NIA_CERT_FILE) self.assertEqual(saml_response.relay_state, 'relay123') with cast(TextIO, (DATA_DIR / 'nia_test_response_decrypted_verified.xml').open('r')) as f: decrypted_verified_xml = f.read() self.assertXMLEqual(dump_xml(saml_response.document).decode('utf-8'), decrypted_verified_xml)
def test_serialize_attributes_not_empty(self): root = Element('root') serialize_attributes(root, 'tagName', OrderedDict([ ('name1', ['value1', 'value2']), ('name2', []), ])) expected = Element('root') attributes = SubElement(expected, 'tagName') attribute = SubElement(attributes, 'attribute') SubElement(attribute, 'definition').text = 'name1' SubElement(attribute, 'value').text = 'value1' SubElement(attribute, 'value').text = 'value2' attribute = SubElement(attributes, 'attribute') SubElement(attribute, 'definition').text = 'name2' self.assertEqual(dump_xml(root).decode('utf-8'), dump_xml(expected).decode('utf-8'))
def test_export_xml_ok(self): self.maxDiff = None user = XMLUser(name=XMLName(first_name='John', last_name='Doe'), superuser=True, department=Department.TECH_DEP) self.assertEqual( dump_xml(user.export_xml()).decode('utf8'), XML_USER_DATA)
def test_get_saml_response_signed(self): with cast(TextIO, (DATA_DIR / 'signed_response_and_assertion.xml').open('r')) as f: tree = parse_xml(f.read()) remove_extra_xml_whitespace(tree) saml_response_encoded = b64encode(dump_xml(tree, pretty_print=False)).decode('ascii') view = IdentityProviderResponseView() view.request = self.factory.post(self.url, {'SAMLResponse': saml_response_encoded, 'RelayState': 'relay123'}) saml_response = view.get_saml_response(None, CERT_FILE) self.assertEqual(saml_response.relay_state, 'relay123') root = Element(Q_NAMES['saml2p:Response'], {'ID': 'id-response'}, nsmap={'saml2': EIDAS_NAMESPACES['saml2'], 'saml2p': EIDAS_NAMESPACES['saml2p']}) assertion = SubElement(root, Q_NAMES['saml2:Assertion'], {'ID': 'id-0uuid4'}) SubElement(assertion, Q_NAMES['saml2:Issuer']).text = 'Test Issuer' self.assertXMLEqual(dump_xml(saml_response.document).decode('utf-8'), dump_xml(root).decode('utf-8'))
def test_export_xml_full_sample(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'light_request.xml').open('rb')) as f: data = f.read() request = LightRequest.load_xml(parse_xml(data)) self.assertEqual(dump_xml(request.export_xml()), data)
def test_post_success(self): self.maxDiff = None request = LightRequest(**LIGHT_REQUEST_DICT) self.cache_mock.get_and_remove.return_value = dump_xml(request.export_xml()).decode('utf-8') token, encoded = self.get_token() response = self.client.post(self.url, {'test_token': encoded}) # Context self.assertIn('saml_request', response.context) self.assertEqual(response.context['identity_provider_endpoint'], 'https://test.example.net/identity-provider-endpoint') self.assertEqual(response.context['relay_state'], 'relay123') self.assertEqual(response.context['error'], None) # SAML Request saml_request_xml = b64decode(response.context['saml_request'].encode('utf-8')).decode('utf-8') self.assertIn(request.id, saml_request_xml) # light_request.id preserved self.assertIn('<saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">' 'https://test.example.net/saml/idp.xml</saml2:Issuer>', saml_request_xml) self.assertIn('Destination="http://testserver/IdentityProviderResponse"', saml_request_xml) self.assertIn('<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo>', saml_request_xml) # Rendering self.assertContains(response, 'Redirect to Identity Provider is in progress') self.assertContains(response, '<form class="auto-submit" action="https://test.example.net/identity-provider-endpoint"') self.assertContains(response, '<input type="hidden" name="SAMLRequest" value="{}"'.format( response.context['saml_request'])) self.assertContains(response, '<input type="hidden" name="RelayState" value="relay123"/>') self.assertNotIn(b'An error occurred', response.content)
def test_export_xml_minimal_sample(self): request = LightRequest( citizen_country_code='CA', id='test-light-request-id', level_of_assurance=LevelOfAssurance.LOW, requested_attributes={}) with cast(BinaryIO, (DATA_DIR / 'light_request_minimal.xml').open('rb')) as f: data = f.read() self.assertEqual(dump_xml(request.export_xml()), data)
def test_sign_xml_node_without_id(self, uuid_mock): self.maxDiff = None root = Element(Q_NAMES['saml2p:Response'], nsmap=self.USED_NAMESPACES) assertion = SubElement(root, Q_NAMES['saml2:Assertion']) SubElement(assertion, Q_NAMES['saml2:Issuer']).text = 'Test Issuer' sign_xml_node(root, position=0, **SIGNATURE_OPTIONS) with cast(TextIO, (DATA_DIR / 'signed_response.xml').open('r')) as f: self.assertXMLEqual(dump_xml(root).decode('utf-8'), f.read())
def test_get_saml_response_invalid_signature(self): with cast(TextIO, (DATA_DIR / 'signed_response_and_assertion.xml').open('r')) as f: tree = parse_xml(f.read()) remove_extra_xml_whitespace(tree) saml_response_encoded = b64encode(dump_xml(tree, pretty_print=False)).decode('ascii') view = IdentityProviderResponseView() view.request = self.factory.post(self.url, {'SAMLResponse': saml_response_encoded}) self.assertRaises(SecurityError, view.get_saml_response, None, WRONG_CERT_FILE)
def test_post_success(self): self.maxDiff = None light_response = self.get_light_response() self.cache_mock.get_and_remove.return_value = dump_xml( light_response.export_xml()).decode('utf-8') token, encoded = self.get_token() response = self.client.post(self.url, {'test_response_token': encoded}) # Context self.assertEqual(response.context['error'], None) self.assertIn('saml_response', response.context) self.assertEqual(response.context['service_provider_endpoint'], '/DemoServiceProviderResponse') self.assertEqual(response.context['relay_state'], 'relay123') # SAML Response saml_response_xml = b64decode( response.context['saml_response'].encode('utf-8')).decode('utf-8') self.assertIn(light_response.id, saml_response_xml) # light_response.id preserved self.assertIn('<saml2:Issuer>test-saml-response-issuer</saml2:Issuer>', saml_response_xml) self.assertIn('Destination="/DemoServiceProviderResponse"', saml_response_xml) # No pretty-printing - it invalidates signatures self.assertIn( '><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo>', saml_response_xml) # Response and assertion signatures self.assertEqual( saml_response_xml.count( '<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">'), 2) self.assertEqual( saml_response_xml.count( 'Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"'), 2) self.assertEqual( saml_response_xml.count( 'Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"'), 2) # Rendering self.assertContains(response, 'Redirect to Service Provider is in progress') self.assertContains(response, 'eidas_node/connector/formautosubmit.js') self.assertContains( response, '<form class="auto-submit" action="/DemoServiceProviderResponse"') self.assertContains( response, '<input type="hidden" name="SAMLResponse" value="{}"'.format( response.context['saml_response'])) self.assertContains( response, '<input type="hidden" name="RelayState" value="relay123"/>') self.assertNotIn(b'An error occurred', response.content)
def test_get_saml_request_valid_signature(self): saml_request_xml, saml_request_encoded = self.load_saml_request( signed=True) view = ServiceProviderRequestView() view.request = self.factory.post(self.url, { 'SAMLRequest': saml_request_encoded, 'country_param': 'ca' }) saml_request = view.get_saml_request('country_param', CERT_FILE) self.assertXMLEqual( dump_xml(saml_request.document).decode('utf-8'), saml_request_xml)
def load_saml_request(self, signed=False) -> Tuple[str, str]: path = 'saml_request.xml' if not signed else 'saml_request_signed.xml' with cast(BinaryIO, (DATA_DIR / path).open('rb')) as f: saml_request_pretty = f.read() saml_request = parse_xml(saml_request_pretty) remove_extra_xml_whitespace(saml_request) saml_request_encoded = b64encode( dump_xml(saml_request, pretty_print=False)) return saml_request_pretty.decode( 'utf-8'), saml_request_encoded.decode('ascii')
def test_get_saml_request_without_relay_state(self): saml_request_xml, saml_request_encoded = self.load_saml_request() view = ServiceProviderRequestView() view.request = self.factory.post(self.url, { 'SAMLRequest': saml_request_encoded, 'country_param': 'ca' }) saml_request = view.get_saml_request('country_param', None) self.assertXMLEqual( dump_xml(saml_request.document).decode('utf-8'), saml_request_xml) self.assertEqual(saml_request.citizen_country_code, 'CA') self.assertEqual(saml_request.relay_state, None)
def test_from_light_request(self): self.maxDiff = None saml_request = SAMLRequest.from_light_request( LightRequest(**LIGHT_REQUEST_DICT), 'test/destination', datetime(2017, 12, 11, 14, 12, 5, 148000)) with cast(TextIO, (DATA_DIR / 'saml_request.xml').open('r')) as f2: data = f2.read() self.assertXMLEqual( dump_xml(saml_request.document).decode('utf-8'), data) self.assertEqual(saml_request.relay_state, 'relay123') self.assertEqual(saml_request.citizen_country_code, 'CA')
def test_from_light_response(self): self.maxDiff = None saml_response = SAMLResponse.from_light_response( self.create_light_response(True), 'saml-request-issuer', 'test/destination', datetime(2017, 12, 11, 14, 12, 5, 148000), timedelta(minutes=5)) with cast(TextIO, (DATA_DIR / 'saml_response_from_light_response.xml').open('r')) as f2: data = f2.read() self.assertXMLEqual( dump_xml(saml_response.document).decode('utf-8'), data)
def test_get_saml_response_encrypted(self): with cast(BinaryIO, (DATA_DIR / 'saml_response_encrypted.xml').open('rb')) as f: saml_response_xml = f.read() with cast(TextIO, (DATA_DIR / 'saml_response_decrypted.xml').open('r')) as f2: decrypted_saml_response_xml = f2.read() view = IdentityProviderResponseView() view.request = self.factory.post(self.url, {'SAMLResponse': b64encode(saml_response_xml).decode('ascii'), 'RelayState': 'relay123'}) saml_response = view.get_saml_response(KEY_FILE, None) self.assertEqual(saml_response.relay_state, 'relay123') self.assertXMLEqual(dump_xml(saml_response.document).decode('utf-8'), decrypted_saml_response_xml)
def test_decrypt(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'saml_response_decrypted.xml').open('rb')) as f: document_decrypted = f.read() with cast(BinaryIO, (DATA_DIR / 'saml_response_encrypted.xml').open('rb')) as f: document_encrypted = f.read() response = SAMLResponse(parse_xml(document_encrypted)) self.assertEqual(response.decrypt(KEY_FILE), 1) self.assertXMLEqual( dump_xml(response.document).decode('utf-8'), document_decrypted.decode('utf-8'))
def test_get_saml_response_fix_not_needed(self): self.maxDiff = None for name in 'saml_response_failed.xml', 'saml_response.xml': with self.subTest(name=name): with cast(BinaryIO, (DATA_DIR / name).open('rb')) as f: saml_response_xml = f.read() view = CzNiaResponseView() view.request = self.factory.post(self.url, {'SAMLResponse': b64encode(saml_response_xml).decode('ascii')}) saml_response = view.get_saml_response(None, None) self.assertXMLEqual(dump_xml(saml_response.document).decode('utf-8'), saml_response_xml.decode('utf-8'))
def get_context_data(self, **kwargs) -> dict: """Adjust template context data.""" context = super().get_context_data(**kwargs) context['error'] = self.error if self.saml_response: context[ 'service_provider_endpoint'] = CONNECTOR_SETTINGS.service_provider[ 'endpoint'] saml_response_xml = dump_xml(self.saml_response.document, pretty_print=False) context['saml_response'] = b64encode(saml_response_xml).decode( 'ascii') context['relay_state'] = self.saml_response.relay_state or '' return context
def get_context_data(self, **kwargs) -> dict: """Adjust template context data.""" context = super().get_context_data(**kwargs) context['error'] = self.error if self.saml_request: encoded_saml_request = b64encode( dump_xml(self.saml_request.document, pretty_print=False)).decode('ascii') context[ 'identity_provider_endpoint'] = PROXY_SERVICE_SETTINGS.identity_provider[ 'endpoint'] context['saml_request'] = encoded_saml_request context['relay_state'] = self.saml_request.relay_state or '' return context
def test_get_light_request_success(self): orig_light_request = LightRequest(**LIGHT_REQUEST_DICT) self.cache_mock.get_and_remove.return_value = dump_xml(orig_light_request.export_xml()).decode('utf-8') token, encoded = self.get_token() view = ProxyServiceRequestView() view.request = self.factory.post(self.url, {'test_token': encoded}) view.light_token = token view.storage = IgniteStorage('test.example.net', 1234, 'test-proxy-service-request-cache', '') light_request = view.get_light_request() self.assertEqual(light_request, orig_light_request) self.maxDiff = None self.assertEqual(self.client_mock.mock_calls, [call.connect('test.example.net', 1234), call.get_cache('test-proxy-service-request-cache'), call.get_cache().get_and_remove('request-token-id')])
def test_post_load_auxiliary_data(self): self.maxDiff = None light_response = self.get_light_response() self.cache_mock.get_and_remove.side_effect = [ dump_xml(light_response.export_xml()).decode('utf-8'), "{}", ] token, encoded = self.get_token() response = self.client.post(self.url, {'test_response_token': encoded}) self.assertEqual(response.status_code, 200) self.maxDiff = None self.assertSequenceEqual(self.client_mock.mock_calls[-3:], [ call.connect('test.example.net', 1234), call.get_cache('aux-cache'), call.get_cache().get_and_remove('aux-test-saml-request-id') ])
def post(self, request: HttpRequest) -> HttpResponse: """Handle a HTTP POST request.""" saml_response_xml = b64decode(self.request.POST.get('SAMLResponse', '').encode('ascii')).decode('utf-8') if saml_response_xml: # Verify signatures cert_file = (CONNECTOR_SETTINGS.service_provider['response_signature'] or {}).get('cert_file') if cert_file: response = SAMLResponse(parse_xml(saml_response_xml)) response.verify_response(cert_file) response.verify_assertion(cert_file) # Reformat with pretty printing for display saml_response_xml = dump_xml(parse_xml(saml_response_xml)).decode('utf-8') self.saml_response = saml_response_xml self.relay_state = self.request.POST.get('RelayState') return self.get(request)
def test_from_light_response_minimal(self): self.maxDiff = None status = Status(failure=False) response = self.create_light_response(True, ip_address=None, status=status, attributes={}) saml_response = SAMLResponse.from_light_response( response, None, None, datetime(2017, 12, 11, 14, 12, 5, 148000), timedelta(minutes=5)) with cast( TextIO, (DATA_DIR / 'saml_response_from_light_response_minimal.xml').open('r')) as f2: data = f2.read() self.assertXMLEqual( dump_xml(saml_response.document).decode('utf-8'), data)
def test_from_light_request_minimal(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'light_request_minimal.xml').open('rb')) as f: request = LightRequest.load_xml(parse_xml(f)) request.id = 'test-saml-request-id' saml_request = SAMLRequest.from_light_request( request, 'test/destination', datetime(2017, 12, 11, 14, 12, 5, 148000)) with cast(TextIO, (DATA_DIR / 'saml_request_minimal.xml').open('r')) as f2: data = f2.read() self.assertXMLEqual( dump_xml(saml_request.document).decode('utf-8'), data) self.assertEqual(saml_request.relay_state, None) self.assertEqual(saml_request.citizen_country_code, 'CA')
def test_from_light_response_version_mismatch(self): self.maxDiff = None status = Status(failure=True, sub_status_code=SubStatusCode.VERSION_MISMATCH, status_message='Oops.') response = self.create_light_response(False, issuer=None, ip_address=None, status=status) saml_response = SAMLResponse.from_light_response( response, None, None, datetime(2017, 12, 11, 14, 12, 5, 148000), timedelta(minutes=5)) with cast(TextIO, (DATA_DIR / 'saml_response_from_light_response_version_mismatch.xml' ).open('r')) as f2: data = f2.read() self.assertXMLEqual( dump_xml(saml_response.document).decode('utf-8'), data)
def test_post_with_signed_saml_response(self): with cast(TextIO, (DATA_DIR / 'signed_response_and_assertion.xml').open('r')) as f: tree = parse_xml(f.read()) remove_extra_xml_whitespace(tree) saml_response_encoded = b64encode(dump_xml( tree, pretty_print=False)).decode('ascii') response = self.client.post(self.url, {'SAMLResponse': saml_response_encoded}) self.assertIn( '\n <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">', response.context['saml_response']) self.assertIn( '\n <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">', response.context['saml_response']) self.assertEqual(response.context['relay_state'], 'None') self.assertContains(response, '<code>None</code>') self.assertContains(response, '<pre style="white-space: pre-wrap;"><?xml')