From b8cb77b858b074e0cc4fde8bdeed197e28ef5d84 Mon Sep 17 00:00:00 2001 From: Jesse Shapiro Date: Mon, 29 Aug 2016 09:47:43 -0400 Subject: [PATCH 01/10] Making certain methods raise exceptions when used inside application --- src/onelogin/saml2/response.py | 28 +- src/onelogin/saml2/utils.py | 281 ++++++++++--------- tests/src/OneLogin/saml2_tests/utils_test.py | 31 ++ 3 files changed, 203 insertions(+), 137 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 61224b16..3734502e 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -16,7 +16,7 @@ from xml.dom.minidom import Document from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception class OneLogin_Saml2_Response(object): @@ -90,13 +90,21 @@ def is_valid(self, request_data, request_id=None): if self.__settings.is_strict(): no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' - res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) + res = OneLogin_Saml2_Utils.validate_xml( + etree.tostring(self.document), + 'saml-schema-protocol-2.0.xsd', + self.__settings.is_debug_active() + ) if not isinstance(res, Document): raise Exception(no_valid_xml_msg) # If encrypted, check also the decrypted document if self.encrypted: - res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.decrypted_document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) + res = OneLogin_Saml2_Utils.validate_xml( + etree.tostring(self.decrypted_document), + 'saml-schema-protocol-2.0.xsd', + self.__settings.is_debug_active() + ) if not isinstance(res, Document): raise Exception(no_valid_xml_msg) @@ -123,8 +131,7 @@ def is_valid(self, request_data, request_id=None): raise Exception('There is no AttributeStatement on the Response') # Validates Assertion timestamps - if not self.validate_timestamps(): - raise Exception('Timing issues (please check your clock settings)') + self.validate_timestamps(raise_exceptions=True) encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute') if encrypted_attributes_nodes: @@ -212,8 +219,7 @@ def is_valid(self, request_data, request_id=None): document_to_validate = self.decrypted_document else: document_to_validate = self.document - if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg): - raise Exception('Signature validation failed. SAML Response rejected') + OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg, raise_exceptions=True) else: raise Exception('No Signature found. SAML Response rejected') @@ -435,10 +441,14 @@ def process_signed_elements(self): signed_elements.append(signed_element) return signed_elements + @return_false_on_exception def validate_timestamps(self): """ Verifies that the document is valid according to Conditions Element + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean + :returns: True if the condition is valid, False otherwise :rtype: bool """ @@ -448,9 +458,9 @@ def validate_timestamps(self): nb_attr = conditions_node.get('NotBefore') nooa_attr = conditions_node.get('NotOnOrAfter') if nb_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nb_attr) > OneLogin_Saml2_Utils.now() + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT: - return False + raise Exception('Could not validate timestamp: not yet valid. Check system clock.') if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT <= OneLogin_Saml2_Utils.now(): - return False + raise Exception('Could not validate timestamp: expired. Check system clock.') return True def __query_assertion(self, xpath_expr): diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index c1d0eff6..2610f3dc 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -25,6 +25,7 @@ from uuid import uuid4 from xml.dom.minidom import Document, Element from defusedxml.minidom import parseString +from functools import wraps import zlib @@ -39,6 +40,24 @@ globals()['xmlsec_setup'] = True +def return_false_on_exception(func): + """ + Decorator. When applied to a function, it will, by default, suppress any exceptions + raised by that function and return False. It may be overridden by passing a + "raise_exceptions" keyword argument when calling the wrapped function. + """ + @wraps(func) + def exceptfalse(*args, **kwargs): + if not kwargs.pop('raise_exceptions', False): + try: + return func(*args, **kwargs) + except Exception: + return False + else: + return func(*args, **kwargs) + return exceptfalse + + def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg): """ Auxiliary method. It overrides the default xmlsec debug message. @@ -866,6 +885,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant return newdoc.saveXML(newdoc.firstChild) @staticmethod + @return_false_on_exception def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): """ Validates a signature (Message or Assertion). @@ -887,53 +907,54 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid :param debug: Activate the xmlsec debug :type: bool + + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean """ - try: - if xml is None or xml == '': - raise Exception('Empty string supplied as input') - elif isinstance(xml, etree._Element): - elem = xml - elif isinstance(xml, Document): - xml = xml.toxml() - elem = fromstring(str(xml)) - elif isinstance(xml, Element): - xml.setAttributeNS( - unicode(OneLogin_Saml2_Constants.NS_SAMLP), - 'xmlns:samlp', - unicode(OneLogin_Saml2_Constants.NS_SAMLP) - ) - xml.setAttributeNS( - unicode(OneLogin_Saml2_Constants.NS_SAML), - 'xmlns:saml', - unicode(OneLogin_Saml2_Constants.NS_SAML) - ) - xml = xml.toxml() - elem = fromstring(str(xml)) - elif isinstance(xml, basestring): - elem = fromstring(str(xml)) - else: - raise Exception('Error parsing xml string') + if xml is None or xml == '': + raise Exception('Empty string supplied as input') + elif isinstance(xml, etree._Element): + elem = xml + elif isinstance(xml, Document): + xml = xml.toxml() + elem = fromstring(str(xml)) + elif isinstance(xml, Element): + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_SAMLP), + 'xmlns:samlp', + unicode(OneLogin_Saml2_Constants.NS_SAMLP) + ) + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_SAML), + 'xmlns:saml', + unicode(OneLogin_Saml2_Constants.NS_SAML) + ) + xml = xml.toxml() + elem = fromstring(str(xml)) + elif isinstance(xml, basestring): + elem = fromstring(str(xml)) + else: + raise Exception('Error parsing xml string') - if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + if debug: + xmlsec.set_error_callback(print_xmlsec_errors) - xmlsec.addIDs(elem, ["ID"]) + xmlsec.addIDs(elem, ["ID"]) - signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature') + signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature') - if not len(signature_nodes) > 0: - signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature') + if not len(signature_nodes) > 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature') - if len(signature_nodes) == 1: - signature_node = signature_nodes[0] + if len(signature_nodes) == 1: + signature_node = signature_nodes[0] - return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug) - else: - return False - except Exception: - return False + return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) + else: + raise Exception('Expected exactly one signature node; got {}.'.format(len(signature_nodes))) @staticmethod + @return_false_on_exception def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): """ Validates a signature of a EntityDescriptor. @@ -955,53 +976,53 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha :param debug: Activate the xmlsec debug :type: bool + + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean """ - try: - if xml is None or xml == '': - raise Exception('Empty string supplied as input') - elif isinstance(xml, etree._Element): - elem = xml - elif isinstance(xml, Document): - xml = xml.toxml() - elem = fromstring(str(xml)) - elif isinstance(xml, Element): - xml.setAttributeNS( - unicode(OneLogin_Saml2_Constants.NS_MD), - 'xmlns:md', - unicode(OneLogin_Saml2_Constants.NS_MD) - ) - xml = xml.toxml() - elem = fromstring(str(xml)) - elif isinstance(xml, basestring): - elem = fromstring(str(xml)) - else: - raise Exception('Error parsing xml string') + if xml is None or xml == '': + raise Exception('Empty string supplied as input') + elif isinstance(xml, etree._Element): + elem = xml + elif isinstance(xml, Document): + xml = xml.toxml() + elem = fromstring(str(xml)) + elif isinstance(xml, Element): + xml.setAttributeNS( + unicode(OneLogin_Saml2_Constants.NS_MD), + 'xmlns:md', + unicode(OneLogin_Saml2_Constants.NS_MD) + ) + xml = xml.toxml() + elem = fromstring(str(xml)) + elif isinstance(xml, basestring): + elem = fromstring(str(xml)) + else: + raise Exception('Error parsing xml string') - if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + if debug: + xmlsec.set_error_callback(print_xmlsec_errors) - xmlsec.addIDs(elem, ["ID"]) + xmlsec.addIDs(elem, ["ID"]) - signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature') + signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature') - if len(signature_nodes) == 0: - signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature') + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature') - if len(signature_nodes) == 0: - signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature') - signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature') + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature') + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature') - if len(signature_nodes) > 0: - for signature_node in signature_nodes: - if not OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug): - return False - return True - else: - return False - except Exception: - return False + if len(signature_nodes) > 0: + for signature_node in signature_nodes: + OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) + return True + else: + raise Exception('Could not validate metadata signature: No signature nodes found.') @staticmethod + @return_false_on_exception def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): """ Validates a signature node. @@ -1026,50 +1047,54 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger :param debug: Activate the xmlsec debug :type: bool + + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean """ - try: - if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + if debug: + xmlsec.set_error_callback(print_xmlsec_errors) - xmlsec.addIDs(elem, ["ID"]) + xmlsec.addIDs(elem, ["ID"]) - if (cert is None or cert == '') and fingerprint: - x509_certificate_nodes = OneLogin_Saml2_Utils.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate') - if len(x509_certificate_nodes) > 0: - x509_certificate_node = x509_certificate_nodes[0] - x509_cert_value = x509_certificate_node.text - x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg) - if fingerprint == x509_fingerprint_value: - cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) + if (cert is None or cert == '') and fingerprint: + x509_certificate_nodes = OneLogin_Saml2_Utils.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate') + if len(x509_certificate_nodes) > 0: + x509_certificate_node = x509_certificate_nodes[0] + x509_cert_value = x509_certificate_node.text + x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg) + if fingerprint == x509_fingerprint_value: + cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) - # Check if Reference URI is empty - # reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference') - # if len(reference_elem) > 0: - # if reference_elem[0].get('URI') == '': - # reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID')) + # Check if Reference URI is empty + # reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference') + # if len(reference_elem) > 0: + # if reference_elem[0].get('URI') == '': + # reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID')) - if cert is None or cert == '': - return False + if cert is None or cert == '': + raise Exception('Could not validate node signature: No certificate provided.') - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) - if validatecert: - mngr = xmlsec.KeysMngr() - mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted) - dsig_ctx = xmlsec.DSigCtx(mngr) - else: - dsig_ctx = xmlsec.DSigCtx() - dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + if validatecert: + mngr = xmlsec.KeysMngr() + mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted) + dsig_ctx = xmlsec.DSigCtx(mngr) + else: + dsig_ctx = xmlsec.DSigCtx() + dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) - file_cert.close() + file_cert.close() - dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) + dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) + try: dsig_ctx.verify(signature_node) - return True except Exception: - return False + raise Exception('Signature validation failed. SAML Response rejected') + return True @staticmethod + @return_false_on_exception def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False): """ Validates signed binary data (Used to validate GET Signature). @@ -1089,31 +1114,31 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ :param debug: Activate the xmlsec debug :type: bool + + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean """ - try: - if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + if debug: + xmlsec.set_error_callback(print_xmlsec_errors) - dsig_ctx = xmlsec.DSigCtx() + dsig_ctx = xmlsec.DSigCtx() - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) - dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) - file_cert.close() + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + file_cert.close() - # Sign the metadata with our private key. - sign_algorithm_transform_map = { - OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1, - OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1, - OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256, - OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384, - OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512 - } - sign_algorithm_transform = sign_algorithm_transform_map.get(algorithm, xmlsec.TransformRsaSha1) - - dsig_ctx.verifyBinary(signed_query, sign_algorithm_transform, signature) - return True - except Exception: - return False + # Sign the metadata with our private key. + sign_algorithm_transform_map = { + OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1, + OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1, + OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256, + OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384, + OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512 + } + sign_algorithm_transform = sign_algorithm_transform_map.get(algorithm, xmlsec.TransformRsaSha1) + + dsig_ctx.verifyBinary(signed_query, sign_algorithm_transform, signature) + return True @staticmethod def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False): diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index ebd221e8..08e617a1 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -856,11 +856,18 @@ def testValidateSign(self): except Exception as e: self.assertEqual('Error parsing xml string', e.message) + with self.assertRaisesRegexp(Exception, 'Empty string supplied as input'): + OneLogin_Saml2_Utils.validate_sign('', cert, raise_exceptions=True) + with self.assertRaisesRegexp(Exception, 'Error parsing xml string'): + OneLogin_Saml2_Utils.validate_sign(1, cert, raise_exceptions=True) + # expired cert xml_metadata_signed = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings1.xml')) self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True, raise_exceptions=True) xml_metadata_signed_2 = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings2.xml')) self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, cert_2)) @@ -872,6 +879,8 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True, raise_exceptions=True) # modified cert other_cert_path = join(dirname(__file__), '..', '..', '..', 'certs') @@ -880,6 +889,10 @@ def testValidateSign(self): f.close() self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x)) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, raise_exceptions=True) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True, raise_exceptions=True) xml_response_msg_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_message_response2.xml.base64'))) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed_2, cert_2)) @@ -893,6 +906,8 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True, raise_exceptions=True) xml_response_assert_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response2.xml.base64'))) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed_2, cert_2)) @@ -904,6 +919,8 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True, raise_exceptions=True) xml_response_double_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'double_signed_response2.xml.base64'))) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed_2, cert_2)) @@ -917,32 +934,46 @@ def testValidateSign(self): dom.firstChild.getAttributeNode('ID').nodeValue = u'_34fg27g212d63k1f923845324475802ac0fc24530b' # Reference validation failed self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom, cert_2)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(dom, cert_2, raise_exceptions=True) invalid_fingerprint = 'afe71c34ef740bc87434be13a2263d31271da1f9' # Wrong fingerprint self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint, raise_exceptions=True) dom_2 = parseString(xml_response_double_signed_2) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2)) dom_2.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp' # Modified message self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2, raise_exceptions=True) # Try to validate directly the Assertion dom_3 = parseString(xml_response_double_signed_2) assert_elem_3 = dom_3.firstChild.firstChild.nextSibling.nextSibling.nextSibling self.assertFalse(OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2, raise_exceptions=True) # Wrong scheme no_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_signature.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_signed, cert)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(no_signed, cert, raise_exceptions=True) no_key = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_key, cert)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(no_key, cert, raise_exceptions=True) # Signature Wrapping attack wrapping_attack1 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'signature_wrapping_attack.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert)) + with self.assertRaises(Exception): + OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert, raise_exceptions=True) if __name__ == '__main__': From 0cd21ec9acda7018e513e8a0e28c3711b513a0a7 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 21 Dec 2016 10:22:13 +0100 Subject: [PATCH 02/10] Add option to raise exceptions on SAML message validation --- src/onelogin/saml2/logout_request.py | 7 +++++-- src/onelogin/saml2/logout_response.py | 8 ++++++-- src/onelogin/saml2/response.py | 4 +++- .../OneLogin/saml2_tests/logout_request_test.py | 16 ++++++++++++++++ .../saml2_tests/logout_response_test.py | 17 +++++++++++++++++ tests/src/OneLogin/saml2_tests/response_test.py | 12 ++++++++++++ 6 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 131c69cc..99339e54 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -260,12 +260,13 @@ def get_session_indexes(request): session_indexes.append(session_index_node.text) return session_indexes - def is_valid(self, request_data): + def is_valid(self, request_data, raise_exceptions=False): """ Checks if the Logout Request received is valid :param request_data: Request Data :type request_data: dict - + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean :return: If the Logout Request is or not valid :rtype: boolean """ @@ -348,6 +349,8 @@ def is_valid(self, request_data): debug = self.__settings.is_debug_active() if debug: print err.__str__() + if raise_exceptions: + raise err return False def get_error(self): diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 3df89064..498272e0 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -68,11 +68,13 @@ def get_status(self): status = entries[0].attrib['Value'] return status - def is_valid(self, request_data, request_id=None): + def is_valid(self, request_data, request_id=None, raise_exceptions=False): """ Determines if the SAML LogoutResponse is valid :param request_id: The ID of the LogoutRequest sent by this SP to the IdP :type request_id: string + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean :return: Returns if the SAML LogoutResponse is or not valid :rtype: boolean """ @@ -89,7 +91,7 @@ def is_valid(self, request_data, request_id=None): if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): - raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') + raise Exception('Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd') security = self.__settings.get_security_data() @@ -142,6 +144,8 @@ def is_valid(self, request_data, request_id=None): debug = self.__settings.is_debug_active() if debug: print err.__str__() + if raise_exceptions: + raise err return False def __query(self, query): diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 620aff71..bd72ac74 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -51,7 +51,7 @@ def __init__(self, settings, response): self.encrypted = True self.decrypted_document = self.__decrypt_assertion(decrypted_document) - def is_valid(self, request_data, request_id=None): + def is_valid(self, request_data, request_id=None, raise_exceptions=False): """ Validates the response object. @@ -239,6 +239,8 @@ def is_valid(self, request_data, request_id=None): debug = self.__settings.is_debug_active() if debug: print err.__str__() + if raise_exceptions: + raise err return False def check_status(self): diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index df115ee0..cbdfdb33 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -312,6 +312,22 @@ def testIsValid(self): logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) self.assertTrue(logout_request5.is_valid(request_data)) + def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): + request = OneLogin_Saml2_Utils.deflate_and_base64_encode('invalid') + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings.set_strict(True) + + logout_request = OneLogin_Saml2_Logout_Request(settings, request) + + self.assertFalse(logout_request.is_valid(request_data)) + + with self.assertRaisesRegexp(Exception, "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd"): + logout_request.is_valid(request_data, raise_exceptions=True) + def testIsValidSign(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 89d7bbf9..f0a1553b 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -228,6 +228,23 @@ def testIsInValidDestination(self): response_4 = OneLogin_Saml2_Logout_Response(settings, message_4) self.assertTrue(response_4.is_valid(request_data)) + def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): + message = OneLogin_Saml2_Utils.deflate_and_base64_encode('invalid') + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings.set_strict(True) + + response = OneLogin_Saml2_Logout_Response(settings, message) + + self.assertFalse(response.is_valid(request_data)) + + with self.assertRaisesRegexp(Exception, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd"): + response.is_valid(request_data, raise_exceptions=True) + def testIsInValidSign(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 2f34efd8..2bd2aa7c 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1227,6 +1227,18 @@ def testIsValidEnc(self): response_7.is_valid(request_data) self.assertEqual('No Signature found. SAML Response rejected', response_7.get_error()) + def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): + message = b64encode('invalid') + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings.set_strict(True) + + response = OneLogin_Saml2_Response(settings, message) + + self.assertFalse(response.is_valid(self.get_request_data())) + + with self.assertRaisesRegexp(Exception, "Unsupported SAML version"): + response.is_valid(self.get_request_data(), raise_exceptions=True) + def testIsValidSign(self): """ Tests the is_valid method of the OneLogin_Saml2_Response From bc7bac63b72de309fd3361b6a83595da9fdf57c5 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 22 Dec 2016 09:06:17 +0100 Subject: [PATCH 03/10] Implement a more specific exception class for handling some validation errors. Improve tests --- src/onelogin/saml2/auth.py | 2 +- src/onelogin/saml2/errors.py | 77 +++++- src/onelogin/saml2/logout_request.py | 36 ++- src/onelogin/saml2/logout_response.py | 36 ++- src/onelogin/saml2/response.py | 230 ++++++++++++++---- src/onelogin/saml2/settings.py | 5 +- src/onelogin/saml2/utils.py | 55 +++-- tests/src/OneLogin/saml2_tests/auth_test.py | 11 +- .../saml2_tests/logout_request_test.py | 11 +- .../saml2_tests/logout_response_test.py | 7 +- .../src/OneLogin/saml2_tests/response_test.py | 36 +-- tests/src/OneLogin/saml2_tests/utils_test.py | 31 +-- 12 files changed, 410 insertions(+), 127 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index ab8fb2af..096440eb 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -432,7 +432,7 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On if not key: raise OneLogin_Saml2_Error( "Trying to sign the %s but can't load the SP private key" % saml_type, - OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND + OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND ) dsig_ctx = xmlsec.DSigCtx() diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 63d98744..5a859fc1 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -25,7 +25,7 @@ class OneLogin_Saml2_Error(Exception): SETTINGS_INVALID_SYNTAX = 1 SETTINGS_INVALID = 2 METADATA_SP_INVALID = 3 - SP_CERTS_NOT_FOUND = 4 + CERT_NOT_FOUND = 4 REDIRECT_INVALID_URL = 5 PUBLIC_CERT_FILE_NOT_FOUND = 6 PRIVATE_KEY_FILE_NOT_FOUND = 7 @@ -34,6 +34,8 @@ class OneLogin_Saml2_Error(Exception): SAML_LOGOUTREQUEST_INVALID = 10 SAML_LOGOUTRESPONSE_INVALID = 11 SAML_SINGLE_LOGOUT_NOT_SUPPORTED = 12 + PRIVATE_KEY_NOT_FOUND = 13 + UNSUPPORTED_SETTINGS_OBJECT = 14 def __init__(self, message, code=0, errors=None): """ @@ -51,3 +53,76 @@ def __init__(self, message, code=0, errors=None): Exception.__init__(self, message) self.code = code + + +class OneLogin_Saml2_ValidationError(Exception): + """ + + This class implements another custom Exception handler, related + to exceptions that happens during validation process. + Defines custom error codes . + + """ + + # Validation Errors + UNSUPPORTED_SAML_VERSION = 0 + MISSING_ID = 1 + WRONG_NUMBER_OF_ASSERTIONS = 2 + MISSING_STATUS = 3 + MISSING_STATUS_CODE = 4 + STATUS_CODE_IS_NOT_SUCCESS = 5 + WRONG_SIGNED_ELEMENT = 6 + ID_NOT_FOUND_IN_SIGNED_ELEMENT = 7 + DUPLICATED_ID_IN_SIGNED_ELEMENTS = 8 + INVALID_SIGNED_ELEMENT = 9 + DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS = 10 + UNEXPECTED_SIGNED_ELEMENTS = 11 + WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE = 12 + WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION = 13 + INVALID_XML_FORMAT = 14 + WRONG_INRESPONSETO = 15 + NO_ENCRYPTED_ASSERTION = 16 + NO_ENCRYPTED_NAMEID = 17 + MISSING_CONDITIONS = 18 + ASSERTION_TOO_EARLY = 19 + ASSERTION_EXPIRED = 20 + WRONG_NUMBER_OF_AUTHSTATEMENTS = 21 + NO_ATTRIBUTESTATEMENT = 22 + ENCRYPTED_ATTRIBUTES = 23 + WRONG_DESTINATION = 24 + EMPTY_DESTINATION = 25 + WRONG_AUDIENCE = 26 + ISSUER_NOT_FOUND_IN_RESPONSE = 27 + ISSUER_NOT_FOUND_IN_ASSERTION = 28 + WRONG_ISSUER = 29 + SESSION_EXPIRED = 30 + WRONG_SUBJECTCONFIRMATION = 31 + NO_SIGNED_RESPONSE = 32 + NO_SIGNED_ASSERTION = 33 + NO_SIGNATURE_FOUND = 34 + KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35 + CHILDREN_NODE_NOT_FOIND_IN_KEYINFO = 36 + UNSUPPORTED_RETRIEVAL_METHOD = 37 + NO_NAMEID = 38 + EMPTY_NAMEID = 39 + SP_NAME_QUALIFIER_NAME_MISMATCH = 40 + DUPLICATED_ATTRIBUTE_NAME_FOUND = 41 + INVALID_SIGNATURE = 42 + WRONG_NUMBER_OF_SIGNATURES = 43 + + def __init__(self, message, code=0, errors=None): + """ + Initializes the Exception instance. + + Arguments are: + * (str) message. Describes the error. + * (int) code. The code error (defined in the error class). + """ + assert isinstance(message, basestring) + assert isinstance(code, int) + + if errors is not None: + message = message % errors + + Exception.__init__(self, message) + self.code = code \ No newline at end of file diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 99339e54..785c7c64 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -17,6 +17,7 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError class OneLogin_Saml2_Logout_Request(object): @@ -179,7 +180,10 @@ def get_nameid_data(request, key=None): if len(encrypted_entries) == 1: if key is None: - raise Exception('Key is required in order to decrypt the NameID') + raise OneLogin_Saml2_Error( + 'Private Key is required in order to decrypt the NameID, check settings', + OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND + ) encrypted_data_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_data_nodes) == 1: @@ -191,7 +195,10 @@ def get_nameid_data(request, key=None): name_id = entries[0] if name_id is None: - raise Exception('Not NameID found in the Logout Request') + raise OneLogin_Saml2_ValidationError( + 'Not NameID found in the Logout Request', + OneLogin_Saml2_ValidationError.NO_NAMEID + ) name_id_data = { 'Value': name_id.text @@ -289,7 +296,10 @@ def is_valid(self, request_data, raise_exceptions=False): if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): - raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd') + raise OneLogin_Saml2_ValidationError( + 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd', + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) security = self.__settings.get_security_data() @@ -318,11 +328,17 @@ def is_valid(self, request_data, raise_exceptions=False): # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom) if issuer is not None and issuer != idp_entity_id: - raise Exception('Invalid issuer in the Logout Request') + raise OneLogin_Saml2_ValidationError( + 'Invalid issuer in the Logout Request', + OneLogin_Saml2_ValidationError.WRONG_ISSUER + ) if security['wantMessagesSigned']: if 'Signature' not in get_data: - raise Exception('The Message of the Logout Request is not signed and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The Message of the Logout Request is not signed and the SP require it', + OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + ) if 'Signature' in get_data: if 'SigAlg' not in get_data: @@ -336,11 +352,17 @@ def is_valid(self, request_data, raise_exceptions=False): signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) if 'x509cert' not in idp_data or not idp_data['x509cert']: - raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') + raise OneLogin_Saml2_Error( + 'In order to validate the sign on the Logout Request, the x509cert of the IdP is required', + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) cert = idp_data['x509cert'] if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): - raise Exception('Signature validation failed. Logout Request rejected') + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Request rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True except Exception as err: diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 498272e0..f7fd486a 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -17,6 +17,7 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError class OneLogin_Saml2_Logout_Response(object): @@ -91,7 +92,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if not isinstance(res, Document): - raise Exception('Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd') + raise OneLogin_Saml2_ValidationError( + 'Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd', + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) security = self.__settings.get_security_data() @@ -99,12 +103,18 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'): in_response_to = self.document.documentElement.getAttribute('InResponseTo') if request_id != in_response_to: - raise Exception('The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id)) + raise OneLogin_Saml2_ValidationError( + 'The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id), + OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO + ) # Check issuer issuer = self.get_issuer() if issuer is not None and issuer != idp_entity_id: - raise Exception('Invalid issuer in the Logout Request') + raise OneLogin_Saml2_ValidationError( + 'Invalid issuer in the Logout Request', + OneLogin_Saml2_ValidationError.WRONG_ISSUER + ) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) @@ -113,11 +123,17 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): destination = self.document.documentElement.getAttribute('Destination') if destination != '': if current_url not in destination: - raise Exception('The LogoutRequest was received at $currentURL instead of $destination') + raise OneLogin_Saml2_ValidationError( + 'The LogoutResponse was received at %s instead of %s' % (current_url, destination), + OneLogin_Saml2_ValidationError.WRONG_DESTINATION + ) if security['wantMessagesSigned']: if 'Signature' not in get_data: - raise Exception('The Message of the Logout Response is not signed and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The Message of the Logout Response is not signed and the SP require it', + OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + ) if 'Signature' in get_data: if 'SigAlg' not in get_data: @@ -131,11 +147,17 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding)) if 'x509cert' not in idp_data or not idp_data['x509cert']: - raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') + raise OneLogin_Saml2_Error( + 'In order to validate the sign on the Logout Response, the x509cert of the IdP is required', + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) cert = idp_data['x509cert'] if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg): - raise Exception('Signature validation failed. Logout Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True # pylint: disable=R0801 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index bd72ac74..04522b6d 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -17,6 +17,7 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError class OneLogin_Saml2_Response(object): @@ -61,6 +62,9 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP :type request_id: string + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean + :returns: True if the SAML Response is valid, False if not :rtype: bool """ @@ -68,15 +72,24 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): try: # Checks SAML version if self.document.get('Version', None) != '2.0': - raise Exception('Unsupported SAML version') + raise OneLogin_Saml2_ValidationError( + 'Unsupported SAML version', + OneLogin_Saml2_ValidationError.UNSUPPORTED_SAML_VERSION + ) # Checks that ID exists if self.document.get('ID', None) is None: - raise Exception('Missing ID attribute on SAML Response') + raise OneLogin_Saml2_ValidationError( + 'Missing ID attribute on SAML Response', + OneLogin_Saml2_ValidationError.MISSING_ID + ) # Checks that the response only has one assertion if not self.validate_num_assertions(): - raise Exception('SAML Response must contain 1 assertion') + raise OneLogin_Saml2_ValidationError( + 'SAML Response must contain 1 assertion', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS + ) # Checks that the response has the SUCCESS status self.check_status() @@ -99,7 +112,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): self.__settings.is_debug_active() ) if not isinstance(res, Document): - raise Exception(no_valid_xml_msg) + raise OneLogin_Saml2_ValidationError( + no_valid_xml_msg, + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) # If encrypted, check also the decrypted document if self.encrypted: @@ -109,7 +125,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): self.__settings.is_debug_active() ) if not isinstance(res, Document): - raise Exception(no_valid_xml_msg) + raise OneLogin_Saml2_ValidationError( + no_valid_xml_msg, + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) security = self.__settings.get_security_data() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) @@ -118,35 +137,56 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): in_response_to = self.document.get('InResponseTo', None) if in_response_to is not None and request_id is not None: if in_response_to != request_id: - raise Exception('The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id)) + raise OneLogin_Saml2_ValidationError( + 'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id), + OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO + ) if not self.encrypted and security.get('wantAssertionsEncrypted', False): - raise Exception('The assertion of the Response is not encrypted and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The assertion of the Response is not encrypted and the SP require it', + OneLogin_Saml2_ValidationError.NO_ENCRYPTED_ASSERTION + ) if security.get('wantNameIdEncrypted', False): encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_nameid_nodes) != 1: - raise Exception('The NameID of the Response is not encrypted and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The NameID of the Response is not encrypted and the SP require it', + OneLogin_Saml2_ValidationError.NO_ENCRYPTED_NAMEID + ) # Checks that a Conditions element exists if not self.check_one_condition(): - raise Exception('The Assertion must include a Conditions element') + raise OneLogin_Saml2_ValidationError( + 'The Assertion must include a Conditions element', + OneLogin_Saml2_ValidationError.MISSING_CONDITIONS + ) # Validates Assertion timestamps self.validate_timestamps(raise_exceptions=True) # Checks that an AuthnStatement element exists and is unique if not self.check_one_authnstatement(): - raise Exception('The Assertion must include an AuthnStatement element') + raise OneLogin_Saml2_ValidationError( + 'The Assertion must include an AuthnStatement element', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_AUTHSTATEMENTS + ) # Checks that there is at least one AttributeStatement if required attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement') if security.get('wantAttributeStatement', True) and not attribute_statement_nodes: - raise Exception('There is no AttributeStatement on the Response') + raise OneLogin_Saml2_ValidationError( + 'There is no AttributeStatement on the Response', + OneLogin_Saml2_ValidationError.NO_ATTRIBUTESTATEMENT + ) encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute') if encrypted_attributes_nodes: - raise Exception('There is an EncryptedAttribute in the Response and this SP not support them') + raise OneLogin_Saml2_ValidationError( + 'There is an EncryptedAttribute in the Response and this SP not support them', + OneLogin_Saml2_ValidationError.ENCRYPTED_ATTRIBUTES + ) # Checks destination destination = self.document.get('Destination', None) @@ -156,25 +196,40 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): # request_data # current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data) # if not destination.startswith(current_url_routed): - raise Exception('The response was received at %s instead of %s' % (current_url, destination)) + raise OneLogin_Saml2_ValidationError( + 'The response was received at %s instead of %s' % (current_url, destination), + OneLogin_Saml2_ValidationError.WRONG_DESTINATION + ) elif destination == '': - raise Exception('The response has an empty Destination value') + raise OneLogin_Saml2_ValidationError( + 'The response has an empty Destination value', + OneLogin_Saml2_ValidationError.EMPTY_DESTINATION + ) # Checks audience valid_audiences = self.get_audiences() if valid_audiences and sp_entity_id not in valid_audiences: - raise Exception('%s is not a valid audience for this Response' % sp_entity_id) + raise OneLogin_Saml2_ValidationError( + '%s is not a valid audience for this Response' % sp_entity_id, + OneLogin_Saml2_ValidationError.WRONG_AUDIENCE + ) # Checks the issuers issuers = self.get_issuers() for issuer in issuers: if issuer is None or issuer != idp_entity_id: - raise Exception('Invalid issuer in the Assertion/Response') + raise OneLogin_Saml2_ValidationError( + 'Invalid issuer in the Assertion/Response', + OneLogin_Saml2_ValidationError.WRONG_ISSUER + ) # Checks the session Expiration session_expiration = self.get_session_not_on_or_after() if session_expiration and session_expiration <= OneLogin_Saml2_Utils.now(): - raise Exception('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response') + raise OneLogin_Saml2_ValidationError( + 'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response', + OneLogin_Saml2_ValidationError.SESSION_EXPIRED + ) # Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid any_subject_confirmation = False @@ -208,30 +263,46 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): break if not any_subject_confirmation: - raise Exception('A valid SubjectConfirmation was not found on this Response') + raise OneLogin_Saml2_ValidationError( + 'A valid SubjectConfirmation was not found on this Response', + OneLogin_Saml2_ValidationError.WRONG_SUBJECTCONFIRMATION + ) if security.get('wantAssertionsSigned', False) and not has_signed_assertion: - raise Exception('The Assertion of the Response is not signed and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The Assertion of the Response is not signed and the SP require it', + OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + ) if security.get('wantMessagesSigned', False) and not has_signed_response: - raise Exception('The Message of the Response is not signed and the SP require it') + raise OneLogin_Saml2_ValidationError( + 'The Message of the Response is not signed and the SP require it', + OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION + ) if not signed_elements or (not has_signed_response and not has_signed_assertion): - raise Exception('No Signature found. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'No Signature found. SAML Response rejected', + OneLogin_Saml2_ValidationError.NO_SIGNATURE_FOUND + ) else: cert = idp_data.get('x509cert', None) fingerprint = idp_data.get('certFingerprint', None) fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) # If find a Signature on the Response, validates it checking the original response - if has_signed_response: - # Raise exception if response signature is invalid - OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, raise_exceptions=True) + if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, raise_exceptions=False): + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. SAML Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) document_check_assertion = self.decrypted_document if self.encrypted else self.document - if has_signed_assertion: - # Raise exception if assertion signature is invalid - OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=True) + if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=False): + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. SAML Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True except Exception as err: @@ -258,7 +329,10 @@ def check_status(self): status_msg = status.get('msg', None) if status_msg: status_exception_msg += ' -> ' + status_msg - raise Exception(status_exception_msg) + raise OneLogin_Saml2_ValidationError( + status_exception_msg, + OneLogin_Saml2_ValidationError.STATUS_CODE_IS_NOT_SUCCESS + ) def check_one_condition(self): """ @@ -303,13 +377,19 @@ def get_issuers(self): if len(message_issuer_nodes) == 1: issuers.append(message_issuer_nodes[0].text) else: - raise Exception('Issuer of the Response not found or multiple.') + raise OneLogin_Saml2_ValidationError( + 'Issuer of the Response not found or multiple.', + OneLogin_Saml2_ValidationError.ISSUER_NOT_FOUND_IN_RESPONSE + ) assertion_issuer_nodes = self.__query_assertion('/saml:Issuer') if len(assertion_issuer_nodes) == 1: issuers.append(assertion_issuer_nodes[0].text) else: - raise Exception('Issuer of the Assertion not found or multiple.') + raise OneLogin_Saml2_ValidationError( + 'Issuer of the Assertion not found or multiple.', + OneLogin_Saml2_ValidationError.ISSUER_NOT_FOUND_IN_ASSERTION + ) return list(set(issuers)) @@ -336,10 +416,16 @@ def get_nameid_data(self): security = self.__settings.get_security_data() if security.get('wantNameId', True): - raise Exception('Not NameID found in the assertion of the Response') + raise OneLogin_Saml2_ValidationError( + 'Not NameID found in the assertion of the Response', + OneLogin_Saml2_ValidationError.NO_NAMEID + ) else: if self.__settings.is_strict() and not nameid.text: - raise Exception('An empty NameID value found') + raise OneLogin_Saml2_ValidationError( + 'An empty NameID value found', + OneLogin_Saml2_ValidationError.EMPTY_NAMEID + ) nameid_data = {'Value': nameid.text} for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: @@ -349,7 +435,10 @@ def get_nameid_data(self): sp_data = self.__settings.get_sp_data() sp_entity_id = sp_data.get('entityId', '') if sp_entity_id != value: - raise Exception('The SPNameQualifier value mistmatch the SP entityID value.') + raise OneLogin_Saml2_ValidationError( + 'The SPNameQualifier value mistmatch the SP entityID value.', + OneLogin_Saml2_ValidationError.SP_NAME_QUALIFIER_NAME_MISMATCH + ) nameid_data[attr] = value return nameid_data @@ -407,7 +496,10 @@ def get_attributes(self): for attribute_node in attribute_nodes: attr_name = attribute_node.get('Name') if attr_name in attributes.keys(): - raise Exception('Found an Attribute element with duplicated Name') + raise OneLogin_Saml2_ValidationError( + 'Found an Attribute element with duplicated Name', + OneLogin_Saml2_ValidationError.DUPLICATED_ATTRIBUTE_NAME_FOUND + ) values = [] for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP['saml']): @@ -469,14 +561,23 @@ def process_signed_elements(self): for sign_node in sign_nodes: signed_element = sign_node.getparent().tag if signed_element != response_tag and signed_element != assertion_tag: - raise Exception('Invalid Signature Element %s SAML Response rejected' % signed_element) + raise OneLogin_Saml2_ValidationError( + 'Invalid Signature Element %s SAML Response rejected' % signed_element, + OneLogin_Saml2_ValidationError.WRONG_SIGNED_ELEMENT + ) if not sign_node.getparent().get('ID'): - raise Exception('Signed Element must contain an ID. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Signed Element must contain an ID. SAML Response rejected', + OneLogin_Saml2_ValidationError.ID_NOT_FOUND_IN_SIGNED_ELEMENT + ) id_value = sign_node.getparent().get('ID') if id_value in verified_ids: - raise Exception('Duplicated ID. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Duplicated ID. SAML Response rejected', + OneLogin_Saml2_ValidationError.DUPLICATED_ID_IN_SIGNED_ELEMENTS + ) verified_ids.append(id_value) # Check that reference URI matches the parent ID and no duplicate References or IDs @@ -487,17 +588,26 @@ def process_signed_elements(self): sei = ref.get('URI')[1:] if sei != id_value: - raise Exception('Found an invalid Signed Element. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Found an invalid Signed Element. SAML Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNED_ELEMENT + ) if sei in verified_seis: - raise Exception('Duplicated Reference URI. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Duplicated Reference URI. SAML Response rejected', + OneLogin_Saml2_ValidationError.DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS + ) verified_seis.append(sei) signed_elements.append(signed_element) if signed_elements: if not self.validate_signed_elements(signed_elements, raise_exceptions=True): - raise Exception('Found an unexpected Signature Element. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Found an unexpected Signature Element. SAML Response rejected', + OneLogin_Saml2_ValidationError.UNEXPECTED_SIGNED_ELEMENT + ) return signed_elements @return_false_on_exception @@ -527,12 +637,18 @@ def validate_signed_elements(self, signed_elements): if response_tag in signed_elements: expected_signature_nodes = OneLogin_Saml2_Utils.query(self.document, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH) if len(expected_signature_nodes) != 1: - raise Exception('Unexpected number of Response signatures found. SAML Response rejected.') + raise OneLogin_Saml2_ValidationError( + 'Unexpected number of Response signatures found. SAML Response rejected.', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE + ) if assertion_tag in signed_elements: expected_signature_nodes = self.__query(OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH) if len(expected_signature_nodes) != 1: - raise Exception('Unexpected number of Assertion signatures found. SAML Response rejected.') + raise OneLogin_Saml2_ValidationError( + 'Unexpected number of Assertion signatures found. SAML Response rejected.', + OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION + ) return True @@ -553,9 +669,15 @@ def validate_timestamps(self): nb_attr = conditions_node.get('NotBefore') nooa_attr = conditions_node.get('NotOnOrAfter') if nb_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nb_attr) > OneLogin_Saml2_Utils.now() + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT: - raise Exception('Could not validate timestamp: not yet valid. Check system clock.') + raise OneLogin_Saml2_ValidationError( + 'Could not validate timestamp: not yet valid. Check system clock.', + OneLogin_Saml2_ValidationError.ASSERTION_TOO_EARLY + ) if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT <= OneLogin_Saml2_Utils.now(): - raise Exception('Could not validate timestamp: expired. Check system clock.') + raise OneLogin_Saml2_ValidationError( + 'Could not validate timestamp: expired. Check system clock.', + OneLogin_Saml2_ValidationError.ASSERTION_EXPIRED + ) return True def __query_assertion(self, xpath_expr): @@ -621,7 +743,10 @@ def __decrypt_assertion(self, dom): debug = self.__settings.is_debug_active() if not key: - raise Exception('No private key available, check settings') + raise OneLogin_Saml2_Error( + 'No private key available to decrypt the assertion, check settings', + OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND + ) encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/saml:EncryptedAssertion') if encrypted_assertion_nodes: @@ -629,15 +754,24 @@ def __decrypt_assertion(self, dom): if encrypted_data_nodes: keyinfo = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData/ds:KeyInfo') if not keyinfo: - raise Exception('No KeyInfo present, invalid Assertion') + raise OneLogin_Saml2_ValidationError( + 'No KeyInfo present, invalid Assertion', + OneLogin_Saml2_ValidationError.KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA + ) keyinfo = keyinfo[0] children = keyinfo.getchildren() if not children: - raise Exception('No child to KeyInfo, invalid Assertion') + raise OneLogin_Saml2_ValidationError( + 'KeyInfo has no children nodes, invalid Assertion', + OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOIND_IN_KEYINFO + ) for child in children: if 'RetrievalMethod' in child.tag: if child.attrib['Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey': - raise Exception('Unsupported Retrieval Method found') + raise OneLogin_Saml2_ValidationError( + 'Unsupported Retrieval Method found', + OneLogin_Saml2_ValidationError.UNSUPPORTED_RETRIEVAL_METHOD + ) uri = child.attrib['URI'] if not uri.startswith('#'): break diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 8e3b151a..b596e493 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -104,7 +104,10 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals ','.join(self.__errors) ) else: - raise Exception('Unsupported settings object') + raise OneLogin_Saml2_Error( + 'Unsupported settings object', + OneLogin_Saml2_Error.UNSUPPORTED_SETTINGS_OBJECT + ) self.format_idp_cert() self.format_sp_cert() diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 16363a4d..ddde9d60 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -33,7 +33,8 @@ from dm.xmlsec.binding.tmpl import EncData, Signature from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.errors import OneLogin_Saml2_Error +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError + if not globals().get('xmlsec_setup', False): xmlsec.initialize() @@ -652,8 +653,10 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None): xml = name_id_container.toxml() elem = fromstring(xml) + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) # Load the public cert mngr = xmlsec.KeysMngr() @@ -717,11 +720,17 @@ def get_status(dom): status_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status') if len(status_entry) != 1: - raise Exception('Missing valid Status on response') + raise OneLogin_Saml2_ValidationError( + 'Missing Status on response', + OneLogin_Saml2_ValidationError.MISSING_STATUS + ) code_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode', status_entry[0]) if len(code_entry) != 1: - raise Exception('Missing valid Status Code on response') + raise OneLogin_Saml2_ValidationError( + 'Missing Status Code on response', + OneLogin_Saml2_ValidationError.MISSING_STATUS_CODE + ) code = code_entry[0].values()[0] status['code'] = code @@ -758,8 +767,10 @@ def decrypt_element(encrypted_data, key, debug=False): elif isinstance(encrypted_data, basestring): encrypted_data = fromstring(str(encrypted_data)) + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) mngr = xmlsec.KeysMngr() @@ -831,8 +842,10 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant else: raise Exception('Error parsing xml string') + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) # Sign the metadata with our private key. sign_algorithm_transform_map = { @@ -941,8 +954,10 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid else: raise Exception('Error parsing xml string') + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) xmlsec.addIDs(elem, ["ID"]) @@ -959,7 +974,7 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True) else: - raise Exception('Expected exactly one signature node; got {}.'.format(len(signature_nodes))) + raise OneLogin_Saml2_ValidationError('Expected exactly one signature node; got {}.'.format(len(signature_nodes)), OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES) @staticmethod @return_false_on_exception @@ -1008,8 +1023,10 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha else: raise Exception('Error parsing xml string') + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) xmlsec.addIDs(elem, ["ID"]) @@ -1059,8 +1076,10 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean """ + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) xmlsec.addIDs(elem, ["ID"]) @@ -1080,7 +1099,10 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger # reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID')) if cert is None or cert == '': - raise Exception('Could not validate node signature: No certificate provided.') + raise OneLogin_Saml2_Error( + 'Could not validate node signature: No certificate provided.', + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) @@ -1095,10 +1117,9 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger file_cert.close() dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) - try: - dsig_ctx.verify(signature_node) - except Exception: - raise Exception('Signature validation failed. SAML Response rejected') + + dsig_ctx.verify(signature_node) + return True @staticmethod @@ -1126,8 +1147,10 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ :param raise_exceptions: Whether to return false on failure or raise an exception :type raise_exceptions: Boolean """ + error_callback_method = None if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) dsig_ctx = xmlsec.DSigCtx() diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index cdd996e4..d7e50949 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -16,6 +16,7 @@ from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request +from onelogin.saml2.errors import OneLogin_Saml2_Error class OneLogin_Saml2_Auth_Test(unittest.TestCase): @@ -142,7 +143,7 @@ def testProcessNoResponse(self): """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) - with self.assertRaisesRegexp(Exception, 'SAML Response not found'): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML Response not found'): auth.process_response() self.assertEqual(auth.get_errors(), ['invalid_binding']) @@ -257,7 +258,7 @@ def testProcessNoSLO(self): Case No Message, An exception is throw """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) - with self.assertRaisesRegexp(Exception, 'SAML LogoutRequest/LogoutResponse not found'): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML LogoutRequest/LogoutResponse not found'): auth.process_slo(True) def testProcessSLOResponseInvalid(self): @@ -783,7 +784,7 @@ def testLogoutNoSLO(self): del settings_info['idp']['singleLogoutService'] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) - with self.assertRaisesRegexp(Exception, 'The IdP does not support Single Log Out'): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'The IdP does not support Single Log Out'): # The Header of the redirect produces an Exception auth.logout('http://example.com/returnto') @@ -872,7 +873,7 @@ def testBuildRequestSignature(self): settings['sp']['privatekey'] = '' settings['custom_base_path'] = u'invalid/path/' auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) - with self.assertRaisesRegexp(Exception, "Trying to sign the SAMLRequest but can't load the SP private key"): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLRequest but can't load the SP private key"): auth2.build_request_signature(message, relay_state) def testBuildResponseSignature(self): @@ -891,7 +892,7 @@ def testBuildResponseSignature(self): settings['sp']['privatekey'] = '' settings['custom_base_path'] = u'invalid/path/' auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) - with self.assertRaisesRegexp(Exception, "Trying to sign the SAMLResponse but can't load the SP private key"): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLResponse but can't load the SP private key"): auth2.build_response_signature(message, relay_state) def testGetLastSAMLResponse(self): diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index cbdfdb33..8c484b94 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -15,6 +15,7 @@ from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError class OneLogin_Saml2_Logout_Request_Test(unittest.TestCase): @@ -113,7 +114,7 @@ def testGetNameIdData(self): self.assertEqual(expected_name_id_data, name_id_data_2) request_2 = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_encrypted_nameid.xml')) - with self.assertRaisesRegexp(Exception, 'Key is required in order to decrypt the NameID'): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Key is required in order to decrypt the NameID'): OneLogin_Saml2_Logout_Request.get_nameid_data(request_2) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) @@ -130,11 +131,11 @@ def testGetNameIdData(self): encrypted_id_nodes = dom_2.getElementsByTagName('saml:EncryptedID') encrypted_data = encrypted_id_nodes[0].firstChild.nextSibling encrypted_id_nodes[0].removeChild(encrypted_data) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the Logout Request'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key) inv_request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'no_nameId.xml')) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the Logout Request'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid_data(inv_request) def testGetNameId(self): @@ -146,7 +147,7 @@ def testGetNameId(self): self.assertEqual(name_id, 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c') request_2 = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_encrypted_nameid.xml')) - with self.assertRaisesRegexp(Exception, 'Key is required in order to decrypt the NameID'): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Key is required in order to decrypt the NameID'): OneLogin_Saml2_Logout_Request.get_nameid(request_2) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) @@ -325,7 +326,7 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): self.assertFalse(logout_request.is_valid(request_data)) - with self.assertRaisesRegexp(Exception, "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd"): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd"): logout_request.is_valid(request_data, raise_exceptions=True) def testIsValidSign(self): diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index f0a1553b..3f86df19 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -15,6 +15,7 @@ from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.errors import OneLogin_Saml2_ValidationError class OneLogin_Saml2_Logout_Response_Test(unittest.TestCase): @@ -211,7 +212,7 @@ def testIsInValidDestination(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) self.assertFalse(response_2.is_valid(request_data)) - self.assertIn('The LogoutRequest was received at', response_2.get_error()) + self.assertIn('The LogoutResponse was received at', response_2.get_error()) # Empty destination dom = parseString(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) @@ -242,7 +243,7 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): self.assertFalse(response.is_valid(request_data)) - with self.assertRaisesRegexp(Exception, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd"): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd"): response.is_valid(request_data, raise_exceptions=True) def testIsInValidSign(self): @@ -356,7 +357,7 @@ def testIsValid(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) self.assertFalse(response_2.is_valid(request_data)) - self.assertIn('The LogoutRequest was received at', response_2.get_error()) + self.assertIn('The LogoutResponse was received at', response_2.get_error()) plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 2bd2aa7c..2803cd3e 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -17,6 +17,7 @@ from onelogin.saml2.response import OneLogin_Saml2_Response from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.errors import OneLogin_Saml2_ValidationError class OneLogin_Saml2_Response_Test(unittest.TestCase): @@ -99,14 +100,14 @@ def testReturnNameId(self): xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): response_4.get_nameid() json_settings['security']['wantNameId'] = True settings = OneLogin_Saml2_Settings(json_settings) response_5 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): response_5.get_nameid() json_settings['security']['wantNameId'] = False @@ -120,7 +121,7 @@ def testReturnNameId(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): response_7.get_nameid() json_settings['strict'] = True @@ -128,12 +129,12 @@ def testReturnNameId(self): xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64')) response_8 = OneLogin_Saml2_Response(settings, xml_5) - with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value'): response_8.get_nameid() xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) response_9 = OneLogin_Saml2_Response(settings, xml_6) - with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): response_9.get_nameid() def testGetNameIdData(self): @@ -174,14 +175,14 @@ def testGetNameIdData(self): xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): response_4.get_nameid_data() json_settings['security']['wantNameId'] = True settings = OneLogin_Saml2_Settings(json_settings) response_5 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): response_5.get_nameid_data() json_settings['security']['wantNameId'] = False @@ -195,7 +196,7 @@ def testGetNameIdData(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): response_7.get_nameid_data() json_settings['strict'] = True @@ -203,13 +204,13 @@ def testGetNameIdData(self): xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64')) response_8 = OneLogin_Saml2_Response(settings, xml_5) - with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value.'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value.'): response_8.get_nameid_data() self.assertTrue(False) xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64')) response_9 = OneLogin_Saml2_Response(settings, xml_6) - with self.assertRaisesRegexp(Exception, 'An empty NameID value found'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'): response_9.get_nameid_data() def testCheckStatus(self): @@ -227,12 +228,12 @@ def testCheckStatus(self): xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64')) response_2 = OneLogin_Saml2_Response(settings, xml_2) - with self.assertRaisesRegexp(Exception, 'The status code of the Response was not Success, was Responder'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder'): response_2.check_status() xml_3 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responer_and_msg.xml.base64')) response_3 = OneLogin_Saml2_Response(settings, xml_3) - with self.assertRaisesRegexp(Exception, 'The status code of the Response was not Success, was Responder -> something_is_wrong'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder -> something_is_wrong'): response_3.check_status() def testCheckOneCondition(self): @@ -343,12 +344,12 @@ def testGetIssuers(self): xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_response.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(Exception, 'Issuer of the Response not found or multiple.'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Issuer of the Response not found or multiple.'): response_4.get_issuers() xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_assertion.xml.base64')) response_5 = OneLogin_Saml2_Response(settings, xml_5) - with self.assertRaisesRegexp(Exception, 'Issuer of the Assertion not found or multiple.'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Issuer of the Assertion not found or multiple.'): response_5.get_issuers() def testGetSessionIndex(self): @@ -692,7 +693,7 @@ def testIsInValidDuplicatedAttrs(self): xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'duplicated_attributes.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) self.assertTrue(response.is_valid(self.get_request_data())) - with self.assertRaisesRegexp(Exception, 'Found an Attribute element with duplicated Name'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Found an Attribute element with duplicated Name'): response.get_attributes() def testIsInValidDestination(self): @@ -1105,9 +1106,8 @@ def testIsInValidCert(self): settings = OneLogin_Saml2_Settings(settings_info) xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - self.assertFalse(response.is_valid(self.get_request_data())) - self.assertIn('failed to load key from file', response.get_error()) + self.assertIn('Signature validation failed. SAML Response rejected', response.get_error()) def testIsInValidCert2(self): """ @@ -1236,7 +1236,7 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self): self.assertFalse(response.is_valid(self.get_request_data())) - with self.assertRaisesRegexp(Exception, "Unsupported SAML version"): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Unsupported SAML version"): response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsValidSign(self): diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 7498096f..63439f68 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -15,6 +15,7 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError class OneLogin_Saml2_Utils_Test(unittest.TestCase): @@ -426,14 +427,14 @@ def testGetStatus(self): xml_inv = b64decode(xml_inv) dom_inv = etree.fromstring(xml_inv) - with self.assertRaisesRegexp(Exception, 'Missing valid Status on response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Missing Status on response'): OneLogin_Saml2_Utils.get_status(dom_inv) xml_inv2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_status_code.xml.base64')) xml_inv2 = b64decode(xml_inv2) dom_inv2 = etree.fromstring(xml_inv2) - with self.assertRaisesRegexp(Exception, 'Missing valid Status Code on response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Missing Status Code on response'): OneLogin_Saml2_Utils.get_status(dom_inv2) def testParseDuration(self): @@ -837,7 +838,7 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True, raise_exceptions=True) xml_metadata_signed_2 = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings2.xml')) @@ -850,7 +851,7 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True, raise_exceptions=True) # modified cert @@ -860,9 +861,9 @@ def testValidateSign(self): f.close() self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x)) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('signature verification failed', 2)"): OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, raise_exceptions=True) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True, raise_exceptions=True) xml_response_msg_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_message_response2.xml.base64'))) @@ -877,7 +878,7 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True, raise_exceptions=True) xml_response_assert_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response2.xml.base64'))) @@ -890,7 +891,7 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert)) # expired cert, verified it self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True, raise_exceptions=True) xml_response_double_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'double_signed_response2.xml.base64'))) @@ -905,13 +906,13 @@ def testValidateSign(self): dom.firstChild.getAttributeNode('ID').nodeValue = u'_34fg27g212d63k1f923845324475802ac0fc24530b' # Reference validation failed self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom, cert_2)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(dom, cert_2, raise_exceptions=True) invalid_fingerprint = 'afe71c34ef740bc87434be13a2263d31271da1f9' # Wrong fingerprint self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Could not validate node signature: No certificate provided.'): OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint, raise_exceptions=True) dom_2 = parseString(xml_response_double_signed_2) @@ -919,31 +920,31 @@ def testValidateSign(self): dom_2.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp' # Modified message self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('signature verification failed', 2)"): OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2, raise_exceptions=True) # Try to validate directly the Assertion dom_3 = parseString(xml_response_double_signed_2) assert_elem_3 = dom_3.firstChild.firstChild.nextSibling.nextSibling.nextSibling self.assertFalse(OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."): OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2, raise_exceptions=True) # Wrong scheme no_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_signature.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_signed, cert)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."): OneLogin_Saml2_Utils.validate_sign(no_signed, cert, raise_exceptions=True) no_key = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_key, cert)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"): OneLogin_Saml2_Utils.validate_sign(no_key, cert, raise_exceptions=True) # Signature Wrapping attack wrapping_attack1 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'signature_wrapping_attack.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert)) - with self.assertRaises(Exception): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."): OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert, raise_exceptions=True) From a0b20152b88687e079030354a5d7334fbbcd81a0 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 22 Dec 2016 09:54:21 +0100 Subject: [PATCH 04/10] Fix pep8 --- src/onelogin/saml2/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 5a859fc1..ceb2730e 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -125,4 +125,4 @@ def __init__(self, message, code=0, errors=None): message = message % errors Exception.__init__(self, message) - self.code = code \ No newline at end of file + self.code = code From 731d01f3dfdbf5631af5529b4d775a93045ba96f Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 30 Dec 2016 00:58:13 +0100 Subject: [PATCH 05/10] Minor test fixes --- src/onelogin/saml2/errors.py | 1 + src/onelogin/saml2/logout_request.py | 8 ++++++-- tests/src/OneLogin/saml2_tests/response_test.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index ceb2730e..10c43d11 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -109,6 +109,7 @@ class OneLogin_Saml2_ValidationError(Exception): DUPLICATED_ATTRIBUTE_NAME_FOUND = 41 INVALID_SIGNATURE = 42 WRONG_NUMBER_OF_SIGNATURES = 43 + RESPONSE_EXPIRED = 44 def __init__(self, message, code=0, errors=None): """ diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 785c7c64..c49995e3 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -309,7 +309,10 @@ def is_valid(self, request_data, raise_exceptions=False): if dom.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): - raise Exception('Could not validate timestamp: expired. Check system clock.') + raise OneLogin_Saml2_ValidationError( + 'Could not validate timestamp: expired. Check system clock.', + OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED + ) # Check destination if dom.get('Destination', None): @@ -322,7 +325,8 @@ def is_valid(self, request_data, raise_exceptions=False): { 'currentURL': current_url, 'destination': destination, - } + }, + OneLogin_Saml2_ValidationError.WRONG_DESTINATION ) # Check issuer diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 2803cd3e..ce940984 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -61,7 +61,7 @@ def testConstruct(self): self.assertIsInstance(response_enc, OneLogin_Saml2_Response) - def test_get_xml_document(self): + def testGetXMLDocument(self): """ Tests that we can retrieve the raw text of an encrypted XML response without going through intermediate steps From 3b94b705cb2840a0faa22c3e4bd015d58a043eaf Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 30 Dec 2016 17:04:52 +0100 Subject: [PATCH 06/10] Some minor improvements based on suggestions --- src/onelogin/saml2/errors.py | 2 ++ src/onelogin/saml2/logout_request.py | 2 +- src/onelogin/saml2/response.py | 2 +- src/onelogin/saml2/utils.py | 9 ++++++++- .../src/OneLogin/saml2_tests/logout_request_test.py | 4 ++-- tests/src/OneLogin/saml2_tests/response_test.py | 12 ++++++------ 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 10c43d11..cd5e2fdc 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -25,6 +25,8 @@ class OneLogin_Saml2_Error(Exception): SETTINGS_INVALID_SYNTAX = 1 SETTINGS_INVALID = 2 METADATA_SP_INVALID = 3 + # SP_CERTS_NOT_FOUND is deprecated, use CERT_NOT_FOUND instead + SP_CERTS_NOT_FOUND = 4 CERT_NOT_FOUND = 4 REDIRECT_INVALID_URL = 5 PUBLIC_CERT_FILE_NOT_FOUND = 6 diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index c49995e3..93eabbf3 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -196,7 +196,7 @@ def get_nameid_data(request, key=None): if name_id is None: raise OneLogin_Saml2_ValidationError( - 'Not NameID found in the Logout Request', + 'NameID not found in the Logout Request', OneLogin_Saml2_ValidationError.NO_NAMEID ) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 04522b6d..70c4bdb5 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -417,7 +417,7 @@ def get_nameid_data(self): if security.get('wantNameId', True): raise OneLogin_Saml2_ValidationError( - 'Not NameID found in the assertion of the Response', + 'NameID not found in the assertion of the Response', OneLogin_Saml2_ValidationError.NO_NAMEID ) else: diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index ddde9d60..6fa42e2d 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -1118,7 +1118,14 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) - dsig_ctx.verify(signature_node) + try: + dsig_ctx.verify(signature_node) + except Exception as err: + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. SAML Response rejected. %s', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE, + err.__str__() + ) return True diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 8c484b94..b58404cc 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -131,11 +131,11 @@ def testGetNameIdData(self): encrypted_id_nodes = dom_2.getElementsByTagName('saml:EncryptedID') encrypted_data = encrypted_id_nodes[0].firstChild.nextSibling encrypted_id_nodes[0].removeChild(encrypted_data) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key) inv_request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'no_nameId.xml')) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'): OneLogin_Saml2_Logout_Request.get_nameid_data(inv_request) def testGetNameId(self): diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index ce940984..593bb12b 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -100,14 +100,14 @@ def testReturnNameId(self): xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): response_4.get_nameid() json_settings['security']['wantNameId'] = True settings = OneLogin_Saml2_Settings(json_settings) response_5 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): response_5.get_nameid() json_settings['security']['wantNameId'] = False @@ -121,7 +121,7 @@ def testReturnNameId(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): response_7.get_nameid() json_settings['strict'] = True @@ -175,14 +175,14 @@ def testGetNameIdData(self): xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): response_4.get_nameid_data() json_settings['security']['wantNameId'] = True settings = OneLogin_Saml2_Settings(json_settings) response_5 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): response_5.get_nameid_data() json_settings['security']['wantNameId'] = False @@ -196,7 +196,7 @@ def testGetNameIdData(self): settings = OneLogin_Saml2_Settings(json_settings) response_7 = OneLogin_Saml2_Response(settings, xml_4) - with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'): + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'): response_7.get_nameid_data() json_settings['strict'] = True From cd8ea02ceadf3df1d91d42b2d58441611ca31e55 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 31 Dec 2016 10:00:29 +0100 Subject: [PATCH 07/10] Fix typo --- src/onelogin/saml2/errors.py | 2 +- src/onelogin/saml2/response.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index cd5e2fdc..532844fe 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -103,7 +103,7 @@ class OneLogin_Saml2_ValidationError(Exception): NO_SIGNED_ASSERTION = 33 NO_SIGNATURE_FOUND = 34 KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35 - CHILDREN_NODE_NOT_FOIND_IN_KEYINFO = 36 + CHILDREN_NODE_NOT_FOUND_IN_KEYINFO = 36 UNSUPPORTED_RETRIEVAL_METHOD = 37 NO_NAMEID = 38 EMPTY_NAMEID = 39 diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 70c4bdb5..e1b44f0f 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -763,7 +763,7 @@ def __decrypt_assertion(self, dom): if not children: raise OneLogin_Saml2_ValidationError( 'KeyInfo has no children nodes, invalid Assertion', - OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOIND_IN_KEYINFO + OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOUND_IN_KEYINFO ) for child in children: if 'RetrievalMethod' in child.tag: From c3e696e013885714ea25e6f830129fc434b7dc44 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Mon, 2 Jan 2017 17:22:38 +0100 Subject: [PATCH 08/10] Minor fix on docs --- README.md | 7 +++++-- src/onelogin/saml2/response.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bb1d9e5a..cecfc25b 100644 --- a/README.md +++ b/README.md @@ -833,7 +833,7 @@ Main class of OneLogin Python Toolkit * ***get_settings*** Returns the settings info. * ***set_strict*** Set the strict mode active/disable. * ***get_last_request_xml*** Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest) -* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse was encrypted, by default tries to return the decrypted XML. +* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse had an encrypted assertion, decrypts it. ####OneLogin_Saml2_Auth - authn_request.py#### @@ -842,7 +842,7 @@ SAML 2 Authentication Request class * `__init__` This class handles an AuthNRequest. It builds an AuthNRequest object. * ***get_request*** Returns unsigned AuthnRequest. * ***get_id*** Returns the AuthNRequest ID. - +* ***get_xml*** Returns the XML that will be sent as part of the request. ####OneLogin_Saml2_Response - response.py#### @@ -861,6 +861,7 @@ SAML 2 Authentication Response class * ***validate_num_assertions*** Verifies that the document only contains a single Assertion (encrypted or not) * ***validate_timestamps*** Verifies that the document is valid according to Conditions Element * ***get_error*** After execute a validation process, if fails this method returns the cause +* ***get_xml_document*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it). ####OneLogin_Saml2_LogoutRequest - logout_request.py#### @@ -875,6 +876,7 @@ SAML 2 Logout Request class * ***get_session_indexes*** Gets the SessionIndexes from the Logout Request. * ***is_valid*** Checks if the Logout Request recieved is valid. * ***get_error*** After execute a validation process, if fails this method returns the cause. +* ***get_xml*** Returns the XML that will be sent as part of the request or that was received at the SP ####OneLogin_Saml2_LogoutResponse - logout_response.py#### @@ -887,6 +889,7 @@ SAML 2 Logout Response class * ***build*** Creates a Logout Response object. * ***get_response*** Returns a Logout Response object. * ***get_error*** After execute a validation process, if fails this method returns the cause. +* ***get_xml*** Returns the XML that will be sent as part of the response or that was received at the SP ####OneLogin_Saml2_Settings - settings.py#### diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index e1b44f0f..69346bf1 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -793,10 +793,10 @@ def get_error(self): def get_xml_document(self): """ - If necessary, decrypt the XML response document, and return it. + Returns the SAML Response document (If contains an encrypted assertion, decrypts it) :return: Decrypted XML response document - :rtype: string + :rtype: DOMDocument """ if self.encrypted: return self.decrypted_document From 667b132ca30df5672ac25e15d560a5a708b6074e Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 3 Jan 2017 17:10:01 +0100 Subject: [PATCH 09/10] Fix name --- src/onelogin/saml2/errors.py | 2 +- src/onelogin/saml2/logout_request.py | 2 +- src/onelogin/saml2/logout_response.py | 2 +- src/onelogin/saml2/response.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 532844fe..b85016a6 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -99,7 +99,7 @@ class OneLogin_Saml2_ValidationError(Exception): WRONG_ISSUER = 29 SESSION_EXPIRED = 30 WRONG_SUBJECTCONFIRMATION = 31 - NO_SIGNED_RESPONSE = 32 + NO_SIGNED_MESSAGE = 32 NO_SIGNED_ASSERTION = 33 NO_SIGNATURE_FOUND = 34 KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35 diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 93eabbf3..8dea50ef 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -341,7 +341,7 @@ def is_valid(self, request_data, raise_exceptions=False): if 'Signature' not in get_data: raise OneLogin_Saml2_ValidationError( 'The Message of the Logout Request is not signed and the SP require it', - OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE ) if 'Signature' in get_data: diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index f7fd486a..2653b210 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -132,7 +132,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if 'Signature' not in get_data: raise OneLogin_Saml2_ValidationError( 'The Message of the Logout Response is not signed and the SP require it', - OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE ) if 'Signature' in get_data: diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 69346bf1..a9843dc6 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -271,7 +271,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if security.get('wantAssertionsSigned', False) and not has_signed_assertion: raise OneLogin_Saml2_ValidationError( 'The Assertion of the Response is not signed and the SP require it', - OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE ) if security.get('wantMessagesSigned', False) and not has_signed_response: From 74305d53c6cd92e0ffd64289111b7e0dd98a0a03 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 3 Jan 2017 22:23:48 +0100 Subject: [PATCH 10/10] Typo --- src/onelogin/saml2/response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index a9843dc6..051e1ccf 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -271,13 +271,13 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if security.get('wantAssertionsSigned', False) and not has_signed_assertion: raise OneLogin_Saml2_ValidationError( 'The Assertion of the Response is not signed and the SP require it', - OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE + OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION ) if security.get('wantMessagesSigned', False) and not has_signed_response: raise OneLogin_Saml2_ValidationError( 'The Message of the Response is not signed and the SP require it', - OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION + OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE ) if not signed_elements or (not has_signed_response and not has_signed_assertion):