def get_saml_request(self, country_parameter: str, cert_file: Optional[str]) -> SAMLRequest: """ Extract and decrypt a SAML request from POST data. :param country_parameter: A parameter containing citizen country code. :param cert_file: The path of a certificate to verify the signature. :return: A SAML request. """ try: request = SAMLRequest( parse_xml( b64decode( self.request.POST.get('SAMLRequest', '').encode('ascii'))), self.request.POST[country_parameter].upper(), self.request.POST.get('RelayState')) except XMLSyntaxError as e: raise ParseError(str(e)) from None LOGGER.info('[#%r] Received SAML request: id=%r, issuer=%r', self.log_id, request.id, request.issuer) if cert_file: request.verify_request(cert_file) return request
def test_sign_request_with_issuer(self): root = Element(Q_NAMES['saml2p:AuthnRequest']) SubElement(root, Q_NAMES['saml2:Issuer']) request = SAMLRequest(ElementTree(root)) request.sign_request(**SIGNATURE_OPTIONS) self.assertIsNotNone(request.request_signature) self.assertEqual(root.index(request.request_signature), 1)
def test_create_light_request_without_extensions(self): root = Element(Q_NAMES['saml2p:AuthnRequest'], nsmap=EIDAS_NAMESPACES) saml_request = SAMLRequest(ElementTree(root), 'CZ', 'relay123') expected = LightRequest(citizen_country_code='CZ', relay_state='relay123', requested_attributes=OrderedDict()) self.assertEqual(saml_request.create_light_request(), expected)
def test_str(self): self.assertEqual( str(SAMLRequest(ElementTree(Element('root')), 'CZ', 'relay')), "citizen_country_code = 'CZ', relay_state = 'relay', document = " "<?xml version='1.0' encoding='utf-8' standalone='yes'?>\n<root/>\n" ) self.assertEqual( str(SAMLRequest(None, None, None)), 'citizen_country_code = None, relay_state = None, document = None')
def test_sign_request_already_exists(self): root = Element(Q_NAMES['saml2p:AuthnRequest']) signature = SubElement(root, Q_NAMES['ds:Signature']) SubElement(root, Q_NAMES['saml2:Issuer']) request = SAMLRequest(ElementTree(root)) with self.assertRaisesMessage(SecurityError, 'Request signature already exists.'): request.sign_request(**SIGNATURE_OPTIONS) self.assertIs(request.request_signature, signature)
def test_create_light_request_success(self): self.maxDiff = None with cast(TextIO, (DATA_DIR / 'saml_request.xml').open('r')) as f: data = f.read() saml_request = SAMLRequest(parse_xml(data), 'CA', 'relay123') self.assertEqual( saml_request.create_light_request().get_data_as_dict(), LIGHT_REQUEST_DICT)
def test_from_light_request_invalid_id(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 = '0day' with self.assert_validation_error( 'id', "Light request id is not a valid XML id: '0day'"): SAMLRequest.from_light_request( request, 'test/destination', datetime(2017, 12, 11, 14, 12, 5, 148000))
def test_create_light_request_extra_elements(self): self.maxDiff = None with cast(TextIO, (DATA_DIR / 'saml_request.xml').open('r')) as f: document = parse_xml(f.read()) SubElement(document.getroot(), 'extra').text = 'extra' SubElement( document.find(".//{}".format( Q_NAMES['eidas:RequestedAttributes'])), 'extra').text = 'extra' saml_request = SAMLRequest(document, 'CA', 'relay123') self.assertEqual( saml_request.create_light_request().get_data_as_dict(), LIGHT_REQUEST_DICT)
def test_request_signature_not_exists(self): root = Element(Q_NAMES['saml2p:AuthnRequest']) # Booby trap SubElement(SubElement(root, Q_NAMES['saml2:Issuer']), Q_NAMES['ds:Signature']) # No signature must be found self.assertIsNone(SAMLRequest(ElementTree(root)).request_signature)
def post(self, request: HttpRequest) -> HttpResponse: """Handle a HTTP POST request.""" try: preset = PRESETS[int(request.POST.get('Request', ''))] except (ValueError, KeyError): return HttpResponseBadRequest() light_request = LightRequest( id=create_xml_uuid(), issuer=CONNECTOR_SETTINGS.service_provider['request_issuer'], level_of_assurance=LevelOfAssurance.LOW, provider_name="Demo Service Provider", sp_type=ServiceProviderType.PUBLIC, relay_state=request.POST.get('RelayState') or None, origin_country_code='EU', citizen_country_code=request.POST.get('Country'), name_id_format=preset.id_format, requested_attributes={name: [] for name in preset.attributes} ) if not light_request.citizen_country_code: # Use a placeholder to get through light request validation. light_request.citizen_country_code = COUNTRY_PLACEHOLDER self.saml_request = SAMLRequest.from_light_request(light_request, '/dest', datetime.utcnow()) signature_options = CONNECTOR_SETTINGS.service_provider['response_signature'] if signature_options and signature_options.get('key_file') and signature_options.get('cert_file'): self.saml_request.sign_request(**signature_options) return self.get(request)
def create_saml_request( self, issuer: str, signature_options: Optional[Dict[str, str]]) -> SAMLRequest: """ Create a SAML request from a light request. :param issuer: Issuer of the SAML request. :param signature_options: Optional options to create a signed request: `key_file`, `cert_file`. `signature_method`, abd `digest_method`. :return: A SAML request. """ # Replace the original issuer with our issuer registered at the Identity Provider. self.light_request.issuer = issuer destination = self.request.build_absolute_uri( reverse('identity-provider-response')) saml_request = SAMLRequest.from_light_request(self.light_request, destination, datetime.utcnow()) LOGGER.info('[#%r] Created SAML request: id=%r, issuer=%r', self.log_id, saml_request.id, saml_request.issuer) if signature_options and signature_options.get( 'key_file') and signature_options.get('cert_file'): saml_request.sign_request(**signature_options) return saml_request
def test_create_light_request_our_issuer_set(self): saml_request_xml, _saml_request_encoded = self.load_saml_request() view = ServiceProviderRequestView() view.saml_request = SAMLRequest(parse_xml(saml_request_xml), 'ca', 'xyz') light_request = view.create_light_request('test-saml-request-issuer', 'test-light-request-issuer') self.assertEqual(light_request.issuer, 'test-light-request-issuer')
def test_verify_request_not_found(self, signatures_mock): root = Element(Q_NAMES['saml2p:AuthnRequest']) SubElement(root, Q_NAMES['ds:Signature']) signatures_mock.return_value = [ SignatureInfo(Element(Q_NAMES['ds:Signature']), (root, )) ] with self.assertRaisesMessage(SecurityError, 'Signature not found'): SAMLRequest(ElementTree(root)).verify_request('cert.pem') self.assertEqual(signatures_mock.mock_calls, [call(root, 'cert.pem')])
def test_create_light_request_wrong_issuer(self): saml_request_xml, _saml_request_encoded = self.load_saml_request() view = ServiceProviderRequestView() view.saml_request = SAMLRequest(parse_xml(saml_request_xml), 'ca', 'xyz') with self.assertRaisesMessage(SecurityError, 'Invalid SAML request issuer'): view.create_light_request('wrong-saml-issuer', 'test-light-request-issuer')
def test_verify_request_wrong_parent(self, signatures_mock): root = Element(Q_NAMES['saml2p:AuthnRequest']) signature = SubElement(root, Q_NAMES['ds:Signature']) signatures_mock.return_value = [ SignatureInfo(signature, (Element('whatever'), )) ] with self.assertRaisesMessage( SecurityError, 'Signature does not reference parent element'): SAMLRequest(ElementTree(root)).verify_request('cert.pem') self.assertEqual(signatures_mock.mock_calls, [call(root, 'cert.pem')])
def test_create_light_request_missing_attribute_name(self): root = Element(Q_NAMES['saml2p:AuthnRequest'], nsmap=EIDAS_NAMESPACES) extensions = SubElement(root, Q_NAMES['saml2p:Extensions']) attributes = SubElement(extensions, Q_NAMES['eidas:RequestedAttributes']) SubElement(attributes, Q_NAMES['eidas:RequestedAttribute']) saml_request = SAMLRequest(ElementTree(root), 'CZ', 'relay123') self.assert_validation_error( '<saml2p:AuthnRequest><saml2p:Extensions><eidas:RequestedAttributes><eidas:RequestedAttribute>', "Missing attribute 'Name'", saml_request.create_light_request)
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_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_create_light_request_invalid_root_element(self): root = Element('wrongRoot') saml_request = SAMLRequest(ElementTree(root), 'CZ', 'relay123') self.assert_validation_error('<wrongRoot>', "Wrong root element: 'wrongRoot'", saml_request.create_light_request)
def test_verify_request(self, signatures_mock): root = Element(Q_NAMES['saml2p:AuthnRequest']) signature = SubElement(root, Q_NAMES['ds:Signature']) signatures_mock.return_value = [SignatureInfo(signature, (root, ))] SAMLRequest(ElementTree(root)).verify_request('cert.pem') self.assertEqual(signatures_mock.mock_calls, [call(root, 'cert.pem')])
def test_issuer_none(self): root = Element(Q_NAMES['saml2p:AuthnRequest'], nsmap=EIDAS_NAMESPACES) request = SAMLRequest(ElementTree(root), 'CZ', 'relay123') self.assertIsNone(request.issuer)
def test_issuer(self): root = Element(Q_NAMES['saml2p:AuthnRequest'], nsmap=EIDAS_NAMESPACES) SubElement(root, Q_NAMES['saml2:Issuer']).text = 'test-issuer' request = SAMLRequest(ElementTree(root), 'CZ', 'relay123') self.assertEqual(request.issuer, 'test-issuer')
def test_id(self): root = Element(Q_NAMES['saml2p:AuthnRequest'], {'ID': 'test-id'}, nsmap=EIDAS_NAMESPACES) request = SAMLRequest(ElementTree(root), 'CZ', 'relay123') self.assertEqual(request.id, 'test-id')
def test_verify_request_none(self, signatures_mock): root = Element(Q_NAMES['saml2p:AuthnRequest']) with self.assertRaisesMessage(SecurityError, 'Signature does not exist'): SAMLRequest(ElementTree(root)).verify_request('cert.pem') self.assertEqual(signatures_mock.mock_calls, [])