def extract_tag_text(xml, tagname): open_tag = compat.to_bytes("<%s" % tagname) close_tag = compat.to_bytes("</%s>" % tagname) xml = OneLogin_Saml2_XML.to_string(xml) start = xml.find(open_tag) assert start != -1 end = xml.find(close_tag, start) + len(close_tag) assert end != -1 return compat.to_string(xml[start:end])
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). :param signed_query: The element we should validate :type: string :param signature: The signature that will be validate :type: string :param cert: The public cert :type: string :param algorithm: Signature algorithm :type: string :param debug: Activate the xmlsec debug :type: bool """ try: xmlsec.enable_debug_trace(debug) dsig_ctx = xmlsec.SignatureContext() dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None) sign_algorithm_transform_map = { OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.Transform.DSA_SHA1, OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.Transform.RSA_SHA1, OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.Transform.RSA_SHA256, OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.Transform.RSA_SHA384, OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.Transform.RSA_SHA512 } sign_algorithm_transform = sign_algorithm_transform_map.get( algorithm, xmlsec.Transform.RSA_SHA1) dsig_ctx.verify_binary(compat.to_bytes(signed_query), sign_algorithm_transform, compat.to_bytes(signature)) return True except xmlsec.Error as e: if debug: print(e) return False
def testProcessResponseInvalidRequestId(self): """ Tests the process_response method of the OneLogin_Saml2_Auth class Case Invalid Response, Invalid requestID """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) plain_message = compat.to_string(b64decode(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) del request_data['get_data'] request_data['post_data'] = { 'SAMLResponse': compat.to_string(b64encode(compat.to_bytes(plain_message))) } auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) request_id = 'invalid' auth.process_response(request_id) self.assertEqual('No Signature found. SAML Response rejected', auth.get_last_error_reason()) auth.set_strict(True) auth.process_response(request_id) self.assertEqual(auth.get_errors(), ['invalid_response']) self.assertEqual('The InResponseTo of the Response: _57bcbf70-7b1f-012e-c821-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid', auth.get_last_error_reason()) valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38' auth.process_response(valid_request_id) self.assertEqual('No Signature found. SAML Response rejected', auth.get_last_error_reason())
def sign_binary(msg, key, algorithm=xmlsec.Transform.RSA_SHA1, debug=False): """ Sign binary message :param msg: The element we should validate :type: bytes :param key: The private key :type: string :param debug: Activate the xmlsec debug :type: bool :return signed message :rtype str """ if isinstance(msg, str): msg = msg.encode('utf8') xmlsec.enable_debug_trace(debug) dsig_ctx = xmlsec.SignatureContext() dsig_ctx.key = xmlsec.Key.from_memory(key, xmlsec.KeyFormat.PEM, None) return dsig_ctx.sign_binary(compat.to_bytes(msg), algorithm)
def generate_unique_id(): """ Generates an unique string (used for example as ID for assertions). :return: A unique string :rtype: string """ return 'ONELOGIN_%s' % sha1(compat.to_bytes(uuid4().hex)).hexdigest()
def deflate_and_base64_encode(value): """ Deflates and the base64 encodes a string :param value: The string to deflate and encode :type value: string :returns: The deflated and encoded string :rtype: string """ return OneLogin_Saml2_Utils.b64encode(zlib.compress(compat.to_bytes(value))[2:-4])
def deflate_and_base64_encode(value): """ Deflates and then base64 encodes a string :param value: The string to deflate and encode :type value: string :returns: The deflated and encoded string :rtype: string """ return OneLogin_Saml2_Utils.b64encode(zlib.compress(compat.to_bytes(value))[2:-4])
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). :param signed_query: The element we should validate :type: string :param signature: The signature that will be validate :type: string :param cert: The public cert :type: string :param algorithm: Signature algorithm :type: string :param debug: Activate the xmlsec debug :type: bool """ try: xmlsec.enable_debug_trace(debug) dsig_ctx = xmlsec.SignatureContext() dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None) sign_algorithm_transform_map = { OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.Transform.DSA_SHA1, OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.Transform.RSA_SHA1, OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.Transform.RSA_SHA256, OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.Transform.RSA_SHA384, OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.Transform.RSA_SHA512 } sign_algorithm_transform = sign_algorithm_transform_map.get(algorithm, xmlsec.Transform.RSA_SHA1) dsig_ctx.verify_binary(compat.to_bytes(signed_query), sign_algorithm_transform, compat.to_bytes(signature)) return True except xmlsec.Error as e: if debug: print(e) return False
def calculate_x509_fingerprint(x509_cert, alg='sha1'): """ Calculates the fingerprint of a formatted x509cert. :param x509_cert: x509 cert formatted :type: string :param alg: The algorithm to build the fingerprint :type: string :returns: fingerprint :rtype: string """ assert isinstance(x509_cert, compat.str_type) lines = x509_cert.split('\n') data = '' inData = False for line in lines: # Remove '\r' from end of line if present. line = line.rstrip() if not inData: if line == '-----BEGIN CERTIFICATE-----': inData = True elif line == '-----BEGIN PUBLIC KEY-----' or line == '-----BEGIN RSA PRIVATE KEY-----': # This isn't an X509 certificate. return None else: if line == '-----END CERTIFICATE-----': break # Append the current line to the certificate data. data += line if not data: return None decoded_data = base64.b64decode(compat.to_bytes(data)) if alg == 'sha512': fingerprint = sha512(decoded_data) elif alg == 'sha384': fingerprint = sha384(decoded_data) elif alg == 'sha256': fingerprint = sha256(decoded_data) else: fingerprint = sha1(decoded_data) return fingerprint.hexdigest().lower()
def to_etree(xml): """ Parses an XML document or fragment from a string. :param xml: the string to parse :type xml: str|bytes|xml.dom.minidom.Document|etree.Element :returns: the root node :rtype: OneLogin_Saml2_XML._element_class """ if isinstance(xml, OneLogin_Saml2_XML._element_class): return xml if isinstance(xml, OneLogin_Saml2_XML._bytes_class): return OneLogin_Saml2_XML._parse_etree(xml, forbid_dtd=True) if isinstance(xml, OneLogin_Saml2_XML._text_class): return OneLogin_Saml2_XML._parse_etree(compat.to_bytes(xml), forbid_dtd=True) raise ValueError('unsupported type %r' % type(xml))
def calculate_x509_fingerprint(x509_cert, alg='sha1'): """ Calculates the fingerprint of a x509cert. :param x509_cert: x509 cert :type: string :param alg: The algorithm to build the fingerprint :type: string :returns: fingerprint :rtype: string """ assert isinstance(x509_cert, compat.str_type) lines = x509_cert.split('\n') data = '' for line in lines: # Remove '\r' from end of line if present. line = line.rstrip() if line == '-----BEGIN CERTIFICATE-----': # Delete junk from before the certificate. data = '' elif line == '-----END CERTIFICATE-----': # Ignore data after the certificate. break elif line == '-----BEGIN PUBLIC KEY-----' or line == '-----BEGIN RSA PRIVATE KEY-----': # This isn't an X509 certificate. return None else: # Append the current line to the certificate data. data += line decoded_data = base64.b64decode(compat.to_bytes(data)) if alg == 'sha512': fingerprint = sha512(decoded_data) elif alg == 'sha384': fingerprint = sha384(decoded_data) elif alg == 'sha256': fingerprint = sha256(decoded_data) else: fingerprint = sha1(decoded_data) return fingerprint.hexdigest().lower()
def __init__(self, settings, response=None): """ Constructs a Logout Response object (Initialize params from settings and if provided load the Logout Response. Arguments are: * (OneLogin_Saml2_Settings) settings. Setting data * (string) response. An UUEncoded SAML Logout response from the IdP. """ self.__settings = settings self.__error = None self.id = None if response is not None: self.__logout_response = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(response, ignore_zip=True)) self.document = OneLogin_Saml2_XML.to_etree(compat.to_bytes(self.__logout_response)) self.id = self.document.get('ID', None)
def to_etree(cls, xml): """ Parses an XML document or fragment from a string. Args: xml (`str` or `bytes` or `xml.dom.minidom.Document` or `etree.Element`): the string to parse Returns: (OneLogin_Saml2_XML._element_class) the root node """ # This is nearly verbatim the method from the super method, # except this method calls other methods from this class using cls. # The super class explicitly calls these methods by explicitly using # its name instead of using cls, which is why modifying the parser # and parsing with the inherited method doesn't work. if isinstance(xml, cls._element_class): return xml if isinstance(xml, cls._bytes_class): return cls._parse_etree(xml, forbid_dtd=True) if isinstance(xml, cls._text_class): return cls._parse_etree(compat.to_bytes(xml), forbid_dtd=True)
def get_metadata(url, validate_cert=True): """ Gets the metadata XML from the provided URL :param url: Url where the XML of the Identity Provider Metadata is published. :type url: string :param validate_cert: If the url uses https schema, that flag enables or not the verification of the associated certificate. :type validate_cert: bool :returns: metadata XML :rtype: string """ valid = False if validate_cert: response = urllib2.urlopen(url) else: ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE response = urllib2.urlopen(url, context=ctx) xml = response.read() if xml: try: dom = OneLogin_Saml2_XML.to_etree(compat.to_bytes(xml)) idp_descriptor_nodes = OneLogin_Saml2_XML.query( dom, '//md:IDPSSODescriptor') if idp_descriptor_nodes: valid = True except: pass if not valid: raise Exception('Not valid IdP XML found from URL: %s' % (url)) return xml
def b64encode(data): """base64 encode""" return compat.to_string(base64.b64encode(compat.to_bytes(data)))
def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None): """ Constructs the Logout Request object. :param settings: Setting data :type settings: OneLogin_Saml2_Settings :param request: Optional. A LogoutRequest to be loaded instead build one. :type request: string :param name_id: The NameID that will be set in the LogoutRequest. :type name_id: string :param session_index: SessionIndex that identifies the session of the user. :type session_index: string :param nq: IDP Name Qualifier :type: string :param name_id_format: The NameID Format that will be set in the LogoutRequest. :type: string """ self.__settings = settings self.__error = None self.id = None if request is None: sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() security = self.__settings.get_security_data() uid = OneLogin_Saml2_Utils.generate_unique_id() self.id = uid issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML( OneLogin_Saml2_Utils.now()) cert = None if security['nameIdEncrypted']: exists_multix509enc = 'x509certMulti' in idp_data and \ 'encryption' in idp_data['x509certMulti'] and \ idp_data['x509certMulti']['encryption'] if exists_multix509enc: cert = idp_data['x509certMulti']['encryption'][0] else: cert = idp_data['x509cert'] if name_id is not None: if not name_id_format and sp_data[ 'NameIDFormat'] != OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED: name_id_format = sp_data['NameIDFormat'] else: name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY sp_name_qualifier = None if name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY: name_id = idp_data['entityId'] nq = None elif nq is not None: # We only gonna include SPNameQualifier if NameQualifier is provided sp_name_qualifier = sp_data['entityId'] name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, sp_name_qualifier, name_id_format, cert, False, nq) if session_index: session_index_str = '<samlp:SessionIndex>%s</samlp:SessionIndex>' % session_index else: session_index_str = '' logout_request = OneLogin_Saml2_Templates.LOGOUT_REQUEST % \ { 'id': uid, 'issue_instant': issue_instant, 'single_logout_url': idp_data['singleLogoutService']['url'], 'entity_id': sp_data['entityId'], 'name_id': name_id_obj, 'session_index': session_index_str, } else: logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate( request, ignore_zip=True) self.id = self.get_id(logout_request) self.__logout_request = compat.to_bytes(logout_request)
def parse(idp_metadata, required_sso_binding=OneLogin_Saml2_Constants. BINDING_HTTP_REDIRECT, required_slo_binding=OneLogin_Saml2_Constants. BINDING_HTTP_REDIRECT, entity_id=None): """ Parses the Identity Provider metadata and return a dict with extracted data. If there are multiple <IDPSSODescriptor> tags, parse only the first. Parses only those SSO endpoints with the same binding as given by the `required_sso_binding` parameter. Parses only those SLO endpoints with the same binding as given by the `required_slo_binding` parameter. If the metadata specifies multiple SSO endpoints with the required binding, extract only the first (the same holds true for SLO endpoints). :param idp_metadata: XML of the Identity Provider Metadata. :type idp_metadata: string :param required_sso_binding: Parse only POST or REDIRECT SSO endpoints. :type required_sso_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT or OneLogin_Saml2_Constants.BINDING_HTTP_POST :param required_slo_binding: Parse only POST or REDIRECT SLO endpoints. :type required_slo_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT or OneLogin_Saml2_Constants.BINDING_HTTP_POST :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML that contains multiple EntityDescriptor. :type entity_id: string :returns: settings dict with extracted data :rtype: dict """ data = {} dom = OneLogin_Saml2_XML.to_etree(compat.to_bytes(idp_metadata)) idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = certs = None entity_desc_path = '//md:EntityDescriptor' if entity_id: entity_desc_path += "[@entityID='%s']" % entity_id entity_descriptor_nodes = OneLogin_Saml2_XML.query( dom, entity_desc_path) if len(entity_descriptor_nodes) > 0: entity_descriptor_node = entity_descriptor_nodes[0] idp_descriptor_nodes = OneLogin_Saml2_XML.query( entity_descriptor_node, './md:IDPSSODescriptor') if len(idp_descriptor_nodes) > 0: idp_descriptor_node = idp_descriptor_nodes[0] idp_entity_id = entity_descriptor_node.get('entityID', None) want_authn_requests_signed = entity_descriptor_node.get( 'WantAuthnRequestsSigned', None) name_id_format_nodes = OneLogin_Saml2_XML.query( idp_descriptor_node, './md:NameIDFormat') if len(name_id_format_nodes) > 0: idp_name_id_format = name_id_format_nodes[0].text sso_nodes = OneLogin_Saml2_XML.query( idp_descriptor_node, "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding) if len(sso_nodes) > 0: idp_sso_url = sso_nodes[0].get('Location', None) slo_nodes = OneLogin_Saml2_XML.query( idp_descriptor_node, "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding) if len(slo_nodes) > 0: idp_slo_url = slo_nodes[0].get('Location', None) signing_nodes = OneLogin_Saml2_XML.query( idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate" ) encryption_nodes = OneLogin_Saml2_XML.query( idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate" ) if len(signing_nodes) > 0 or len(encryption_nodes) > 0: certs = {} if len(signing_nodes) > 0: certs['signing'] = [] for cert_node in signing_nodes: certs['signing'].append(''.join( cert_node.text.split())) if len(encryption_nodes) > 0: certs['encryption'] = [] for cert_node in encryption_nodes: certs['encryption'].append(''.join( cert_node.text.split())) data['idp'] = {} if idp_entity_id is not None: data['idp']['entityId'] = idp_entity_id if idp_sso_url is not None: data['idp']['singleSignOnService'] = {} data['idp']['singleSignOnService']['url'] = idp_sso_url data['idp']['singleSignOnService'][ 'binding'] = required_sso_binding if idp_slo_url is not None: data['idp']['singleLogoutService'] = {} data['idp']['singleLogoutService']['url'] = idp_slo_url data['idp']['singleLogoutService'][ 'binding'] = required_slo_binding if want_authn_requests_signed is not None: data['security'] = {} data['security'][ 'authnRequestsSigned'] = want_authn_requests_signed if idp_name_id_format: data['sp'] = {} data['sp']['NameIDFormat'] = idp_name_id_format if certs is not None: if (len(certs) == 1 and (('signing' in certs and len(certs['signing']) == 1) or ('encryption' in certs and len(certs['encryption']) == 1))) or \ (('signing' in certs and len(certs['signing']) == 1) and ('encryption' in certs and len(certs['encryption']) == 1 and certs['signing'][0] == certs['encryption'][0])): if 'signing' in certs: data['idp']['x509cert'] = certs['signing'][0] else: data['idp']['x509cert'] = certs['encryption'][0] else: data['idp']['x509certMulti'] = certs return data