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 response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML if (response_tag in signed_elements and signed_elements.count(response_tag) > 1) or \ (assertion_tag in signed_elements and signed_elements.count(assertion_tag) > 1) or \ (response_tag not in signed_elements and assertion_tag not in signed_elements): return False # Check that the signed elements found here, are the ones that will be verified # by OneLogin_Saml2_Utils.validate_sign 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 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 OneLogin_Saml2_ValidationError( 'Unexpected number of Assertion signatures found. SAML Response rejected.', OneLogin_Saml2_ValidationError. WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION) return True
def get_assertion_issue_instant(self): """ :returns: the IssueInstant of the assertion in the response :rtype: unix/posix timestamp|None """ if not self.validate_num_assertions(): raise OneLogin_Saml2_ValidationError( 'SAML Response must contain 1 assertion', OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS ) issue_instant = self._query_assertion('')[0].get('IssueInstant', None) return OneLogin_Saml2_Utils.parse_SAML_to_time(issue_instant)
def check_assertion_issuer_format(self): issuer_node = self.__query_assertion('/saml:Issuer') if len(issuer_node) != 1: return False fmt = issuer_node[0].get('Format', None) issuer_format = self.__settings.get_sp_data().get('Issuer', {}).get( 'Format', None) if not fmt or fmt != self.__settings.get_sp_data().get( 'Issuer', {}).get('Format', None): raise OneLogin_Saml2_ValidationError( f'Wrong assertion issuer format expected: {issuer_format}, found: {fmt}', OneLogin_Saml2_ValidationError.WRONG_ISSUER_FORMAT)
def validate_timestamps(self): """ Verifies that the document is valid according to Conditions Element :returns: True if the condition is valid, False otherwise :rtype: bool """ conditions_nodes = self.__query_assertion('/saml:Conditions') for conditions_node in conditions_nodes: 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 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 OneLogin_Saml2_ValidationError( 'Could not validate timestamp: expired. Check system clock.', OneLogin_Saml2_ValidationError.ASSERTION_EXPIRED ) return True
def check_issuer(self): """ Check issuer element exists and format match with the one from settings :return: Exception. If the Issuer is missing or empty """ issuer = OneLogin_Saml2_Utils.get_issuer(self.document) fmt = issuer.get('Format', None) issuer_format = self.__settings.get_sp_data().get('Issuer', {}).get( 'Format', None) if not fmt or fmt != self.__settings.get_sp_data().get( 'Issuer', {}).get('Format', None): raise OneLogin_Saml2_ValidationError( f'Wrong issuer format expected: {issuer_format}, found: {fmt}', OneLogin_Saml2_ValidationError.WRONG_ISSUER_FORMAT)
def get_nameid_data(request, key=None): """ Gets the NameID Data of the the Logout Request :param request: Logout Request Message :type request: string|DOMDocument :param key: The SP key :type key: string :return: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) :rtype: dict """ elem = OneLogin_Saml2_XML.to_etree(request) name_id = None encrypted_entries = OneLogin_Saml2_XML.query( elem, '/samlp:LogoutRequest/saml:EncryptedID') if len(encrypted_entries) == 1: if key is None: 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: encrypted_data = encrypted_data_nodes[0] name_id = OneLogin_Saml2_Utils.decrypt_element( encrypted_data, key) else: entries = OneLogin_Saml2_XML.query( elem, '/samlp:LogoutRequest/saml:NameID') if len(entries) == 1: name_id = entries[0] if name_id is None: raise OneLogin_Saml2_ValidationError( 'NameID not found in the Logout Request', OneLogin_Saml2_ValidationError.NO_NAMEID) name_id_data = {'Value': OneLogin_Saml2_XML.element_text(name_id)} for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: if attr in name_id.attrib: name_id_data[attr] = name_id.attrib[attr] return name_id_data
def check_status(self): """ Check if the status of the response is success or not :raises: Exception. If the status is not success """ status = OneLogin_Saml2_Utils.get_status(self.document) code = status.get('code', None) if code and code != OneLogin_Saml2_Constants.STATUS_SUCCESS: splited_code = code.split(':') printable_code = splited_code.pop() status_exception_msg = 'The status code of the Response was not Success, was %s' % printable_code status_msg = status.get('msg', None) if status_msg: status_exception_msg += ' -> ' + status_msg raise OneLogin_Saml2_ValidationError( status_exception_msg, OneLogin_Saml2_ValidationError.STATUS_CODE_IS_NOT_SUCCESS)
def get_attributes(self): """ Gets the Attributes from the AttributeStatement element. EncryptedAttributes are not supported """ attributes = {} attribute_nodes = self.__query_assertion('/saml:AttributeStatement/saml:Attribute') for attribute_node in attribute_nodes: attr_name = attribute_node.get('Name') if attr_name in attributes.keys(): 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']): values.append(attr.text) attributes[attr_name] = values return attributes
def get_attributes(self): """ Gets the Attributes from the AttributeStatement element. EncryptedAttributes are not supported """ attributes = {} attribute_nodes = self.__query_assertion( '/saml2:AttributeStatement/saml2:Attribute') for attribute_node in attribute_nodes: attr_name = attribute_node.get('Name') if attr_name in attributes.keys(): 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['saml2']): attr_text = OneLogin_Saml2_XML.element_text(attr) if attr_text: attr_text = attr_text.strip() if attr_text: values.append(attr_text) # Parse any nested NameID children for nameid in attr.iterchildren( '{%s}NameID' % OneLogin_Saml2_Constants.NSMAP['saml2']): values.append({ 'NameID': { 'Format': nameid.get('Format'), 'NameQualifier': nameid.get('NameQualifier'), 'value': nameid.text } }) attributes[attr_name] = values return attributes
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 """ self.__error = None try: root = OneLogin_Saml2_XML.to_etree(self.__logout_request) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = ('get_data' in request_data and request_data['get_data']) or dict() 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 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() current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check NotOnOrAfter if root.get('NotOnOrAfter', None): na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): 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 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 OneLogin_Saml2_ValidationError( 'Invalid issuer in the Logout Request (expected %(idpEntityId)s, got %(issuer)s)' % { 'idpEntityId': idp_entity_id, 'issuer': issuer }, OneLogin_Saml2_ValidationError.WRONG_ISSUER ) if security['wantMessagesSigned']: 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_MESSAGE ) return True except Exception as err: # pylint: disable=R0801 self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) if raise_exceptions: raise return False
def process_signed_elements(self): """ Verifies the signature nodes: - Checks that are Response or Assertion - Check that IDs and reference URI are unique and consistent. :returns: The signed elements tag names :rtype: list """ sign_nodes = self.__query('//ds:Signature') signed_elements = [] verified_seis = [] verified_ids = [] response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML for sign_node in sign_nodes: signed_element = sign_node.getparent().tag if signed_element != response_tag and signed_element != assertion_tag: 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 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 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 ref = OneLogin_Saml2_XML.query(sign_node, './/ds:Reference') if ref: ref = ref[0] if ref.get('URI'): sei = ref.get('URI')[1:] if sei != id_value: raise OneLogin_Saml2_ValidationError( 'Found an invalid Signed Element. SAML Response rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNED_ELEMENT ) if sei in verified_seis: 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 OneLogin_Saml2_ValidationError( 'Found an unexpected Signature Element. SAML Response rejected', OneLogin_Saml2_ValidationError.UNEXPECTED_SIGNED_ELEMENT ) return signed_elements
def is_valid(self, request_data, request_id=None, raise_exceptions=False): """ Validates the response object. :param request_data: Request Data :type request_data: dict :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 """ self.__error = None try: # Checks SAML version if self.document.get('Version', None) != '2.0': 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 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 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() idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] sp_data = self.__settings.get_sp_data() sp_entity_id = sp_data['entityId'] signed_elements = self.process_signed_elements() has_signed_response = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements has_signed_assertion = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML in signed_elements if self.__settings.is_strict(): 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 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 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) # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided 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 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 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 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 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 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 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 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) if destination: if not destination.startswith(current_url): # TODO: Review if following lines are required, since we can control the # request_data # current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data) # if not destination.startswith(current_url_routed): raise OneLogin_Saml2_ValidationError( 'The response was received at %s instead of %s' % (current_url, destination), OneLogin_Saml2_ValidationError.WRONG_DESTINATION ) elif destination == '': 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 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 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 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 subject_confirmation_nodes = self.__query_assertion('/saml:Subject/saml:SubjectConfirmation') for scn in subject_confirmation_nodes: method = scn.get('Method', None) if method and method != OneLogin_Saml2_Constants.CM_BEARER: continue sc_data = scn.find('saml:SubjectConfirmationData', namespaces=OneLogin_Saml2_Constants.NSMAP) if sc_data is None: continue else: irt = sc_data.get('InResponseTo', None) if in_response_to and irt and irt != in_response_to: continue recipient = sc_data.get('Recipient', None) if recipient and current_url not in recipient: continue nooa = sc_data.get('NotOnOrAfter', None) if nooa: parsed_nooa = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa) if parsed_nooa <= OneLogin_Saml2_Utils.now(): continue nb = sc_data.get('NotBefore', None) if nb: parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time(nb) if parsed_nb > OneLogin_Saml2_Utils.now(): continue any_subject_confirmation = True break if not any_subject_confirmation: 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 OneLogin_Saml2_ValidationError( 'The Assertion of the Response is not signed and the SP require it', OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION ) if security['wantMessagesSigned'] 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_MESSAGE ) if not signed_elements or (not has_signed_response and not has_signed_assertion): 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) if fingerprint: fingerprint = OneLogin_Saml2_Utils.format_finger_print(fingerprint) fingerprintalg = idp_data.get('certFingerprintAlgorithm', None) # 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 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 OneLogin_Saml2_ValidationError( 'Signature validation failed. SAML Response rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNATURE ) return True except Exception as err: self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) if raise_exceptions: raise return False
def __decrypt_assertion(self, xml): """ Decrypts the Assertion :raises: Exception if no private key available :param xml: Encrypted Assertion :type xml: Element :returns: Decrypted Assertion :rtype: Element """ key = self.__settings.get_sp_key() debug = self.__settings.is_debug_active() if not key: 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: encrypted_data_nodes = OneLogin_Saml2_XML.query( encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData') if encrypted_data_nodes: keyinfo = OneLogin_Saml2_XML.query( encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData/ds:KeyInfo') if not keyinfo: 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 OneLogin_Saml2_ValidationError( 'KeyInfo has no children nodes, invalid Assertion', OneLogin_Saml2_ValidationError. CHILDREN_NODE_NOT_FOUND_IN_KEYINFO) for child in children: if 'RetrievalMethod' in child.tag: if child.attrib[ 'Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey': raise OneLogin_Saml2_ValidationError( 'Unsupported Retrieval Method found', OneLogin_Saml2_ValidationError. UNSUPPORTED_RETRIEVAL_METHOD) uri = child.attrib['URI'] if not uri.startswith('#'): break uri = uri.split('#')[1] encrypted_key = OneLogin_Saml2_XML.query( encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id=$tagid]', None, uri) if encrypted_key: keyinfo.append(encrypted_key[0]) encrypted_data = encrypted_data_nodes[0] decrypted = OneLogin_Saml2_Utils.decrypt_element( encrypted_data, key, debug=debug, inplace=True) xml.replace(encrypted_assertion_nodes[0], decrypted) return xml
def __validate_signature(self, data, saml_type, raise_exceptions=False): """ Validate Signature :param data: The Request data :type data: dict :param cert: The certificate to check signature :type cert: str :param saml_type: The target URL the user should be redirected to :type saml_type: string SAMLRequest | SAMLResponse :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_MESSAGE) return True idp_data = self.get_settings().get_idp_data() exists_x509cert = self.get_settings().get_idp_cert() is not None exists_multix509sign = 'x509certMulti' in idp_data and \ 'signing' in idp_data['x509certMulti'] and \ idp_data['x509certMulti']['signing'] if not (exists_x509cert or exists_multix509sign): 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') lowercase_urlencoding = False if 'lowercase_urlencoding' in self.__request_data.keys(): lowercase_urlencoding = self.__request_data[ 'lowercase_urlencoding'] signed_query = self.__build_sign_query( data[saml_type], data.get('RelayState', None), sign_alg, saml_type, lowercase_urlencoding) if exists_multix509sign: for cert in idp_data['x509certMulti']['signing']: if OneLogin_Saml2_Utils.validate_binary_sign( signed_query, OneLogin_Saml2_Utils.b64decode(signature), cert, sign_alg): return True raise OneLogin_Saml2_ValidationError( 'Signature validation failed. %s rejected' % saml_type, OneLogin_Saml2_ValidationError.INVALID_SIGNATURE) else: cert = self.get_settings().get_idp_cert() if not OneLogin_Saml2_Utils.validate_binary_sign( signed_query, OneLogin_Saml2_Utils.b64decode(signature), cert, sign_alg, self.__settings.is_debug_active()): 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
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 """ self.__error = None try: idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] get_data = request_data['get_data'] 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 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 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 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 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 OneLogin_Saml2_ValidationError( 'The Message of the Logout Response is not signed and the SP require it', OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE) return True # pylint: disable=R0801 except Exception as err: self.__error = str(err) debug = self.__settings.is_debug_active() if debug: print(err) if raise_exceptions: raise return False
def get_nameid_data(self): """ Gets the NameID Data provided by the SAML Response from the IdP :returns: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) :rtype: dict """ nameid = None nameid_data = {} encrypted_id_data_nodes = self.__query_assertion( '/saml:Subject/saml:EncryptedID/xenc:EncryptedData') if encrypted_id_data_nodes: encrypted_data = encrypted_id_data_nodes[0] key = self.__settings.get_sp_key() nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) else: nameid_nodes = self.__query_assertion('/saml:Subject/saml:NameID') if nameid_nodes: nameid = nameid_nodes[0] if isinstance(nameid, str): nameid = nameid if nameid.strip() else None is_strict = self.__settings.is_strict() want_nameid = self.__settings.get_security_data().get( 'wantNameId', True) if nameid is None: if is_strict and want_nameid: raise OneLogin_Saml2_ValidationError( 'NameID not found in the assertion of the Response', OneLogin_Saml2_ValidationError.NO_NAMEID) else: if is_strict and want_nameid and not OneLogin_Saml2_XML.element_text( nameid).strip(): raise OneLogin_Saml2_ValidationError( 'An empty NameID value found', OneLogin_Saml2_ValidationError.EMPTY_NAMEID) nameid_data = {'Value': OneLogin_Saml2_XML.element_text(nameid)} expected_format_name_id = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' if nameid.get('Format', None) != expected_format_name_id: raise OneLogin_Saml2_ValidationError( f'Format has a wrong format, {nameid.get("Format", None)} expected: {expected_format_name_id}', OneLogin_Saml2_ValidationError.WRONG_ATTRIBUTE) for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: value = nameid.get(attr, None) if value: if is_strict and attr == 'SPNameQualifier': sp_data = self.__settings.get_sp_data() sp_entity_id = sp_data.get('entityId', '') if sp_entity_id != 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 elif attr is 'SPNameQualifier': continue elif value is None: raise OneLogin_Saml2_ValidationError( f'{attr} missing', OneLogin_Saml2_ValidationError.MISSING_ATTRIBUTE) elif not value: raise OneLogin_Saml2_ValidationError( f'An empty {attr} value found', OneLogin_Saml2_ValidationError.EMPTY_ATTRIBUTE) return nameid_data
def process_signed_elements(self): """ Verifies the signature nodes: - Checks that are Response or Assertion - Check that IDs and reference URI are unique and consistent. :returns: The signed elements tag names :rtype: list """ sign_nodes = self._query('//ds:Signature') signed_elements = [] verified_seis = [] verified_ids = [] response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML security = self._settings.get_security_data() reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) for sign_node in sign_nodes: signed_element = sign_node.getparent().tag if signed_element != response_tag and signed_element != assertion_tag: 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 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 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 ref = OneLogin_Saml2_XML.query(sign_node, './/ds:Reference') if ref: ref = ref[0] if ref.get('URI'): sei = ref.get('URI')[1:] if sei != id_value: raise OneLogin_Saml2_ValidationError( 'Found an invalid Signed Element. SAML Response rejected', OneLogin_Saml2_ValidationError.INVALID_SIGNED_ELEMENT ) if sei in verified_seis: raise OneLogin_Saml2_ValidationError( 'Duplicated Reference URI. SAML Response rejected', OneLogin_Saml2_ValidationError.DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS ) verified_seis.append(sei) # Check the signature and digest algorithm if reject_deprecated_alg: sig_method_node = OneLogin_Saml2_XML.query(sign_node, './/ds:SignatureMethod') if sig_method_node: sig_method = sig_method_node[0].get("Algorithm") if sig_method in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: raise OneLogin_Saml2_ValidationError( 'Deprecated signature algorithm found: %s' % sig_method, OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD ) dig_method_node = OneLogin_Saml2_XML.query(sign_node, './/ds:DigestMethod') if dig_method_node: dig_method = dig_method_node[0].get("Algorithm") if dig_method in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: raise OneLogin_Saml2_ValidationError( 'Deprecated digest algorithm found: %s' % dig_method, OneLogin_Saml2_ValidationError.DEPRECATED_DIGEST_METHOD ) signed_elements.append(signed_element) if signed_elements: 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_ELEMENTS ) return signed_elements