From 9ea32f567508a532101a9888b199062f105b77c6 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Fri, 30 Dec 2016 00:16:43 +0100 Subject: [PATCH] Implement a more specific exception class for handling some validation errors. Improve tests --- src/onelogin/saml2/auth.py | 50 ++-- src/onelogin/saml2/errors.py | 74 +++++- src/onelogin/saml2/logout_request.py | 38 ++- src/onelogin/saml2/logout_response.py | 27 +- src/onelogin/saml2/response.py | 231 ++++++++++++++---- src/onelogin/saml2/settings.py | 5 +- src/onelogin/saml2/utils.py | 28 ++- tests/src/OneLogin/saml2_tests/auth_test.py | 22 +- .../saml2_tests/logout_response_test.py | 13 +- .../src/OneLogin/saml2_tests/response_test.py | 20 +- tests/src/OneLogin/saml2_tests/utils_test.py | 8 +- 11 files changed, 384 insertions(+), 132 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 1e470b32..4b855119 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -16,10 +16,9 @@ from onelogin.saml2 import compat from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.response import OneLogin_Saml2_Response -from onelogin.saml2.errors import OneLogin_Saml2_Error from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request @@ -429,7 +428,7 @@ def __build_signature(self, data, saml_type, sign_algorithm=OneLogin_Saml2_Const 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 ) msg = self.__build_sign_query(data[saml_type], @@ -472,7 +471,7 @@ def validate_response_signature(self, request_data): return self.__validate_signature(request_data, 'SAMLResponse') - def __validate_signature(self, data, saml_type): + def __validate_signature(self, data, saml_type, raise_exceptions=False): """ Validate Signature @@ -484,22 +483,30 @@ def __validate_signature(self, data, saml_type): :param saml_type: The target URL the user should be redirected to :type saml_type: string SAMLRequest | SAMLResponse - """ - - signature = data.get('Signature', None) - if signature is None: - if self.__settings.is_strict() and self.__settings.get_security_data().get('wantMessagesSigned', False): - self.__error_reason = 'The %s is not signed. Rejected.' % saml_type - return False - return True - - x509cert = self.get_settings().get_idp_cert() - - if x509cert is None: - self.__errors.append("In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type) - return False + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean + """ try: + signature = data.get('Signature', None) + if signature is None: + if self.__settings.is_strict() and self.__settings.get_security_data().get('wantMessagesSigned', False): + raise OneLogin_Saml2_ValidationError( + 'The %s is not signed. Rejected.' % saml_type, + OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE + ) + return True + + x509cert = self.get_settings().get_idp_cert() + + if not x509cert: + error_msg = "In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type + self.__errors.append(error_msg) + raise OneLogin_Saml2_Error( + error_msg, + OneLogin_Saml2_Error.CERT_NOT_FOUND + ) + sign_alg = data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) if isinstance(sign_alg, bytes): sign_alg = sign_alg.decode('utf8') @@ -520,8 +527,13 @@ def __validate_signature(self, data, saml_type): x509cert, sign_alg, self.__settings.is_debug_active()): - raise Exception('Signature validation failed. %s rejected.' % saml_type) + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. %s rejected.' % saml_type, + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True except Exception as e: self.__error_reason = str(e) + if raise_exceptions: + raise e return False diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index c28a23bd..0f84a09b 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): """ @@ -50,3 +52,73 @@ 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 + RESPONSE_EXPIRED = 44 + + 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(code, int) + + if errors is not None: + message = message % errors + + Exception.__init__(self, message) + self.code = code diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index c4061a71..93a57bb2 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -10,7 +10,7 @@ """ from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception +from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -141,7 +141,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_XML.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_data_nodes) == 1: @@ -153,7 +156,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 @@ -236,7 +242,10 @@ def is_valid(self, request_data, raise_exceptions=False): if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml(root, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): - 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() @@ -246,30 +255,41 @@ def is_valid(self, request_data, raise_exceptions=False): if root.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.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 root.get('Destination', None): destination = root.get('Destination') if destination != '': if current_url not in destination: - raise Exception( + raise OneLogin_Saml2_ValidationError( 'The LogoutRequest was received at ' '%(currentURL)s instead of %(destination)s' % { 'currentURL': current_url, 'destination': destination, - } + }, + OneLogin_Saml2_ValidationError.WRONG_DESTINATION ) # Check issuer issuer = OneLogin_Saml2_Logout_Request.get_issuer(root) 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 + ) + return True except Exception as err: # pylint: disable=R0801 diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index a9cdba8d..18152651 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -9,7 +9,7 @@ """ -from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_ValidationError from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -84,30 +84,45 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): if self.__settings.is_strict(): res = OneLogin_Saml2_XML.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): - 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() # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided in_response_to = self.document.get('InResponseTo', None) if request_id is not None and in_response_to and in_response_to != request_id: - 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) # Check destination destination = self.document.get('Destination', None) if destination and 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 + ) return True # pylint: disable=R0801 except Exception as err: diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index db739ffe..b8f233aa 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -11,7 +11,7 @@ from copy import deepcopy from onelogin.saml2.constants import OneLogin_Saml2_Constants -from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception +from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError, return_false_on_exception from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -67,15 +67,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() @@ -94,13 +103,19 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd' res = OneLogin_Saml2_XML.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): - 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: res = OneLogin_Saml2_XML.validate_xml(self.decrypted_document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) if isinstance(res, str): - 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) @@ -109,35 +124,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['wantAssertionsEncrypted']: - 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['wantNameIdEncrypted']: 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) @@ -147,25 +183,39 @@ 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 @@ -199,16 +249,28 @@ 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['wantAssertionsSigned'] 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['wantMessagesSigned'] 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) @@ -216,11 +278,17 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False): # If find a Signature on the Response, validates it checking the original response 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 Exception('Signature validation failed. SAML Response rejected') + 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 and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=False): - raise Exception('Signature validation failed. SAML Response rejected') + raise OneLogin_Saml2_ValidationError( + 'Signature validation failed. SAML Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) return True except Exception as err: @@ -247,7 +315,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): """ @@ -292,13 +363,19 @@ def get_issuers(self): if len(message_issuer_nodes) == 1: issuers.add(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.add(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)) @@ -324,10 +401,16 @@ def get_nameid_data(self): if nameid is None: 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']: @@ -337,7 +420,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 @@ -395,7 +481,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']): @@ -441,14 +530,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 @@ -459,22 +557,37 @@ 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 Exception('Found an unexpected Signature Element. SAML Response rejected') + if not self.validate_signed_elements(signed_elements, raise_exceptions=True): + 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 def validate_signed_elements(self, signed_elements): """ Verifies that the document has the expected signed nodes. + + :param signed_elements: The signed elements to be checked + :type signed_elements: list + :param raise_exceptions: Whether to return false on failure or raise an exception + :type raise_exceptions: Boolean """ if len(signed_elements) > 2: return False @@ -492,12 +605,18 @@ def validate_signed_elements(self, signed_elements): if response_tag in signed_elements: expected_signature_nodes = OneLogin_Saml2_XML.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 @@ -515,9 +634,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): @@ -582,7 +707,10 @@ def __decrypt_assertion(self, xml): 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_XML.query(xml, '/samlp:Response/saml:EncryptedAssertion') if encrypted_assertion_nodes: @@ -590,15 +718,24 @@ def __decrypt_assertion(self, xml): if encrypted_data_nodes: keyinfo = OneLogin_Saml2_XML.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 ea0cf4ce..a96523d3 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -114,7 +114,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 9765d4f9..4d042374 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -24,7 +24,7 @@ from onelogin.saml2 import compat 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 from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -628,11 +628,17 @@ def get_status(dom): status_entry = OneLogin_Saml2_XML.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_XML.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 @@ -787,7 +793,10 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid # Raises expection if invalid 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 @@ -880,7 +889,10 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value) 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 + ) # Check if Reference URI is empty reference_elem = OneLogin_Saml2_XML.query(signature_node, '//ds:Reference') @@ -897,10 +909,8 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None) dsig_ctx.set_enabled_key_data([xmlsec.KeyData.X509]) - try: - dsig_ctx.verify(signature_node) - except Exception: - raise Exception('Signature validation failed. SAML Response rejected') + dsig_ctx.verify(signature_node) + return True @staticmethod diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index b7ff8586..41567617 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -12,7 +12,7 @@ from onelogin.saml2.auth import OneLogin_Saml2_Auth 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.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request try: @@ -143,10 +143,8 @@ def testProcessNoResponse(self): Case No Response, An exception is throw """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML Response not found'): auth.process_response() - exception = context.exception - self.assertIn("SAML Response not found", str(exception)) self.assertEqual(auth.get_errors(), ['invalid_binding']) def testProcessResponseInvalid(self): @@ -259,10 +257,8 @@ def testProcessNoSLO(self): Case No Message, An exception is throw """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML LogoutRequest/LogoutResponse not found'): auth.process_slo(True) - exception = context.exception - self.assertIn("SAML LogoutRequest/LogoutResponse not found", str(exception)) self.assertEqual(auth.get_errors(), ['invalid_binding']) def testProcessSLOResponseInvalid(self): @@ -773,10 +769,8 @@ def testLogoutNoSLO(self): del settings_info['idp']['singleLogoutService'] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) # The Header of the redirect produces an Exception - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'The IdP does not support Single Log Out'): auth.logout('http://example.com/returnto') - exception = context.exception - self.assertIn("The IdP does not support Single Log Out", str(exception)) def testLogoutNameIDandSessionIndex(self): """ @@ -863,10 +857,8 @@ 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.assertRaises(Exception) as context: + with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLRequest but can't load the SP private key"): auth2.add_request_signature(parameters) - exception = context.exception - self.assertIn("Trying to sign the SAMLRequest but can't load the SP private key", str(exception)) def testBuildResponseSignature(self): """ @@ -886,10 +878,8 @@ 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.assertRaises(Exception) as context: + with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLResponse but can't load the SP private key"): auth2.add_response_signature(parameters) - exception = context.exception - self.assertIn("Trying to sign the SAMLResponse but can't load the SP private key", str(exception)) def testIsInValidLogoutResponseSign(self): """ diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 2cc1083b..8ca47436 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -170,11 +170,10 @@ def testIsInValidRequestId(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) - try: - valid = response_2.is_valid(request_data, request_id) - self.assertFalse(valid) - except Exception as e: - self.assertIn('The InResponseTo of the Logout Response:', str(e)) + self.assertFalse(response_2.is_valid(request_data, request_id)) + self.assertIn('The InResponseTo of the Logout Response:', response_2.get_error()) + with self.assertRaisesRegexp(Exception, 'The InResponseTo of the Logout Response:'): + response_2.is_valid(request_data, request_id, raise_exceptions=True) def testIsInValidIssuer(self): """ @@ -223,7 +222,7 @@ def testIsInValidDestination(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) - with self.assertRaisesRegexp(Exception, 'The LogoutRequest was received at'): + with self.assertRaisesRegexp(Exception, 'The LogoutResponse was received at'): response_2.is_valid(request_data, raise_exceptions=True) # Empty destination @@ -258,7 +257,7 @@ def testIsValid(self): settings.set_strict(True) response_2 = OneLogin_Saml2_Logout_Response(settings, message) - with self.assertRaisesRegexp(Exception, 'The LogoutRequest was received at'): + with self.assertRaisesRegexp(Exception, 'The LogoutResponse was received at'): response_2.is_valid(request_data, raise_exceptions=True) plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 1bd726c0..b3c6e6af 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -388,12 +388,10 @@ def testOnlyRetrieveAssertionWithIDThatMatchesSignatureReference(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'wrapped_response_2.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - try: - self.assertTrue(response.is_valid(self.get_request_data())) - nameid = response.get_nameid() - self.assertNotEqual('root@example.com', nameid) - except Exception: - self.assertEqual('Invalid Signature Element {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor SAML Response rejected', response.get_error()) + with self.assertRaisesRegexp(Exception, 'Invalid Signature Element {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor SAML Response rejected'): + response.is_valid(self.get_request_data(), raise_exceptions=True) + nameid = response.get_nameid() + self.assertEqual('root@example.com', nameid) def testDoesNotAllowSignatureWrappingAttack(self): """ @@ -514,11 +512,11 @@ def testIsInValidReference(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - try: - valid = response.is_valid(self.get_request_data()) - self.assertFalse(valid) - except Exception as e: - self.assertEqual('Reference validation failed', str(e)) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error()) + + with self.assertRaisesRegexp(Exception, 'Signature validation failed. SAML Response rejected'): + response.is_valid(self.get_request_data(), raise_exceptions=True) def testIsInValidExpired(self): """ diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index c1b04ba6..60f79c08 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -391,19 +391,15 @@ def testGetStatus(self): xml_inv = b64decode(xml_inv) dom_inv = etree.fromstring(xml_inv) - with self.assertRaises(Exception) as context: + with self.assertRaisesRegexp(Exception, 'Missing Status on response'): OneLogin_Saml2_Utils.get_status(dom_inv) - exception = context.exception - self.assertIn("Missing valid Status on response", str(exception)) 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.assertRaises(Exception) as context: + with self.assertRaisesRegexp(Exception, 'Missing Status Code on response'): OneLogin_Saml2_Utils.get_status(dom_inv2) - exception = context.exception - self.assertIn("Missing valid Status Code on response", str(exception)) def testParseDuration(self): """