From e0896dc0fb967c3bb74835d73e048c28e3363b5b Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Sat, 14 Feb 2015 02:14:46 +0300 Subject: [PATCH 01/35] initial python3 support --- .gitignore | 1 + setup.py | 13 +- src/onelogin/saml2/auth.py | 33 +- src/onelogin/saml2/authn_request.py | 10 +- src/onelogin/saml2/errors.py | 1 - src/onelogin/saml2/logout_request.py | 36 +- src/onelogin/saml2/logout_response.py | 20 +- src/onelogin/saml2/metadata.py | 8 +- src/onelogin/saml2/response.py | 22 +- src/onelogin/saml2/settings.py | 8 +- src/onelogin/saml2/utils.py | 328 ++++++++++-------- tests/src/OneLogin/saml2_tests/auth_test.py | 80 ++--- .../saml2_tests/authn_request_test.py | 54 ++- tests/src/OneLogin/saml2_tests/error_test.py | 2 +- .../saml2_tests/logout_request_test.py | 120 +++---- .../saml2_tests/logout_response_test.py | 52 +-- .../src/OneLogin/saml2_tests/metadata_test.py | 30 +- .../src/OneLogin/saml2_tests/response_test.py | 139 ++++---- .../src/OneLogin/saml2_tests/settings_test.py | 99 +++--- .../saml2_tests/signed_response_test.py | 14 +- tests/src/OneLogin/saml2_tests/utils_test.py | 106 ++---- 21 files changed, 541 insertions(+), 635 deletions(-) diff --git a/.gitignore b/.gitignore index ad382d79..ca8905ff 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ __pycache_ /venv .coverage .pypirc +/.idea *.key *.crt diff --git a/setup.py b/setup.py index 43396b2e..7b345653 100644 --- a/setup.py +++ b/setup.py @@ -16,26 +16,23 @@ 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 2.7, 3.x', ], author='OneLogin', author_email='support@onelogin.com', license='MIT', url='https://github.com/onelogin/python-saml', - packages=['onelogin','onelogin/saml2'], + packages=['onelogin', 'onelogin/saml2'], include_package_data=True, - package_data = { - 'onelogin/saml2/schemas': ['*.xsd'], + package_data={ + 'onelogin/saml2/schemas': ['*.xsd'], }, package_dir={ '': 'src', }, test_suite='tests', install_requires=[ - 'M2Crypto==0.22.3', - 'dm.xmlsec.binding==1.3.2', - 'isodate==0.5.0', - 'defusedxml==0.4.1', + 'isodate>=0.5.0', ], extras_require={ 'test': ( diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 4be7e87a..0281ea99 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -11,11 +11,6 @@ """ -from base64 import b64encode -from urllib import quote_plus - -import dm.xmlsec.binding as xmlsec - from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.response import OneLogin_Saml2_Response from onelogin.saml2.errors import OneLogin_Saml2_Error @@ -25,6 +20,8 @@ from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request +import xmlsec + class OneLogin_Saml2_Auth(object): """ @@ -51,7 +48,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): """ self.__request_data = request_data self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path) - self.__attributes = [] + self.__attributes = dict() self.__nameid = None self.__session_index = None self.__authenticated = False @@ -241,11 +238,8 @@ def get_attribute(self, name): :returns: Attribute value if exists or [] :rtype: string """ - assert isinstance(name, basestring) - value = None - if self.__attributes and name in self.__attributes.keys(): - value = self.__attributes[name] - return value + assert isinstance(name, OneLogin_Saml2_Utils.str_type) + return self.__attributes.get(name) def login(self, return_to=None, force_authn=False, is_passive=False): """ @@ -388,17 +382,16 @@ def __build_signature(self, saml_data, relay_state, saml_type): OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND ) - xmlsec.initialize() - - dsig_ctx = xmlsec.DSigCtx() - dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + xmlsec.enable_debug_trace(self.__settings.is_debug_active()) + dsig_ctx = xmlsec.SignatureContext() + dsig_ctx.key = xmlsec.Key.from_memory(key, xmlsec.KeyFormat.PEM, None) - saml_data_str = '%s=%s' % (saml_type, quote_plus(saml_data)) - relay_state_str = 'RelayState=%s' % quote_plus(relay_state) - alg_str = 'SigAlg=%s' % quote_plus(OneLogin_Saml2_Constants.RSA_SHA1) + saml_data_str = '%s=%s' % (saml_type, OneLogin_Saml2_Utils.escape_url(saml_data)) + relay_state_str = 'RelayState=%s' % OneLogin_Saml2_Utils.escape_url(relay_state) + alg_str = 'SigAlg=%s' % OneLogin_Saml2_Utils.escape_url(OneLogin_Saml2_Constants.RSA_SHA1) sign_data = [saml_data_str, relay_state_str, alg_str] msg = '&'.join(sign_data) - signature = dsig_ctx.signBinary(str(msg), xmlsec.TransformRsaSha1) - return b64encode(signature) + signature = dsig_ctx.sign_binary(OneLogin_Saml2_Utils.bytes(msg), xmlsec.Transform.RSA_SHA1) + return OneLogin_Saml2_Utils.b64encode(signature) diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 8e8495e7..6e2f9885 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -9,9 +9,6 @@ """ -from base64 import b64encode -from zlib import compress - from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.constants import OneLogin_Saml2_Constants @@ -56,11 +53,11 @@ def __init__(self, settings, force_authn=False, is_passive=False): provider_name_str = '' organization_data = settings.get_organization() if isinstance(organization_data, dict) and organization_data: - langs = organization_data.keys() + langs = organization_data if 'en-US' in langs: lang = 'en-US' else: - lang = langs[0] + lang = sorted(langs)[0] if 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] is not None: provider_name_str = 'ProviderName="%s"' % organization_data[lang]['displayname'] @@ -123,8 +120,7 @@ def get_request(self): :return: Unsigned AuthnRequest :rtype: str object """ - deflated_request = compress(self.__authn_request)[2:-4] - return b64encode(deflated_request) + return OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__authn_request) def get_id(self): """ diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py index 63d98744..c28a23bd 100644 --- a/src/onelogin/saml2/errors.py +++ b/src/onelogin/saml2/errors.py @@ -43,7 +43,6 @@ def __init__(self, message, code=0, errors=None): * (str) message. Describes the error. * (int) code. The code error (defined in the error class). """ - assert isinstance(message, basestring) assert isinstance(code, int) if errors is not None: diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 06efd540..a5a8c888 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -9,11 +9,7 @@ """ -from zlib import decompress -from base64 import b64decode from lxml import etree -from defusedxml.lxml import fromstring -from urllib import quote_plus from xml.dom.minidom import Document from onelogin.saml2.constants import OneLogin_Saml2_Constants @@ -34,7 +30,7 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): Constructs the Logout Request object. :param settings: Setting data - :type request_data: OneLogin_Saml2_Settings + :type settings: OneLogin_Saml2_Settings :param request: Optional. A LogoutRequest to be loaded instead build one. :type request: string @@ -98,13 +94,7 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): 'session_index': session_index_str, } else: - decoded = b64decode(request) - # We try to inflate - try: - inflated = decompress(decoded, -15) - logout_request = inflated - except Exception: - logout_request = decoded + logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(request, ignore_zip=True) self.__logout_request = logout_request @@ -130,7 +120,7 @@ def get_id(request): else: if isinstance(request, Document): request = request.toxml() - elem = fromstring(request) + elem = etree.fromstring(request) return elem.get('ID', None) @staticmethod @@ -149,7 +139,7 @@ def get_nameid_data(request, key=None): else: if isinstance(request, Document): request = request.toxml() - elem = fromstring(request) + elem = etree.fromstring(request) name_id = None encrypted_entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID') @@ -207,7 +197,7 @@ def get_issuer(request): else: if isinstance(request, Document): request = request.toxml() - elem = fromstring(request) + elem = etree.fromstring(request) issuer = None issuer_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:Issuer') @@ -229,7 +219,7 @@ def get_session_indexes(request): else: if isinstance(request, Document): request = request.toxml() - elem = fromstring(request) + elem = etree.fromstring(request) session_indexes = [] session_index_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex') @@ -248,7 +238,7 @@ def is_valid(self, request_data): """ self.__error = None try: - dom = fromstring(self.__logout_request) + dom = etree.fromstring(self.__logout_request) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] @@ -305,25 +295,25 @@ def is_valid(self, request_data): if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: raise Exception('Invalid signAlg in the recieved Logout Request') - signed_query = 'SAMLRequest=%s' % quote_plus(get_data['SAMLRequest']) + signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.escape_url(get_data['SAMLRequest']) if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, quote_plus(get_data['RelayState'])) - signed_query = '%s&SigAlg=%s' % (signed_query, quote_plus(sign_alg)) + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(get_data['RelayState'])) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(sign_alg)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') cert = idp_data['x509cert'] - if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert): + if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, OneLogin_Saml2_Utils.b64decode(get_data['Signature']), cert): raise Exception('Signature validation failed. Logout Request rejected') return True except Exception as err: # pylint: disable=R0801 - self.__error = err.__str__() + self.__error = str(err) debug = self.__settings.is_debug_active() if debug: - print err.__str__() + print(err) return False def get_error(self): diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 1c4572b3..f47e4312 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -9,12 +9,8 @@ """ -from base64 import b64decode -from defusedxml.lxml import fromstring - -from urllib import quote_plus -from xml.dom.minidom import Document -from defusedxml.minidom import parseString +from lxml.etree import fromstring +from xml.dom.minidom import Document, parseString from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils @@ -123,25 +119,25 @@ def is_valid(self, request_data, request_id=None): if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: raise Exception('Invalid signAlg in the recieved Logout Response') - signed_query = 'SAMLResponse=%s' % quote_plus(get_data['SAMLResponse']) + signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.escape_url(get_data['SAMLResponse']) if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, quote_plus(get_data['RelayState'])) - signed_query = '%s&SigAlg=%s' % (signed_query, quote_plus(sign_alg)) + signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(get_data['RelayState'])) + signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(sign_alg)) if 'x509cert' not in idp_data or idp_data['x509cert'] is None: raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') cert = idp_data['x509cert'] - if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert): + if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, OneLogin_Saml2_Utils.b64decode(get_data['Signature']), cert): raise Exception('Signature validation failed. Logout Response rejected') return True # pylint: disable=R0801 except Exception as err: - self.__error = err.__str__() + self.__error = str(err) debug = self.__settings.is_debug_active() if debug: - print err.__str__() + print(err) return False def __query(self, query): diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 2dcb3efc..e0da92c7 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -11,7 +11,7 @@ from time import gmtime, strftime from datetime import datetime -from defusedxml.minidom import parseString +from xml.dom.minidom import parseString from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils @@ -55,7 +55,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N """ if valid_until is None: valid_until = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_VALID - if not isinstance(valid_until, basestring): + if not isinstance(valid_until, OneLogin_Saml2_Utils.str_type): valid_until_time = gmtime(valid_until) valid_until_time = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time) else: @@ -63,7 +63,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N if cache_duration is None: cache_duration = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_CACHED - if not isinstance(cache_duration, basestring): + if not isinstance(cache_duration, OneLogin_Saml2_Utils.str_type): cache_duration_str = 'PT%sS' % cache_duration else: cache_duration_str = cache_duration @@ -188,7 +188,7 @@ def add_x509_key_descriptors(metadata, cert=None): try: xml = parseString(metadata) except Exception as e: - raise Exception('Error parsing metadata. ' + e.message) + raise Exception('Error parsing metadata. ' + str(e)) formated_cert = OneLogin_Saml2_Utils.format_cert(cert, False) x509_certificate = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Certificate') diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index ba7e156c..5e508077 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -9,10 +9,8 @@ """ -from base64 import b64decode from copy import deepcopy from lxml import etree -from defusedxml.lxml import fromstring from xml.dom.minidom import Document from onelogin.saml2.constants import OneLogin_Saml2_Constants @@ -39,8 +37,8 @@ def __init__(self, settings, response): """ self.__settings = settings self.__error = None - self.response = b64decode(response) - self.document = fromstring(self.response) + self.response = OneLogin_Saml2_Utils.b64decode(response) + self.document = etree.fromstring(self.response) self.decrypted_document = None self.encrypted = None @@ -212,10 +210,10 @@ def is_valid(self, request_data, request_id=None): return True except Exception as err: - self.__error = err.__str__() + self.__error = str(err) debug = self.__settings.is_debug_active() if debug: - print err.__str__() + print(err) return False def check_status(self): @@ -256,17 +254,17 @@ def get_issuers(self): :returns: The issuers :rtype: list """ - issuers = [] + issuers = set() message_issuer_nodes = self.__query('/samlp:Response/saml:Issuer') if message_issuer_nodes: - issuers.append(message_issuer_nodes[0].text) + issuers.add(message_issuer_nodes[0].text) assertion_issuer_nodes = self.__query_assertion('/saml:Issuer') if assertion_issuer_nodes: - issuers.append(assertion_issuer_nodes[0].text) + issuers.add(assertion_issuer_nodes[0].text) - return list(set(issuers)) + return list(issuers) def get_nameid_data(self): """ @@ -383,8 +381,8 @@ def __query_assertion(self, xpath_expr): """ Extracts nodes that match the query from the Assertion - :param query: Xpath Expresion - :type query: String + :param xpath_expr: Xpath Expresion + :type xpath_expr: String :returns: The queried nodes :rtype: list diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 5f1e9208..af281d23 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -424,7 +424,7 @@ def check_settings(self, settings): break # Restores the value that had the self.__sp if 'old_sp' in locals(): - self.__sp = old_sp + self.__sp = locals()['old_sp'] return errors @@ -606,7 +606,7 @@ def validate_metadata(self, xml): :rtype: list """ - assert isinstance(xml, basestring) + assert isinstance(xml, OneLogin_Saml2_Utils.str_type) if len(xml) == 0: raise Exception('Empty string supplied as input') @@ -624,7 +624,7 @@ def validate_metadata(self, xml): if len(element.getElementsByTagName('md:SPSSODescriptor')) != 1: errors.append('onlySPSSODescriptor_allowed_xml') else: - valid_until = cache_duration = expire_time = None + valid_until = cache_duration = None if element.hasAttribute('validUntil'): valid_until = OneLogin_Saml2_Utils.parse_SAML_to_time(element.getAttribute('validUntil')) @@ -671,7 +671,7 @@ def set_strict(self, value): Activates or deactivates the strict mode. :param value: Strict parameter - :type xml: boolean + :type value: boolean """ assert isinstance(value, bool) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index cc284620..ef75eb4a 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -9,31 +9,33 @@ """ +from __future__ import absolute_import, print_function, with_statement + import base64 from datetime import datetime import calendar from hashlib import sha1 from isodate import parse_duration as duration_parser from lxml import etree -from defusedxml.lxml import tostring, fromstring -from os.path import basename, dirname, join +from lxml.etree import tostring, fromstring +from os.path import dirname, join import re from sys import stderr -from tempfile import NamedTemporaryFile from textwrap import wrap -from urllib import quote_plus from uuid import uuid4 -from xml.dom.minidom import Document, Element -from defusedxml.minidom import parseString +from xml.dom.minidom import Document, Element, parseString import zlib - -import dm.xmlsec.binding as xmlsec -from dm.xmlsec.binding.tmpl import EncData, Signature +import xmlsec from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.errors import OneLogin_Saml2_Error +try: + from urllib.parse import quote_plus # py3 +except ImportError: + from urllib import quote_plus # py2 + def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg): """ @@ -50,7 +52,7 @@ def print_xmlsec_errors(filename, line, func, error_object, error_subject, reaso if reason != 1: info.append("errno=%d" % reason) if info: - print "%s:%d(%s)" % (filename, line, func), " ".join(info) + print("%s:%d(%s)" % (filename, line, func), " ".join(info)) class OneLogin_Saml2_Utils(object): @@ -60,18 +62,91 @@ class OneLogin_Saml2_Utils(object): urls, add sign, encrypt, decrypt, sign validation, handle xml ... """ + if isinstance(b'', type('')): # py 2.x + text_types = (basestring,) # noqa + str_type = basestring # noqa + + @staticmethod + def utf8(s): + """ return utf8-encoded string """ + if isinstance(s, basestring): + return s.decode("utf8") + return unicode(s) + + @staticmethod + def string(s): + """ return string """ + if isinstance(s, unicode): + return s.encode("utf8") + return str(s) + + @staticmethod + def bytes(s): + """ return bytes """ + return str(s) + else: # py 3.x + str_type = str + text_types = (bytes, str) + + @staticmethod + def utf8(s): + """ return utf8-encoded string """ + if isinstance(s, bytes): + return s.decode("utf8") + return str(s) + + @staticmethod + def string(s): + """convert to string""" + if isinstance(s, bytes): + return s.decode("utf8") + return str(s) + + @staticmethod + def bytes(s): + """return bytes""" + if isinstance(s, str): + return s.encode("utf8") + return bytes(s) @staticmethod - def decode_base64_and_inflate(value): + def escape_url(url): + """ + escape the non-safe symbols in url + :param url: the url to escape + :type url: str + :return: the escaped url + :rtype str + """ + return quote_plus(url) + + @staticmethod + def b64encode(s): + """base64 encode""" + return OneLogin_Saml2_Utils.string(base64.b64encode(OneLogin_Saml2_Utils.bytes(s))) + + @staticmethod + def b64decode(s): + """base64 decode""" + return base64.b64decode(s) + + @staticmethod + def decode_base64_and_inflate(value, ignore_zip=False): """ base64 decodes and then inflates according to RFC1951 :param value: a deflated and encoded string :type value: string + :param ignore_zip: ignore zip errors :returns: the string after decoding and inflating :rtype: string """ - - return zlib.decompress(base64.b64decode(value), -15) + encoded = OneLogin_Saml2_Utils.b64decode(value) + try: + return zlib.decompress(encoded, -15) + except zlib.error: + if not ignore_zip: + raise + return encoded @staticmethod def deflate_and_base64_encode(value): @@ -82,7 +157,7 @@ def deflate_and_base64_encode(value): :returns: The deflated and encoded string :rtype: string """ - return base64.b64encode(zlib.compress(value)[2:-4]) + return OneLogin_Saml2_Utils.b64encode(zlib.compress(OneLogin_Saml2_Utils.bytes(value))[2:-4]) @staticmethod def validate_xml(xml, schema, debug=False): @@ -97,8 +172,8 @@ def validate_xml(xml, schema, debug=False): :returns: Error code or the DomDocument of the xml :rtype: string """ - assert isinstance(xml, basestring) or isinstance(xml, Document) or isinstance(xml, etree._Element) - assert isinstance(schema, basestring) + assert isinstance(xml, OneLogin_Saml2_Utils.text_types) or isinstance(xml, Document) or isinstance(xml, etree._Element) + assert isinstance(schema, OneLogin_Saml2_Utils.str_type) if isinstance(xml, Document): xml = xml.toxml() @@ -107,7 +182,7 @@ def validate_xml(xml, schema, debug=False): # Switch to lxml for schema validation try: - dom = fromstring(str(xml)) + dom = fromstring(xml) except Exception: return 'unloaded_xml' @@ -204,7 +279,7 @@ def redirect(url, parameters={}, request_data={}): :returns: Url :rtype: string """ - assert isinstance(url, basestring) + assert isinstance(url, OneLogin_Saml2_Utils.str_type) assert isinstance(parameters, dict) if url.startswith('/'): @@ -226,15 +301,15 @@ def redirect(url, parameters={}, request_data={}): for name, value in parameters.items(): if value is None: - param = quote_plus(name) + param = OneLogin_Saml2_Utils.escape_url(name) elif isinstance(value, list): param = '' for val in value: - param += quote_plus(name) + '[]=' + quote_plus(val) + '&' + param += OneLogin_Saml2_Utils.escape_url(name) + '[]=' + OneLogin_Saml2_Utils.escape_url(val) + '&' if len(param) > 0: param = param[0:-1] else: - param = quote_plus(name) + '=' + quote_plus(value) + param = OneLogin_Saml2_Utils.escape_url(name) + '=' + OneLogin_Saml2_Utils.escape_url(value) if param: url += param_prefix + param @@ -294,7 +369,7 @@ def get_self_host(request_data): current_host_data = current_host.split(':') possible_port = current_host_data[-1] try: - possible_port = float(possible_port) + int(possible_port) current_host = current_host_data[0] except ValueError: current_host = ':'.join(current_host_data) @@ -391,7 +466,7 @@ def generate_unique_id(): :return: A unique string :rtype: string """ - return 'ONELOGIN_%s' % sha1(uuid4().hex).hexdigest() + return 'ONELOGIN_%s' % sha1(OneLogin_Saml2_Utils.bytes(uuid4().hex)).hexdigest() @staticmethod def parse_time_to_SAML(time): @@ -414,7 +489,7 @@ def parse_SAML_to_time(timestr): Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(\.s+)?Z to a UNIX timestamp. The sub-second part is ignored. - :param time: The time we should convert (SAML Timestamp). + :param timestr: The time we should convert (SAML Timestamp). :type: string :return: Converted to a unix timestamp. @@ -449,7 +524,7 @@ def parse_duration(duration, timestamp=None): :return: The new timestamp, after the duration is applied. :rtype: int """ - assert isinstance(duration, basestring) + assert isinstance(duration, OneLogin_Saml2_Utils.str_type) assert timestamp is None or isinstance(timestamp, int) timedelta = duration_parser(duration) @@ -532,7 +607,7 @@ def calculate_x509_fingerprint(x509_cert): :returns: Formated fingerprint :rtype: string """ - assert isinstance(x509_cert, basestring) + assert isinstance(x509_cert, OneLogin_Saml2_Utils.str_type) lines = x509_cert.split('\n') data = '' @@ -554,7 +629,7 @@ def calculate_x509_fingerprint(x509_cert): data += line # "data" now contains the certificate as a base64-encoded string. The # fingerprint of the certificate is the sha1-hash of the certificate. - return sha1(base64.b64decode(data)).hexdigest().lower() + return sha1(base64.b64decode(OneLogin_Saml2_Utils.bytes(data))).hexdigest().lower() @staticmethod def format_finger_print(fingerprint): @@ -607,32 +682,25 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): xml = name_id_container.toxml() elem = fromstring(xml) - xmlsec.initialize() - - if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + xmlsec.enable_debug_trace(debug) # Load the public cert - mngr = xmlsec.KeysMngr() - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) - key_data = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) - key_data.name = basename(file_cert.name) - mngr.addKey(key_data) - file_cert.close() + manager = xmlsec.KeysManager() + manager.add_key(xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None)) # Prepare for encryption - enc_data = EncData(xmlsec.TransformAes128Cbc, type=xmlsec.TypeEncElement) - enc_data.ensureCipherValue() - key_info = enc_data.ensureKeyInfo() - # enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaPkcs1) - enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaOaep) - enc_key.ensureCipherValue() + enc_data = xmlsec.template.encrypted_data_create( + elem, xmlsec.Transform.AES128, type=xmlsec.EncryptionType.ELEMENT) + xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) + key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_data) + enc_key = xmlsec.template.add_encrypted_key(key_info, xmlsec.Transform.RSA_OAEP) + xmlsec.template.encrypted_data_ensure_cipher_value(enc_key) # Encrypt! - enc_ctx = xmlsec.EncCtx(mngr) - enc_ctx.encKey = xmlsec.Key.generate(xmlsec.KeyDataAes, 128, xmlsec.KeyDataTypeSession) + enc_ctx = xmlsec.EncryptionContext(manager) + enc_ctx.key = xmlsec.Key.generate(xmlsec.KeyData.AES, 128, xmlsec.KeyDataType.SESSION) - edata = enc_ctx.encryptXml(enc_data, elem[0]) + edata = enc_ctx.encrypt_xml(enc_data, elem[0]) newdoc = parseString(etree.tostring(edata)) @@ -645,9 +713,9 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): nodes = newdoc.getElementsByTagName("*") for node in nodes: - if node.tagName == 'ns0:KeyInfo': + if node.tagName == 'KeyInfo': node.tagName = 'dsig:KeyInfo' - node.removeAttribute('xmlns:ns0') + node.removeAttribute('xmlns') node.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS) else: node.tagName = 'xenc:' + node.tagName @@ -713,38 +781,17 @@ def decrypt_element(encrypted_data, key, debug=False): """ if isinstance(encrypted_data, Element): encrypted_data = fromstring(str(encrypted_data.toxml())) - elif isinstance(encrypted_data, basestring): - encrypted_data = fromstring(str(encrypted_data)) - - xmlsec.initialize() - - if debug: - xmlsec.set_error_callback(print_xmlsec_errors) + elif isinstance(encrypted_data, OneLogin_Saml2_Utils.str_type): + encrypted_data = fromstring(encrypted_data) - mngr = xmlsec.KeysMngr() + xmlsec.enable_debug_trace(debug) + manager = xmlsec.KeysManager() - key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) - mngr.addKey(key) - enc_ctx = xmlsec.EncCtx(mngr) + manager.add_key(xmlsec.Key.from_memory(key, xmlsec.KeyFormat.PEM, None)) + enc_ctx = xmlsec.EncryptionContext(manager) return enc_ctx.decrypt(encrypted_data) - @staticmethod - def write_temp_file(content): - """ - Writes some content into a temporary file and returns it. - - :param content: The file content - :type: string - - :returns: The temporary file - :rtype: file-like object - """ - f_temp = NamedTemporaryFile(delete=True) - f_temp.file.write(content) - f_temp.file.flush() - return f_temp - @staticmethod def add_sign(xml, key, cert, debug=False): """ @@ -769,32 +816,29 @@ def add_sign(xml, key, cert, debug=False): elem = xml elif isinstance(xml, Document): xml = xml.toxml() - elem = fromstring(str(xml)) + elem = fromstring(xml) elif isinstance(xml, Element): xml.setAttributeNS( - unicode(OneLogin_Saml2_Constants.NS_SAMLP), + OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAMLP), 'xmlns:samlp', - unicode(OneLogin_Saml2_Constants.NS_SAMLP) + OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAMLP) ) xml.setAttributeNS( - unicode(OneLogin_Saml2_Constants.NS_SAML), + OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAML), 'xmlns:saml', - unicode(OneLogin_Saml2_Constants.NS_SAML) + OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAML) ) xml = xml.toxml() - elem = fromstring(str(xml)) - elif isinstance(xml, basestring): - elem = fromstring(str(xml)) + elem = fromstring(xml) + elif isinstance(xml, OneLogin_Saml2_Utils.text_types): + elem = fromstring(xml) else: raise Exception('Error parsing xml string') - xmlsec.initialize() - - if debug: - xmlsec.set_error_callback(print_xmlsec_errors) - + xmlsec.enable_debug_trace(debug) + xmlsec.tree.add_ids(elem, ["ID"]) # Sign the metadacta with our private key. - signature = Signature(xmlsec.TransformExclC14N, xmlsec.TransformRsaSha1) + signature = xmlsec.template.create(elem, xmlsec.Transform.EXCL_C14N, xmlsec.Transform.RSA_SHA1, ns='ds') issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer') if len(issuer) > 0: @@ -803,37 +847,24 @@ def add_sign(xml, key, cert, debug=False): else: elem[0].insert(0, signature) - ref = signature.addReference(xmlsec.TransformSha1) - ref.addTransform(xmlsec.TransformEnveloped) - ref.addTransform(xmlsec.TransformExclC14N) - - key_info = signature.ensureKeyInfo() - key_info.addX509Data() + elem_id = elem.get('ID', None) + if elem_id: + elem_id = '#' + elem_id - dsig_ctx = xmlsec.DSigCtx() - sign_key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + ref = xmlsec.template.add_reference(signature, xmlsec.Transform.SHA1, uri=elem_id) + xmlsec.template.add_transform(ref, xmlsec.Transform.ENVELOPED) + xmlsec.template.add_transform(ref, xmlsec.Transform.EXCL_C14N) + key_info = xmlsec.template.ensure_key_info(signature) + xmlsec.template.add_x509_data(key_info) - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) - sign_key.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem) - file_cert.close() + dsig_ctx = xmlsec.SignatureContext() + sign_key = xmlsec.Key.from_memory(key, xmlsec.KeyFormat.PEM, None) + sign_key.load_cert_from_memory(cert, xmlsec.KeyFormat.PEM) - dsig_ctx.signKey = sign_key + dsig_ctx.key = sign_key dsig_ctx.sign(signature) newdoc = parseString(etree.tostring(elem)) - - signature_nodes = newdoc.getElementsByTagName("Signature") - - for signature in signature_nodes: - signature.removeAttribute('xmlns') - signature.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) - if not signature.tagName.startswith('ds:'): - signature.tagName = 'ds:' + signature.tagName - nodes = signature.getElementsByTagName("*") - for node in nodes: - if not node.tagName.startswith('ds:'): - node.tagName = 'ds:' + node.tagName - return newdoc.saveXML(newdoc.firstChild) @staticmethod @@ -863,31 +894,27 @@ def validate_sign(xml, cert=None, fingerprint=None, validatecert=False, debug=Fa elem = xml elif isinstance(xml, Document): xml = xml.toxml() - elem = fromstring(str(xml)) + elem = fromstring(xml) elif isinstance(xml, Element): xml.setAttributeNS( - unicode(OneLogin_Saml2_Constants.NS_SAMLP), + OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAMLP), 'xmlns:samlp', - unicode(OneLogin_Saml2_Constants.NS_SAMLP) + OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAMLP) ) xml.setAttributeNS( - unicode(OneLogin_Saml2_Constants.NS_SAML), + OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAML), 'xmlns:saml', - unicode(OneLogin_Saml2_Constants.NS_SAML) + OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAML) ) xml = xml.toxml() elem = fromstring(str(xml)) - elif isinstance(xml, basestring): - elem = fromstring(str(xml)) + elif isinstance(xml, OneLogin_Saml2_Utils.text_types): + elem = fromstring(xml) else: raise Exception('Error parsing xml string') - xmlsec.initialize() - - if debug: - xmlsec.set_error_callback(print_xmlsec_errors) - - xmlsec.addIDs(elem, ["ID"]) + xmlsec.enable_debug_trace(debug) + xmlsec.tree.add_ids(elem, ["ID"]) signature_nodes = OneLogin_Saml2_Utils.query(elem, '//ds:Signature') @@ -906,30 +933,27 @@ def validate_sign(xml, cert=None, fingerprint=None, validatecert=False, debug=Fa if cert is None or cert == '': return False - dsig_ctx = xmlsec.DSigCtx() - - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) - if validatecert: - mngr = xmlsec.KeysMngr() - mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted) - dsig_ctx = xmlsec.DSigCtx(mngr) + manager = xmlsec.KeysManager() + manager.load_cert_from_memory(cert, xmlsec.KeyFormat.CERT_PEM, xmlsec.KeyDataType.TRUSTED) + dsig_ctx = xmlsec.SignatureContext(manager) else: - dsig_ctx = xmlsec.DSigCtx() - dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + dsig_ctx = xmlsec.SignatureContext() + dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None) - file_cert.close() - - dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) + dsig_ctx.set_enabled_key_data([xmlsec.KeyData.X509]) dsig_ctx.verify(signature_node) return True else: return False - except Exception: + except xmlsec.Error as e: + if debug: + print(e) + return False @staticmethod - def validate_binary_sign(signed_query, signature, cert=None, algorithm=xmlsec.TransformRsaSha1, debug=False): + def validate_binary_sign(signed_query, signature, cert=None, algorithm=xmlsec.Transform.RSA_SHA1, debug=False): """ Validates signed bynary data (Used to validate GET Signature). @@ -950,18 +974,14 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=xmlsec.Tr :type: bool """ try: - xmlsec.initialize() - - if debug: - xmlsec.set_error_callback(print_xmlsec_errors) - - dsig_ctx = xmlsec.DSigCtx() - - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) - dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) - file_cert.close() - - dsig_ctx.verifyBinary(signed_query, algorithm, signature) + xmlsec.enable_debug_trace(debug) + dsig_ctx = xmlsec.SignatureContext() + dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None) + dsig_ctx.verify_binary(OneLogin_Saml2_Utils.bytes(signed_query), + algorithm, + OneLogin_Saml2_Utils.bytes(signature)) return True - except Exception: + except xmlsec.Error as e: + if debug: + print(e) return False diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index cf9eb648..6e8d2980 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -7,7 +7,6 @@ import json from os.path import dirname, join, exists import unittest -from urlparse import urlparse, parse_qs from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.constants import OneLogin_Saml2_Constants @@ -15,6 +14,11 @@ from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs + class OneLogin_Saml2_Auth_Test(unittest.TestCase): data_path = join(dirname(__file__), '..', '..', '..', 'data') @@ -118,13 +122,7 @@ def testProcessNoResponse(self): Case No Response, An exception is throw """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) - - try: - auth.process_response() - self.assertFalse(True) - except Exception as e: - self.assertIn('SAML Response not found', e.message) - + self.assertRaisesRegexp(Exception, 'SAML Response not found', auth.process_response) self.assertEqual(auth.get_errors(), ['invalid_binding']) def testProcessResponseInvalid(self): @@ -156,12 +154,12 @@ def testProcessResponseInvalidRequestId(self): """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) - plain_message = b64decode(message) + plain_message = OneLogin_Saml2_Utils.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': b64encode(plain_message) + 'SAMLResponse': OneLogin_Saml2_Utils.string(b64encode(OneLogin_Saml2_Utils.bytes(plain_message))) } auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) request_id = 'invalid' @@ -237,10 +235,7 @@ def testProcessNoSLO(self): Case No Message, An exception is throw """ auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) - try: - auth.process_slo(True) - except Exception as e: - self.assertIn('SAML LogoutRequest/LogoutResponse not found', e.message) + self.assertRaisesRegexp(Exception, 'SAML LogoutRequest/LogoutResponse not found', auth.process_slo, True) self.assertEqual(auth.get_errors(), ['invalid_binding']) def testProcessSLOResponseInvalid(self): @@ -273,7 +268,7 @@ def testProcessSLOResponseNoSucess(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_responses', 'invalids', 'status_code_responder.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -292,7 +287,7 @@ def testProcessSLOResponseRequestId(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -316,7 +311,7 @@ def testProcessSLOResponseValid(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -353,7 +348,7 @@ def testProcessSLOResponseValidDeletingSession(self): # $_SESSION['samltest'] = true; # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -408,7 +403,7 @@ def testProcessSLORequestNotOnOrAfterFailed(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -429,7 +424,7 @@ def testProcessSLORequestDeletingSession(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -477,7 +472,7 @@ def testProcessSLORequestRelayState(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -505,7 +500,7 @@ def testProcessSLORequestSignedResponse(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -589,7 +584,6 @@ def testLoginForceAuthN(self): """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' - sso_url = settings_info['idp']['singleSignOnService']['url'] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url = auth.login(return_to) @@ -597,7 +591,7 @@ def testLoginForceAuthN(self): sso_url = settings_info['idp']['singleSignOnService']['url'] self.assertIn(sso_url, target_url) self.assertIn('SAMLRequest', parsed_query) - request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]) + request = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])) self.assertNotIn('ForceAuthn="true"', request) auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) @@ -605,7 +599,7 @@ def testLoginForceAuthN(self): parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) self.assertIn(sso_url, target_url_2) self.assertIn('SAMLRequest', parsed_query_2) - request_2 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0]) + request_2 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0])) self.assertNotIn('ForceAuthn="true"', request_2) auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) @@ -613,7 +607,7 @@ def testLoginForceAuthN(self): parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) self.assertIn(sso_url, target_url_3) self.assertIn('SAMLRequest', parsed_query_3) - request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0]) + request_3 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])) self.assertIn('ForceAuthn="true"', request_3) def testLoginIsPassive(self): @@ -631,7 +625,7 @@ def testLoginIsPassive(self): sso_url = settings_info['idp']['singleSignOnService']['url'] self.assertIn(sso_url, target_url) self.assertIn('SAMLRequest', parsed_query) - request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]) + request = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])) self.assertNotIn('IsPassive="true"', request) auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) @@ -639,7 +633,7 @@ def testLoginIsPassive(self): parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) self.assertIn(sso_url, target_url_2) self.assertIn('SAMLRequest', parsed_query_2) - request_2 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0]) + request_2 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0])) self.assertNotIn('IsPassive="true"', request_2) auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) @@ -647,7 +641,7 @@ def testLoginIsPassive(self): parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) self.assertIn(sso_url, target_url_3) self.assertIn('SAMLRequest', parsed_query_3) - request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0]) + request_3 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])) self.assertIn('IsPassive="true"', request_3) def testLogout(self): @@ -717,13 +711,9 @@ def testLogoutNoSLO(self): settings_info = self.loadSettingsJSON() del settings_info['idp']['singleLogoutService'] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) - - try: - # The Header of the redirect produces an Exception - auth.logout('http://example.com/returnto') - self.assertFalse(True) - except Exception as e: - self.assertIn('The IdP does not support Single Log Out', e.message) + # The Header of the redirect produces an Exception + self.assertRaisesRegexp(Exception, 'The IdP does not support Single Log Out', + auth.logout, 'http://example.com/returnto') def testLogoutNameIDandSessionIndex(self): """ @@ -791,11 +781,7 @@ def testSetStrict(self): settings = auth.get_settings() self.assertFalse(settings.is_strict()) - try: - auth.set_strict('42') - self.assertFalse(True) - except Exception as e: - self.assertTrue(isinstance(e, AssertionError)) + self.assertRaises(AssertionError, auth.set_strict, '42') def testBuildRequestSignature(self): """ @@ -813,10 +799,8 @@ def testBuildRequestSignature(self): settings['sp']['privatekey'] = '' settings['custom_base_path'] = u'invalid/path/' auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) - try: - auth2.build_request_signature(message, relay_state) - except Exception as e: - self.assertIn("Trying to sign the SAMLRequest but can't load the SP private key", e.message) + self.assertRaisesRegexp(Exception, "Trying to sign the SAMLRequest but can't load the SP private key", + auth2.build_request_signature, message, relay_state) def testBuildResponseSignature(self): """ @@ -834,7 +818,5 @@ def testBuildResponseSignature(self): settings['sp']['privatekey'] = '' settings['custom_base_path'] = u'invalid/path/' auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) - try: - auth2.build_response_signature(message, relay_state) - except Exception as e: - self.assertIn("Trying to sign the SAMLResponse but can't load the SP private key", e.message) + self.assertRaisesRegexp(Exception, "Trying to sign the SAMLRequest but can't load the SP private key", + auth2.build_request_signature, message, relay_state) diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 2d5a0a2f..a7d0e8aa 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -3,18 +3,20 @@ # Copyright (c) 2014, OneLogin, Inc. # All rights reserved. -from base64 import b64decode import json from os.path import dirname, join, exists import unittest -from urlparse import urlparse, parse_qs -from zlib import decompress from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs + class OneLogin_Saml2_Authn_Request_Test(unittest.TestCase): def loadSettingsJSON(self): @@ -47,8 +49,7 @@ def testCreateRequest(self): authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() - decoded = b64decode(authn_request_encoded) - inflated = decompress(decoded, -15) + inflated = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^') self.assertRegexpMatches(inflated, 'http://stuff.com/endpoints/metadata.php') diff --git a/tests/src/OneLogin/saml2_tests/error_test.py b/tests/src/OneLogin/saml2_tests/error_test.py index 76e24308..8758f736 100644 --- a/tests/src/OneLogin/saml2_tests/error_test.py +++ b/tests/src/OneLogin/saml2_tests/error_test.py @@ -14,4 +14,4 @@ class OneLogin_Saml2_Error_Test(unittest.TestCase): def runTest(self): exception = OneLogin_Saml2_Error('test') - self.assertEqual(exception.message, 'test') + self.assertEqual(str(exception), 'test') diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index da201817..971b09fe 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -3,17 +3,20 @@ # Copyright (c) 2014, OneLogin, Inc. # All rights reserved. -from base64 import b64encode import json from os.path import dirname, join, exists import unittest -from urlparse import urlparse, parse_qs from xml.dom.minidom import parseString from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs + class OneLogin_Saml2_Logout_Request_Test(unittest.TestCase): data_path = join(dirname(__file__), '..', '..', '..', 'data') @@ -50,7 +53,7 @@ def testConstructor(self): url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) payload = exploded['SAMLRequest'][0] - inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(payload) + inflated = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)) self.assertRegexpMatches(inflated, '^invalid') + request = OneLogin_Saml2_Utils.b64encode('invalid') request_data = { 'http_host': 'example.com', 'script_name': 'index.html', @@ -231,16 +222,16 @@ def testIsInvalidIssuer(self): current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + logout_request = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) try: - logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) valid = logout_request2.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertIn('Invalid issuer in the Logout Request', e.message) + self.assertIn('Invalid issuer in the Logout Request', str(e)) def testIsInvalidDestination(self): """ @@ -253,24 +244,24 @@ def testIsInvalidDestination(self): } request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + logout_request = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) try: - logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) valid = logout_request2.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertIn('The LogoutRequest was received at', e.message) + self.assertIn('The LogoutRequest was received at', str(e)) dom = parseString(request) dom.documentElement.setAttribute('Destination', None) - logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + logout_request3 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(dom.toxml())) self.assertTrue(logout_request3.is_valid(request_data)) dom.documentElement.removeAttribute('Destination') - logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + logout_request4 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(dom.toxml())) self.assertTrue(logout_request4.is_valid(request_data)) def testIsInvalidNotOnOrAfter(self): @@ -287,16 +278,16 @@ def testIsInvalidNotOnOrAfter(self): request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + logout_request = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) try: - logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) valid = logout_request2.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertIn('Timing issues (please check your clock settings)', e.message) + self.assertIn('Timing issues (please check your clock settings)', str(e)) def testIsValid(self): """ @@ -309,25 +300,25 @@ def testIsValid(self): request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + logout_request = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) settings.set_strict(True) - logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) self.assertFalse(logout_request2.is_valid(request_data)) settings.set_strict(False) dom = parseString(request) - logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + logout_request3 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(dom.toxml())) self.assertTrue(logout_request3.is_valid(request_data)) settings.set_strict(True) - logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + logout_request4 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(dom.toxml())) self.assertFalse(logout_request4.is_valid(request_data)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) - logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + logout_request5 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) self.assertTrue(logout_request5.is_valid(request_data)) def testIsValidSign(self): @@ -347,34 +338,33 @@ def testIsValidSign(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) - request = OneLogin_Saml2_Utils.decode_base64_and_inflate(request_data['get_data']['SAMLRequest']) + request = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(request_data['get_data']['SAMLRequest'])) settings.set_strict(False) - logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + logout_request = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) self.assertTrue(logout_request.is_valid(request_data)) - relayState = request_data['get_data']['RelayState'] + relay_state = request_data['get_data']['RelayState'] del request_data['get_data']['RelayState'] self.assertFalse(logout_request.is_valid(request_data)) - request_data['get_data']['RelayState'] = relayState + request_data['get_data']['RelayState'] = relay_state settings.set_strict(True) try: - logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) valid = logout_request2.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertIn('The LogoutRequest was received at', e.message) + self.assertIn('The LogoutRequest was received at', str(e)) settings.set_strict(False) old_signature = request_data['get_data']['Signature'] request_data['get_data']['Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333=' + logout_request3 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) try: - logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) - valid = logout_request3.is_valid(request_data) - self.assertFalse(valid) + self.assertFalse(logout_request3.is_valid(request_data)) except Exception as e: - self.assertIn('Signature validation failed. Logout Request rejected', e.message) + self.assertIn('Signature validation failed. Logout Request rejected', str(e)) request_data['get_data']['Signature'] = old_signature old_signature_algorithm = request_data['get_data']['SigAlg'] @@ -386,33 +376,33 @@ def testIsValidSign(self): valid = logout_request3.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertIn('Signature validation failed. Logout Request rejected', e.message) + self.assertIn('Signature validation failed. Logout Request rejected', str(e)) settings.set_strict(True) request_2 = request.replace('https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url) request_2 = request_2.replace('https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/') request_data['get_data']['SAMLRequest'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(request_2) try: - logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(request_2)) + logout_request4 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request_2)) valid = logout_request4.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertIn('Signature validation failed. Logout Request rejected', e.message) + self.assertIn('Signature validation failed. Logout Request rejected', str(e)) settings.set_strict(False) + logout_request5 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request_2)) try: - logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request_2)) valid = logout_request5.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertIn('Signature validation failed. Logout Request rejected', e.message) + self.assertIn('Signature validation failed. Logout Request rejected', str(e)) request_data['get_data']['SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' try: valid = logout_request5.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertIn('Invalid signAlg in the recieved Logout Request', e.message) + self.assertIn('Invalid signAlg in the recieved Logout Request', str(e)) settings_info = self.loadSettingsJSON() settings_info['strict'] = True @@ -422,19 +412,19 @@ def testIsValidSign(self): old_signature = request_data['get_data']['Signature'] del request_data['get_data']['Signature'] try: - logout_request6 = OneLogin_Saml2_Logout_Request(settings, b64encode(request_2)) + logout_request6 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request_2)) valid = logout_request6.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertIn('The Message of the Logout Request is not signed and the SP require it', e.message) + self.assertIn('The Message of the Logout Request is not signed and the SP require it', str(e)) request_data['get_data']['Signature'] = old_signature settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9' del settings_info['idp']['x509cert'] settings_2 = OneLogin_Saml2_Settings(settings_info) try: - logout_request7 = OneLogin_Saml2_Logout_Request(settings_2, b64encode(request_2)) + logout_request7 = OneLogin_Saml2_Logout_Request(settings_2, OneLogin_Saml2_Utils.b64encode(request_2)) valid = logout_request7.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertIn('In order to validate the sign on the Logout Request, the x509cert of the IdP is required', e.message) + self.assertIn('In order to validate the sign on the Logout Request, the x509cert of the IdP is required', str(e)) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 5b618b8d..5b02e5a5 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -6,7 +6,6 @@ import json from os.path import dirname, join, exists import unittest -from urlparse import urlparse, parse_qs from xml.dom.minidom import parseString from onelogin.saml2.constants import OneLogin_Saml2_Constants @@ -14,6 +13,11 @@ from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs + class OneLogin_Saml2_Logout_Response_Test(unittest.TestCase): data_path = join(dirname(__file__), '..', '..', '..', 'data') @@ -59,7 +63,7 @@ def testCreateDeflatedSAMLLogoutResponseURLParameter(self): self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLResponse=') url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) - inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(exploded['SAMLResponse'][0]) + inflated = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(exploded['SAMLResponse'][0])) self.assertRegexpMatches(inflated, '^urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', signed_metadata) - self.assertIn('', signed_metadata) + self.assertIn('\n', signed_metadata) self.assertIn('', signed_metadata) self.assertIn('\n', signed_metadata) + self.assertIn('\n\n', signed_metadata) - try: - OneLogin_Saml2_Metadata.sign_metadata('', key, cert) - self.assertTrue(False) - except Exception as e: - self.assertIn('Empty string supplied as input', e.message) + self.assertRaisesRegexp(Exception, 'Empty string supplied as input', + OneLogin_Saml2_Metadata.sign_metadata, '', key, cert) def testAddX509KeyDescriptors(self): """ @@ -163,16 +165,10 @@ def testAddX509KeyDescriptors(self): self.assertIn(' something_is_wrong', e.message) + self.assertIn('The status code of the Response was not Success, was Responder -> something_is_wrong', str(e)) def testGetAudiences(self): """ @@ -184,7 +189,7 @@ def testQueryAssertions(self): xml_3 = self.file_contents(join(self.data_path, 'responses', 'double_signed_encrypted_assertion.xml.base64')) response_3 = OneLogin_Saml2_Response(settings, xml_3) - self.assertEqual(['http://idp.example.com/', 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_3.get_issuers()) + self.assertEqual(['http://idp.example.com/', 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], sorted(response_3.get_issuers())) xml_4 = self.file_contents(join(self.data_path, 'responses', 'double_signed_response.xml.base64')) response_4 = OneLogin_Saml2_Response(settings, xml_4) @@ -192,7 +197,7 @@ def testQueryAssertions(self): xml_5 = self.file_contents(join(self.data_path, 'responses', 'signed_message_encrypted_assertion.xml.base64')) response_5 = OneLogin_Saml2_Response(settings, xml_5) - self.assertEqual(['http://idp.example.com/', 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_5.get_issuers()) + self.assertEqual(['http://idp.example.com/', 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], sorted(response_5.get_issuers())) xml_6 = self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response.xml.base64')) response_6 = OneLogin_Saml2_Response(settings, xml_6) @@ -217,7 +222,7 @@ def testGetIssuers(self): xml_3 = self.file_contents(join(self.data_path, 'responses', 'double_signed_encrypted_assertion.xml.base64')) response_3 = OneLogin_Saml2_Response(settings, xml_3) - self.assertEqual(['http://idp.example.com/', 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_3.get_issuers()) + self.assertEqual(['http://idp.example.com/', 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], sorted(response_3.get_issuers())) def testGetSessionIndex(self): """ @@ -268,7 +273,7 @@ def testOnlyRetrieveAssertionWithIDThatMatchesSignatureReference(self): self.assertTrue(response.is_valid(self.get_request_data())) nameid = response.get_nameid() self.assertNotEqual('root@example.com', nameid) - except: + except Exception: self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error()) def testDoesNotAllowSignatureWrappingAttack(self): @@ -305,7 +310,7 @@ def testIsInvalidXML(self): Tests the is_valid method of the OneLogin_Saml2_Response Case Invalid XML """ - message = b64encode('invalid') + message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64encode('invalid')) request_data = { 'http_host': 'example.com', 'script_name': 'index.html', @@ -372,7 +377,7 @@ def testValidateVersion(self): valid = response.is_valid(self.get_request_data()) self.assertFalse(valid) except Exception as e: - self.assertEqual('Reference validation failed', e.message) + self.assertEqual('Reference validation failed', str(e)) def testValidateID(self): """ @@ -386,7 +391,7 @@ def testValidateID(self): valid = response.is_valid(self.get_request_data()) self.assertFalse(valid) except Exception as e: - self.assertEqual('Missing ID attribute on SAML Response', e.message) + self.assertEqual('Missing ID attribute on SAML Response', str(e)) def testIsInValidReference(self): """ @@ -400,7 +405,7 @@ def testIsInValidReference(self): valid = response.is_valid(self.get_request_data()) self.assertFalse(valid) except Exception as e: - self.assertEqual('Reference validation failed', e.message) + self.assertEqual('Reference validation failed', str(e)) def testIsInValidExpired(self): """ @@ -419,7 +424,7 @@ def testIsInValidExpired(self): valid = response_2.is_valid(self.get_request_data()) self.assertFalse(valid) except Exception as e: - self.assertEqual('Timing issues (please check your clock settings)', e.message) + self.assertEqual('Timing issues (please check your clock settings)', str(e)) def testIsInValidNoStatement(self): """ @@ -438,7 +443,7 @@ def testIsInValidNoStatement(self): valid = response_2.is_valid(self.get_request_data()) self.assertFalse(valid) except Exception as e: - self.assertEqual('There is no AttributeStatement on the Response', e.message) + self.assertEqual('There is no AttributeStatement on the Response', str(e)) def testIsInValidNoKey(self): """ @@ -452,7 +457,7 @@ def testIsInValidNoKey(self): valid = response.is_valid(self.get_request_data()) self.assertFalse(valid) except Exception as e: - self.assertEqual('Signature validation failed. SAML Response rejected', e.message) + self.assertEqual('Signature validation failed. SAML Response rejected', str(e)) def testIsInValidMultipleAssertions(self): """ @@ -467,7 +472,7 @@ def testIsInValidMultipleAssertions(self): valid = response.is_valid(self.get_request_data()) self.assertFalse(valid) except Exception as e: - self.assertEqual('SAML Response must contain 1 assertion', e.message) + self.assertEqual('SAML Response must contain 1 assertion', str(e)) def testIsInValidEncAttrs(self): """ @@ -486,7 +491,7 @@ def testIsInValidEncAttrs(self): valid = response_2.is_valid(self.get_request_data()) self.assertFalse(valid) except Exception as e: - self.assertEqual('There is an EncryptedAttribute in the Response and this SP not support them', e.message) + self.assertEqual('There is an EncryptedAttribute in the Response and this SP not support them', str(e)) def testIsInValidDestination(self): """ @@ -506,13 +511,13 @@ def testIsInValidDestination(self): dom = parseString(b64decode(message)) dom.firstChild.setAttribute('Destination', '') - message_2 = b64encode(dom.toxml()) + message_2 = OneLogin_Saml2_Utils.b64encode(dom.toxml()) response_3 = OneLogin_Saml2_Response(settings, message_2) self.assertFalse(response_3.is_valid(self.get_request_data())) self.assertIn('A valid SubjectConfirmation was not found on this Response', response_3.get_error()) dom.firstChild.removeAttribute('Destination') - message_3 = b64encode(dom.toxml()) + message_3 = OneLogin_Saml2_Utils.b64encode(dom.toxml()) response_4 = OneLogin_Saml2_Response(settings, message_3) self.assertFalse(response_4.is_valid(self.get_request_data())) self.assertIn('A valid SubjectConfirmation was not found on this Response', response_4.get_error()) @@ -551,14 +556,14 @@ def testIsInValidIssuer(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_issuer_assertion.xml.base64')) - plain_message = b64decode(xml) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message = b64encode(plain_message) + message = OneLogin_Saml2_Utils.b64encode(plain_message) xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_issuer_message.xml.base64')) - plain_message_2 = b64decode(xml_2) + plain_message_2 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_2)) plain_message_2 = plain_message_2.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message_2 = b64encode(plain_message_2) + message_2 = OneLogin_Saml2_Utils.b64encode(plain_message_2) response = OneLogin_Saml2_Response(settings, message) response.is_valid(request_data) @@ -574,14 +579,14 @@ def testIsInValidIssuer(self): valid = response_3.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertEqual('is not a valid audience for this Response', e.message) + self.assertEqual('is not a valid audience for this Response', str(e)) response_4 = OneLogin_Saml2_Response(settings, message_2) try: valid = response_4.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertEqual('is not a valid audience for this Response', e.message) + self.assertEqual('is not a valid audience for this Response', str(e)) def testIsInValidSessionIndex(self): """ @@ -595,9 +600,9 @@ def testIsInValidSessionIndex(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_sessionindex.xml.base64')) - plain_message = b64decode(xml) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message = b64encode(plain_message) + message = OneLogin_Saml2_Utils.b64encode(plain_message) response = OneLogin_Saml2_Response(settings, message) response.is_valid(request_data) @@ -609,7 +614,7 @@ def testIsInValidSessionIndex(self): valid = response_2.is_valid(request_data) self.assertFalse(valid) except Exception as e: - self.assertEqual('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response', e.message) + self.assertEqual('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response', str(e)) def testDatetimeWithMiliseconds(self): """ @@ -625,9 +630,9 @@ def testDatetimeWithMiliseconds(self): current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response_with_miliseconds.xm.base64')) - plain_message = b64decode(xml) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message = b64encode(plain_message) + message = OneLogin_Saml2_Utils.b64encode(plain_message) response = OneLogin_Saml2_Response(settings, message) response.is_valid(request_data) self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) @@ -644,34 +649,34 @@ def testIsInValidSubjectConfirmation(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_subjectconfirmation_method.xml.base64')) - plain_message = b64decode(xml) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message = b64encode(plain_message) + message = OneLogin_Saml2_Utils.b64encode(plain_message) xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_subjectconfirmation_data.xml.base64')) - plain_message_2 = b64decode(xml_2) + plain_message_2 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_2)) plain_message_2 = plain_message_2.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message_2 = b64encode(plain_message_2) + message_2 = OneLogin_Saml2_Utils.b64encode(plain_message_2) xml_3 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_inresponse.xml.base64')) - plain_message_3 = b64decode(xml_3) + plain_message_3 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_3)) plain_message_3 = plain_message_3.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message_3 = b64encode(plain_message_3) + message_3 = OneLogin_Saml2_Utils.b64encode(plain_message_3) xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_recipient.xml.base64')) - plain_message_4 = b64decode(xml_4) + plain_message_4 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_4)) plain_message_4 = plain_message_4.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message_4 = b64encode(plain_message_4) + message_4 = OneLogin_Saml2_Utils.b64encode(plain_message_4) xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_noa.xml.base64')) - plain_message_5 = b64decode(xml_5) + plain_message_5 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_5)) plain_message_5 = plain_message_5.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message_5 = b64encode(plain_message_5) + message_5 = OneLogin_Saml2_Utils.b64encode(plain_message_5) xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_nb.xml.base64')) - plain_message_6 = b64decode(xml_6) + plain_message_6 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_6)) plain_message_6 = plain_message_6.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message_6 = b64encode(plain_message_6) + message_6 = OneLogin_Saml2_Utils.b64encode(plain_message_6) response = OneLogin_Saml2_Response(settings, message) response.is_valid(request_data) @@ -702,37 +707,37 @@ def testIsInValidSubjectConfirmation(self): try: self.assertFalse(response.is_valid(request_data)) except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message) + self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) response_2 = OneLogin_Saml2_Response(settings, message_2) try: self.assertFalse(response_2.is_valid(request_data)) except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message) + self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) response_3 = OneLogin_Saml2_Response(settings, message_3) try: self.assertFalse(response_3.is_valid(request_data)) except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message) + self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) response_4 = OneLogin_Saml2_Response(settings, message_4) try: self.assertFalse(response_4.is_valid(request_data)) except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message) + self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) response_5 = OneLogin_Saml2_Response(settings, message_5) try: self.assertFalse(response_5.is_valid(request_data)) except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message) + self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) response_6 = OneLogin_Saml2_Response(settings, message_6) try: self.assertFalse(response_6.is_valid(request_data)) except Exception as e: - self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message) + self.assertEqual('A valid SubjectConfirmation was not found on this Response', str(e)) def testIsInValidRequestId(self): """ @@ -746,9 +751,9 @@ def testIsInValidRequestId(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) - plain_message = b64decode(xml) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message = b64encode(plain_message) + message = OneLogin_Saml2_Utils.b64encode(plain_message) response = OneLogin_Saml2_Response(settings, message) request_id = 'invalid' @@ -760,7 +765,7 @@ def testIsInValidRequestId(self): try: self.assertFalse(response.is_valid(request_data, request_id)) except Exception as e: - self.assertEqual('The InResponseTo of the Response', e.message) + self.assertEqual('The InResponseTo of the Response', str(e)) valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38' response.is_valid(request_data, valid_request_id) @@ -778,9 +783,9 @@ def testIsInValidSignIssues(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) - plain_message = b64decode(xml) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message = b64encode(plain_message) + message = OneLogin_Saml2_Utils.b64encode(plain_message) settings_info['security']['wantAssertionsSigned'] = False settings = OneLogin_Saml2_Settings(settings_info) @@ -807,7 +812,7 @@ def testIsInValidSignIssues(self): try: self.assertFalse(response_4.is_valid(request_data)) except Exception as e: - self.assertEqual('The Assertion of the Response is not signed and the SP require it', e.message) + self.assertEqual('The Assertion of the Response is not signed and the SP require it', str(e)) settings_info['security']['wantAssertionsSigned'] = False settings_info['strict'] = False @@ -837,7 +842,7 @@ def testIsInValidSignIssues(self): try: self.assertFalse(response_8.is_valid(request_data)) except Exception as e: - self.assertEqual('The Message of the Response is not signed and the SP require it', e.message) + self.assertEqual('The Message of the Response is not signed and the SP require it', str(e)) def testIsInValidEncIssues(self): """ @@ -851,9 +856,9 @@ def testIsInValidEncIssues(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) - plain_message = b64decode(xml) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) - message = b64encode(plain_message) + message = OneLogin_Saml2_Utils.b64encode(plain_message) settings_info['security']['wantAssertionsEncrypted'] = True settings = OneLogin_Saml2_Settings(settings_info) @@ -921,7 +926,7 @@ def testIsInValidCert(self): try: self.assertFalse(response.is_valid(self.get_request_data())) except Exception as e: - self.assertIn('openssl_x509_read(): supplied parameter cannot be', e.message) + self.assertIn('openssl_x509_read(): supplied parameter cannot be', str(e)) def testIsInValidCert2(self): """ @@ -1012,14 +1017,14 @@ def testIsValidEnc(self): settings.set_strict(True) xml_7 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) # In order to avoid the destination problem - plain_message = b64decode(xml_7) + plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_7)) request_data = { 'http_host': 'example.com', 'script_name': 'index.html' } 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) - message = b64encode(plain_message) + message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64encode(plain_message)) response_7 = OneLogin_Saml2_Response(settings, message) response_7.is_valid(request_data) self.assertEqual('No Signature found. SAML Response rejected', response_7.get_error()) @@ -1062,21 +1067,21 @@ def testIsValidSign(self): dom = parseString(b64decode(xml_4)) dom.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp' - xml_7 = b64encode(dom.toxml()) + xml_7 = OneLogin_Saml2_Utils.b64encode(dom.toxml()) response_7 = OneLogin_Saml2_Response(settings, xml_7) # Modified message self.assertFalse(response_7.is_valid(self.get_request_data())) - dom_2 = parseString(b64decode(xml_5)) + dom_2 = parseString(OneLogin_Saml2_Utils.b64decode(xml_5)) dom_2.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp' - xml_8 = b64encode(dom_2.toxml()) + xml_8 = OneLogin_Saml2_Utils.b64encode(dom_2.toxml()) response_8 = OneLogin_Saml2_Response(settings, xml_8) # Modified message self.assertFalse(response_8.is_valid(self.get_request_data())) - dom_3 = parseString(b64decode(xml_6)) + dom_3 = parseString(OneLogin_Saml2_Utils.b64decode(xml_6)) dom_3.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp' - xml_9 = b64encode(dom_3.toxml()) + xml_9 = OneLogin_Saml2_Utils.b64encode(dom_3.toxml()) response_9 = OneLogin_Saml2_Response(settings, xml_9) # Modified message self.assertFalse(response_9.is_valid(self.get_request_data())) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 6af09960..3db0adbb 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -10,6 +10,11 @@ from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs + class OneLogin_Saml2_Settings_Test(unittest.TestCase): data_path = join(dirname(__file__), '..', '..', '..', 'data') @@ -60,14 +65,14 @@ def testLoadSettingsFromDict(self): settings_2 = OneLogin_Saml2_Settings(settings_info) self.assertNotEqual(len(settings_2.get_errors()), 0) except Exception as e: - self.assertIn('Invalid dict settings: idp_sso_url_invalid', e.message) + self.assertIn('Invalid dict settings: idp_sso_url_invalid', str(e)) settings_info['idp']['singleSignOnService']['url'] = 'http://invalid_domain' try: settings_3 = OneLogin_Saml2_Settings(settings_info) self.assertNotEqual(len(settings_3.get_errors()), 0) except Exception as e: - self.assertIn('Invalid dict settings: idp_sso_url_invalid', e.message) + self.assertIn('Invalid dict settings: idp_sso_url_invalid', str(e)) del settings_info['sp'] del settings_info['idp'] @@ -75,9 +80,9 @@ def testLoadSettingsFromDict(self): settings_4 = OneLogin_Saml2_Settings(settings_info) self.assertNotEqual(len(settings_4.get_errors()), 0) except Exception as e: - self.assertIn('Invalid dict settings', e.message) - self.assertIn('idp_not_found', e.message) - self.assertIn('sp_not_found', e.message) + self.assertIn('Invalid dict settings', str(e)) + self.assertIn('idp_not_found', str(e)) + self.assertIn('sp_not_found', str(e)) settings_info = self.loadSettingsJSON() settings_info['security']['authnRequestsSigned'] = True @@ -86,7 +91,7 @@ def testLoadSettingsFromDict(self): settings_5 = OneLogin_Saml2_Settings(settings_info) self.assertNotEqual(len(settings_5.get_errors()), 0) except Exception as e: - self.assertIn('Invalid dict settings: sp_cert_not_found_and_required', e.message) + self.assertIn('Invalid dict settings: sp_cert_not_found_and_required', str(e)) settings_info = self.loadSettingsJSON() settings_info['security']['nameIdEncrypted'] = True @@ -95,7 +100,7 @@ def testLoadSettingsFromDict(self): settings_6 = OneLogin_Saml2_Settings(settings_info) self.assertNotEqual(len(settings_6.get_errors()), 0) except Exception as e: - self.assertIn('Invalid dict settings: idp_cert_not_found_and_required', e.message) + self.assertIn('Invalid dict settings: idp_cert_not_found_and_required', str(e)) def testLoadSettingsFromInvalidData(self): """ @@ -105,10 +110,10 @@ def testLoadSettingsFromInvalidData(self): invalid_settings = ('param1', 'param2') try: - settings = OneLogin_Saml2_Settings(invalid_settings) + OneLogin_Saml2_Settings(invalid_settings) self.assertTrue(False) except Exception as e: - self.assertIn('Unsupported settings object', e.message) + self.assertIn('Unsupported settings object', str(e)) settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) self.assertEqual(len(settings.get_errors()), 0) @@ -126,7 +131,7 @@ def testLoadSettingsFromFile(self): try: OneLogin_Saml2_Settings(custom_base_path=custom_base_path) except Exception as e: - self.assertIn('Settings file not found', e.message) + self.assertIn('Settings file not found', str(e)) custom_base_path = join(dirname(__file__), '..', '..', '..', 'data', 'customPath') settings_3 = OneLogin_Saml2_Settings(custom_base_path=custom_base_path) @@ -270,15 +275,15 @@ def testCheckSettings(self): OneLogin_Saml2_Settings(settings_info) self.assertTrue(False) except Exception as e: - self.assertIn('Invalid dict settings: invalid_syntax', e.message) + self.assertIn('Invalid dict settings: invalid_syntax', str(e)) settings_info['strict'] = True try: OneLogin_Saml2_Settings(settings_info) self.assertTrue(False) except Exception as e: - self.assertIn('idp_not_found', e.message) - self.assertIn('sp_not_found', e.message) + self.assertIn('idp_not_found', str(e)) + self.assertIn('sp_not_found', str(e)) settings_info['idp'] = {} settings_info['idp']['x509cert'] = '' @@ -290,10 +295,10 @@ def testCheckSettings(self): OneLogin_Saml2_Settings(settings_info) self.assertTrue(False) except Exception as e: - self.assertIn('idp_entityId_not_found', e.message) - self.assertIn('idp_sso_not_found', e.message) - self.assertIn('sp_entityId_not_found', e.message) - self.assertIn('sp_acs_not_found', e.message) + self.assertIn('idp_entityId_not_found', str(e)) + self.assertIn('idp_sso_not_found', str(e)) + self.assertIn('sp_entityId_not_found', str(e)) + self.assertIn('sp_acs_not_found', str(e)) settings_info['idp']['entityID'] = 'entityId' settings_info['idp']['singleSignOnService'] = {} @@ -308,17 +313,17 @@ def testCheckSettings(self): OneLogin_Saml2_Settings(settings_info) self.assertTrue(False) except Exception as e: - self.assertIn('idp_sso_url_invalid', e.message) - self.assertIn('idp_slo_url_invalid', e.message) - self.assertIn('sp_acs_url_invalid', e.message) - self.assertIn('sp_sls_url_invalid', e.message) + self.assertIn('idp_sso_url_invalid', str(e)) + self.assertIn('idp_slo_url_invalid', str(e)) + self.assertIn('sp_acs_url_invalid', str(e)) + self.assertIn('sp_sls_url_invalid', str(e)) settings_info['security']['wantAssertionsSigned'] = True try: OneLogin_Saml2_Settings(settings_info) self.assertTrue(False) except Exception as e: - self.assertIn('idp_cert_or_fingerprint_not_found_and_required', e.message) + self.assertIn('idp_cert_or_fingerprint_not_found_and_required', str(e)) settings_info = self.loadSettingsJSON() settings_info['security']['signMetadata'] = {} @@ -341,9 +346,9 @@ def testCheckSettings(self): OneLogin_Saml2_Settings(settings_info) self.assertTrue(False) except Exception as e: - self.assertIn('sp_signMetadata_invalid', e.message) - self.assertIn('organization_not_enought_data', e.message) - self.assertIn('contact_type_invalid', e.message) + self.assertIn('sp_signMetadata_invalid', str(e)) + self.assertIn('organization_not_enought_data', str(e)) + self.assertIn('contact_type_invalid', str(e)) def testGetSPMetadata(self): """ @@ -381,7 +386,7 @@ def testGetSPMetadataSigned(self): self.assertIn('', metadata) self.assertIn('', metadata) self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', metadata) - self.assertIn('', metadata) + self.assertIn('\n', metadata) self.assertIn('', metadata) self.assertIn('', metadata) @@ -396,41 +401,29 @@ def testGetSPMetadataSignedNoMetadataCert(self): settings_info['security'] = {} settings_info['security']['signMetadata'] = {} - try: - OneLogin_Saml2_Settings(settings_info) - self.assertTrue(False) - except Exception as e: - self.assertIn('sp_signMetadata_invalid', e.message) + self.assertRaisesRegexp(Exception, 'sp_signMetadata_invalid', + OneLogin_Saml2_Settings, settings_info) settings_info['security']['signMetadata'] = { 'keyFileName': 'noexist.key', 'certFileName': 'sp.crt' } settings = OneLogin_Saml2_Settings(settings_info) - try: - settings.get_sp_metadata() - self.assertTrue(False) - except Exception as e: - self.assertIn('Private key file not found', e.message) + self.assertRaisesRegexp(Exception, 'Private key file not found', + settings.get_sp_metadata) settings_info['security']['signMetadata'] = { 'keyFileName': 'sp.key', 'certFileName': 'noexist.crt' } settings = OneLogin_Saml2_Settings(settings_info) - try: - settings.get_sp_metadata() - self.assertTrue(False) - except Exception as e: - self.assertIn('Public cert file not found', e.message) + self.assertRaisesRegexp(Exception, 'Public cert file not found', + settings.get_sp_metadata) settings_info['security']['signMetadata'] = 'invalid_value' settings = OneLogin_Saml2_Settings(settings_info) - try: - settings.get_sp_metadata() - self.assertTrue(False) - except Exception as e: - self.assertIn('Invalid Setting: signMetadata value of the sp is not valid', e.message) + self.assertRaisesRegexp(Exception, 'Invalid Setting: signMetadata value of the sp is not valid', + settings.get_sp_metadata) def testValidateMetadata(self): """ @@ -473,11 +466,8 @@ def testValidateMetadataNoXML(self): """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) metadata = '' - try: - errors = settings.validate_metadata(metadata) - self.assertTrue(False) - except Exception as e: - self.assertIn('Empty string supplied as input', e.message) + self.assertRaisesRegexp(Exception, 'Empty string supplied as input', + settings.validate_metadata, metadata) metadata = '' errors = settings.validate_metadata(metadata) @@ -590,12 +580,7 @@ def testSetStrict(self): settings.set_strict(False) self.assertFalse(settings.is_strict()) - - try: - settings.set_strict('a') - 1 / 0 - except Exception as e: - self.assertIsInstance(e, AssertionError) + self.assertRaises(AssertionError, settings.set_strict, 'a') def testIsStrict(self): """ diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py index 0b7860ae..03207d10 100644 --- a/tests/src/OneLogin/saml2_tests/signed_response_test.py +++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py @@ -10,6 +10,12 @@ from onelogin.saml2.response import OneLogin_Saml2_Response from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.utils import OneLogin_Saml2_Utils + +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs class OneLogin_Saml2_SignedResponse_Test(unittest.TestCase): @@ -38,9 +44,9 @@ def testResponseSignedAssertionNot(self): """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents(join(self.data_path, 'responses', 'open_saml_response.xml')) - response = OneLogin_Saml2_Response(settings, b64encode(message)) + response = OneLogin_Saml2_Response(settings, OneLogin_Saml2_Utils.b64encode(message)) - self.assertEquals('someone@example.org', response.get_nameid()) + self.assertEqual('someone@example.org', response.get_nameid()) def testResponseAndAssertionSigned(self): """ @@ -49,6 +55,6 @@ def testResponseAndAssertionSigned(self): """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents(join(self.data_path, 'responses', 'simple_saml_php.xml')) - response = OneLogin_Saml2_Response(settings, b64encode(message)) + response = OneLogin_Saml2_Response(settings, OneLogin_Saml2_Utils.b64encode(message)) - self.assertEquals('someone@example.com', response.get_nameid()) + self.assertEqual('someone@example.com', response.get_nameid()) diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 409c280e..0662f4ed 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -152,12 +152,8 @@ def testRedirect(self): target_url3 = OneLogin_Saml2_Utils.redirect(url3, {}, request_data) self.assertIn('test=true', target_url3) - - try: - target_url4 = OneLogin_Saml2_Utils.redirect(url4, {}, request_data) - self.assertTrue(target_url4 == 42) - except Exception as e: - self.assertIn('Redirect to invalid URL', e.message) + self.assertRaisesRegexp(Exception, 'Redirect to invalid URL', + OneLogin_Saml2_Utils.redirect, url4, {}, request_data) # Review parameter prefix parameters1 = { @@ -178,26 +174,27 @@ def testRedirect(self): } target_url7 = OneLogin_Saml2_Utils.redirect(url, parameters2, request_data) - self.assertEqual('http://%s/example?numvaluelist[]=1&numvaluelist[]=2&testing&alphavalue=a' % hostname, target_url7) + parameters2_decoded = {"alphavalue": "alphavalue=a", "numvaluelist": "numvaluelist[]=1&numvaluelist[]=2", "testing": "testing"} + parameters2_str = "&".join(parameters2_decoded[x] for x in parameters2) + self.assertEqual('http://%s/example?%s' % (hostname, parameters2_str), target_url7) parameters3 = { 'alphavalue': 'a', 'emptynumvaluelist': [], 'numvaluelist': [''], } + parameters3_decoded = {"alphavalue": "alphavalue=a", "numvaluelist": "numvaluelist[]="} + parameters3_str = "&".join((parameters3_decoded[x] for x in parameters3.keys() if x in parameters3_decoded)) target_url8 = OneLogin_Saml2_Utils.redirect(url, parameters3, request_data) - self.assertEqual('http://%s/example?numvaluelist[]=&alphavalue=a' % hostname, target_url8) + self.assertEqual('http://%s/example?%s' % (hostname, parameters3_str), target_url8) def testGetselfhost(self): """ Tests the get_self_host method of the OneLogin_Saml2_Utils """ request_data = {} - try: - OneLogin_Saml2_Utils.get_self_host(request_data) - self.assertTrue(False) - except Exception as e: - self.assertEqual('No hostname defined', e.message) + self.assertRaisesRegexp(Exception, 'No hostname defined', + OneLogin_Saml2_Utils.get_self_host, request_data) request_data = { 'server_name': 'example.com' @@ -287,11 +284,8 @@ def testGetSelfURLhost(self): request_data2 = { 'request_uri': 'example.com/onelogin/sso' } - try: - self.assertEqual('https://example.com', OneLogin_Saml2_Utils.get_self_url_host(request_data2)) - self.assertTrue(False) - except Exception as e: - self.assertEqual('No hostname defined', e.message) + self.assertRaisesRegexp(Exception, 'No hostname defined', + OneLogin_Saml2_Utils.get_self_url_host, request_data2) def testGetSelfURL(self): """ @@ -420,21 +414,15 @@ def testGetStatus(self): xml_inv = b64decode(xml_inv) dom_inv = etree.fromstring(xml_inv) - try: - status_inv = OneLogin_Saml2_Utils.get_status(dom_inv) - self.assertEqual(status_inv, 42) - except Exception as e: - self.assertEqual('Missing Status on response', e.message) + self.assertRaisesRegexp(Exception, 'Missing Status on response', + OneLogin_Saml2_Utils.get_status, dom_inv) xml_inv2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_status_code.xml.base64')) xml_inv2 = b64decode(xml_inv2) dom_inv2 = etree.fromstring(xml_inv2) - try: - status_inv2 = OneLogin_Saml2_Utils.get_status(dom_inv2) - self.assertEqual(status_inv2, 42) - except Exception as e: - self.assertEqual('Missing Status Code on response', e.message) + self.assertRaisesRegexp(Exception, 'Missing Status Code on response', + OneLogin_Saml2_Utils.get_status, dom_inv2) def testParseDuration(self): """ @@ -450,11 +438,8 @@ def testParseDuration(self): self.assertTrue(parsed_duration_2 > parsed_duration) invalid_duration = 'PT1Y' - try: - parsed_duration_3 = OneLogin_Saml2_Utils.parse_duration(invalid_duration) - self.assertEqual(parsed_duration_3, 42) - except Exception as e: - self.assertIn('Unrecognised ISO 8601 date format', e.message) + self.assertRaisesRegexp(Exception, 'Unrecognised ISO 8601 date format', + OneLogin_Saml2_Utils.parse_duration, invalid_duration) new_duration = 'P1Y1M' parsed_duration_4 = OneLogin_Saml2_Utils.parse_duration(new_duration, timestamp) @@ -472,11 +457,8 @@ def testParseSAML2Time(self): saml_time = '2013-12-10T04:39:31Z' self.assertEqual(time, OneLogin_Saml2_Utils.parse_SAML_to_time(saml_time)) - try: - OneLogin_Saml2_Utils.parse_SAML_to_time('invalidSAMLTime') - self.assertTrue(False) - except Exception as e: - self.assertIn('does not match format', e.message) + self.assertRaisesRegexp(Exception, 'does not match format', + OneLogin_Saml2_Utils.parse_SAML_to_time, 'invalidSAMLTime') # Now test if toolkit supports miliseconds saml_time2 = '2013-12-10T04:39:31.120Z' @@ -490,11 +472,8 @@ def testParseTime2SAML(self): saml_time = '2013-12-10T04:39:31Z' self.assertEqual(saml_time, OneLogin_Saml2_Utils.parse_time_to_SAML(time)) - try: - OneLogin_Saml2_Utils.parse_time_to_SAML('invalidtime') - self.assertTrue(False) - except Exception as e: - self.assertIn('could not convert string to float', e.message) + self.assertRaisesRegexp(Exception, 'could not convert string to float', + OneLogin_Saml2_Utils.parse_time_to_SAML, 'invalidtime') def testGetExpireTime(self): """ @@ -574,7 +553,7 @@ def testGenerateNameId(self): key = OneLogin_Saml2_Utils.format_cert(x509cert) name_id_enc = OneLogin_Saml2_Utils.generate_name_id(name_id_value, entity_id, name_id_format, key) - expected_name_id_enc = '' + expected_name_id_enc = '\n\n\n\n\n\n' self.assertIn(expected_name_id_enc, name_id_enc) def testCalculateX509Fingerprint(self): @@ -635,11 +614,11 @@ def testDecryptElement(self): encrypted_data = encrypted_nameid_nodes[0].firstChild encrypted_data_str = str(encrypted_nameid_nodes[0].firstChild.toxml()) decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) - self.assertEqual('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag) + self.assertEqual('{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML, decrypted_nameid.tag) self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text) decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data_str, key) - self.assertEqual('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag) + self.assertEqual('{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML, decrypted_nameid.tag) self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text) xml_assertion_enc = b64decode(self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion_encrypted_nameid.xml.base64'))) @@ -648,14 +627,14 @@ def testDecryptElement(self): encrypted_data_assert = encrypted_assertion_enc_nodes[0].firstChild decrypted_assertion = OneLogin_Saml2_Utils.decrypt_element(encrypted_data_assert, key) - self.assertEqual('{%s}Assertion' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_assertion.tag) + self.assertEqual('{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML, decrypted_assertion.tag) self.assertEqual('_6fe189b1c241827773902f2b1d3a843418206a5c97', decrypted_assertion.get('ID')) decrypted_assertion.xpath('/saml:Assertion/saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) encrypted_nameid_nodes = decrypted_assertion.xpath('/saml:Assertion/saml:Subject/saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) encrypted_data = encrypted_nameid_nodes[0][0] decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) - self.assertEqual('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag) + self.assertEqual('{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML, decrypted_nameid.tag) self.assertEqual('457bdb600de717891c77647b0806ce59c089d5b8', decrypted_nameid.text) key_2_file_name = join(self.data_path, 'misc', 'sp2.key') @@ -663,41 +642,24 @@ def testDecryptElement(self): key2 = f.read() f.close() - try: - OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key2) - self.assertTrue(False) - except: - pass + self.assertRaises(Exception, OneLogin_Saml2_Utils.decrypt_element, encrypted_data, key2) key_3_file_name = join(self.data_path, 'misc', 'sp2.key') f = open(key_3_file_name, 'r') key3 = f.read() f.close() - try: - OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key3) - self.assertTrue(False) - except: - pass - + self.assertRaises(Exception, OneLogin_Saml2_Utils.decrypt_element, encrypted_data, key3) xml_nameid_enc_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_EncMethod.xml.base64'))) dom_nameid_enc_2 = parseString(xml_nameid_enc_2) encrypted_nameid_nodes_2 = dom_nameid_enc_2.getElementsByTagName('saml:EncryptedID') encrypted_data_2 = encrypted_nameid_nodes_2[0].firstChild - try: - OneLogin_Saml2_Utils.decrypt_element(encrypted_data_2, key) - self.assertTrue(False) - except: - pass + self.assertRaises(Exception, OneLogin_Saml2_Utils.decrypt_element, encrypted_data_2, key) xml_nameid_enc_3 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_keyinfo.xml.base64'))) dom_nameid_enc_3 = parseString(xml_nameid_enc_3) encrypted_nameid_nodes_3 = dom_nameid_enc_3.getElementsByTagName('saml:EncryptedID') encrypted_data_3 = encrypted_nameid_nodes_3[0].firstChild - try: - OneLogin_Saml2_Utils.decrypt_element(encrypted_data_3, key) - self.assertTrue(False) - except: - pass + self.assertRaises(Exception, OneLogin_Saml2_Utils.decrypt_element, encrypted_data_3, key) def testAddSign(self): """ @@ -765,7 +727,7 @@ def testAddSign(self): try: OneLogin_Saml2_Utils.add_sign(1, key, cert) except Exception as e: - self.assertEqual('Error parsing xml string', e.message) + self.assertEqual('Error parsing xml string', str(e)) def testValidateSign(self): """ @@ -783,12 +745,12 @@ def testValidateSign(self): try: self.assertFalse(OneLogin_Saml2_Utils.validate_sign('', cert)) except Exception as e: - self.assertEqual('Empty string supplied as input', e.message) + self.assertEqual('Empty string supplied as input', str(e)) try: self.assertFalse(OneLogin_Saml2_Utils.validate_sign(1, cert)) except Exception as e: - self.assertEqual('Error parsing xml string', e.message) + self.assertEqual('Error parsing xml string', str(e)) # expired cert xml_metadata_signed = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings1.xml')) From 4eff111b3a09926971fbd2ffa5f298833a2ae7cc Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Sat, 14 Feb 2015 02:18:31 +0300 Subject: [PATCH 02/35] added xmlsec requirements --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 7b345653..4eca360b 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ test_suite='tests', install_requires=[ 'isodate>=0.5.0', + 'xmlsec>=0.2.1' ], extras_require={ 'test': ( From 0f5b3ef171693cbb0e9fd5ed46ae8fbd178e647a Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Mon, 16 Feb 2015 23:57:17 +0300 Subject: [PATCH 03/35] optimize lookup in settings --- src/onelogin/saml2/auth.py | 32 +++-- src/onelogin/saml2/authn_request.py | 10 +- src/onelogin/saml2/logout_request.py | 23 ++-- src/onelogin/saml2/logout_response.py | 10 +- src/onelogin/saml2/response.py | 16 +-- src/onelogin/saml2/settings.py | 135 +++++++------------- src/onelogin/saml2/utils.py | 53 +++----- tests/src/OneLogin/saml2_tests/auth_test.py | 6 +- 8 files changed, 105 insertions(+), 180 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 0281ea99..43058ed6 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -40,8 +40,8 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): :param request_data: Request Data :type request_data: dict - :param settings: Optional. SAML Toolkit Settings - :type settings: dict|object + :param old_settings: Optional. SAML Toolkit Settings + :type old_settings: dict|object :param custom_base_path: Optional. Path where are stored the settings file and the cert folder :type custom_base_path: string @@ -118,8 +118,8 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ :returns: Redirection url """ self.__errors = [] - - if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']: + get_data = self.__request_data.get('get_data') + if get_data and 'SAMLResponse' in self.__request_data['get_data']: logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) if not logout_response.is_valid(self.__request_data, request_id): self.__errors.append('invalid_logout_response') @@ -129,7 +129,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ elif not keep_local_session: OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) - elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']: + elif get_data and 'SAMLRequest' in self.__request_data['get_data']: logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest']) if not logout_request.is_valid(self.__request_data): self.__errors.append('invalid_logout_request') @@ -148,7 +148,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ parameters['RelayState'] = self.__request_data['get_data']['RelayState'] security = self.__settings.get_security_data() - if 'logoutResponseSigned' in security and security['logoutResponseSigned']: + if security['logoutResponseSigned']: parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None)) @@ -171,8 +171,12 @@ def redirect_to(self, url=None, parameters={}): :returns: Redirection url """ - if url is None and 'RelayState' in self.__request_data['get_data']: - url = self.__request_data['get_data']['RelayState'] + if url is None: + try: + url = self.__request_data['get_data']['RelayState'] + except KeyError: + pass + return OneLogin_Saml2_Utils.redirect(url, parameters, request_data=self.__request_data) def is_authenticated(self): @@ -330,11 +334,11 @@ def get_slo_url(self): :returns: An URL, the SLO endpoint of the IdP :rtype: string """ - url = None idp_data = self.__settings.get_idp_data() - if 'singleLogoutService' in idp_data.keys() and 'url' in idp_data['singleLogoutService']: - url = idp_data['singleLogoutService']['url'] - return url + try: + return idp_data['singleLogoutService']['url'] + except KeyError: + pass def build_request_signature(self, saml_request, relay_state): """ @@ -351,8 +355,8 @@ def build_request_signature(self, saml_request, relay_state): def build_response_signature(self, saml_response, relay_state): """ Builds the Signature of the SAML Response. - :param saml_request: The SAML Response - :type saml_request: string + :param saml_response: The SAML Response + :type saml_response: string :param relay_state: The target URL the user should be redirected to :type relay_state: string diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 6e2f9885..72d4e608 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -26,7 +26,7 @@ def __init__(self, settings, force_authn=False, is_passive=False): Constructs the AuthnRequest object. :param settings: OSetting data - :type return_to: OneLogin_Saml2_Settings + :type settings: OneLogin_Saml2_Settings :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'. :type force_authn: bool @@ -47,7 +47,7 @@ def __init__(self, settings, force_authn=False, is_passive=False): destination = idp_data['singleSignOnService']['url'] name_id_policy_format = sp_data['NameIDFormat'] - if 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted']: + if security['wantNameIdEncrypted']: name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED provider_name_str = '' @@ -58,7 +58,9 @@ def __init__(self, settings, force_authn=False, is_passive=False): lang = 'en-US' else: lang = sorted(langs)[0] - if 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] is not None: + + display_name = organization_data[lang].get('displayname') + if display_name is not None: provider_name_str = 'ProviderName="%s"' % organization_data[lang]['displayname'] force_authn_str = '' @@ -70,7 +72,7 @@ def __init__(self, settings, force_authn=False, is_passive=False): is_passive_str = 'IsPassive="true"' requested_authn_context_str = '' - if 'requestedAuthnContext' in security.keys() and security['requestedAuthnContext'] is not False: + if security['requestedAuthnContext'] is not False: if security['requestedAuthnContext'] is True: requested_authn_context_str = """ urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index a5a8c888..bf27339e 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -53,19 +53,19 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) cert = None - if 'nameIdEncrypted' in security and security['nameIdEncrypted']: + if security['nameIdEncrypted']: cert = idp_data['x509cert'] if name_id is not None: - nameIdFormat = sp_data['NameIDFormat'] + name_id_format = sp_data['NameIDFormat'] else: name_id = idp_data['entityId'] - nameIdFormat = OneLogin_Saml2_Constants.NAMEID_ENTITY + name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, sp_data['entityId'], - nameIdFormat, + name_id_format, cert ) @@ -243,10 +243,7 @@ def is_valid(self, request_data): idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] - if 'get_data' in request_data.keys(): - get_data = request_data['get_data'] - else: - get_data = {} + get_data = request_data.get('get_data', dict()) if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) @@ -287,11 +284,7 @@ def is_valid(self, request_data): raise Exception('The Message of the Logout Request is not signed and the SP require it') if 'Signature' in get_data: - if 'SigAlg' not in get_data: - sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 - else: - sign_alg = get_data['SigAlg'] - + sign_alg = get_data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: raise Exception('Invalid signAlg in the recieved Logout Request') @@ -300,9 +293,9 @@ def is_valid(self, request_data): signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(get_data['RelayState'])) signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(sign_alg)) - if 'x509cert' not in idp_data or idp_data['x509cert'] is None: - raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') cert = idp_data['x509cert'] + if not cert: + raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, OneLogin_Saml2_Utils.b64decode(get_data['Signature']), cert): raise Exception('Signature validation failed. Logout Request rejected') diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index f47e4312..659bd2ae 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -111,11 +111,7 @@ def is_valid(self, request_data, request_id=None): raise Exception('The Message of the Logout Response is not signed and the SP require it') if 'Signature' in get_data: - if 'SigAlg' not in get_data: - sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 - else: - sign_alg = get_data['SigAlg'] - + sign_alg = get_data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: raise Exception('Invalid signAlg in the recieved Logout Response') @@ -124,9 +120,9 @@ def is_valid(self, request_data, request_id=None): signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(get_data['RelayState'])) signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(sign_alg)) - if 'x509cert' not in idp_data or idp_data['x509cert'] is None: - raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') cert = idp_data['x509cert'] + if not cert: + raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, OneLogin_Saml2_Utils.b64decode(get_data['Signature']), cert): raise Exception('Signature validation failed. Logout Response rejected') diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 5e508077..3f50db4f 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -80,9 +80,9 @@ def is_valid(self, request_data, request_id=None): self.check_status() idp_data = self.__settings.get_idp_data() - idp_entity_id = idp_data.get('entityId', '') + idp_entity_id = idp_data['entityId'] sp_data = self.__settings.get_sp_data() - sp_entity_id = sp_data.get('entityId', '') + sp_entity_id = sp_data['entityId'] sign_nodes = self.__query('//ds:Signature') @@ -104,10 +104,10 @@ def is_valid(self, request_data, request_id=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)) - if not self.encrypted and security.get('wantAssertionsEncrypted', False): + if not self.encrypted and security['wantAssertionsEncrypted']: raise Exception('The assertion of the Response is not encrypted and the SP require it') - if security.get('wantNameIdEncrypted', False): + if security['wantNameIdEncrypted']: encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData') if len(encrypted_nameid_nodes) == 0: raise Exception('The NameID of the Response is not encrypted and the SP require it') @@ -185,15 +185,15 @@ def is_valid(self, request_data, request_id=None): if not any_subject_confirmation: raise Exception('A valid SubjectConfirmation was not found on this Response') - if security.get('wantAssertionsSigned', False) and ('{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML) not in signed_elements: + if security['wantAssertionsSigned'] and ('{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML) not in signed_elements: raise Exception('The Assertion of the Response is not signed and the SP require it') - if security.get('wantMessagesSigned', False) and ('{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP) not in signed_elements: + if security['wantMessagesSigned'] and ('{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP) not in signed_elements: raise Exception('The Message of the Response is not signed and the SP require it') if len(signed_elements) > 0: - cert = idp_data.get('x509cert', None) - fingerprint = idp_data.get('certFingerprint', None) + cert = idp_data['x509cert'] + fingerprint = idp_data['certFingerprint'] # Only validates the first sign found if '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements: diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index af281d23..a0cb2135 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -94,7 +94,6 @@ def __init__(self, settings=None, custom_base_path=None): OneLogin_Saml2_Error.SETTINGS_INVALID, ','.join(self.__errors) ) - self.__add_default_values() elif isinstance(settings, dict): if not self.__load_settings_from_dict(settings): raise OneLogin_Saml2_Error( @@ -196,20 +195,11 @@ def __load_settings_from_dict(self, settings): self.__errors = [] self.__sp = settings['sp'] self.__idp = settings['idp'] - - if 'strict' in settings: - self.__strict = settings['strict'] - if 'debug' in settings: - self.__debug = settings['debug'] - if 'security' in settings: - self.__security = settings['security'] - else: - self.__security = {} - if 'contactPerson' in settings: - self.__contacts = settings['contactPerson'] - if 'organization' in settings: - self.__organization = settings['organization'] - + self.__strict = settings.get('strict', self.__strict) + self.__debug = settings.get('debug', self.__debug) + self.__security = settings.get('security', dict()) + self.__contacts = settings.get('contactPerson', self.__contacts) + self.__organization = settings.get('organization', self.__organization) self.__add_default_values() return True @@ -250,56 +240,34 @@ def __add_default_values(self): """ Add default values if the settings info is not complete """ - if 'assertionConsumerService' not in self.__sp.keys(): - self.__sp['assertionConsumerService'] = {} - if 'binding' not in self.__sp['assertionConsumerService'].keys(): - self.__sp['assertionConsumerService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_POST - - if 'singleLogoutService' not in self.__sp.keys(): - self.__sp['singleLogoutService'] = {} - if 'binding' not in self.__sp['singleLogoutService']: - self.__sp['singleLogoutService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT + self.__sp.setdefault('assertionConsumerService', dict()).setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_POST) + self.__sp.setdefault('singleLogoutService', dict()).setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT) # Related to nameID - if 'NameIDFormat' not in self.__sp: - self.__sp['NameIDFormat'] = OneLogin_Saml2_Constants.NAMEID_PERSISTENT - if 'nameIdEncrypted' not in self.__security: - self.__security['nameIdEncrypted'] = False + self.__sp.setdefault('NameIDFormat', OneLogin_Saml2_Constants.NAMEID_PERSISTENT) + self.__security.setdefault('nameIdEncrypted', False) # Sign provided - if 'authnRequestsSigned' not in self.__security.keys(): - self.__security['authnRequestsSigned'] = False - if 'logoutRequestSigned' not in self.__security.keys(): - self.__security['logoutRequestSigned'] = False - if 'logoutResponseSigned' not in self.__security.keys(): - self.__security['logoutResponseSigned'] = False - if 'signMetadata' not in self.__security.keys(): - self.__security['signMetadata'] = False + self.__security.setdefault('authnRequestsSigned', False) + self.__security.setdefault('logoutRequestSigned', False) + self.__security.setdefault('logoutResponseSigned', False) + self.__security.setdefault('signMetadata', False) # Sign expected - if 'wantMessagesSigned' not in self.__security.keys(): - self.__security['wantMessagesSigned'] = False - if 'wantAssertionsSigned' not in self.__security.keys(): - self.__security['wantAssertionsSigned'] = False + self.__security.setdefault('wantMessagesSigned', False) + self.__security.setdefault('wantAssertionsSigned', False) # Encrypt expected - if 'wantAssertionsEncrypted' not in self.__security.keys(): - self.__security['wantAssertionsEncrypted'] = False - if 'wantNameIdEncrypted' not in self.__security.keys(): - self.__security['wantNameIdEncrypted'] = False + self.__security.setdefault('wantAssertionsEncrypted', False) + self.__security.setdefault('wantNameIdEncrypted', False) - if 'x509cert' not in self.__idp: - self.__idp['x509cert'] = '' - if 'certFingerprint' not in self.__idp: - self.__idp['certFingerprint'] = '' + self.__idp.setdefault('x509cert', '') + self.__idp.setdefault('certFingerprint', '') - if 'x509cert' not in self.__sp: - self.__sp['x509cert'] = '' - if 'privateKey' not in self.__sp: - self.__sp['privateKey'] = '' + self.__sp.setdefault('x509cert', '') + self.__sp.setdefault('privateKey', '') - if 'requestedAuthnContext' not in self.__security.keys(): - self.__security['requestedAuthnContext'] = True + self.__security.setdefault('requestedAuthnContext', True) def check_settings(self, settings): """ @@ -322,12 +290,10 @@ def check_settings(self, settings): errors.append('idp_not_found') else: idp = settings['idp'] - if 'entityId' not in idp or len(idp['entityId']) == 0: + if len(idp.get('entityId', '')) == 0: errors.append('idp_entityId_not_found') - if 'singleSignOnService' not in idp or \ - 'url' not in idp['singleSignOnService'] or \ - len(idp['singleSignOnService']['url']) == 0: + if len(idp.get('singleSignOnService', dict()).get('url', '')) == 0: errors.append('idp_sso_not_found') elif not validate_url(idp['singleSignOnService']['url']): errors.append('idp_sso_url_invalid') @@ -350,12 +316,10 @@ def check_settings(self, settings): if 'security' in settings: security = settings['security'] - if 'entityId' not in sp or len(sp['entityId']) == 0: + if len(sp.get('entityId', '')) == 0: errors.append('sp_entityId_not_found') - if 'assertionConsumerService' not in sp or \ - 'url' not in sp['assertionConsumerService'] or \ - len(sp['assertionConsumerService']['url']) == 0: + if len(sp.get('assertionConsumerService', dict()).get('url', '')) == 0: errors.append('sp_acs_not_found') elif not validate_url(sp['assertionConsumerService']['url']): errors.append('sp_acs_url_invalid') @@ -371,11 +335,11 @@ def check_settings(self, settings): 'certFileName' not in security['signMetadata']: errors.append('sp_signMetadata_invalid') - authn_sign = 'authnRequestsSigned' in security.keys() and security['authnRequestsSigned'] - logout_req_sign = 'logoutRequestSigned' in security.keys() and security['logoutRequestSigned'] - logout_res_sign = 'logoutResponseSigned' in security.keys() and security['logoutResponseSigned'] - want_assert_enc = 'wantAssertionsEncrypted' in security.keys() and security['wantAssertionsEncrypted'] - want_nameid_enc = 'wantNameIdEncrypted' in security.keys() and security['wantNameIdEncrypted'] + authn_sign = security.get('authnRequestsSigned', False) + logout_req_sign = security.get('logoutRequestSigned', False) + logout_res_sign = security.get('logoutResponseSigned', False) + want_assert_enc = security.get('wantAssertionsEncrypted', False) + want_nameid_enc = security.get('wantNameIdEncrypted', False) if not self.check_sp_certs(): if authn_sign or logout_req_sign or logout_res_sign or \ @@ -446,18 +410,14 @@ def get_sp_key(self): :returns: SP private key :rtype: string """ - key = None - - if 'privateKey' in self.__sp.keys() and self.__sp['privateKey']: - key = self.__sp['privateKey'] - else: + key = self.__sp.get('privateKey') + if not key: key_file_name = self.__paths['cert'] + 'sp.key' if exists(key_file_name): - f_key = open(key_file_name, 'r') - key = f_key.read() - f_key.close() - return key + with open(key_file_name, 'r') as f_key: + key = self.__sp['privateKey'] = f_key.read() + return key or None def get_sp_cert(self): """ @@ -466,18 +426,13 @@ def get_sp_cert(self): :returns: SP public cert :rtype: string """ - cert = None - - if 'x509cert' in self.__sp.keys() and self.__sp['x509cert']: - cert = self.__sp['x509cert'] - else: + cert = self.__sp.get('x509cert') + if not cert: cert_file_name = self.__paths['cert'] + 'sp.crt' if exists(cert_file_name): - f_cert = open(cert_file_name, 'r') - cert = f_cert.read() - f_cert.close() - - return cert + with open(cert_file_name, 'r') as f_cert: + cert = self.__sp['x509cert'] = f_cert.read() + return cert or None def get_idp_cert(self): """ @@ -486,11 +441,7 @@ def get_idp_cert(self): :returns: IdP public cert :rtype: string """ - cert = None - - if 'x509cert' in self.__idp.keys() and self.__idp['x509cert']: - cert = self.__idp['x509cert'] - return cert + return self.__idp['x509cert'] or None def get_idp_data(self): """ @@ -553,7 +504,7 @@ def get_sp_metadata(self): metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert) # Sign metadata - if 'signMetadata' in self.__security and self.__security['signMetadata'] is not False: + if self.__security['signMetadata'] is not False: if self.__security['signMetadata'] is True: key_file_name = 'sp.key' cert_file_name = 'sp.crt' diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index ef75eb4a..97c3a234 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -37,24 +37,6 @@ from urllib import quote_plus # py2 -def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg): - """ - Auxiliary method. It override the default xmlsec debug message. - """ - - info = [] - if error_object != "unknown": - info.append("obj=" + error_object) - if error_subject != "unknown": - info.append("subject=" + error_subject) - if msg.strip(): - info.append("msg=" + msg) - if reason != 1: - info.append("errno=%d" % reason) - if info: - print("%s:%d(%s)" % (filename, line, func), " ".join(info)) - - class OneLogin_Saml2_Utils(object): """ @@ -336,14 +318,15 @@ def get_self_url_host(request_data): else: protocol = 'http' - if 'server_port' in request_data: - port_number = str(request_data['server_port']) - port = ':' + port_number - + server_port = request_data.get('server_port') + if server_port: + port_number = str(server_port) if protocol == 'http' and port_number == '80': port = '' elif protocol == 'https' and port_number == '443': port = '' + else: + port = ':' + port_number return '%s://%s%s' % (protocol, current_host, port) @@ -358,11 +341,8 @@ def get_self_host(request_data): :return: The current host :rtype: string """ - if 'http_host' in request_data: - current_host = request_data['http_host'] - elif 'server_name' in request_data: - current_host = request_data['server_name'] - else: + current_host = request_data.get('http_host') or request_data.get('server_name') + if not current_host: raise Exception('No hostname defined') if ':' in current_host: @@ -387,8 +367,8 @@ def is_https(request_data): :return: False if https is not active :rtype: boolean """ - is_https = 'https' in request_data and request_data['https'] != 'off' - is_https = is_https or ('server_port' in request_data and str(request_data['server_port']) == '443') + is_https = request_data.get('https', 'off') != 'off' + is_https = is_https or (str(request_data.get('server_port', '')) == '443') return is_https @staticmethod @@ -427,11 +407,11 @@ def get_self_routed_url_no_query(request_data): :rtype: string """ self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) - route = '' - if 'request_uri' in request_data.keys() and request_data['request_uri']: - route = request_data['request_uri'] - if 'query_string' in request_data.keys() and request_data['query_string']: - route = route.replace(request_data['query_string'], '') + route = request_data.get('request_uri', '') + if route: + query_string = request_data.get('query_string', '') + if query_string: + route = route.replace(query_string, '') return self_url_host + route @@ -448,9 +428,8 @@ def get_self_url(request_data): """ self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) - request_uri = '' - if 'request_uri' in request_data: - request_uri = request_data['request_uri'] + request_uri = request_data.get('request_uri', '') + if request_uri: if not request_uri.startswith('/'): match = re.search('^https?://[^/]*(/.*)', request_uri) if match is not None: diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 6e8d2980..02ed104c 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -617,7 +617,7 @@ def testLoginIsPassive(self): """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' - sso_url = settings_info['idp']['singleSignOnService']['url'] + settings_info['idp']['singleSignOnService']['url'] auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) target_url = auth.login(return_to) @@ -796,7 +796,7 @@ def testBuildRequestSignature(self): valid_signature = 'E17GU1STzanOXxBTKjweB1DovP8aMJdj5BEy0fnGoEslKdP6hpPc3enjT/bu7I8D8QzLoir8SxZVWdUDXgIxJIEgfK5snr+jJwfc5U2HujsOa/Xb3c4swoyPcyQhcxLRDhDjPq5cQxJfYoPeElvCuI6HAD1mtdd5PS/xDvbIxuw=' self.assertEqual(signature, valid_signature) - settings['sp']['privatekey'] = '' + settings['sp']['privateKey'] = '' settings['custom_base_path'] = u'invalid/path/' auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) self.assertRaisesRegexp(Exception, "Trying to sign the SAMLRequest but can't load the SP private key", @@ -815,7 +815,7 @@ def testBuildResponseSignature(self): valid_signature = 'IcyWLRX6Dz3wHBfpcUaNLVDMGM3uo6z2Z11Gjq0/APPJaHboKGljffsgMVAGBml497yckq+eYKmmz+jpURV9yTj2sF9qfD6CwX2dEzSzMdRzB40X7pWyHgEJGIhs6BhaOt5oXEk4T+h3AczERqpVYFpL00yo7FNtyQkhZFpHFhM=' self.assertEqual(signature, valid_signature) - settings['sp']['privatekey'] = '' + settings['sp']['privateKey'] = '' settings['custom_base_path'] = u'invalid/path/' auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) self.assertRaisesRegexp(Exception, "Trying to sign the SAMLRequest but can't load the SP private key", From 5d76250b8b7df7466707f2aa4952a4a6baeee613 Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Mon, 2 Mar 2015 00:12:33 +0300 Subject: [PATCH 04/35] settings optimization, part2 --- src/onelogin/saml2/auth.py | 23 +++--- src/onelogin/saml2/authn_request.py | 4 +- src/onelogin/saml2/logout_request.py | 4 +- src/onelogin/saml2/settings.py | 108 ++++++++++++++++++--------- src/onelogin/saml2/utils.py | 35 +++++---- 5 files changed, 105 insertions(+), 69 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 43058ed6..eb649be9 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -118,9 +118,10 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ :returns: Redirection url """ self.__errors = [] - get_data = self.__request_data.get('get_data') - if get_data and 'SAMLResponse' in self.__request_data['get_data']: - logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) + + get_data = 'get_data' in self.__request_data and self.__request_data['get_data'] + if get_data and 'SAMLResponse' in get_data: + logout_response = OneLogin_Saml2_Logout_Response(self.__settings, get_data['SAMLResponse']) if not logout_response.is_valid(self.__request_data, request_id): self.__errors.append('invalid_logout_response') self.__error_reason = logout_response.get_error() @@ -129,8 +130,8 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ elif not keep_local_session: OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) - elif get_data and 'SAMLRequest' in self.__request_data['get_data']: - logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest']) + elif get_data and 'SAMLRequest' in get_data: + logout_request = OneLogin_Saml2_Logout_Request(self.__settings, get_data['SAMLRequest']) if not logout_request.is_valid(self.__request_data): self.__errors.append('invalid_logout_request') self.__error_reason = logout_request.get_error() @@ -171,12 +172,8 @@ def redirect_to(self, url=None, parameters={}): :returns: Redirection url """ - if url is None: - try: - url = self.__request_data['get_data']['RelayState'] - except KeyError: - pass - + if url is None and 'RelayState' in self.__request_data['get_data']: + url = self.__request_data['get_data']['RelayState'] return OneLogin_Saml2_Utils.redirect(url, parameters, request_data=self.__request_data) def is_authenticated(self): @@ -335,10 +332,8 @@ def get_slo_url(self): :rtype: string """ idp_data = self.__settings.get_idp_data() - try: + if 'url' in idp_data['singleLogoutService']: return idp_data['singleLogoutService']['url'] - except KeyError: - pass def build_request_signature(self, saml_request, relay_state): """ diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 72d4e608..e1b1a8ca 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -59,8 +59,8 @@ def __init__(self, settings, force_authn=False, is_passive=False): else: lang = sorted(langs)[0] - display_name = organization_data[lang].get('displayname') - if display_name is not None: + display_name = 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] + if display_name: provider_name_str = 'ProviderName="%s"' % organization_data[lang]['displayname'] force_authn_str = '' diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index bf27339e..4fd8d5d5 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -164,7 +164,7 @@ def get_nameid_data(request, key=None): 'Value': name_id.text } for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: - if attr in name_id.attrib.keys(): + if attr in name_id.attrib: name_id_data[attr] = name_id.attrib[attr] return name_id_data @@ -243,7 +243,7 @@ def is_valid(self, request_data): idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] - get_data = request_data.get('get_data', dict()) + get_data = ('get_data' in request_data and request_data['get_data']) or dict() if self.__settings.is_strict(): res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index a0cb2135..96a9d70b 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -195,11 +195,20 @@ def __load_settings_from_dict(self, settings): self.__errors = [] self.__sp = settings['sp'] self.__idp = settings['idp'] - self.__strict = settings.get('strict', self.__strict) - self.__debug = settings.get('debug', self.__debug) - self.__security = settings.get('security', dict()) - self.__contacts = settings.get('contactPerson', self.__contacts) - self.__organization = settings.get('organization', self.__organization) + + if 'strict' in settings: + self.__strict = settings['strict'] + if 'debug' in settings: + self.__debug = settings['debug'] + if 'security' in settings: + self.__security = settings['security'] + else: + self.__security = {} + if 'contactPerson' in settings: + self.__contacts = settings['contactPerson'] + if 'organization' in settings: + self.__organization = settings['organization'] + self.__add_default_values() return True @@ -240,34 +249,59 @@ def __add_default_values(self): """ Add default values if the settings info is not complete """ - self.__sp.setdefault('assertionConsumerService', dict()).setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_POST) - self.__sp.setdefault('singleLogoutService', dict()).setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT) + if 'assertionConsumerService' not in self.__sp: + self.__sp['assertionConsumerService'] = {} + if 'binding' not in self.__sp['assertionConsumerService']: + self.__sp['assertionConsumerService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_POST + + if 'singleLogoutService' not in self.__sp: + self.__sp['singleLogoutService'] = {} + if 'binding' not in self.__sp['singleLogoutService']: + self.__sp['singleLogoutService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT + + if 'singleLogoutService' not in self.__idp: + self.__idp['singleLogoutService'] = {} # Related to nameID - self.__sp.setdefault('NameIDFormat', OneLogin_Saml2_Constants.NAMEID_PERSISTENT) - self.__security.setdefault('nameIdEncrypted', False) + if 'NameIDFormat' not in self.__sp: + self.__sp['NameIDFormat'] = OneLogin_Saml2_Constants.NAMEID_PERSISTENT + if 'nameIdEncrypted' not in self.__security: + self.__security['nameIdEncrypted'] = False # Sign provided - self.__security.setdefault('authnRequestsSigned', False) - self.__security.setdefault('logoutRequestSigned', False) - self.__security.setdefault('logoutResponseSigned', False) - self.__security.setdefault('signMetadata', False) + if 'authnRequestsSigned' not in self.__security: + self.__security['authnRequestsSigned'] = False + if 'logoutRequestSigned' not in self.__security: + self.__security['logoutRequestSigned'] = False + if 'logoutResponseSigned' not in self.__security: + self.__security['logoutResponseSigned'] = False + if 'signMetadata' not in self.__security: + self.__security['signMetadata'] = False # Sign expected - self.__security.setdefault('wantMessagesSigned', False) - self.__security.setdefault('wantAssertionsSigned', False) + if 'wantMessagesSigned' not in self.__security: + self.__security['wantMessagesSigned'] = False + if 'wantAssertionsSigned' not in self.__security: + self.__security['wantAssertionsSigned'] = False # Encrypt expected - self.__security.setdefault('wantAssertionsEncrypted', False) - self.__security.setdefault('wantNameIdEncrypted', False) + if 'wantAssertionsEncrypted' not in self.__security: + self.__security['wantAssertionsEncrypted'] = False + if 'wantNameIdEncrypted' not in self.__security: + self.__security['wantNameIdEncrypted'] = False - self.__idp.setdefault('x509cert', '') - self.__idp.setdefault('certFingerprint', '') + if 'x509cert' not in self.__idp: + self.__idp['x509cert'] = '' + if 'certFingerprint' not in self.__idp: + self.__idp['certFingerprint'] = '' - self.__sp.setdefault('x509cert', '') - self.__sp.setdefault('privateKey', '') + if 'x509cert' not in self.__sp: + self.__sp['x509cert'] = '' + if 'privateKey' not in self.__sp: + self.__sp['privateKey'] = '' - self.__security.setdefault('requestedAuthnContext', True) + if 'requestedAuthnContext' not in self.__security: + self.__security['requestedAuthnContext'] = True def check_settings(self, settings): """ @@ -290,10 +324,12 @@ def check_settings(self, settings): errors.append('idp_not_found') else: idp = settings['idp'] - if len(idp.get('entityId', '')) == 0: + if 'entityId' not in idp or len(idp['entityId']) == 0: errors.append('idp_entityId_not_found') - if len(idp.get('singleSignOnService', dict()).get('url', '')) == 0: + if 'singleSignOnService' not in idp or \ + 'url' not in idp['singleSignOnService'] or \ + len(idp['singleSignOnService']['url']) == 0: errors.append('idp_sso_not_found') elif not validate_url(idp['singleSignOnService']['url']): errors.append('idp_sso_url_invalid') @@ -316,10 +352,12 @@ def check_settings(self, settings): if 'security' in settings: security = settings['security'] - if len(sp.get('entityId', '')) == 0: + if 'entityId' not in sp or len(sp['entityId']) == 0: errors.append('sp_entityId_not_found') - if len(sp.get('assertionConsumerService', dict()).get('url', '')) == 0: + if 'assertionConsumerService' not in sp or \ + 'url' not in sp['assertionConsumerService'] or \ + len(sp['assertionConsumerService']['url']) == 0: errors.append('sp_acs_not_found') elif not validate_url(sp['assertionConsumerService']['url']): errors.append('sp_acs_url_invalid') @@ -335,11 +373,11 @@ def check_settings(self, settings): 'certFileName' not in security['signMetadata']: errors.append('sp_signMetadata_invalid') - authn_sign = security.get('authnRequestsSigned', False) - logout_req_sign = security.get('logoutRequestSigned', False) - logout_res_sign = security.get('logoutResponseSigned', False) - want_assert_enc = security.get('wantAssertionsEncrypted', False) - want_nameid_enc = security.get('wantNameIdEncrypted', False) + authn_sign = 'authnRequestsSigned' in security and security['authnRequestsSigned'] + logout_req_sign = 'logoutRequestSigned' in security and security['logoutRequestSigned'] + logout_res_sign = 'logoutResponseSigned' in security and security['logoutResponseSigned'] + want_assert_enc = 'wantAssertionsEncrypted' in security and security['wantAssertionsEncrypted'] + want_nameid_enc = 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted'] if not self.check_sp_certs(): if authn_sign or logout_req_sign or logout_res_sign or \ @@ -353,9 +391,9 @@ def check_settings(self, settings): 'certFingerprint' in settings['idp'] and len(settings['idp']['certFingerprint']) > 0) - want_assert_sign = 'wantAssertionsSigned' in security.keys() and security['wantAssertionsSigned'] - want_mes_signed = 'wantMessagesSigned' in security.keys() and security['wantMessagesSigned'] - nameid_enc = 'nameIdEncrypted' in security.keys() and security['nameIdEncrypted'] + want_assert_sign = 'wantAssertionsSigned' in security and security['wantAssertionsSigned'] + want_mes_signed = 'wantMessagesSigned' in security and security['wantMessagesSigned'] + nameid_enc = 'nameIdEncrypted' in security and security['nameIdEncrypted'] if (want_assert_sign or want_mes_signed) and \ not(exists_x509 or exists_fingerprint): @@ -364,7 +402,7 @@ def check_settings(self, settings): errors.append('idp_cert_not_found_and_required') if 'contactPerson' in settings: - types = settings['contactPerson'].keys() + types = settings['contactPerson'] valid_types = ['technical', 'support', 'administrative', 'billing', 'other'] for c_type in types: if c_type not in valid_types: diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 97c3a234..6585f86f 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -318,15 +318,14 @@ def get_self_url_host(request_data): else: protocol = 'http' - server_port = request_data.get('server_port') - if server_port: - port_number = str(server_port) + if 'server_port' in request_data: + port_number = str(request_data['server_port']) + port = ':' + port_number + if protocol == 'http' and port_number == '80': port = '' elif protocol == 'https' and port_number == '443': port = '' - else: - port = ':' + port_number return '%s://%s%s' % (protocol, current_host, port) @@ -341,8 +340,11 @@ def get_self_host(request_data): :return: The current host :rtype: string """ - current_host = request_data.get('http_host') or request_data.get('server_name') - if not current_host: + if 'http_host' in request_data: + current_host = request_data['http_host'] + elif 'server_name' in request_data: + current_host = request_data['server_name'] + else: raise Exception('No hostname defined') if ':' in current_host: @@ -367,8 +369,8 @@ def is_https(request_data): :return: False if https is not active :rtype: boolean """ - is_https = request_data.get('https', 'off') != 'off' - is_https = is_https or (str(request_data.get('server_port', '')) == '443') + is_https = 'https' in request_data and request_data['https'] != 'off' + is_https = is_https or ('server_port' in request_data and str(request_data['server_port']) == '443') return is_https @staticmethod @@ -407,11 +409,11 @@ def get_self_routed_url_no_query(request_data): :rtype: string """ self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) - route = request_data.get('request_uri', '') - if route: - query_string = request_data.get('query_string', '') - if query_string: - route = route.replace(query_string, '') + route = '' + if 'request_uri' in request_data and request_data['request_uri']: + route = request_data['request_uri'] + if 'query_string' in request_data and request_data['query_string']: + route = route.replace(request_data['query_string'], '') return self_url_host + route @@ -428,8 +430,9 @@ def get_self_url(request_data): """ self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) - request_uri = request_data.get('request_uri', '') - if request_uri: + request_uri = '' + if 'request_uri' in request_data: + request_uri = request_data['request_uri'] if not request_uri.startswith('/'): match = re.search('^https?://[^/]*(/.*)', request_uri) if match is not None: From 86691cbe44b6c0ca3453db503dbef57fb783e6ac Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Mon, 2 Mar 2015 01:27:40 +0300 Subject: [PATCH 05/35] refactoring: move all xml-related functional to separate module --- src/onelogin/saml2/logout_request.py | 50 ++--- src/onelogin/saml2/logout_response.py | 35 ++- src/onelogin/saml2/metadata.py | 6 +- src/onelogin/saml2/response.py | 16 +- src/onelogin/saml2/settings.py | 9 +- src/onelogin/saml2/utils.py | 148 ++----------- src/onelogin/saml2/xml_utils.py | 164 ++++++++++++++ .../saml2_tests/logout_response_test.py | 4 +- .../src/OneLogin/saml2_tests/metadata_test.py | 3 +- .../src/OneLogin/saml2_tests/settings_test.py | 2 +- tests/src/OneLogin/saml2_tests/utils_test.py | 106 +--------- .../OneLogin/saml2_tests/xml_utils_test.py | 200 ++++++++++++++++++ 12 files changed, 436 insertions(+), 307 deletions(-) create mode 100644 src/onelogin/saml2/xml_utils.py create mode 100644 tests/src/OneLogin/saml2_tests/xml_utils_test.py diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 4fd8d5d5..ee31516b 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -9,11 +9,9 @@ """ -from lxml import etree -from xml.dom.minidom import Document - from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xml_utils import OneLogin_Saml2_XML class OneLogin_Saml2_Logout_Request(object): @@ -115,12 +113,8 @@ def get_id(request): :return: string ID :rtype: str object """ - if isinstance(request, etree._Element): - elem = request - else: - if isinstance(request, Document): - request = request.toxml() - elem = etree.fromstring(request) + + elem = OneLogin_Saml2_XML.to_etree(request) return elem.get('ID', None) @staticmethod @@ -134,26 +128,20 @@ def get_nameid_data(request, key=None): :return: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) :rtype: dict """ - if isinstance(request, etree._Element): - elem = request - else: - if isinstance(request, Document): - request = request.toxml() - elem = etree.fromstring(request) - + elem = OneLogin_Saml2_XML.to_etree(request) name_id = None - encrypted_entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID') + encrypted_entries = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:EncryptedID') if len(encrypted_entries) == 1: if key is None: raise Exception('Key is required in order to decrypt the NameID') - encrypted_data_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData') + 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_Utils.query(elem, '/samlp:LogoutRequest/saml:NameID') + entries = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:NameID') if len(entries) == 1: name_id = entries[0] @@ -192,15 +180,10 @@ def get_issuer(request): :return: The Issuer :rtype: string """ - if isinstance(request, etree._Element): - elem = request - else: - if isinstance(request, Document): - request = request.toxml() - elem = etree.fromstring(request) + elem = OneLogin_Saml2_XML.to_etree(request) issuer = None - issuer_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:Issuer') + issuer_nodes = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:Issuer') if len(issuer_nodes) == 1: issuer = issuer_nodes[0].text return issuer @@ -214,15 +197,10 @@ def get_session_indexes(request): :return: The SessionIndex value :rtype: list """ - if isinstance(request, etree._Element): - elem = request - else: - if isinstance(request, Document): - request = request.toxml() - elem = etree.fromstring(request) + elem = OneLogin_Saml2_XML.to_etree(request) session_indexes = [] - session_index_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex') + session_index_nodes = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex') for session_index_node in session_index_nodes: session_indexes.append(session_index_node.text) return session_indexes @@ -238,7 +216,7 @@ def is_valid(self, request_data): """ self.__error = None try: - dom = etree.fromstring(self.__logout_request) + dom = OneLogin_Saml2_XML.to_etree(self.__logout_request) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] @@ -246,8 +224,8 @@ def is_valid(self, request_data): get_data = ('get_data' in request_data and request_data['get_data']) or dict() if self.__settings.is_strict(): - res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) - if not isinstance(res, Document): + res = OneLogin_Saml2_XML.validate_xml(dom, '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') security = self.__settings.get_security_data() diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index 659bd2ae..cf8b6825 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -9,11 +9,9 @@ """ -from lxml.etree import fromstring -from xml.dom.minidom import Document, parseString - from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xml_utils import OneLogin_Saml2_XML class OneLogin_Saml2_Logout_Response(object): @@ -39,7 +37,7 @@ def __init__(self, settings, response=None): if response is not None: self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response) - self.document = parseString(self.__logout_response) + self.document = OneLogin_Saml2_XML.to_etree(self.__logout_response) def get_issuer(self): """ @@ -80,17 +78,16 @@ def is_valid(self, request_data, request_id=None): get_data = request_data['get_data'] if self.__settings.is_strict(): - res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) - if not isinstance(res, Document): + 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') security = self.__settings.get_security_data() # Check if the InResponseTo of the Logout Response matchs the ID of the Logout Request (requestId) if provided - if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'): - in_response_to = self.document.documentElement.getAttribute('InResponseTo') - if request_id != in_response_to: - raise Exception('The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id)) + 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)) # Check issuer issuer = self.get_issuer() @@ -100,11 +97,9 @@ def is_valid(self, request_data, request_id=None): current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check destination - if self.document.documentElement.hasAttribute('Destination'): - destination = self.document.documentElement.getAttribute('Destination') - if destination != '': - if current_url not in destination: - raise Exception('The LogoutRequest was received at $currentURL instead of $destination') + 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') if security['wantMessagesSigned']: if 'Signature' not in get_data: @@ -138,15 +133,13 @@ def is_valid(self, request_data, request_id=None): def __query(self, query): """ - Extracts a node from the DOMDocument (Logout Response Menssage) - :param query: Xpath Expresion + Extracts a node from the Etree (Logout Response Message) + :param query: Xpath Expression :type query: string :return: The queried node - :rtype: DOMNodeList + :rtype: Element """ - # Switch to lxml for querying - xml = self.document.toxml() - return OneLogin_Saml2_Utils.query(fromstring(xml), query) + return OneLogin_Saml2_XML.query(self.document, query) def build(self, in_response_to): """ diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index e0da92c7..f74a1442 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -11,10 +11,10 @@ from time import gmtime, strftime from datetime import datetime -from xml.dom.minidom import parseString from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xml_utils import OneLogin_Saml2_XML class OneLogin_Saml2_Metadata(object): @@ -186,7 +186,7 @@ def add_x509_key_descriptors(metadata, cert=None): if cert is None or cert == '': return metadata try: - xml = parseString(metadata) + xml = OneLogin_Saml2_XML.to_dom(metadata) except Exception as e: raise Exception('Error parsing metadata. ' + str(e)) @@ -221,4 +221,4 @@ def add_x509_key_descriptors(metadata, cert=None): signing.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) encryption.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) - return xml.toxml() + return OneLogin_Saml2_XML.to_string(xml) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 3f50db4f..9ff8b574 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -10,11 +10,9 @@ """ from copy import deepcopy -from lxml import etree -from xml.dom.minidom import Document - from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xml_utils import OneLogin_Saml2_XML class OneLogin_Saml2_Response(object): @@ -38,7 +36,7 @@ def __init__(self, settings, response): self.__settings = settings self.__error = None self.response = OneLogin_Saml2_Utils.b64decode(response) - self.document = etree.fromstring(self.response) + self.document = OneLogin_Saml2_XML.to_etree(self.response) self.decrypted_document = None self.encrypted = None @@ -91,8 +89,8 @@ def is_valid(self, request_data, request_id=None): signed_elements.append(sign_node.getparent().tag) if self.__settings.is_strict(): - res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) - if not isinstance(res, Document): + 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 Response. Not match the saml-schema-protocol-2.0.xsd') security = self.__settings.get_security_data() @@ -425,7 +423,7 @@ def __query(self, query): document = self.decrypted_document else: document = self.document - return OneLogin_Saml2_Utils.query(document, query) + return OneLogin_Saml2_XML.query(document, query) def __decrypt_assertion(self, dom): """ @@ -442,9 +440,9 @@ def __decrypt_assertion(self, dom): if not key: raise Exception('No private key available, check settings') - encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(dom, '//saml:EncryptedAssertion') + encrypted_assertion_nodes = OneLogin_Saml2_XML.query(dom, '//saml:EncryptedAssertion') if encrypted_assertion_nodes: - encrypted_data_nodes = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData') + encrypted_data_nodes = OneLogin_Saml2_XML.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData') if encrypted_data_nodes: encrypted_data = encrypted_data_nodes[0] OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 96a9d70b..e9966246 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -19,7 +19,7 @@ from onelogin.saml2.errors import OneLogin_Saml2_Error from onelogin.saml2.metadata import OneLogin_Saml2_Metadata from onelogin.saml2.utils import OneLogin_Saml2_Utils - +from onelogin.saml2.xml_utils import OneLogin_Saml2_XML # Regex from Django Software Foundation and individual contributors. # Released under a BSD 3-Clause License @@ -601,11 +601,10 @@ def validate_metadata(self, xml): raise Exception('Empty string supplied as input') errors = [] - res = OneLogin_Saml2_Utils.validate_xml(xml, 'saml-schema-metadata-2.0.xsd', self.__debug) - if not isinstance(res, Document): - errors.append(res) + dom = OneLogin_Saml2_XML.validate_xml(xml, 'saml-schema-metadata-2.0.xsd', self.__debug) + if isinstance(dom, str): + errors.append(dom) else: - dom = res element = dom.documentElement if element.tagName not in 'md:EntityDescriptor': errors.append('noEntityDescriptor_xml') diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 6585f86f..be67f697 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -16,20 +16,16 @@ import calendar from hashlib import sha1 from isodate import parse_duration as duration_parser -from lxml import etree -from lxml.etree import tostring, fromstring -from os.path import dirname, join import re -from sys import stderr from textwrap import wrap from uuid import uuid4 -from xml.dom.minidom import Document, Element, parseString import zlib import xmlsec from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.errors import OneLogin_Saml2_Error +from onelogin.saml2.xml_utils import OneLogin_Saml2_XML try: from urllib.parse import quote_plus # py3 @@ -141,50 +137,6 @@ def deflate_and_base64_encode(value): """ return OneLogin_Saml2_Utils.b64encode(zlib.compress(OneLogin_Saml2_Utils.bytes(value))[2:-4]) - @staticmethod - def validate_xml(xml, schema, debug=False): - """ - Validates a xml against a schema - :param xml: The xml that will be validated - :type: string|DomDocument - :param schema: The schema - :type: string - :param debug: If debug is active, the parse-errors will be showed - :type: bool - :returns: Error code or the DomDocument of the xml - :rtype: string - """ - assert isinstance(xml, OneLogin_Saml2_Utils.text_types) or isinstance(xml, Document) or isinstance(xml, etree._Element) - assert isinstance(schema, OneLogin_Saml2_Utils.str_type) - - if isinstance(xml, Document): - xml = xml.toxml() - elif isinstance(xml, etree._Element): - xml = tostring(xml) - - # Switch to lxml for schema validation - try: - dom = fromstring(xml) - except Exception: - return 'unloaded_xml' - - schema_file = join(dirname(__file__), 'schemas', schema) - f_schema = open(schema_file, 'r') - schema_doc = etree.parse(f_schema) - f_schema.close() - xmlschema = etree.XMLSchema(schema_doc) - - if not xmlschema.validate(dom): - if debug: - stderr.write('Errors validating the metadata') - stderr.write(':\n\n') - for error in xmlschema.error_log: - stderr.write('%s\n' % error.message) - - return 'invalid_xml' - - return parseString(etree.tostring(dom)) - @staticmethod def format_cert(cert, heads=True): """ @@ -547,28 +499,6 @@ def get_expire_time(cache_duration=None, valid_until=None): return '%d' % expire_time return None - @staticmethod - def query(dom, query, context=None): - """ - Extracts nodes that match the query from the Element - - :param dom: The root of the lxml objet - :type: Element - - :param query: Xpath Expresion - :type: string - - :param context: Context Node - :type: DOMElement - - :returns: The queried nodes - :rtype: list - """ - if context is None: - return dom.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) - else: - return context.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) - @staticmethod def delete_local_session(callback=None): """ @@ -650,7 +580,7 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): :returns: DOMElement | XMLSec nameID :rtype: string """ - doc = Document() + doc = OneLogin_Saml2_XML.make_dom() name_id_container = doc.createElementNS(OneLogin_Saml2_Constants.NS_SAML, 'container') name_id_container.setAttribute("xmlns:saml", OneLogin_Saml2_Constants.NS_SAML) @@ -662,7 +592,7 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): if cert is not None: xml = name_id_container.toxml() - elem = fromstring(xml) + elem = OneLogin_Saml2_XML.to_etree(xml) xmlsec.enable_debug_trace(debug) @@ -684,8 +614,7 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): edata = enc_ctx.encrypt_xml(enc_data, elem[0]) - newdoc = parseString(etree.tostring(edata)) - + newdoc = OneLogin_Saml2_XML.to_dom(edata) if newdoc.hasChildNodes(): child = newdoc.firstChild child.removeAttribute('xmlns') @@ -722,19 +651,19 @@ def get_status(dom): """ status = {} - status_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status') + status_entry = OneLogin_Saml2_XML.query(dom, '/samlp:Response/samlp:Status') if len(status_entry) == 0: raise Exception('Missing Status on response') - code_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode', status_entry[0]) + code_entry = OneLogin_Saml2_XML.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode', status_entry[0]) if len(code_entry) == 0: raise Exception('Missing Status Code on response') code = code_entry[0].values()[0] status['code'] = code - message_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusMessage', status_entry[0]) + message_entry = OneLogin_Saml2_XML.query(dom, '/samlp:Response/samlp:Status/samlp:StatusMessage', status_entry[0]) if len(message_entry) == 0: - subcode_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode', status_entry[0]) + subcode_entry = OneLogin_Saml2_XML.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode', status_entry[0]) if len(subcode_entry) > 0: status['msg'] = subcode_entry[0].values()[0] else: @@ -761,11 +690,7 @@ def decrypt_element(encrypted_data, key, debug=False): :returns: The decrypted element. :rtype: lxml.etree.Element """ - if isinstance(encrypted_data, Element): - encrypted_data = fromstring(str(encrypted_data.toxml())) - elif isinstance(encrypted_data, OneLogin_Saml2_Utils.str_type): - encrypted_data = fromstring(encrypted_data) - + encrypted_data = OneLogin_Saml2_XML.to_etree(encrypted_data) xmlsec.enable_debug_trace(debug) manager = xmlsec.KeysManager() @@ -794,35 +719,14 @@ def add_sign(xml, key, cert, debug=False): """ if xml is None or xml == '': raise Exception('Empty string supplied as input') - elif isinstance(xml, etree._Element): - elem = xml - elif isinstance(xml, Document): - xml = xml.toxml() - elem = fromstring(xml) - elif isinstance(xml, Element): - xml.setAttributeNS( - OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAMLP), - 'xmlns:samlp', - OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAMLP) - ) - xml.setAttributeNS( - OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAML), - 'xmlns:saml', - OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAML) - ) - xml = xml.toxml() - elem = fromstring(xml) - elif isinstance(xml, OneLogin_Saml2_Utils.text_types): - elem = fromstring(xml) - else: - raise Exception('Error parsing xml string') + elem = OneLogin_Saml2_XML.to_etree(OneLogin_Saml2_XML.set_node_ns_attributes(xml)) xmlsec.enable_debug_trace(debug) xmlsec.tree.add_ids(elem, ["ID"]) # Sign the metadacta with our private key. signature = xmlsec.template.create(elem, xmlsec.Transform.EXCL_C14N, xmlsec.Transform.RSA_SHA1, ns='ds') - issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer') + issuer = OneLogin_Saml2_XML.query(elem, '//saml:Issuer') if len(issuer) > 0: issuer = issuer[0] issuer.addnext(signature) @@ -846,8 +750,7 @@ def add_sign(xml, key, cert, debug=False): dsig_ctx.key = sign_key dsig_ctx.sign(signature) - newdoc = parseString(etree.tostring(elem)) - return newdoc.saveXML(newdoc.firstChild) + return OneLogin_Saml2_XML.to_string(elem) @staticmethod def validate_sign(xml, cert=None, fingerprint=None, validatecert=False, debug=False): @@ -872,39 +775,18 @@ def validate_sign(xml, cert=None, fingerprint=None, validatecert=False, debug=Fa try: if xml is None or xml == '': raise Exception('Empty string supplied as input') - elif isinstance(xml, etree._Element): - elem = xml - elif isinstance(xml, Document): - xml = xml.toxml() - elem = fromstring(xml) - elif isinstance(xml, Element): - xml.setAttributeNS( - OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAMLP), - 'xmlns:samlp', - OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAMLP) - ) - xml.setAttributeNS( - OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAML), - 'xmlns:saml', - OneLogin_Saml2_Utils.utf8(OneLogin_Saml2_Constants.NS_SAML) - ) - xml = xml.toxml() - elem = fromstring(str(xml)) - elif isinstance(xml, OneLogin_Saml2_Utils.text_types): - elem = fromstring(xml) - else: - raise Exception('Error parsing xml string') + elem = OneLogin_Saml2_XML.to_etree(OneLogin_Saml2_XML.set_node_ns_attributes(xml)) xmlsec.enable_debug_trace(debug) xmlsec.tree.add_ids(elem, ["ID"]) - signature_nodes = OneLogin_Saml2_Utils.query(elem, '//ds:Signature') + signature_nodes = OneLogin_Saml2_XML.query(elem, '//ds:Signature') if len(signature_nodes) > 0: signature_node = signature_nodes[0] if (cert is None or cert == '') and fingerprint: - x509_certificate_nodes = OneLogin_Saml2_Utils.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate') + x509_certificate_nodes = OneLogin_Saml2_XML.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate') if len(x509_certificate_nodes) > 0: x509_certificate_node = x509_certificate_nodes[0] x509_cert_value = x509_certificate_node.text diff --git a/src/onelogin/saml2/xml_utils.py b/src/onelogin/saml2/xml_utils.py new file mode 100644 index 00000000..3f65cc4f --- /dev/null +++ b/src/onelogin/saml2/xml_utils.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_XML class + +Copyright (c) 2015, OneLogin, Inc. +All rights reserved. + +Auxiliary class of OneLogin's Python Toolkit. + +""" + +from os.path import join, dirname +from lxml import etree +from xml.dom import minidom +from onelogin.saml2.constants import OneLogin_Saml2_Constants + + +class OneLogin_Saml2_XML(object): + _document_class = minidom.Document + _element_class = type(etree.Element('root')) + _node_class = minidom.Element + _fromstring = etree.fromstring + _parse_document = minidom.parseString + _schema_class = etree.XMLSchema + _text_class = (str, bytes) + _tostring = etree.tostring + + make_dom = _document_class + + @staticmethod + def to_string(xml): + """ + Serialize an element to an encoded string representation of its XML tree. + :param xml: The root node + :type xml: str|bytes|xml.dom.minidom.Document|etree.Element + :returns: string representation of xml + :rtype: string + """ + + if isinstance(xml, OneLogin_Saml2_XML._text_class): + return xml + if isinstance(xml, (OneLogin_Saml2_XML._document_class, OneLogin_Saml2_XML._node_class)): + return xml.toxml() + + if isinstance(xml, OneLogin_Saml2_XML._element_class): + return OneLogin_Saml2_XML._tostring(xml) + + raise ValueError("unsupported type %r" % type(xml)) + + @staticmethod + def to_dom(xml): + """ + Convert xml to dom + :param xml: The root node + :type xml: str|bytes|xml.dom.minidom.Document|etree.Element + :returns: dom + :rtype: xml.dom.minidom.Document + """ + if isinstance(xml, OneLogin_Saml2_XML._document_class): + return xml + if isinstance(xml, OneLogin_Saml2_XML._node_class): + return OneLogin_Saml2_XML._parse_document(xml.toxml()) + if isinstance(xml, OneLogin_Saml2_XML._element_class): + return OneLogin_Saml2_XML._parse_document(OneLogin_Saml2_XML._tostring(xml)) + if isinstance(xml, OneLogin_Saml2_XML._text_class): + return OneLogin_Saml2_XML._parse_document(xml) + raise ValueError("unsupported type %r" % type(xml)) + + @staticmethod + 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._document_class, OneLogin_Saml2_XML._node_class)): + return OneLogin_Saml2_XML._fromstring(xml.toxml()) + if isinstance(xml, OneLogin_Saml2_XML._text_class): + return OneLogin_Saml2_XML._fromstring(xml) + + raise ValueError('unsupported type %r' % type(xml)) + + @staticmethod + def set_node_ns_attributes(xml): + """ + Set minidom.Element Attribute NS + Note: function does not affect if input is not instance of Element + :param xml: the element + :type xml: xml.dom.minidom.Element + :returns: the input + :rtype: xml.dom.minidom.Element + """ + if isinstance(xml, OneLogin_Saml2_XML._node_class): + xml.setAttributeNS( + OneLogin_Saml2_Constants.NS_SAMLP, + 'xmlns:samlp', + OneLogin_Saml2_Constants.NS_SAMLP + ) + xml.setAttributeNS( + OneLogin_Saml2_Constants.NS_SAML, + 'xmlns:saml', + OneLogin_Saml2_Constants.NS_SAML + ) + return xml + + @staticmethod + def validate_xml(xml, schema, debug=False): + """ + Validates a xml against a schema + :param xml: The xml that will be validated + :type xml: str|bytes|xml.dom.minidom.Document|etree.Element + :param schema: The schema + :type schema: string + :param debug: If debug is active, the parse-errors will be showed + :type debug: bool + :returns: Error code or the DomDocument of the xml + :rtype: xml.dom.minidom.Document + """ + assert isinstance(schema, str) + + try: + dom = OneLogin_Saml2_XML.to_etree(xml) + except Exception as e: + if debug: + print(e) + return 'unloaded_xml' + + schema_file = join(dirname(__file__), 'schemas', schema) + with open(schema_file, 'r') as f_schema: + xmlschema = OneLogin_Saml2_XML._schema_class(etree.parse(f_schema)) + + if not xmlschema.validate(dom): + if debug: + print('Errors validating the metadata: ') + for error in xmlschema.error_log: + print(error.message) + return 'invalid_xml' + return OneLogin_Saml2_XML.to_dom(dom) + + @staticmethod + def query(dom, query, context=None): + """ + Extracts nodes that match the query from the Element + + :param dom: The root of the lxml objet + :type: Element + + :param query: Xpath Expresion + :type: string + + :param context: Context Node + :type: DOMElement + + :returns: The queried nodes + :rtype: list + """ + if context is None: + return dom.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) + else: + return context.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 5b02e5a5..9b166e5e 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -12,6 +12,7 @@ from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.utils import OneLogin_Saml2_XML try: from urllib.parse import urlparse, parse_qs @@ -45,7 +46,8 @@ def testConstructor(self): settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) response = OneLogin_Saml2_Logout_Response(settings, message) - self.assertRegexpMatches(response.document.toxml(), '', xml_authn_signed) res = parseString(xml_authn_signed) @@ -678,57 +600,52 @@ def testAddSign(self): self.assertIn('ds:Signature', ds_signature.tagName) xml_authn_dom = parseString(xml_authn) - xml_authn_signed_2 = OneLogin_Saml2_Utils.add_sign(xml_authn_dom, key, cert) + xml_authn_signed_2 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_authn_dom, key, cert)) self.assertIn('', xml_authn_signed_2) res_2 = parseString(xml_authn_signed_2) ds_signature_2 = res_2.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_2.tagName) - xml_authn_signed_3 = OneLogin_Saml2_Utils.add_sign(xml_authn_dom.firstChild, key, cert) + xml_authn_signed_3 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_authn_dom.firstChild, key, cert)) self.assertIn('', xml_authn_signed_3) res_3 = parseString(xml_authn_signed_3) ds_signature_3 = res_3.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_3.tagName) xml_authn_etree = etree.fromstring(xml_authn) - xml_authn_signed_4 = OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert) + xml_authn_signed_4 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert)) self.assertIn('', xml_authn_signed_4) res_4 = parseString(xml_authn_signed_4) ds_signature_4 = res_4.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_4.tagName) - xml_authn_signed_5 = OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert) + xml_authn_signed_5 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert)) self.assertIn('', xml_authn_signed_5) res_5 = parseString(xml_authn_signed_5) ds_signature_5 = res_5.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_5.tagName) xml_logout_req = b64decode(self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml.base64'))) - xml_logout_req_signed = OneLogin_Saml2_Utils.add_sign(xml_logout_req, key, cert) + xml_logout_req_signed = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_logout_req, key, cert)) self.assertIn('', xml_logout_req_signed) res_6 = parseString(xml_logout_req_signed) ds_signature_6 = res_6.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_6.tagName) xml_logout_res = b64decode(self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml.base64'))) - xml_logout_res_signed = OneLogin_Saml2_Utils.add_sign(xml_logout_res, key, cert) + xml_logout_res_signed = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_logout_res, key, cert)) self.assertIn('', xml_logout_res_signed) res_7 = parseString(xml_logout_res_signed) ds_signature_7 = res_7.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_7.tagName) xml_metadata = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml')) - xml_metadata_signed = OneLogin_Saml2_Utils.add_sign(xml_metadata, key, cert) + xml_metadata_signed = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_metadata, key, cert)) self.assertIn('', xml_metadata_signed) res_8 = parseString(xml_metadata_signed) ds_signature_8 = res_8.firstChild.firstChild.nextSibling.firstChild.nextSibling self.assertIn('ds:Signature', ds_signature_8.tagName) - try: - OneLogin_Saml2_Utils.add_sign(1, key, cert) - except Exception as e: - self.assertEqual('Error parsing xml string', str(e)) - def testValidateSign(self): """ Tests the validate_sign method of the OneLogin_Saml2_Utils @@ -747,11 +664,6 @@ def testValidateSign(self): except Exception as e: self.assertEqual('Empty string supplied as input', str(e)) - try: - self.assertFalse(OneLogin_Saml2_Utils.validate_sign(1, cert)) - except Exception as e: - self.assertEqual('Error parsing xml string', str(e)) - # expired cert xml_metadata_signed = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings1.xml')) self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed, cert)) diff --git a/tests/src/OneLogin/saml2_tests/xml_utils_test.py b/tests/src/OneLogin/saml2_tests/xml_utils_test.py new file mode 100644 index 00000000..a3371b12 --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/xml_utils_test.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2014, OneLogin, Inc. +# All rights reserved. + +import json +import unittest + +from base64 import b64decode +from lxml import etree +from os.path import dirname, join, exists +from xml.dom.minidom import Document, parseString +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.utils import OneLogin_Saml2_XML + + +class TestOneLoginSaml2Xml(unittest.TestCase): + data_path = join(dirname(__file__), '..', '..', '..', 'data') + + def loadSettingsJSON(self, filename=None): + if filename: + filename = join(dirname(__file__), '..', '..', '..', 'settings', filename) + else: + filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json') + if exists(filename): + stream = open(filename, 'r') + settings = json.load(stream) + stream.close() + return settings + else: + raise Exception('Settings json file does not exist') + + def file_contents(self, filename): + f = open(filename, 'r') + content = f.read() + f.close() + return content + + def testValidateXML(self): + """ + Tests the validate_xml method of the OneLogin_Saml2_XML + """ + metadata_unloaded = '' + res = OneLogin_Saml2_XML.validate_xml(metadata_unloaded, 'saml-schema-metadata-2.0.xsd') + self.assertIsInstance(res, str) + self.assertIn('unloaded_xml', res) + + metadata_invalid = self.file_contents(join(self.data_path, 'metadata', 'noentity_metadata_settings1.xml')) + + res = OneLogin_Saml2_XML.validate_xml(metadata_invalid, 'saml-schema-metadata-2.0.xsd') + self.assertIsInstance(res, str) + self.assertIn('invalid_xml', res) + + metadata_expired = self.file_contents(join(self.data_path, 'metadata', 'expired_metadata_settings1.xml')) + res = OneLogin_Saml2_XML.validate_xml(metadata_expired, 'saml-schema-metadata-2.0.xsd') + self.assertIsInstance(res, Document) + + metadata_ok = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml')) + res = OneLogin_Saml2_XML.validate_xml(metadata_ok, 'saml-schema-metadata-2.0.xsd') + self.assertIsInstance(res, Document) + + dom = parseString(metadata_ok) + res = OneLogin_Saml2_XML.validate_xml(dom, 'saml-schema-metadata-2.0.xsd') + self.assertIsInstance(res, Document) + + def testToString(self): + """ + Tests the to_string method of the OneLogin_Saml2_XML + """ + xml = 'test1' + doc = parseString(xml) + elem = etree.fromstring(xml) + bxml = xml.encode('utf8') + + self.assertIs(xml, OneLogin_Saml2_XML.to_string(xml)) + self.assertIs(bxml, OneLogin_Saml2_XML.to_string(bxml)) + self.assertEqual(etree.tostring(elem), OneLogin_Saml2_XML.to_string(elem)) + self.assertEqual(doc.toxml(), OneLogin_Saml2_XML.to_string(doc)) + self.assertRaisesRegex(ValueError, + 'unsupported type', + OneLogin_Saml2_XML.to_string, 1) + + def testToDom(self): + """ + Tests the to_dom method of the OneLogin_Saml2_XML + """ + xml = 'test1' + doc = parseString(xml) + elem = etree.fromstring(xml) + + res = OneLogin_Saml2_XML.to_dom(xml) + self.assertIsInstance(res, Document) + self.assertEqual(doc.toxml(), res.toxml()) + + res = OneLogin_Saml2_XML.to_dom(xml.encode('utf8')) + self.assertIsInstance(res, Document) + self.assertEqual(doc.toxml(), res.toxml()) + + res = OneLogin_Saml2_XML.to_dom(elem) + self.assertIsInstance(res, Document) + self.assertEqual(doc.toxml(), res.toxml()) + + res = OneLogin_Saml2_XML.to_dom(doc) + self.assertIs(res, doc) + + self.assertRaisesRegex(ValueError, + 'unsupported type', + OneLogin_Saml2_XML.to_dom, 1) + + def testToElement(self): + """ + Tests the to_etree method of the OneLogin_Saml2_XML + """ + xml = 'test1' + doc = parseString(xml) + elem = etree.fromstring(xml) + xml_expected = etree.tostring(elem) + + res = OneLogin_Saml2_XML.to_etree(xml) + self.assertIsInstance(res, etree._Element) + self.assertEqual(xml_expected, etree.tostring(res)) + + res = OneLogin_Saml2_XML.to_etree(xml.encode('utf8')) + self.assertIsInstance(res, etree._Element) + self.assertEqual(xml_expected, etree.tostring(res)) + + res = OneLogin_Saml2_XML.to_etree(doc) + self.assertIsInstance(res, etree._Element) + self.assertEqual(xml_expected, etree.tostring(res)) + + res = OneLogin_Saml2_XML.to_etree(elem) + self.assertIs(res, elem) + + self.assertRaisesRegex(ValueError, + 'unsupported type', + OneLogin_Saml2_XML.to_etree, 1) + + def testSetNodeAttributeNS(self): + """ + Tests the set_node_attribute_ns method of the OneLogin_Saml2_XML + """ + xml = 'test1' + doc = parseString(xml) + elem = etree.fromstring(xml) + + res = OneLogin_Saml2_XML.set_node_ns_attributes(doc.createElement('test2')) + self.assertEqual(OneLogin_Saml2_Constants.NS_SAMLP, res.getAttributeNS(OneLogin_Saml2_Constants.NS_SAMLP, 'samlp')) + self.assertEqual(OneLogin_Saml2_Constants.NS_SAML, res.getAttributeNS(OneLogin_Saml2_Constants.NS_SAML, 'saml')) + + self.assertIs(xml, OneLogin_Saml2_XML.set_node_ns_attributes(xml)) + self.assertIs(elem, OneLogin_Saml2_XML.set_node_ns_attributes(elem)) + self.assertIs(doc, OneLogin_Saml2_XML.set_node_ns_attributes(doc)) + + def testQuery(self): + """ + Tests the query method of the OneLogin_Saml2_Utils + """ + xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + xml = b64decode(xml) + dom = etree.fromstring(xml) + + assertion_nodes = OneLogin_Saml2_XML.query(dom, '/samlp:Response/saml:Assertion') + self.assertEqual(1, len(assertion_nodes)) + assertion = assertion_nodes[0] + self.assertIn('Assertion', assertion.tag) + + attribute_statement_nodes = OneLogin_Saml2_XML.query(dom, '/samlp:Response/saml:Assertion/saml:AttributeStatement') + self.assertEqual(1, len(assertion_nodes)) + attribute_statement = attribute_statement_nodes[0] + self.assertIn('AttributeStatement', attribute_statement.tag) + + attribute_statement_nodes_2 = OneLogin_Saml2_XML.query(dom, './saml:AttributeStatement', assertion) + self.assertEqual(1, len(attribute_statement_nodes_2)) + attribute_statement_2 = attribute_statement_nodes_2[0] + self.assertEqual(attribute_statement, attribute_statement_2) + + signature_res_nodes = OneLogin_Saml2_XML.query(dom, '/samlp:Response/ds:Signature') + self.assertEqual(1, len(signature_res_nodes)) + signature_res = signature_res_nodes[0] + self.assertIn('Signature', signature_res.tag) + + signature_nodes = OneLogin_Saml2_XML.query(dom, '/samlp:Response/saml:Assertion/ds:Signature') + self.assertEqual(1, len(signature_nodes)) + signature = signature_nodes[0] + self.assertIn('Signature', signature.tag) + + signature_nodes_2 = OneLogin_Saml2_XML.query(dom, './ds:Signature', assertion) + self.assertEqual(1, len(signature_nodes_2)) + signature2 = signature_nodes_2[0] + self.assertNotEqual(signature_res, signature2) + self.assertEqual(signature, signature2) + + signature_nodes_3 = OneLogin_Saml2_XML.query(dom, './ds:SignatureValue', assertion) + self.assertEqual(0, len(signature_nodes_3)) + + signature_nodes_4 = OneLogin_Saml2_XML.query(dom, './ds:Signature/ds:SignatureValue', assertion) + self.assertEqual(1, len(signature_nodes_4)) + + signature_nodes_5 = OneLogin_Saml2_XML.query(dom, './/ds:SignatureValue', assertion) + self.assertEqual(1, len(signature_nodes_5)) From 86f0c4b16389649b720022229a1ad9e8a42628af Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Mon, 2 Mar 2015 01:31:31 +0300 Subject: [PATCH 06/35] added support ujson library instead of standard json --- src/onelogin/saml2/settings.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index e9966246..b9eb77c8 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -8,12 +8,11 @@ Setting class of OneLogin's Python Toolkit. """ +from __future__ import absolute_import, print_function, with_statement from datetime import datetime -import json import re from os.path import dirname, exists, join, sep -from xml.dom.minidom import Document from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.errors import OneLogin_Saml2_Error @@ -21,6 +20,11 @@ from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.xml_utils import OneLogin_Saml2_XML +try: + import ujson as json +except ImportError: + import json + # Regex from Django Software Foundation and individual contributors. # Released under a BSD 3-Clause License url_regex = re.compile( @@ -233,15 +237,13 @@ def __load_settings_from_file(self): # In the php toolkit instead of being a json file it is a php file and # it is directly included - json_data = open(filename, 'r') - settings = json.load(json_data) - json_data.close() + with open(filename, 'r') as json_data: + settings = json.loads(json_data.read()) advanced_filename = self.get_base_path() + 'advanced_settings.json' if exists(advanced_filename): - json_data = open(advanced_filename, 'r') - settings.update(json.load(json_data)) # Merge settings - json_data.close() + with open(advanced_filename, 'r') as json_data: + settings.update(json.loads(json_data.read())) # Merge settings return self.__load_settings_from_dict(settings) From 10c56d39143771f19ac0d9012b7c4010c1999cb0 Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Mon, 2 Mar 2015 01:41:52 +0300 Subject: [PATCH 07/35] refactor: move all xml-templates to separate module --- src/onelogin/saml2/authn_request.py | 22 +--- src/onelogin/saml2/logout_request.py | 13 +-- src/onelogin/saml2/logout_response.py | 16 +-- src/onelogin/saml2/metadata.py | 29 +---- src/onelogin/saml2/xml_templates.py | 150 ++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 67 deletions(-) create mode 100644 src/onelogin/saml2/xml_templates.py diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index e1b1a8ca..22659b4e 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -9,8 +9,9 @@ """ -from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates class OneLogin_Saml2_Authn_Request(object): @@ -83,24 +84,7 @@ def __init__(self, settings, force_authn=False, is_passive=False): requested_authn_context_str += '%s' % authn_context requested_authn_context_str += ' ' - request = """ - %(entity_id)s - -%(requested_authn_context_str)s -""" % \ + request = OneLogin_Saml2_Templates.AUTHN_REQUEST % \ { 'id': uid, 'provider_name': provider_name_str, diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index ee31516b..b6987bda 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -11,6 +11,7 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -72,17 +73,7 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): else: session_index_str = '' - logout_request = """ - %(entity_id)s - %(name_id)s - %(session_index)s - """ % \ + logout_request = OneLogin_Saml2_Templates.LOGOUT_REQUEST % \ { 'id': uid, 'issue_instant': issue_instant, diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index cf8b6825..66f47bcc 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -11,6 +11,7 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -153,25 +154,14 @@ def build(self, in_response_to): uid = OneLogin_Saml2_Utils.generate_unique_id() issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) - logout_response = """ - %(entity_id)s - - - -""" % \ + logout_response = OneLogin_Saml2_Templates.LOGOUT_RESPONSE % \ { 'id': uid, 'issue_instant': issue_instant, 'destination': idp_data['singleLogoutService']['url'], 'in_response_to': in_response_to, 'entity_id': sp_data['entityId'], + 'status': "urn:oasis:names:tc:SAML:2.0:status:Success" } self.__logout_response = logout_response diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index f74a1442..de737c9f 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -""" OneLogin_Saml2_Metadata class +""" OneLoginSaml2Metadata class Copyright (c) 2014, OneLogin, Inc. All rights reserved. @@ -14,6 +14,7 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates from onelogin.saml2.xml_utils import OneLogin_Saml2_XML @@ -89,11 +90,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N if len(organization) > 0: organization_info = [] for (lang, info) in organization.items(): - org = """ - %(name)s - %(display_name)s - %(url)s - """ % \ + org = OneLogin_Saml2_Templates.MD_ORGANISATION % \ { 'lang': lang, 'name': info['name'], @@ -107,10 +104,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N if len(contacts) > 0: contacts_info = [] for (ctype, info) in contacts.items(): - contact = """ - %(name)s - %(email)s - """ % \ + contact = OneLogin_Saml2_Templates.MD_CONTACT_PERSON % \ { 'type': ctype, 'name': info['givenName'], @@ -119,20 +113,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N contacts_info.append(contact) str_contacts = '\n'.join(contacts_info) - metadata = """ - - -%(sls)s %(name_id_format)s - - -%(organization)s -%(contacts)s -""" % \ + metadata = OneLogin_Saml2_Templates.MD_ENTITY_DESCRIPTOR % \ { 'valid': valid_until_time, 'cache': cache_duration_str, diff --git a/src/onelogin/saml2/xml_templates.py b/src/onelogin/saml2/xml_templates.py new file mode 100644 index 00000000..ed113d77 --- /dev/null +++ b/src/onelogin/saml2/xml_templates.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_Auth class + +Copyright (c) 2014, OneLogin, Inc. +All rights reserved. + +Main class of OneLogin's Python Toolkit. + +Initializes the SP SAML instance + +""" + + +class OneLogin_Saml2_Templates(object): + + ATTRIBUTE = """ + + %s + """ + + AUTHN_REQUEST = """\ + + %(entity_id)s + + %(requested_authn_context_str)s +""" + + LOGOUT_REQUEST = """\ + + %(entity_id)s + %(name_id)s + %(session_index)s +""" + + LOGOUT_RESPONSE = """\ + + %(entity_id)s + + + +""" + + MD_CONTACT_PERSON = """\ + + %(name)s + %(email)s + """ + + MD_ENTITY_DESCRIPTOR = """\ + + + +%(sls)s %(name_id_format)s + + +%(organization)s +%(contacts)s +""" + + MD_ORGANISATION = """\ + + %(name)s + %(display_name)s + %(url)s + """ + + RESPONSE = """\ + + %(entity_id)s + + + + + + %(entity_id)s + + %(name_id)s + + + + + + + + %(requester)s + + + +%(authn_context)s + + +%(attributes)s + + +""" From bc47ae221cac764e827459effedeb469242acb06 Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Wed, 4 Mar 2015 20:13:53 +0300 Subject: [PATCH 08/35] global refactoring, fully switch to lxml library, remove depends on xml.minidom --- src/onelogin/saml2/compat.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/onelogin/saml2/compat.py diff --git a/src/onelogin/saml2/compat.py b/src/onelogin/saml2/compat.py new file mode 100644 index 00000000..74328320 --- /dev/null +++ b/src/onelogin/saml2/compat.py @@ -0,0 +1,4 @@ +# @copyright (c) 2002-2015 Acronis International GmbH. All rights reserved. +# since $Id: $ + +__author__ = "Bulat Gaifullin (bulat.gaifullin@acronis.com)" From 71fc03cfa5dfa9080449048988e153dc87f962e7 Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Wed, 4 Mar 2015 20:14:03 +0300 Subject: [PATCH 09/35] global refactoring, fully switch to lxml library, remove depends on xml.minidom --- src/onelogin/saml2/auth.py | 5 +- src/onelogin/saml2/compat.py | 55 +++++++- src/onelogin/saml2/constants.py | 3 +- src/onelogin/saml2/logout_request.py | 14 +- src/onelogin/saml2/metadata.py | 58 ++++----- src/onelogin/saml2/response.py | 10 +- src/onelogin/saml2/settings.py | 25 ++-- src/onelogin/saml2/utils.py | 123 ++++-------------- src/onelogin/saml2/xml_utils.py | 94 +++++-------- tests/src/OneLogin/saml2_tests/auth_test.py | 33 ++--- .../saml2_tests/authn_request_test.py | 31 ++--- .../saml2_tests/logout_request_test.py | 17 +-- .../saml2_tests/logout_response_test.py | 15 ++- .../src/OneLogin/saml2_tests/metadata_test.py | 6 +- .../src/OneLogin/saml2_tests/response_test.py | 35 ++--- .../src/OneLogin/saml2_tests/settings_test.py | 7 +- tests/src/OneLogin/saml2_tests/utils_test.py | 66 +++++----- .../OneLogin/saml2_tests/xml_utils_test.py | 69 ++-------- 18 files changed, 275 insertions(+), 391 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index eb649be9..1f6b825d 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -11,6 +11,7 @@ """ +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 @@ -239,7 +240,7 @@ def get_attribute(self, name): :returns: Attribute value if exists or [] :rtype: string """ - assert isinstance(name, OneLogin_Saml2_Utils.str_type) + assert isinstance(name, compat.str_type) return self.__attributes.get(name) def login(self, return_to=None, force_authn=False, is_passive=False): @@ -392,5 +393,5 @@ def __build_signature(self, saml_data, relay_state, saml_type): sign_data = [saml_data_str, relay_state_str, alg_str] msg = '&'.join(sign_data) - signature = dsig_ctx.sign_binary(OneLogin_Saml2_Utils.bytes(msg), xmlsec.Transform.RSA_SHA1) + signature = dsig_ctx.sign_binary(compat.to_bytes(msg), xmlsec.Transform.RSA_SHA1) return OneLogin_Saml2_Utils.b64encode(signature) diff --git a/src/onelogin/saml2/compat.py b/src/onelogin/saml2/compat.py index 74328320..853824cc 100644 --- a/src/onelogin/saml2/compat.py +++ b/src/onelogin/saml2/compat.py @@ -1,4 +1,53 @@ -# @copyright (c) 2002-2015 Acronis International GmbH. All rights reserved. -# since $Id: $ +# -*- coding: utf-8 -*- -__author__ = "Bulat Gaifullin (bulat.gaifullin@acronis.com)" +""" py3 compatibility class + +Copyright (c) 2014, OneLogin, Inc. +All rights reserved. + +""" + +from __future__ import absolute_import, print_function, with_statement + + +if isinstance(b'', type('')): # py 2.x + text_types = (basestring,) # noqa + str_type = basestring # noqa + + def utf8(s): + """ return utf8-encoded string """ + if isinstance(s, basestring): + return s.decode("utf8") + return unicode(s) + + def to_string(s): + """ return string """ + if isinstance(s, unicode): + return s.encode("utf8") + return str(s) + + def to_bytes(s): + """ return bytes """ + return str(s) + +else: # py 3.x + text_types = (bytes, str) + str_type = str + + def utf8(s): + """ return utf8-encoded string """ + if isinstance(s, bytes): + return s.decode("utf8") + return str(s) + + def to_string(s): + """convert to string""" + if isinstance(s, bytes): + return s.decode("utf8") + return str(s) + + def to_bytes(s): + """return bytes""" + if isinstance(s, str): + return s.encode("utf8") + return bytes(s) diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py index 698d8ee7..99a0c3a5 100644 --- a/src/onelogin/saml2/constants.py +++ b/src/onelogin/saml2/constants.py @@ -83,5 +83,6 @@ class OneLogin_Saml2_Constants(object): 'samlp': NS_SAMLP, 'saml': NS_SAML, 'ds': NS_DS, - 'xenc': NS_XENC + 'xenc': NS_XENC, + 'md': NS_MD } diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index b6987bda..1979a5ed 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -207,7 +207,7 @@ def is_valid(self, request_data): """ self.__error = None try: - dom = OneLogin_Saml2_XML.to_etree(self.__logout_request) + root = OneLogin_Saml2_XML.to_etree(self.__logout_request) idp_data = self.__settings.get_idp_data() idp_entity_id = idp_data['entityId'] @@ -215,7 +215,7 @@ def is_valid(self, request_data): 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(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active()) + 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') @@ -224,14 +224,14 @@ def is_valid(self, request_data): current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) # Check NotOnOrAfter - if dom.get('NotOnOrAfter', None): - na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.get('NotOnOrAfter')) + if root.get('NotOnOrAfter', None): + na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.get('NotOnOrAfter')) if na <= OneLogin_Saml2_Utils.now(): raise Exception('Timing issues (please check your clock settings)') # Check destination - if dom.get('Destination', None): - destination = dom.get('Destination') + if root.get('Destination', None): + destination = root.get('Destination') if destination != '': if current_url not in destination: raise Exception( @@ -244,7 +244,7 @@ def is_valid(self, request_data): ) # Check issuer - issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom) + 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') diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index de737c9f..69fde258 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -12,6 +12,7 @@ from time import gmtime, strftime from datetime import datetime +from onelogin.saml2 import compat from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates @@ -56,7 +57,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N """ if valid_until is None: valid_until = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_VALID - if not isinstance(valid_until, OneLogin_Saml2_Utils.str_type): + if not isinstance(valid_until, compat.str_type): valid_until_time = gmtime(valid_until) valid_until_time = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time) else: @@ -64,7 +65,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N if cache_duration is None: cache_duration = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_CACHED - if not isinstance(cache_duration, OneLogin_Saml2_Utils.str_type): + if not isinstance(cache_duration, compat.str_type): cache_duration_str = 'PT%sS' % cache_duration else: cache_duration_str = cache_duration @@ -149,6 +150,18 @@ def sign_metadata(metadata, key, cert): """ return OneLogin_Saml2_Utils.add_sign(metadata, key, cert) + @staticmethod + def __add_x509_key_descriptors(root, cert, signing): + key_descriptor = OneLogin_Saml2_XML.make_child(root, '{%s}KeyDescriptor' % OneLogin_Saml2_Constants.NS_MD) + root.remove(key_descriptor) + root.insert(0, key_descriptor) + key_info = OneLogin_Saml2_XML.make_child(key_descriptor, '{%s}KeyInfo' % OneLogin_Saml2_Constants.NS_DS) + key_data = OneLogin_Saml2_XML.make_child(key_info, '{%s}X509Data' % OneLogin_Saml2_Constants.NS_DS) + + x509_certificate = OneLogin_Saml2_XML.make_child(key_data, '{%s}X509Certificate' % OneLogin_Saml2_Constants.NS_DS) + x509_certificate.text = OneLogin_Saml2_Utils.format_cert(cert, False) + key_descriptor.set('use', ('encryption', 'signing')[signing]) + @staticmethod def add_x509_key_descriptors(metadata, cert=None): """ @@ -167,39 +180,16 @@ def add_x509_key_descriptors(metadata, cert=None): if cert is None or cert == '': return metadata try: - xml = OneLogin_Saml2_XML.to_dom(metadata) + root = OneLogin_Saml2_XML.to_etree(metadata) except Exception as e: raise Exception('Error parsing metadata. ' + str(e)) - formated_cert = OneLogin_Saml2_Utils.format_cert(cert, False) - x509_certificate = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Certificate') - content = xml.createTextNode(formated_cert) - x509_certificate.appendChild(content) - - key_data = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Data') - key_data.appendChild(x509_certificate) - - key_info = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:KeyInfo') - key_info.appendChild(key_data) - - key_descriptor = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'md:KeyDescriptor') - - entity_descriptor = xml.getElementsByTagName('md:EntityDescriptor')[0] - - sp_sso_descriptor = entity_descriptor.getElementsByTagName('md:SPSSODescriptor')[0] - sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) - sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild) - - signing = xml.getElementsByTagName('md:KeyDescriptor')[0] - signing.setAttribute('use', 'signing') - - encryption = xml.getElementsByTagName('md:KeyDescriptor')[1] - encryption.setAttribute('use', 'encryption') - - signing.appendChild(key_info) - encryption.appendChild(key_info.cloneNode(True)) - - signing.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) - encryption.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS) + assert root.tag == '{%s}EntityDescriptor' % OneLogin_Saml2_Constants.NS_MD + try: + sp_sso_descriptor = next(root.iterfind('.//md:SPSSODescriptor', namespaces=OneLogin_Saml2_Constants.NSMAP)) + except StopIteration: + raise Exception('Malformed metadata.') - return OneLogin_Saml2_XML.to_string(xml) + OneLogin_Saml2_Metadata.__add_x509_key_descriptors(sp_sso_descriptor, cert, False) + OneLogin_Saml2_Metadata.__add_x509_key_descriptors(sp_sso_descriptor, cert, True) + return OneLogin_Saml2_XML.to_string(root) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 9ff8b574..745f683e 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -425,13 +425,13 @@ def __query(self, query): document = self.document return OneLogin_Saml2_XML.query(document, query) - def __decrypt_assertion(self, dom): + def __decrypt_assertion(self, xml): """ Decrypts the Assertion :raises: Exception if no private key available - :param dom: Encrypted Assertion - :type dom: Element + :param xml: Encrypted Assertion + :type xml: Element :returns: Decrypted Assertion :rtype: Element """ @@ -440,13 +440,13 @@ def __decrypt_assertion(self, dom): if not key: raise Exception('No private key available, check settings') - encrypted_assertion_nodes = OneLogin_Saml2_XML.query(dom, '//saml:EncryptedAssertion') + encrypted_assertion_nodes = OneLogin_Saml2_XML.query(xml, '//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: encrypted_data = encrypted_data_nodes[0] OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) - return dom + return xml def get_error(self): """ diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index b9eb77c8..7be05fbd 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -8,12 +8,11 @@ Setting class of OneLogin's Python Toolkit. """ -from __future__ import absolute_import, print_function, with_statement - from datetime import datetime import re from os.path import dirname, exists, join, sep +from onelogin.saml2 import compat from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.errors import OneLogin_Saml2_Error from onelogin.saml2.metadata import OneLogin_Saml2_Metadata @@ -597,30 +596,26 @@ def validate_metadata(self, xml): :rtype: list """ - assert isinstance(xml, OneLogin_Saml2_Utils.str_type) + assert isinstance(xml, compat.text_types) if len(xml) == 0: raise Exception('Empty string supplied as input') errors = [] - dom = OneLogin_Saml2_XML.validate_xml(xml, 'saml-schema-metadata-2.0.xsd', self.__debug) - if isinstance(dom, str): - errors.append(dom) + root = OneLogin_Saml2_XML.validate_xml(xml, 'saml-schema-metadata-2.0.xsd', self.__debug) + if isinstance(root, str): + errors.append(root) else: - element = dom.documentElement - if element.tagName not in 'md:EntityDescriptor': + if root.tag != '{%s}EntityDescriptor' % OneLogin_Saml2_Constants.NS_MD: errors.append('noEntityDescriptor_xml') else: - if len(element.getElementsByTagName('md:SPSSODescriptor')) != 1: + if (len(root.findall('.//md:SPSSODescriptor', namespaces=OneLogin_Saml2_Constants.NSMAP))) != 1: errors.append('onlySPSSODescriptor_allowed_xml') else: - valid_until = cache_duration = None - - if element.hasAttribute('validUntil'): - valid_until = OneLogin_Saml2_Utils.parse_SAML_to_time(element.getAttribute('validUntil')) - if element.hasAttribute('cacheDuration'): - cache_duration = element.getAttribute('cacheDuration') + valid_until, cache_duration = root.get('validUntil'), root.get('cacheDuration') + if valid_until: + valid_until = OneLogin_Saml2_Utils.parse_SAML_to_time(valid_until) expire_time = OneLogin_Saml2_Utils.get_expire_time(cache_duration, valid_until) if expire_time is not None and int(datetime.now().strftime('%s')) > int(expire_time): errors.append('expired_xml') diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index be67f697..96d7951f 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -9,8 +9,6 @@ """ -from __future__ import absolute_import, print_function, with_statement - import base64 from datetime import datetime import calendar @@ -23,10 +21,12 @@ import zlib import xmlsec +from onelogin.saml2 import compat from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.errors import OneLogin_Saml2_Error from onelogin.saml2.xml_utils import OneLogin_Saml2_XML + try: from urllib.parse import quote_plus # py3 except ImportError: @@ -40,53 +40,6 @@ class OneLogin_Saml2_Utils(object): urls, add sign, encrypt, decrypt, sign validation, handle xml ... """ - if isinstance(b'', type('')): # py 2.x - text_types = (basestring,) # noqa - str_type = basestring # noqa - - @staticmethod - def utf8(s): - """ return utf8-encoded string """ - if isinstance(s, basestring): - return s.decode("utf8") - return unicode(s) - - @staticmethod - def string(s): - """ return string """ - if isinstance(s, unicode): - return s.encode("utf8") - return str(s) - - @staticmethod - def bytes(s): - """ return bytes """ - return str(s) - else: # py 3.x - str_type = str - text_types = (bytes, str) - - @staticmethod - def utf8(s): - """ return utf8-encoded string """ - if isinstance(s, bytes): - return s.decode("utf8") - return str(s) - - @staticmethod - def string(s): - """convert to string""" - if isinstance(s, bytes): - return s.decode("utf8") - return str(s) - - @staticmethod - def bytes(s): - """return bytes""" - if isinstance(s, str): - return s.encode("utf8") - return bytes(s) - @staticmethod def escape_url(url): """ @@ -101,7 +54,7 @@ def escape_url(url): @staticmethod def b64encode(s): """base64 encode""" - return OneLogin_Saml2_Utils.string(base64.b64encode(OneLogin_Saml2_Utils.bytes(s))) + return compat.to_string(base64.b64encode(compat.to_bytes(s))) @staticmethod def b64decode(s): @@ -135,7 +88,7 @@ def deflate_and_base64_encode(value): :returns: The deflated and encoded string :rtype: string """ - return OneLogin_Saml2_Utils.b64encode(zlib.compress(OneLogin_Saml2_Utils.bytes(value))[2:-4]) + return OneLogin_Saml2_Utils.b64encode(zlib.compress(compat.to_bytes(value))[2:-4]) @staticmethod def format_cert(cert, heads=True): @@ -213,7 +166,7 @@ def redirect(url, parameters={}, request_data={}): :returns: Url :rtype: string """ - assert isinstance(url, OneLogin_Saml2_Utils.str_type) + assert isinstance(url, compat.str_type) assert isinstance(parameters, dict) if url.startswith('/'): @@ -400,7 +353,7 @@ def generate_unique_id(): :return: A unique string :rtype: string """ - return 'ONELOGIN_%s' % sha1(OneLogin_Saml2_Utils.bytes(uuid4().hex)).hexdigest() + return 'ONELOGIN_%s' % sha1(compat.to_bytes(uuid4().hex)).hexdigest() @staticmethod def parse_time_to_SAML(time): @@ -458,7 +411,7 @@ def parse_duration(duration, timestamp=None): :return: The new timestamp, after the duration is applied. :rtype: int """ - assert isinstance(duration, OneLogin_Saml2_Utils.str_type) + assert isinstance(duration, compat.str_type) assert timestamp is None or isinstance(timestamp, int) timedelta = duration_parser(duration) @@ -519,7 +472,7 @@ def calculate_x509_fingerprint(x509_cert): :returns: Formated fingerprint :rtype: string """ - assert isinstance(x509_cert, OneLogin_Saml2_Utils.str_type) + assert isinstance(x509_cert, compat.str_type) lines = x509_cert.split('\n') data = '' @@ -541,7 +494,7 @@ def calculate_x509_fingerprint(x509_cert): data += line # "data" now contains the certificate as a base64-encoded string. The # fingerprint of the certificate is the sha1-hash of the certificate. - return sha1(base64.b64decode(OneLogin_Saml2_Utils.bytes(data))).hexdigest().lower() + return sha1(base64.b64decode(compat.to_bytes(data))).hexdigest().lower() @staticmethod def format_finger_print(fingerprint): @@ -580,20 +533,14 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): :returns: DOMElement | XMLSec nameID :rtype: string """ - doc = OneLogin_Saml2_XML.make_dom() - name_id_container = doc.createElementNS(OneLogin_Saml2_Constants.NS_SAML, 'container') - name_id_container.setAttribute("xmlns:saml", OneLogin_Saml2_Constants.NS_SAML) - name_id = doc.createElement('saml:NameID') - name_id.setAttribute('SPNameQualifier', sp_nq) - name_id.setAttribute('Format', sp_format) - name_id.appendChild(doc.createTextNode(value)) - name_id_container.appendChild(name_id) + root = OneLogin_Saml2_XML.make_root("{%s}container" % OneLogin_Saml2_Constants.NS_SAML) + name_id = OneLogin_Saml2_XML.make_child(root, '{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML) + name_id.set('SPNameQualifier', sp_nq) + name_id.set('Format', sp_format) + name_id.text = value if cert is not None: - xml = name_id_container.toxml() - elem = OneLogin_Saml2_XML.to_etree(xml) - xmlsec.enable_debug_trace(debug) # Load the public cert @@ -602,7 +549,8 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): # Prepare for encryption enc_data = xmlsec.template.encrypted_data_create( - elem, xmlsec.Transform.AES128, type=xmlsec.EncryptionType.ELEMENT) + root, xmlsec.Transform.AES128, type=xmlsec.EncryptionType.ELEMENT) + xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_data) enc_key = xmlsec.template.add_encrypted_key(key_info, xmlsec.Transform.RSA_OAEP) @@ -611,32 +559,12 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): # Encrypt! enc_ctx = xmlsec.EncryptionContext(manager) enc_ctx.key = xmlsec.Key.generate(xmlsec.KeyData.AES, 128, xmlsec.KeyDataType.SESSION) - - edata = enc_ctx.encrypt_xml(enc_data, elem[0]) - - newdoc = OneLogin_Saml2_XML.to_dom(edata) - if newdoc.hasChildNodes(): - child = newdoc.firstChild - child.removeAttribute('xmlns') - child.removeAttribute('xmlns:saml') - child.setAttribute('xmlns:xenc', OneLogin_Saml2_Constants.NS_XENC) - child.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS) - - nodes = newdoc.getElementsByTagName("*") - for node in nodes: - if node.tagName == 'KeyInfo': - node.tagName = 'dsig:KeyInfo' - node.removeAttribute('xmlns') - node.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS) - else: - node.tagName = 'xenc:' + node.tagName - - encrypted_id = newdoc.createElement('saml:EncryptedID') - encrypted_data = newdoc.replaceChild(encrypted_id, newdoc.firstChild) - encrypted_id.appendChild(encrypted_data) - return newdoc.saveXML(encrypted_id) + enc_ctx.encrypt_xml(enc_data, name_id) + new_root = OneLogin_Saml2_XML.make_root(root.tag, nsmap={"dsig": OneLogin_Saml2_Constants.NS_DS, "xenc": OneLogin_Saml2_Constants.NS_XENC}) + new_root[:] = root[:] + return '' + compat.to_string(OneLogin_Saml2_XML.to_string(new_root[0])) + '' else: - return doc.saveXML(name_id) + return OneLogin_Saml2_XML.extract_tag_text(root, "saml:NameID") @staticmethod def get_status(dom): @@ -696,7 +624,6 @@ def decrypt_element(encrypted_data, key, debug=False): manager.add_key(xmlsec.Key.from_memory(key, xmlsec.KeyFormat.PEM, None)) enc_ctx = xmlsec.EncryptionContext(manager) - return enc_ctx.decrypt(encrypted_data) @staticmethod @@ -720,7 +647,7 @@ def add_sign(xml, key, cert, debug=False): if xml is None or xml == '': raise Exception('Empty string supplied as input') - elem = OneLogin_Saml2_XML.to_etree(OneLogin_Saml2_XML.set_node_ns_attributes(xml)) + elem = OneLogin_Saml2_XML.to_etree(xml) xmlsec.enable_debug_trace(debug) xmlsec.tree.add_ids(elem, ["ID"]) # Sign the metadacta with our private key. @@ -776,7 +703,7 @@ def validate_sign(xml, cert=None, fingerprint=None, validatecert=False, debug=Fa if xml is None or xml == '': raise Exception('Empty string supplied as input') - elem = OneLogin_Saml2_XML.to_etree(OneLogin_Saml2_XML.set_node_ns_attributes(xml)) + elem = OneLogin_Saml2_XML.to_etree(xml) xmlsec.enable_debug_trace(debug) xmlsec.tree.add_ids(elem, ["ID"]) @@ -841,9 +768,9 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=xmlsec.Tr xmlsec.enable_debug_trace(debug) dsig_ctx = xmlsec.SignatureContext() dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None) - dsig_ctx.verify_binary(OneLogin_Saml2_Utils.bytes(signed_query), + dsig_ctx.verify_binary(compat.to_bytes(signed_query), algorithm, - OneLogin_Saml2_Utils.bytes(signature)) + compat.to_bytes(signature)) return True except xmlsec.Error as e: if debug: diff --git a/src/onelogin/saml2/xml_utils.py b/src/onelogin/saml2/xml_utils.py index 3f65cc4f..bc7500d5 100644 --- a/src/onelogin/saml2/xml_utils.py +++ b/src/onelogin/saml2/xml_utils.py @@ -11,24 +11,28 @@ from os.path import join, dirname from lxml import etree -from xml.dom import minidom +from onelogin.saml2 import compat from onelogin.saml2.constants import OneLogin_Saml2_Constants +for prefix, url in OneLogin_Saml2_Constants.NSMAP.items(): + etree.register_namespace(prefix, url) + + class OneLogin_Saml2_XML(object): - _document_class = minidom.Document _element_class = type(etree.Element('root')) - _node_class = minidom.Element - _fromstring = etree.fromstring - _parse_document = minidom.parseString + _parse_etree = staticmethod(etree.fromstring) _schema_class = etree.XMLSchema - _text_class = (str, bytes) - _tostring = etree.tostring + _text_class = compat.text_types + _unparse_etree = staticmethod(etree.tostring) - make_dom = _document_class + dump = staticmethod(etree.dump) + make_root = etree.Element + make_child = etree.SubElement + cleanup_namespaces = etree.cleanup_namespaces @staticmethod - def to_string(xml): + def to_string(xml, **kwargs): """ Serialize an element to an encoded string representation of its XML tree. :param xml: The root node @@ -39,33 +43,13 @@ def to_string(xml): if isinstance(xml, OneLogin_Saml2_XML._text_class): return xml - if isinstance(xml, (OneLogin_Saml2_XML._document_class, OneLogin_Saml2_XML._node_class)): - return xml.toxml() if isinstance(xml, OneLogin_Saml2_XML._element_class): - return OneLogin_Saml2_XML._tostring(xml) + OneLogin_Saml2_XML.cleanup_namespaces(xml) + return OneLogin_Saml2_XML._unparse_etree(xml, **kwargs) raise ValueError("unsupported type %r" % type(xml)) - @staticmethod - def to_dom(xml): - """ - Convert xml to dom - :param xml: The root node - :type xml: str|bytes|xml.dom.minidom.Document|etree.Element - :returns: dom - :rtype: xml.dom.minidom.Document - """ - if isinstance(xml, OneLogin_Saml2_XML._document_class): - return xml - if isinstance(xml, OneLogin_Saml2_XML._node_class): - return OneLogin_Saml2_XML._parse_document(xml.toxml()) - if isinstance(xml, OneLogin_Saml2_XML._element_class): - return OneLogin_Saml2_XML._parse_document(OneLogin_Saml2_XML._tostring(xml)) - if isinstance(xml, OneLogin_Saml2_XML._text_class): - return OneLogin_Saml2_XML._parse_document(xml) - raise ValueError("unsupported type %r" % type(xml)) - @staticmethod def to_etree(xml): """ @@ -77,36 +61,11 @@ def to_etree(xml): """ if isinstance(xml, OneLogin_Saml2_XML._element_class): return xml - if isinstance(xml, (OneLogin_Saml2_XML._document_class, OneLogin_Saml2_XML._node_class)): - return OneLogin_Saml2_XML._fromstring(xml.toxml()) if isinstance(xml, OneLogin_Saml2_XML._text_class): - return OneLogin_Saml2_XML._fromstring(xml) + return OneLogin_Saml2_XML._parse_etree(xml) raise ValueError('unsupported type %r' % type(xml)) - @staticmethod - def set_node_ns_attributes(xml): - """ - Set minidom.Element Attribute NS - Note: function does not affect if input is not instance of Element - :param xml: the element - :type xml: xml.dom.minidom.Element - :returns: the input - :rtype: xml.dom.minidom.Element - """ - if isinstance(xml, OneLogin_Saml2_XML._node_class): - xml.setAttributeNS( - OneLogin_Saml2_Constants.NS_SAMLP, - 'xmlns:samlp', - OneLogin_Saml2_Constants.NS_SAMLP - ) - xml.setAttributeNS( - OneLogin_Saml2_Constants.NS_SAML, - 'xmlns:saml', - OneLogin_Saml2_Constants.NS_SAML - ) - return xml - @staticmethod def validate_xml(xml, schema, debug=False): """ @@ -120,10 +79,10 @@ def validate_xml(xml, schema, debug=False): :returns: Error code or the DomDocument of the xml :rtype: xml.dom.minidom.Document """ - assert isinstance(schema, str) + assert isinstance(schema, compat.str_type) try: - dom = OneLogin_Saml2_XML.to_etree(xml) + xml = OneLogin_Saml2_XML.to_etree(xml) except Exception as e: if debug: print(e) @@ -133,13 +92,13 @@ def validate_xml(xml, schema, debug=False): with open(schema_file, 'r') as f_schema: xmlschema = OneLogin_Saml2_XML._schema_class(etree.parse(f_schema)) - if not xmlschema.validate(dom): + if not xmlschema.validate(xml): if debug: print('Errors validating the metadata: ') for error in xmlschema.error_log: print(error.message) return 'invalid_xml' - return OneLogin_Saml2_XML.to_dom(dom) + return xml @staticmethod def query(dom, query, context=None): @@ -162,3 +121,16 @@ def query(dom, query, context=None): return dom.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) else: return context.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) + + @staticmethod + def extract_tag_text(xml, tagname): + open_tag = compat.to_bytes("<%s" % tagname) + close_tag = compat.to_bytes("" % 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]) diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 02ed104c..77fbf1cc 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -8,6 +8,7 @@ from os.path import dirname, join, exists import unittest +from onelogin.saml2 import compat from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.settings import OneLogin_Saml2_Settings @@ -154,12 +155,12 @@ def testProcessResponseInvalidRequestId(self): """ request_data = self.get_request() message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) - plain_message = OneLogin_Saml2_Utils.string(b64decode(message)) + 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': OneLogin_Saml2_Utils.string(b64encode(OneLogin_Saml2_Utils.bytes(plain_message))) + 'SAMLResponse': compat.to_string(b64encode(compat.to_bytes(plain_message))) } auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) request_id = 'invalid' @@ -268,7 +269,7 @@ def testProcessSLOResponseNoSucess(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_responses', 'invalids', 'status_code_responder.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -287,7 +288,7 @@ def testProcessSLOResponseRequestId(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -311,7 +312,7 @@ def testProcessSLOResponseValid(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -348,7 +349,7 @@ def testProcessSLOResponseValidDeletingSession(self): # $_SESSION['samltest'] = true; # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -403,7 +404,7 @@ def testProcessSLORequestNotOnOrAfterFailed(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -424,7 +425,7 @@ def testProcessSLORequestDeletingSession(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -472,7 +473,7 @@ def testProcessSLORequestRelayState(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -500,7 +501,7 @@ def testProcessSLORequestSignedResponse(self): request_data = self.get_request() message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(message)) current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) @@ -591,7 +592,7 @@ def testLoginForceAuthN(self): sso_url = settings_info['idp']['singleSignOnService']['url'] self.assertIn(sso_url, target_url) self.assertIn('SAMLRequest', parsed_query) - request = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])) + request = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])) self.assertNotIn('ForceAuthn="true"', request) auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) @@ -599,7 +600,7 @@ def testLoginForceAuthN(self): parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) self.assertIn(sso_url, target_url_2) self.assertIn('SAMLRequest', parsed_query_2) - request_2 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0])) + request_2 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0])) self.assertNotIn('ForceAuthn="true"', request_2) auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) @@ -607,7 +608,7 @@ def testLoginForceAuthN(self): parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) self.assertIn(sso_url, target_url_3) self.assertIn('SAMLRequest', parsed_query_3) - request_3 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])) + request_3 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])) self.assertIn('ForceAuthn="true"', request_3) def testLoginIsPassive(self): @@ -625,7 +626,7 @@ def testLoginIsPassive(self): sso_url = settings_info['idp']['singleSignOnService']['url'] self.assertIn(sso_url, target_url) self.assertIn('SAMLRequest', parsed_query) - request = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])) + request = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])) self.assertNotIn('IsPassive="true"', request) auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) @@ -633,7 +634,7 @@ def testLoginIsPassive(self): parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) self.assertIn(sso_url, target_url_2) self.assertIn('SAMLRequest', parsed_query_2) - request_2 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0])) + request_2 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0])) self.assertNotIn('IsPassive="true"', request_2) auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) @@ -641,7 +642,7 @@ def testLoginIsPassive(self): parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) self.assertIn(sso_url, target_url_3) self.assertIn('SAMLRequest', parsed_query_3) - request_3 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])) + request_3 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])) self.assertIn('IsPassive="true"', request_3) def testLogout(self): diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index a7d0e8aa..6289912d 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -7,6 +7,7 @@ from os.path import dirname, join, exists import unittest +from onelogin.saml2 import compat from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.settings import OneLogin_Saml2_Settings @@ -49,7 +50,7 @@ def testCreateRequest(self): authn_request = OneLogin_Saml2_Authn_Request(settings) authn_request_encoded = authn_request.get_request() - inflated = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) + inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) self.assertRegexpMatches(inflated, '^') self.assertRegexpMatches(inflated, 'http://stuff.com/endpoints/metadata.php') diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 971b09fe..9e2a619a 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -8,6 +8,7 @@ import unittest from xml.dom.minidom import parseString +from onelogin.saml2 import compat from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils @@ -53,7 +54,7 @@ def testConstructor(self): url_parts = urlparse(logout_url) exploded = parse_qs(url_parts.query) payload = exploded['SAMLRequest'][0] - inflated = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)) + inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)) self.assertRegexpMatches(inflated, '^invalid')) + message = compat.to_string(OneLogin_Saml2_Utils.b64encode('invalid')) request_data = { 'http_host': 'example.com', 'script_name': 'index.html', @@ -556,12 +557,12 @@ def testIsInValidIssuer(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_issuer_assertion.xml.base64')) - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message = OneLogin_Saml2_Utils.b64encode(plain_message) xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_issuer_message.xml.base64')) - plain_message_2 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_2)) + plain_message_2 = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml_2)) plain_message_2 = plain_message_2.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message_2 = OneLogin_Saml2_Utils.b64encode(plain_message_2) @@ -600,7 +601,7 @@ def testIsInValidSessionIndex(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_sessionindex.xml.base64')) - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message = OneLogin_Saml2_Utils.b64encode(plain_message) @@ -630,7 +631,7 @@ def testDatetimeWithMiliseconds(self): current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response_with_miliseconds.xm.base64')) - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message = OneLogin_Saml2_Utils.b64encode(plain_message) response = OneLogin_Saml2_Response(settings, message) @@ -649,32 +650,32 @@ def testIsInValidSubjectConfirmation(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_subjectconfirmation_method.xml.base64')) - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message = OneLogin_Saml2_Utils.b64encode(plain_message) xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_subjectconfirmation_data.xml.base64')) - plain_message_2 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_2)) + plain_message_2 = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml_2)) plain_message_2 = plain_message_2.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message_2 = OneLogin_Saml2_Utils.b64encode(plain_message_2) xml_3 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_inresponse.xml.base64')) - plain_message_3 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_3)) + plain_message_3 = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml_3)) plain_message_3 = plain_message_3.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message_3 = OneLogin_Saml2_Utils.b64encode(plain_message_3) xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_recipient.xml.base64')) - plain_message_4 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_4)) + plain_message_4 = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml_4)) plain_message_4 = plain_message_4.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message_4 = OneLogin_Saml2_Utils.b64encode(plain_message_4) xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_noa.xml.base64')) - plain_message_5 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_5)) + plain_message_5 = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml_5)) plain_message_5 = plain_message_5.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message_5 = OneLogin_Saml2_Utils.b64encode(plain_message_5) xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_nb.xml.base64')) - plain_message_6 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_6)) + plain_message_6 = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml_6)) plain_message_6 = plain_message_6.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message_6 = OneLogin_Saml2_Utils.b64encode(plain_message_6) @@ -751,7 +752,7 @@ def testIsInValidRequestId(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message = OneLogin_Saml2_Utils.b64encode(plain_message) @@ -783,7 +784,7 @@ def testIsInValidSignIssues(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message = OneLogin_Saml2_Utils.b64encode(plain_message) @@ -856,7 +857,7 @@ def testIsInValidEncIssues(self): } current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml)) plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) message = OneLogin_Saml2_Utils.b64encode(plain_message) @@ -1017,14 +1018,14 @@ def testIsValidEnc(self): settings.set_strict(True) xml_7 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) # In order to avoid the destination problem - plain_message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64decode(xml_7)) + plain_message = compat.to_string(OneLogin_Saml2_Utils.b64decode(xml_7)) request_data = { 'http_host': 'example.com', 'script_name': 'index.html' } 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) - message = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.b64encode(plain_message)) + message = compat.to_string(OneLogin_Saml2_Utils.b64encode(plain_message)) response_7 = OneLogin_Saml2_Response(settings, message) response_7.is_valid(request_data) self.assertEqual('No Signature found. SAML Response rejected', response_7.get_error()) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 5f57ff86..df1ad661 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -7,6 +7,7 @@ from os.path import dirname, join, exists, sep import unittest +from onelogin.saml2 import compat from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils @@ -356,7 +357,7 @@ def testGetSPMetadata(self): Case unsigned metadata """ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - metadata = settings.get_sp_metadata() + metadata = compat.to_string(settings.get_sp_metadata()) self.assertNotEqual(len(metadata), 0) self.assertIn('\n', metadata) self.assertIn('', metadata) self.assertIn('', metadata) + self.assertIn('\n\n', metadata) def testGetSPMetadataSignedNoMetadataCert(self): """ diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index 87e19079..d8192e88 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -10,6 +10,7 @@ import unittest from xml.dom.minidom import parseString +from onelogin.saml2 import compat from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils @@ -467,15 +468,15 @@ def testGenerateNameId(self): name_id_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified' name_id = OneLogin_Saml2_Utils.generate_name_id(name_id_value, entity_id, name_id_format) - expected_name_id = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde' - self.assertEqual(name_id, expected_name_id) + expected_name_id = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde' + self.assertEqual(expected_name_id, name_id) settings_info = self.loadSettingsJSON() x509cert = settings_info['idp']['x509cert'] key = OneLogin_Saml2_Utils.format_cert(x509cert) name_id_enc = OneLogin_Saml2_Utils.generate_name_id(name_id_value, entity_id, name_id_format, key) - expected_name_id_enc = '\n\n\n\n\n\n' + expected_name_id_enc = '\n\n\n\n\n\n' self.assertIn(expected_name_id_enc, name_id_enc) def testCalculateX509Fingerprint(self): @@ -531,29 +532,23 @@ def testDecryptElement(self): key = settings.get_sp_key() xml_nameid_enc = b64decode(self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64'))) - dom_nameid_enc = parseString(xml_nameid_enc) - encrypted_nameid_nodes = dom_nameid_enc.getElementsByTagName('saml:EncryptedID') - encrypted_data = encrypted_nameid_nodes[0].firstChild - encrypted_data_str = str(encrypted_nameid_nodes[0].firstChild.toxml()) + dom_nameid_enc = etree.fromstring(xml_nameid_enc) + encrypted_nameid_nodes = dom_nameid_enc.find('.//saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) + encrypted_data = encrypted_nameid_nodes[0] decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) self.assertEqual('{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML, decrypted_nameid.tag) self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text) - decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data_str, key) - self.assertEqual('{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML, decrypted_nameid.tag) - self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text) - xml_assertion_enc = b64decode(self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion_encrypted_nameid.xml.base64'))) - dom_assertion_enc = parseString(xml_assertion_enc) - encrypted_assertion_enc_nodes = dom_assertion_enc.getElementsByTagName('saml:EncryptedAssertion') - encrypted_data_assert = encrypted_assertion_enc_nodes[0].firstChild + dom_assertion_enc = etree.fromstring(xml_assertion_enc) + encrypted_assertion_enc_nodes = dom_assertion_enc.find('.//saml:EncryptedAssertion', namespaces=OneLogin_Saml2_Constants.NSMAP) + encrypted_data_assert = encrypted_assertion_enc_nodes[0] decrypted_assertion = OneLogin_Saml2_Utils.decrypt_element(encrypted_data_assert, key) self.assertEqual('{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML, decrypted_assertion.tag) self.assertEqual('_6fe189b1c241827773902f2b1d3a843418206a5c97', decrypted_assertion.get('ID')) - decrypted_assertion.xpath('/saml:Assertion/saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) - encrypted_nameid_nodes = decrypted_assertion.xpath('/saml:Assertion/saml:Subject/saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) + encrypted_nameid_nodes = decrypted_assertion.xpath('./saml:Subject/saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) encrypted_data = encrypted_nameid_nodes[0][0] decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) self.assertEqual('{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML, decrypted_nameid.tag) @@ -572,15 +567,15 @@ def testDecryptElement(self): f.close() self.assertRaises(Exception, OneLogin_Saml2_Utils.decrypt_element, encrypted_data, key3) xml_nameid_enc_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_EncMethod.xml.base64'))) - dom_nameid_enc_2 = parseString(xml_nameid_enc_2) - encrypted_nameid_nodes_2 = dom_nameid_enc_2.getElementsByTagName('saml:EncryptedID') - encrypted_data_2 = encrypted_nameid_nodes_2[0].firstChild + dom_nameid_enc_2 = etree.fromstring(xml_nameid_enc_2) + encrypted_nameid_nodes_2 = dom_nameid_enc_2.find('.//saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) + encrypted_data_2 = encrypted_nameid_nodes_2[0] self.assertRaises(Exception, OneLogin_Saml2_Utils.decrypt_element, encrypted_data_2, key) xml_nameid_enc_3 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_keyinfo.xml.base64'))) - dom_nameid_enc_3 = parseString(xml_nameid_enc_3) - encrypted_nameid_nodes_3 = dom_nameid_enc_3.getElementsByTagName('saml:EncryptedID') - encrypted_data_3 = encrypted_nameid_nodes_3[0].firstChild + dom_nameid_enc_3 = etree.fromstring(xml_nameid_enc_3) + encrypted_nameid_nodes_3 = dom_nameid_enc_3.find('.//saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) + encrypted_data_3 = encrypted_nameid_nodes_3[0] self.assertRaises(Exception, OneLogin_Saml2_Utils.decrypt_element, encrypted_data_3, key) def testAddSign(self): @@ -592,7 +587,7 @@ def testAddSign(self): cert = settings.get_sp_cert() xml_authn = b64decode(self.file_contents(join(self.data_path, 'requests', 'authn_request.xml.base64'))) - xml_authn_signed = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert)) + xml_authn_signed = compat.to_string(OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert)) self.assertIn('', xml_authn_signed) res = parseString(xml_authn_signed) @@ -600,47 +595,47 @@ def testAddSign(self): self.assertIn('ds:Signature', ds_signature.tagName) xml_authn_dom = parseString(xml_authn) - xml_authn_signed_2 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_authn_dom, key, cert)) + xml_authn_signed_2 = compat.to_string(OneLogin_Saml2_Utils.add_sign(xml_authn_dom.toxml(), key, cert)) self.assertIn('', xml_authn_signed_2) res_2 = parseString(xml_authn_signed_2) ds_signature_2 = res_2.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_2.tagName) - xml_authn_signed_3 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_authn_dom.firstChild, key, cert)) + xml_authn_signed_3 = compat.to_string(OneLogin_Saml2_Utils.add_sign(xml_authn_dom.firstChild.toxml(), key, cert)) self.assertIn('', xml_authn_signed_3) res_3 = parseString(xml_authn_signed_3) ds_signature_3 = res_3.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_3.tagName) xml_authn_etree = etree.fromstring(xml_authn) - xml_authn_signed_4 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert)) + xml_authn_signed_4 = compat.to_string(OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert)) self.assertIn('', xml_authn_signed_4) res_4 = parseString(xml_authn_signed_4) ds_signature_4 = res_4.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_4.tagName) - xml_authn_signed_5 = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert)) + xml_authn_signed_5 = compat.to_string(OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert)) self.assertIn('', xml_authn_signed_5) res_5 = parseString(xml_authn_signed_5) ds_signature_5 = res_5.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_5.tagName) xml_logout_req = b64decode(self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml.base64'))) - xml_logout_req_signed = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_logout_req, key, cert)) + xml_logout_req_signed = compat.to_string(OneLogin_Saml2_Utils.add_sign(xml_logout_req, key, cert)) self.assertIn('', xml_logout_req_signed) res_6 = parseString(xml_logout_req_signed) ds_signature_6 = res_6.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_6.tagName) xml_logout_res = b64decode(self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml.base64'))) - xml_logout_res_signed = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_logout_res, key, cert)) + xml_logout_res_signed = compat.to_string(OneLogin_Saml2_Utils.add_sign(xml_logout_res, key, cert)) self.assertIn('', xml_logout_res_signed) res_7 = parseString(xml_logout_res_signed) ds_signature_7 = res_7.firstChild.firstChild.nextSibling.nextSibling self.assertIn('ds:Signature', ds_signature_7.tagName) xml_metadata = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml')) - xml_metadata_signed = OneLogin_Saml2_Utils.string(OneLogin_Saml2_Utils.add_sign(xml_metadata, key, cert)) + xml_metadata_signed = compat.to_string(OneLogin_Saml2_Utils.add_sign(xml_metadata, key, cert)) self.assertIn('', xml_metadata_signed) res_8 = parseString(xml_metadata_signed) ds_signature_8 = res_8.firstChild.firstChild.nextSibling.firstChild.nextSibling @@ -716,27 +711,28 @@ def testValidateSign(self): self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed_2, None, fingerprint_2)) dom = parseString(xml_response_msg_signed_2) - self.assertTrue(OneLogin_Saml2_Utils.validate_sign(dom, cert_2)) + self.assertTrue(OneLogin_Saml2_Utils.validate_sign(dom.toxml(), cert_2)) dom.firstChild.firstChild.firstChild.nodeValue = 'https://idp.example.com/simplesaml/saml2/idp/metadata.php' dom.firstChild.getAttributeNode('ID').nodeValue = u'_34fg27g212d63k1f923845324475802ac0fc24530b' # Reference validation failed - self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom, cert_2)) + self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom.toxml(), cert_2)) invalid_fingerprint = 'afe71c34ef740bc87434be13a2263d31271da1f9' # Wrong fingerprint self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed_2, None, invalid_fingerprint)) dom_2 = parseString(xml_response_double_signed_2) - self.assertTrue(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2)) + self.assertTrue(OneLogin_Saml2_Utils.validate_sign(dom_2.toxml(), cert_2)) dom_2.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp' # Modified message - self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2)) + self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom_2.toxml(), cert_2)) dom_3 = parseString(xml_response_double_signed_2) assert_elem_3 = dom_3.firstChild.firstChild.nextSibling.nextSibling.nextSibling - self.assertTrue(OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2)) + assert_elem_3.setAttributeNS(OneLogin_Saml2_Constants.NS_SAML, 'xmlns:saml', OneLogin_Saml2_Constants.NS_SAML) + self.assertTrue(OneLogin_Saml2_Utils.validate_sign(assert_elem_3.toxml(), cert_2)) no_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_signature.xml.base64'))) self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_signed, cert)) diff --git a/tests/src/OneLogin/saml2_tests/xml_utils_test.py b/tests/src/OneLogin/saml2_tests/xml_utils_test.py index a3371b12..1c5efd86 100644 --- a/tests/src/OneLogin/saml2_tests/xml_utils_test.py +++ b/tests/src/OneLogin/saml2_tests/xml_utils_test.py @@ -9,8 +9,6 @@ from base64 import b64decode from lxml import etree from os.path import dirname, join, exists -from xml.dom.minidom import Document, parseString -from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_XML @@ -53,66 +51,32 @@ def testValidateXML(self): metadata_expired = self.file_contents(join(self.data_path, 'metadata', 'expired_metadata_settings1.xml')) res = OneLogin_Saml2_XML.validate_xml(metadata_expired, 'saml-schema-metadata-2.0.xsd') - self.assertIsInstance(res, Document) + self.assertIsInstance(res, OneLogin_Saml2_XML._element_class) metadata_ok = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml')) res = OneLogin_Saml2_XML.validate_xml(metadata_ok, 'saml-schema-metadata-2.0.xsd') - self.assertIsInstance(res, Document) - - dom = parseString(metadata_ok) - res = OneLogin_Saml2_XML.validate_xml(dom, 'saml-schema-metadata-2.0.xsd') - self.assertIsInstance(res, Document) + self.assertIsInstance(res, OneLogin_Saml2_XML._element_class) def testToString(self): """ Tests the to_string method of the OneLogin_Saml2_XML """ xml = 'test1' - doc = parseString(xml) elem = etree.fromstring(xml) bxml = xml.encode('utf8') self.assertIs(xml, OneLogin_Saml2_XML.to_string(xml)) self.assertIs(bxml, OneLogin_Saml2_XML.to_string(bxml)) self.assertEqual(etree.tostring(elem), OneLogin_Saml2_XML.to_string(elem)) - self.assertEqual(doc.toxml(), OneLogin_Saml2_XML.to_string(doc)) - self.assertRaisesRegex(ValueError, - 'unsupported type', - OneLogin_Saml2_XML.to_string, 1) - - def testToDom(self): - """ - Tests the to_dom method of the OneLogin_Saml2_XML - """ - xml = 'test1' - doc = parseString(xml) - elem = etree.fromstring(xml) - - res = OneLogin_Saml2_XML.to_dom(xml) - self.assertIsInstance(res, Document) - self.assertEqual(doc.toxml(), res.toxml()) - - res = OneLogin_Saml2_XML.to_dom(xml.encode('utf8')) - self.assertIsInstance(res, Document) - self.assertEqual(doc.toxml(), res.toxml()) - - res = OneLogin_Saml2_XML.to_dom(elem) - self.assertIsInstance(res, Document) - self.assertEqual(doc.toxml(), res.toxml()) - - res = OneLogin_Saml2_XML.to_dom(doc) - self.assertIs(res, doc) - - self.assertRaisesRegex(ValueError, - 'unsupported type', - OneLogin_Saml2_XML.to_dom, 1) + self.assertRaisesRegexp(ValueError, + 'unsupported type', + OneLogin_Saml2_XML.to_string, 1) def testToElement(self): """ Tests the to_etree method of the OneLogin_Saml2_XML """ xml = 'test1' - doc = parseString(xml) elem = etree.fromstring(xml) xml_expected = etree.tostring(elem) @@ -124,32 +88,15 @@ def testToElement(self): self.assertIsInstance(res, etree._Element) self.assertEqual(xml_expected, etree.tostring(res)) - res = OneLogin_Saml2_XML.to_etree(doc) self.assertIsInstance(res, etree._Element) self.assertEqual(xml_expected, etree.tostring(res)) res = OneLogin_Saml2_XML.to_etree(elem) self.assertIs(res, elem) - self.assertRaisesRegex(ValueError, - 'unsupported type', - OneLogin_Saml2_XML.to_etree, 1) - - def testSetNodeAttributeNS(self): - """ - Tests the set_node_attribute_ns method of the OneLogin_Saml2_XML - """ - xml = 'test1' - doc = parseString(xml) - elem = etree.fromstring(xml) - - res = OneLogin_Saml2_XML.set_node_ns_attributes(doc.createElement('test2')) - self.assertEqual(OneLogin_Saml2_Constants.NS_SAMLP, res.getAttributeNS(OneLogin_Saml2_Constants.NS_SAMLP, 'samlp')) - self.assertEqual(OneLogin_Saml2_Constants.NS_SAML, res.getAttributeNS(OneLogin_Saml2_Constants.NS_SAML, 'saml')) - - self.assertIs(xml, OneLogin_Saml2_XML.set_node_ns_attributes(xml)) - self.assertIs(elem, OneLogin_Saml2_XML.set_node_ns_attributes(elem)) - self.assertIs(doc, OneLogin_Saml2_XML.set_node_ns_attributes(doc)) + self.assertRaisesRegexp(ValueError, + 'unsupported type', + OneLogin_Saml2_XML.to_etree, 1) def testQuery(self): """ From d16479a940aeed9548a728379cd6a34e73e9a3c7 Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Wed, 4 Mar 2015 22:29:40 +0300 Subject: [PATCH 10/35] refactor response/request signing and validation approach --- src/onelogin/saml2/auth.py | 174 +++++++++++---- src/onelogin/saml2/logout_request.py | 18 -- src/onelogin/saml2/logout_response.py | 18 -- src/onelogin/saml2/utils.py | 26 +++ tests/src/OneLogin/saml2_tests/auth_test.py | 202 +++++++++++++++++- .../saml2_tests/logout_request_test.py | 108 ---------- .../saml2_tests/logout_response_test.py | 117 ---------- 7 files changed, 348 insertions(+), 315 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 1f6b825d..c82f397b 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -21,8 +21,6 @@ from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request -import xmlsec - class OneLogin_Saml2_Auth(object): """ @@ -48,7 +46,10 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): :type custom_base_path: string """ self.__request_data = request_data - self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path) + if isinstance(old_settings, OneLogin_Saml2_Settings): + self.__settings = old_settings + else: + self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path) self.__attributes = dict() self.__nameid = None self.__session_index = None @@ -123,7 +124,9 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ get_data = 'get_data' in self.__request_data and self.__request_data['get_data'] if get_data and 'SAMLResponse' in get_data: logout_response = OneLogin_Saml2_Logout_Response(self.__settings, get_data['SAMLResponse']) - if not logout_response.is_valid(self.__request_data, request_id): + if not self.validate_response_signature(get_data): + self.__errors.append('invalid_logout_response_signature') + elif not logout_response.is_valid(self.__request_data, request_id): self.__errors.append('invalid_logout_response') self.__error_reason = logout_response.get_error() elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS: @@ -133,7 +136,9 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ elif get_data and 'SAMLRequest' in get_data: logout_request = OneLogin_Saml2_Logout_Request(self.__settings, get_data['SAMLRequest']) - if not logout_request.is_valid(self.__request_data): + if not self.validate_request_signature(get_data): + self.__errors.append("invalid_logout_request_signature") + elif not logout_request.is_valid(self.__request_data): self.__errors.append('invalid_logout_request') self.__error_reason = logout_request.get_error() else: @@ -151,8 +156,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ security = self.__settings.get_security_data() if security['logoutResponseSigned']: - parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 - parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None)) + self.add_response_signature(parameters) return self.redirect_to(self.get_slo_url(), parameters) else: @@ -270,8 +274,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False): security = self.__settings.get_security_data() if security.get('authnRequestsSigned', False): - parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 - parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState']) + self.add_request_signature(parameters) return self.redirect_to(self.get_sso_url(), parameters) def logout(self, return_to=None, name_id=None, session_index=None): @@ -301,8 +304,6 @@ def logout(self, return_to=None, name_id=None, session_index=None): logout_request = OneLogin_Saml2_Logout_Request(self.__settings, name_id=name_id, session_index=session_index) - saml_request = logout_request.get_request() - parameters = {'SAMLRequest': logout_request.get_request()} if return_to is not None: parameters['RelayState'] = return_to @@ -311,8 +312,7 @@ def logout(self, return_to=None, name_id=None, session_index=None): security = self.__settings.get_security_data() if security.get('logoutRequestSigned', False): - parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 - parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState']) + self.add_request_signature(parameters) return self.redirect_to(slo_url, parameters) def get_sso_url(self): @@ -336,62 +336,142 @@ def get_slo_url(self): if 'url' in idp_data['singleLogoutService']: return idp_data['singleLogoutService']['url'] - def build_request_signature(self, saml_request, relay_state): + def add_request_signature(self, request_data): """ Builds the Signature of the SAML Request. - :param saml_request: The SAML Request - :type saml_request: string - - :param relay_state: The target URL the user should be redirected to - :type relay_state: string + :param request_data: The Request parameters + :type request_data: dict """ - return self.__build_signature(saml_request, relay_state, 'SAMLRequest') + return self.__build_signature(request_data, 'SAMLRequest') - def build_response_signature(self, saml_response, relay_state): + def add_response_signature(self, response_data): """ Builds the Signature of the SAML Response. - :param saml_response: The SAML Response - :type saml_response: string - - :param relay_state: The target URL the user should be redirected to - :type relay_state: string + :param response_data: The Response parameters + :type response_data: dict """ - return self.__build_signature(saml_response, relay_state, 'SAMLResponse') + return self.__build_signature(response_data, 'SAMLResponse') - def __build_signature(self, saml_data, relay_state, saml_type): + @staticmethod + def __build_sign_query(saml_data, relay_state, algorithm, saml_type): """ - Builds the Signature - :param saml_data: The SAML Data - :type saml_data: string + Build sign query + + :param saml_data: The Request data + :type saml_data: str - :param relay_state: The target URL the user should be redirected to - :type relay_state: string + :param relay_state: The Relay State + :type relay_state: str + + :param algorithm: The Signature Algorithm + :type algorithm: str :param saml_type: The target URL the user should be redirected to :type saml_type: string SAMLRequest | SAMLResponse """ - assert saml_type in ['SAMLRequest', 'SAMLResponse'] - # Load the key into the xmlsec context - key = self.__settings.get_sp_key() + sign_data = ['%s=%s' % (saml_type, OneLogin_Saml2_Utils.escape_url(saml_data))] + if relay_state is not None: + sign_data.append('RelayState=%s' % OneLogin_Saml2_Utils.escape_url(relay_state)) + sign_data.append('SigAlg=%s' % OneLogin_Saml2_Utils.escape_url(algorithm)) + return '&'.join(sign_data) + + def __build_signature(self, data, saml_type): + """ + Builds the Signature + :param data: The Request data + :type data: dict + + :param saml_type: The target URL the user should be redirected to + :type saml_type: string SAMLRequest | SAMLResponse + """ + assert saml_type in ('SAMLRequest', 'SAMLResponse') + key = self.get_settings().get_sp_key() if not key: raise OneLogin_Saml2_Error( - "Trying to sign the %s but can't load the SP private key" % saml_type, + "Trying to sign the %s but can't load the SP private key." % saml_type, OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND ) - xmlsec.enable_debug_trace(self.__settings.is_debug_active()) - dsig_ctx = xmlsec.SignatureContext() - dsig_ctx.key = xmlsec.Key.from_memory(key, xmlsec.KeyFormat.PEM, None) + msg = self.__build_sign_query(data[saml_type], + data.get('RelayState', None), + OneLogin_Saml2_Constants.RSA_SHA1, + saml_type) + + signature = OneLogin_Saml2_Utils.sign_binary(msg, key, debug=self.__settings.is_debug_active()) + data['Signature'] = OneLogin_Saml2_Utils.b64encode(signature) + data['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 - saml_data_str = '%s=%s' % (saml_type, OneLogin_Saml2_Utils.escape_url(saml_data)) - relay_state_str = 'RelayState=%s' % OneLogin_Saml2_Utils.escape_url(relay_state) - alg_str = 'SigAlg=%s' % OneLogin_Saml2_Utils.escape_url(OneLogin_Saml2_Constants.RSA_SHA1) + def validate_request_signature(self, request_data): + """ + Validate Request Signature - sign_data = [saml_data_str, relay_state_str, alg_str] - msg = '&'.join(sign_data) + :param request_data: The Request data + :type request_data: dict + + """ + + return self.__validate_signature(request_data, 'SAMLRequest') + + def validate_response_signature(self, request_data): + """ + Validate Response Signature + + :param request_data: The Request data + :type request_data: dict + + """ + + return self.__validate_signature(request_data, 'SAMLResponse') + + def __validate_signature(self, data, saml_type): + """ + 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 + """ - signature = dsig_ctx.sign_binary(compat.to_bytes(msg), xmlsec.Transform.RSA_SHA1) - return OneLogin_Saml2_Utils.b64encode(signature) + 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 + + try: + sign_alg = data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) + if isinstance(sign_alg, bytes): + sign_alg = sign_alg.decode('utf8') + + if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: + raise Exception('Invalid SigAlg, the %s rejected.' % saml_type) + + signed_query = self.__build_sign_query(data[saml_type], + data.get('RelayState', None), + sign_alg, + saml_type) + + if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, + OneLogin_Saml2_Utils.b64decode(signature), + x509cert, + debug=self.__settings.is_debug_active()): + raise Exception('Signature validation failed. %s rejected.' % saml_type) + return True + except Exception as e: + self._error_reason = str(e) + return False diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 1979a5ed..8581363b 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -251,24 +251,6 @@ def is_valid(self, request_data): 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') - - if 'Signature' in get_data: - sign_alg = get_data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) - if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: - raise Exception('Invalid signAlg in the recieved Logout Request') - - signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.escape_url(get_data['SAMLRequest']) - if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(get_data['RelayState'])) - signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(sign_alg)) - - cert = idp_data['x509cert'] - if not cert: - raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required') - - if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, OneLogin_Saml2_Utils.b64decode(get_data['Signature']), cert): - raise Exception('Signature validation failed. Logout Request rejected') - 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 66f47bcc..ffdce35f 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -105,24 +105,6 @@ def is_valid(self, request_data, request_id=None): 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') - - if 'Signature' in get_data: - sign_alg = get_data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1) - if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1: - raise Exception('Invalid signAlg in the recieved Logout Response') - - signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.escape_url(get_data['SAMLResponse']) - if 'RelayState' in get_data: - signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(get_data['RelayState'])) - signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.escape_url(sign_alg)) - - cert = idp_data['x509cert'] - if not cert: - raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required') - - if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, OneLogin_Saml2_Utils.b64decode(get_data['Signature']), cert): - raise Exception('Signature validation failed. Logout Response rejected') - return True # pylint: disable=R0801 except Exception as err: diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 96d7951f..9684e41c 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -743,6 +743,32 @@ def validate_sign(xml, cert=None, fingerprint=None, validatecert=False, debug=Fa return False + @staticmethod + 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) + @staticmethod def validate_binary_sign(signed_query, signature, cert=None, algorithm=xmlsec.Transform.RSA_SHA1, debug=False): """ diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 77fbf1cc..b17e24fe 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -791,17 +791,18 @@ def testBuildRequestSignature(self): settings = self.loadSettingsJSON() message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) relay_state = 'http://relaystate.com' + parameters = {"SAMLRequest": message, "RelayState": relay_state} auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) - signature = auth.build_request_signature(message, relay_state) + auth.add_request_signature(parameters) valid_signature = 'E17GU1STzanOXxBTKjweB1DovP8aMJdj5BEy0fnGoEslKdP6hpPc3enjT/bu7I8D8QzLoir8SxZVWdUDXgIxJIEgfK5snr+jJwfc5U2HujsOa/Xb3c4swoyPcyQhcxLRDhDjPq5cQxJfYoPeElvCuI6HAD1mtdd5PS/xDvbIxuw=' - self.assertEqual(signature, valid_signature) + self.assertEqual(valid_signature, parameters["Signature"]) settings['sp']['privateKey'] = '' settings['custom_base_path'] = u'invalid/path/' auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) self.assertRaisesRegexp(Exception, "Trying to sign the SAMLRequest but can't load the SP private key", - auth2.build_request_signature, message, relay_state) + auth2.add_request_signature, parameters) def testBuildResponseSignature(self): """ @@ -812,12 +813,199 @@ def testBuildResponseSignature(self): relay_state = 'http://relaystate.com' auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) - signature = auth.build_response_signature(message, relay_state) + parameters = {"SAMLResponse": message, 'RelayState': relay_state} + + auth.add_response_signature(parameters) valid_signature = 'IcyWLRX6Dz3wHBfpcUaNLVDMGM3uo6z2Z11Gjq0/APPJaHboKGljffsgMVAGBml497yckq+eYKmmz+jpURV9yTj2sF9qfD6CwX2dEzSzMdRzB40X7pWyHgEJGIhs6BhaOt5oXEk4T+h3AczERqpVYFpL00yo7FNtyQkhZFpHFhM=' - self.assertEqual(signature, valid_signature) + self.assertEqual(valid_signature, parameters['Signature']) settings['sp']['privateKey'] = '' settings['custom_base_path'] = u'invalid/path/' auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) - self.assertRaisesRegexp(Exception, "Trying to sign the SAMLRequest but can't load the SP private key", - auth2.build_request_signature, message, relay_state) + self.assertRaisesRegexp(Exception, "Trying to sign the SAMLResponse but can't load the SP private key", + auth2.add_response_signature, parameters) + + def testIsInValidLogoutResponseSign(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + settings.set_strict(False) + request_data['get_data'] = { + 'SAMLResponse': 'fZJva8IwEMa/Ssl7TZrW/gnqGHMMwSlM8cXeyLU9NaxNQi9lfvxVZczB5ptwSe733MPdjQma2qmFPdjOvyE5awiDU1MbUpevCetaoyyQJmWgQVK+VOvH14WSQ6Fca70tbc1ukPsEEGHrtTUsmM8mbDfKUhnFci8gliGINI/yXIAAiYnsw6JIRgWWAKlkwRZb6skJ64V6nKjDuSEPxvdPIowHIhpIsQkTFaYqSt9ZMEPy2oC/UEfvHSnOnfZFV38MjR1oN7TtgRv8tAZre9CGV9jYkGtT4Wnoju6Bauprme/ebOyErZbPi9XLfLnDoohwhHGc5WVSVhjCKM6rBMpYQpWJrIizfZ4IZNPxuTPqYrmd/m+EdONqPOfy8yG5rhxv0EMFHs52xvxWaHyd3tqD7+j37clWGGyh7vD+POiSrdZdWSIR49NrhR9R/teGTL8A', + 'RelayState': 'https://pitbulk.no-ip.org/newonelogin/demo1/index.php', + 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', + 'Signature': 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVfNKGA=' + } + + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertEqual([], auth.get_errors()) + + relay_state = request_data['get_data']['RelayState'] + del request_data['get_data']['RelayState'] + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn("invalid_logout_response_signature", auth.get_errors()) + request_data['get_data']['RelayState'] = relay_state + + settings.set_strict(True) + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_response', auth.get_errors()) + + settings.set_strict(False) + old_signature = request_data['get_data']['Signature'] + request_data['get_data']['Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333=' + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_response_signature', auth.get_errors()) + + request_data['get_data']['Signature'] = old_signature + old_signature_algorithm = request_data['get_data']['SigAlg'] + del request_data['get_data']['SigAlg'] + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertEqual([], auth.get_errors()) + + request_data['get_data']['RelayState'] = 'http://example.com/relaystate' + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_response_signature', auth.get_errors()) + + settings.set_strict(True) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message_6 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(request_data['get_data']['SAMLResponse'])) + plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url) + plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/') + request_data['get_data']['SAMLResponse'] = compat.to_string(OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6)) + + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_response_signature', auth.get_errors()) + + settings.set_strict(False) + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_response_signature', auth.get_errors()) + + request_data['get_data']['SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_response_signature', auth.get_errors()) + + settings_info = self.loadSettingsJSON() + settings_info['strict'] = True + settings_info['security']['wantMessagesSigned'] = True + settings = OneLogin_Saml2_Settings(settings_info) + + request_data['get_data']['SigAlg'] = old_signature_algorithm + old_signature = request_data['get_data']['Signature'] + del request_data['get_data']['Signature'] + request_data['get_data']['SAMLResponse'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6) + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_response_signature', auth.get_errors()) + + request_data['get_data']['Signature'] = old_signature + settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9' + del settings_info['idp']['x509cert'] + settings_2 = OneLogin_Saml2_Settings(settings_info) + + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_2) + auth.process_slo() + self.assertIn('In order to validate the sign on the SAMLResponse, the x509cert of the IdP is required', auth.get_errors()) + + def testIsValidLogoutRequestSign(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': { + 'SAMLRequest': 'lVLBitswEP0Vo7tjeWzJtki8LIRCYLvbNksPewmyPc6K2pJqyXQ/v1LSQlroQi/DMJr33rwZbZ2cJysezNms/gt+X9H55G2etBOXlx1ZFy2MdMoJLWd0wvfieP/xQcCGCrsYb3ozkRvI+wjpHC5eGU2Sw35HTg3lA8hqZFwWFcMKsStpxbEsxoLXeQN9OdY1VAgk+YqLC8gdCUQB7tyKB+281D6UaF6mtEiBPudcABcMXkiyD26Ulv6CevXeOpFlVvlunb5ttEmV3ZjlnGn8YTRO5qx0NuBs8kzpAd829tXeucmR5NH4J/203I8el6gFRUqbFPJnyEV51Wq30by4TLW0/9ZyarYTxt4sBsjUYLMZvRykl1Fxm90SXVkfwx4P++T4KSafVzmpUcVJ/sfSrQZJPphllv79W8WKGtLx0ir8IrVTqD1pT2MH3QAMSs4KTvui71jeFFiwirOmprwPkYW063+5uRq4urHiiC4e8hCX3J5wqAEGaPpw9XB5JmkBdeDqSlkz6CmUXdl0Qae5kv2F/1384wu3PwE=', + 'RelayState': '_1037fbc88ec82ce8e770b2bed1119747bb812a07e6', + 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', + 'Signature': 'XCwCyI5cs7WhiJlB5ktSlWxSBxv+6q2xT3c8L7dLV6NQG9LHWhN7gf8qNsahSXfCzA0Ey9dp5BQ0EdRvAk2DIzKmJY6e3hvAIEp1zglHNjzkgcQmZCcrkK9Czi2Y1WkjOwR/WgUTUWsGJAVqVvlRZuS3zk3nxMrLH6f7toyvuJc=' + } + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + + request = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(request_data['get_data']['SAMLRequest'])) + + settings.set_strict(False) + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertEqual([], auth.get_errors()) + + relay_state = request_data['get_data']['RelayState'] + del request_data['get_data']['RelayState'] + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_request_signature', auth.get_errors()) + + request_data['get_data']['RelayState'] = relay_state + + settings.set_strict(True) + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_request', auth.get_errors()) + + settings.set_strict(False) + old_signature = request_data['get_data']['Signature'] + request_data['get_data']['Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333=' + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_request_signature', auth.get_errors()) + + request_data['get_data']['Signature'] = old_signature + old_signature_algorithm = request_data['get_data']['SigAlg'] + del request_data['get_data']['SigAlg'] + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertEqual([], auth.get_errors()) + + settings.set_strict(True) + request_2 = request.replace('https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url) + request_2 = request_2.replace('https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/') + request_data['get_data']['SAMLRequest'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(request_2) + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_request_signature', auth.get_errors()) + + settings.set_strict(False) + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_request_signature', auth.get_errors()) + + request_data['get_data']['SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_request_signature', auth.get_errors()) + + settings_info = self.loadSettingsJSON() + settings_info['strict'] = True + settings_info['security']['wantMessagesSigned'] = True + settings = OneLogin_Saml2_Settings(settings_info) + request_data['get_data']['SigAlg'] = old_signature_algorithm + old_signature = request_data['get_data']['Signature'] + del request_data['get_data']['Signature'] + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) + auth.process_slo() + self.assertIn('invalid_logout_request_signature', auth.get_errors()) + + request_data['get_data']['Signature'] = old_signature + settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9' + del settings_info['idp']['x509cert'] + settings_2 = OneLogin_Saml2_Settings(settings_info) + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_2) + auth.process_slo() + self.assertIn('In order to validate the sign on the SAMLRequest, the x509cert of the IdP is required', auth.get_errors()) diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py index 9e2a619a..52b4ef1d 100644 --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -321,111 +321,3 @@ def testIsValid(self): request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) logout_request5 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) self.assertTrue(logout_request5.is_valid(request_data)) - - def testIsValidSign(self): - """ - Tests the is_valid method of the OneLogin_Saml2_LogoutRequest - """ - request_data = { - 'http_host': 'example.com', - 'script_name': 'index.html', - 'get_data': { - 'SAMLRequest': 'lVLBitswEP0Vo7tjeWzJtki8LIRCYLvbNksPewmyPc6K2pJqyXQ/v1LSQlroQi/DMJr33rwZbZ2cJysezNms/gt+X9H55G2etBOXlx1ZFy2MdMoJLWd0wvfieP/xQcCGCrsYb3ozkRvI+wjpHC5eGU2Sw35HTg3lA8hqZFwWFcMKsStpxbEsxoLXeQN9OdY1VAgk+YqLC8gdCUQB7tyKB+281D6UaF6mtEiBPudcABcMXkiyD26Ulv6CevXeOpFlVvlunb5ttEmV3ZjlnGn8YTRO5qx0NuBs8kzpAd829tXeucmR5NH4J/203I8el6gFRUqbFPJnyEV51Wq30by4TLW0/9ZyarYTxt4sBsjUYLMZvRykl1Fxm90SXVkfwx4P++T4KSafVzmpUcVJ/sfSrQZJPphllv79W8WKGtLx0ir8IrVTqD1pT2MH3QAMSs4KTvui71jeFFiwirOmprwPkYW063+5uRq4urHiiC4e8hCX3J5wqAEGaPpw9XB5JmkBdeDqSlkz6CmUXdl0Qae5kv2F/1384wu3PwE=', - 'RelayState': '_1037fbc88ec82ce8e770b2bed1119747bb812a07e6', - 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', - 'Signature': 'XCwCyI5cs7WhiJlB5ktSlWxSBxv+6q2xT3c8L7dLV6NQG9LHWhN7gf8qNsahSXfCzA0Ey9dp5BQ0EdRvAk2DIzKmJY6e3hvAIEp1zglHNjzkgcQmZCcrkK9Czi2Y1WkjOwR/WgUTUWsGJAVqVvlRZuS3zk3nxMrLH6f7toyvuJc=' - } - } - settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) - - request = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(request_data['get_data']['SAMLRequest'])) - - settings.set_strict(False) - logout_request = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) - self.assertTrue(logout_request.is_valid(request_data)) - - relay_state = request_data['get_data']['RelayState'] - del request_data['get_data']['RelayState'] - self.assertFalse(logout_request.is_valid(request_data)) - request_data['get_data']['RelayState'] = relay_state - - settings.set_strict(True) - try: - logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) - valid = logout_request2.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('The LogoutRequest was received at', str(e)) - - settings.set_strict(False) - old_signature = request_data['get_data']['Signature'] - request_data['get_data']['Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333=' - logout_request3 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request)) - try: - self.assertFalse(logout_request3.is_valid(request_data)) - except Exception as e: - self.assertIn('Signature validation failed. Logout Request rejected', str(e)) - - request_data['get_data']['Signature'] = old_signature - old_signature_algorithm = request_data['get_data']['SigAlg'] - del request_data['get_data']['SigAlg'] - self.assertTrue(logout_request3.is_valid(request_data)) - - request_data['get_data']['RelayState'] = 'http://example.com/relaystate' - try: - valid = logout_request3.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Signature validation failed. Logout Request rejected', str(e)) - - settings.set_strict(True) - request_2 = request.replace('https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url) - request_2 = request_2.replace('https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/') - request_data['get_data']['SAMLRequest'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(request_2) - try: - logout_request4 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request_2)) - valid = logout_request4.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Signature validation failed. Logout Request rejected', str(e)) - - settings.set_strict(False) - logout_request5 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request_2)) - try: - valid = logout_request5.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Signature validation failed. Logout Request rejected', str(e)) - - request_data['get_data']['SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' - try: - valid = logout_request5.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Invalid signAlg in the recieved Logout Request', str(e)) - - settings_info = self.loadSettingsJSON() - settings_info['strict'] = True - settings_info['security']['wantMessagesSigned'] = True - settings = OneLogin_Saml2_Settings(settings_info) - request_data['get_data']['SigAlg'] = old_signature_algorithm - old_signature = request_data['get_data']['Signature'] - del request_data['get_data']['Signature'] - try: - logout_request6 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request_2)) - valid = logout_request6.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('The Message of the Logout Request is not signed and the SP require it', str(e)) - - request_data['get_data']['Signature'] = old_signature - settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9' - del settings_info['idp']['x509cert'] - settings_2 = OneLogin_Saml2_Settings(settings_info) - try: - logout_request7 = OneLogin_Saml2_Logout_Request(settings_2, OneLogin_Saml2_Utils.b64encode(request_2)) - valid = logout_request7.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('In order to validate the sign on the Logout Request, the x509cert of the IdP is required', str(e)) diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py index 2930e728..cb639fe6 100644 --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -241,123 +241,6 @@ def testIsInValidDestination(self): response_4 = OneLogin_Saml2_Logout_Response(settings, message_4) self.assertTrue(response_4.is_valid(request_data)) - def testIsInValidSign(self): - """ - Tests the is_valid method of the OneLogin_Saml2_LogoutResponse - """ - request_data = { - 'http_host': 'example.com', - 'script_name': 'index.html', - 'get_data': {} - } - settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) - - settings.set_strict(False) - request_data['get_data'] = { - 'SAMLResponse': 'fZJva8IwEMa/Ssl7TZrW/gnqGHMMwSlM8cXeyLU9NaxNQi9lfvxVZczB5ptwSe733MPdjQma2qmFPdjOvyE5awiDU1MbUpevCetaoyyQJmWgQVK+VOvH14WSQ6Fca70tbc1ukPsEEGHrtTUsmM8mbDfKUhnFci8gliGINI/yXIAAiYnsw6JIRgWWAKlkwRZb6skJ64V6nKjDuSEPxvdPIowHIhpIsQkTFaYqSt9ZMEPy2oC/UEfvHSnOnfZFV38MjR1oN7TtgRv8tAZre9CGV9jYkGtT4Wnoju6Bauprme/ebOyErZbPi9XLfLnDoohwhHGc5WVSVhjCKM6rBMpYQpWJrIizfZ4IZNPxuTPqYrmd/m+EdONqPOfy8yG5rhxv0EMFHs52xvxWaHyd3tqD7+j37clWGGyh7vD+POiSrdZdWSIR49NrhR9R/teGTL8A', - 'RelayState': 'https://pitbulk.no-ip.org/newonelogin/demo1/index.php', - 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', - 'Signature': 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVfNKGA=' - } - response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) - self.assertTrue(response.is_valid(request_data)) - - relay_state = request_data['get_data']['RelayState'] - del request_data['get_data']['RelayState'] - inv_response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) - self.assertFalse(inv_response.is_valid(request_data)) - request_data['get_data']['RelayState'] = relay_state - - settings.set_strict(True) - response_2 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) - try: - valid = response_2.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Invalid issuer in the Logout Request', str(e)) - - settings.set_strict(False) - old_signature = request_data['get_data']['Signature'] - request_data['get_data']['Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333=' - response_3 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) - try: - valid = response_3.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Signature validation failed. Logout Response rejected', str(e)) - - request_data['get_data']['Signature'] = old_signature - old_signature_algorithm = request_data['get_data']['SigAlg'] - del request_data['get_data']['SigAlg'] - response_4 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) - self.assertTrue(response_4.is_valid(request_data)) - - request_data['get_data']['RelayState'] = 'http://example.com/relaystate' - response_5 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) - try: - valid = response_5.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Signature validation failed. Logout Response rejected', str(e)) - - settings.set_strict(True) - current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) - plain_message_6 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(request_data['get_data']['SAMLResponse'])) - plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url) - plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/') - request_data['get_data']['SAMLResponse'] = compat.to_string(OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6)) - - response_6 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) - try: - valid = response_6.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Signature validation failed. Logout Response rejected', str(e)) - - settings.set_strict(False) - response_7 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) - try: - valid = response_7.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Signature validation failed. Logout Response rejected', str(e)) - - request_data['get_data']['SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' - response_8 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) - try: - valid = response_8.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('Invalid signAlg in the recieved Logout Response', str(e)) - - settings_info = self.loadSettingsJSON() - settings_info['strict'] = True - settings_info['security']['wantMessagesSigned'] = True - settings = OneLogin_Saml2_Settings(settings_info) - - request_data['get_data']['SigAlg'] = old_signature_algorithm - old_signature = request_data['get_data']['Signature'] - del request_data['get_data']['Signature'] - request_data['get_data']['SAMLResponse'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6) - response_9 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) - try: - valid = response_9.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('The Message of the Logout Response is not signed and the SP require it', str(e)) - - request_data['get_data']['Signature'] = old_signature - settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9' - del settings_info['idp']['x509cert'] - settings_2 = OneLogin_Saml2_Settings(settings_info) - - response_10 = OneLogin_Saml2_Logout_Response(settings_2, request_data['get_data']['SAMLResponse']) - try: - valid = response_10.is_valid(request_data) - self.assertFalse(valid) - except Exception as e: - self.assertIn('In order to validate the sign on the Logout Response, the x509cert of the IdP is required', str(e)) - def testIsValid(self): """ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse From 72b4f87ed4490279c881bf9a97afbb45fc2626b6 Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Thu, 5 Mar 2015 17:35:52 +0300 Subject: [PATCH 11/35] support relative uri instead of full-url in for entry-points --- src/onelogin/saml2/auth.py | 12 +++++------ src/onelogin/saml2/authn_request.py | 10 +++++---- src/onelogin/saml2/logout_request.py | 18 +++++++--------- src/onelogin/saml2/logout_response.py | 10 +++++---- src/onelogin/saml2/metadata.py | 13 +++++++----- src/onelogin/saml2/response.py | 4 ++-- src/onelogin/saml2/settings.py | 21 ++++++++++++------- src/onelogin/saml2/utils.py | 19 +++++++++++++++++ .../src/OneLogin/saml2_tests/settings_test.py | 5 +++++ 9 files changed, 74 insertions(+), 38 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index c82f397b..462945a8 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -135,7 +135,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) elif get_data and 'SAMLRequest' in get_data: - logout_request = OneLogin_Saml2_Logout_Request(self.__settings, get_data['SAMLRequest']) + logout_request = OneLogin_Saml2_Logout_Request(self.__settings, get_data['SAMLRequest'], request_data=self.__request_data) if not self.validate_request_signature(get_data): self.__errors.append("invalid_logout_request_signature") elif not logout_request.is_valid(self.__request_data): @@ -147,7 +147,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ in_response_to = OneLogin_Saml2_Logout_Request.get_id(OneLogin_Saml2_Utils.decode_base64_and_inflate(self.__request_data['get_data']['SAMLRequest'])) response_builder = OneLogin_Saml2_Logout_Response(self.__settings) - response_builder.build(in_response_to) + response_builder.build(in_response_to, self.__request_data) logout_response = response_builder.get_response() parameters = {'SAMLResponse': logout_response} @@ -262,7 +262,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False): :returns: Redirection url """ - authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive) + authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, request_data=self.__request_data) saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} @@ -302,7 +302,7 @@ def logout(self, return_to=None, name_id=None, session_index=None): if name_id is None and self.__nameid is not None: name_id = self.__nameid - logout_request = OneLogin_Saml2_Logout_Request(self.__settings, name_id=name_id, session_index=session_index) + logout_request = OneLogin_Saml2_Logout_Request(self.__settings, name_id=name_id, session_index=session_index, request_data=self.__request_data) parameters = {'SAMLRequest': logout_request.get_request()} if return_to is not None: @@ -443,7 +443,7 @@ def __validate_signature(self, data, saml_type): 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 + self.__errors.append('The %s is not signed. Rejected.' % saml_type) return False return True @@ -473,5 +473,5 @@ def __validate_signature(self, data, saml_type): raise Exception('Signature validation failed. %s rejected.' % saml_type) return True except Exception as e: - self._error_reason = str(e) + self.__errors.append(str(e)) return False diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 22659b4e..8671434e 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -22,7 +22,7 @@ class OneLogin_Saml2_Authn_Request(object): """ - def __init__(self, settings, force_authn=False, is_passive=False): + def __init__(self, settings, force_authn=False, is_passive=False, request_data=None): """ Constructs the AuthnRequest object. @@ -34,6 +34,8 @@ def __init__(self, settings, force_authn=False, is_passive=False): :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. :type is_passive: bool + :param request_data: Optional, the request data + :type request_data: dict """ self.__settings = settings @@ -45,7 +47,7 @@ def __init__(self, settings, force_authn=False, is_passive=False): self.__id = uid issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) - destination = idp_data['singleSignOnService']['url'] + destination = OneLogin_Saml2_Utils.abs_url(idp_data['singleSignOnService']['url'], request_data) name_id_policy_format = sp_data['NameIDFormat'] if security['wantNameIdEncrypted']: @@ -92,8 +94,8 @@ def __init__(self, settings, force_authn=False, is_passive=False): 'is_passive_str': is_passive_str, 'issue_instant': issue_instant, 'destination': destination, - 'assertion_url': sp_data['assertionConsumerService']['url'], - 'entity_id': sp_data['entityId'], + 'assertion_url': OneLogin_Saml2_Utils.abs_url(sp_data['assertionConsumerService']['url'], request_data), + 'entity_id': OneLogin_Saml2_Utils.abs_url(sp_data['entityId'], request_data), 'name_id_policy': name_id_policy_format, 'requested_authn_context_str': requested_authn_context_str, } diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index 8581363b..ef2119c9 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -24,7 +24,7 @@ class OneLogin_Saml2_Logout_Request(object): """ - def __init__(self, settings, request=None, name_id=None, session_index=None): + def __init__(self, settings, request=None, name_id=None, session_index=None, request_data=None): """ Constructs the Logout Request object. @@ -39,6 +39,8 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): :param session_index: SessionIndex that identifies the session of the user. :type session_index: string + :param request_data: the request data + :type request_data: dict """ self.__settings = settings self.__error = None @@ -58,12 +60,12 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): if name_id is not None: name_id_format = sp_data['NameIDFormat'] else: - name_id = idp_data['entityId'] + name_id = OneLogin_Saml2_Utils.abs_url(idp_data['entityId'], request_data) name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, - sp_data['entityId'], + OneLogin_Saml2_Utils.abs_url(sp_data['entityId'], request_data), name_id_format, cert ) @@ -77,8 +79,8 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): { 'id': uid, 'issue_instant': issue_instant, - 'single_logout_url': idp_data['singleLogoutService']['url'], - 'entity_id': sp_data['entityId'], + 'single_logout_url': OneLogin_Saml2_Utils.abs_url(idp_data['singleLogoutService']['url'], request_data), + 'entity_id': OneLogin_Saml2_Utils.abs_url(sp_data['entityId'], request_data), 'name_id': name_id_obj, 'session_index': session_index_str, } @@ -210,7 +212,7 @@ def is_valid(self, request_data): root = OneLogin_Saml2_XML.to_etree(self.__logout_request) idp_data = self.__settings.get_idp_data() - idp_entity_id = idp_data['entityId'] + idp_entity_id = OneLogin_Saml2_Utils.abs_url(idp_data['entityId'], request_data) get_data = ('get_data' in request_data and request_data['get_data']) or dict() @@ -247,10 +249,6 @@ def is_valid(self, request_data): 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') - - 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') 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 ffdce35f..159823f2 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -75,7 +75,7 @@ def is_valid(self, request_data, request_id=None): self.__error = None try: idp_data = self.__settings.get_idp_data() - idp_entity_id = idp_data['entityId'] + idp_entity_id = OneLogin_Saml2_Utils.abs_url(idp_data['entityId'], request_data) get_data = request_data['get_data'] if self.__settings.is_strict(): @@ -124,11 +124,13 @@ def __query(self, query): """ return OneLogin_Saml2_XML.query(self.document, query) - def build(self, in_response_to): + def build(self, in_response_to, request_data=None): """ Creates a Logout Response object. :param in_response_to: InResponseTo value for the Logout Response. :type in_response_to: string + :param request_data: the request data + :type request_data: dict """ sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() @@ -140,9 +142,9 @@ def build(self, in_response_to): { 'id': uid, 'issue_instant': issue_instant, - 'destination': idp_data['singleLogoutService']['url'], + 'destination': OneLogin_Saml2_Utils.abs_url(idp_data['singleLogoutService']['url'], request_data), 'in_response_to': in_response_to, - 'entity_id': sp_data['entityId'], + 'entity_id': OneLogin_Saml2_Utils.abs_url(sp_data['entityId'], request_data), 'status': "urn:oasis:names:tc:SAML:2.0:status:Success" } diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 69fde258..37f4ab31 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -30,7 +30,7 @@ class OneLogin_Saml2_Metadata(object): TIME_CACHED = 604800 # 1 week @staticmethod - def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None): + def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None, request_data=None): """ Builds the metadata of the SP @@ -54,6 +54,9 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N :param organization: Organization ingo :type organization: dict + + :param request_data: the request data + :type request_data: dict """ if valid_until is None: valid_until = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_VALID @@ -81,7 +84,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N Location="%(location)s" />\n""" % \ { 'binding': sp['singleLogoutService']['binding'], - 'location': sp['singleLogoutService']['url'], + 'location': OneLogin_Saml2_Utils.abs_url(sp['singleLogoutService']['url'], request_data) } str_authnsign = 'true' if authnsign else 'false' @@ -96,7 +99,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N 'lang': lang, 'name': info['name'], 'display_name': info['displayname'], - 'url': info['url'], + 'url': OneLogin_Saml2_Utils.abs_url(info['url'], request_data) } organization_info.append(org) str_organization = '\n'.join(organization_info) @@ -118,12 +121,12 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N { 'valid': valid_until_time, 'cache': cache_duration_str, - 'entity_id': sp['entityId'], + 'entity_id': OneLogin_Saml2_Utils.abs_url(sp['entityId'], request_data), 'authnsign': str_authnsign, 'wsign': str_wsign, 'name_id_format': sp['NameIDFormat'], 'binding': sp['assertionConsumerService']['binding'], - 'location': sp['assertionConsumerService']['url'], + 'location': OneLogin_Saml2_Utils.abs_url(sp['assertionConsumerService']['url'], request_data), 'sls': sls, 'organization': str_organization, 'contacts': str_contacts, diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 745f683e..bed74fee 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -78,9 +78,9 @@ def is_valid(self, request_data, request_id=None): self.check_status() idp_data = self.__settings.get_idp_data() - idp_entity_id = idp_data['entityId'] + idp_entity_id = OneLogin_Saml2_Utils.abs_url(idp_data['entityId'], request_data) sp_data = self.__settings.get_sp_data() - sp_entity_id = sp_data['entityId'] + sp_entity_id = OneLogin_Saml2_Utils.abs_url(sp_data['entityId'], request_data) sign_nodes = self.__query('//ds:Signature') diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 7be05fbd..9f186363 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -34,10 +34,13 @@ r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) + +uri_regex = re.compile(r'(?:/?|[/?]\S+)$', re.IGNORECASE) + url_schemes = ['http', 'https', 'ftp', 'ftps'] -def validate_url(url): +def validate_uri(url): """ Auxiliary method to validate an urllib :param url: An url to be validated @@ -46,6 +49,9 @@ def validate_url(url): :rtype: bool """ + if url.startswith("/"): + return bool(uri_regex.match(url)) + scheme = url.split('://')[0].lower() if scheme not in url_schemes: return False @@ -332,13 +338,13 @@ def check_settings(self, settings): 'url' not in idp['singleSignOnService'] or \ len(idp['singleSignOnService']['url']) == 0: errors.append('idp_sso_not_found') - elif not validate_url(idp['singleSignOnService']['url']): + elif not validate_uri(idp['singleSignOnService']['url']): errors.append('idp_sso_url_invalid') if 'singleLogoutService' in idp and \ 'url' in idp['singleLogoutService'] and \ len(idp['singleLogoutService']['url']) > 0 and \ - not validate_url(idp['singleLogoutService']['url']): + not validate_uri(idp['singleLogoutService']['url']): errors.append('idp_slo_url_invalid') if 'sp' not in settings or len(settings['sp']) == 0: @@ -360,13 +366,13 @@ def check_settings(self, settings): 'url' not in sp['assertionConsumerService'] or \ len(sp['assertionConsumerService']['url']) == 0: errors.append('sp_acs_not_found') - elif not validate_url(sp['assertionConsumerService']['url']): + elif not validate_uri(sp['assertionConsumerService']['url']): errors.append('sp_acs_url_invalid') if 'singleLogoutService' in sp and \ 'url' in sp['singleLogoutService'] and \ len(sp['singleLogoutService']['url']) > 0 and \ - not validate_url(sp['singleLogoutService']['url']): + not validate_uri(sp['singleLogoutService']['url']): errors.append('sp_sls_url_invalid') if 'signMetadata' in security and isinstance(security['signMetadata'], dict): @@ -527,7 +533,7 @@ def get_organization(self): """ return self.__organization - def get_sp_metadata(self): + def get_sp_metadata(self, request_data=None): """ Gets the SP metadata. The XML representation. @@ -537,7 +543,8 @@ def get_sp_metadata(self): metadata = OneLogin_Saml2_Metadata.builder( self.__sp, self.__security['authnRequestsSigned'], self.__security['wantAssertionsSigned'], None, None, - self.get_contacts(), self.get_organization() + self.get_contacts(), self.get_organization(), + request_data=request_data ) cert = self.get_sp_cert() metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 9684e41c..b9aeefcb 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -104,6 +104,8 @@ def format_cert(cert, heads=True): :returns: Formated cert :rtype: string """ + + cert = compat.to_string(cert) x509_cert = cert.replace('\x0D', '') x509_cert = x509_cert.replace('\r', '') x509_cert = x509_cert.replace('\n', '') @@ -131,6 +133,8 @@ def format_private_key(key, heads=True): :returns: Formated private key :rtype: string """ + + key = compat.to_string(key) private_key = key.replace('\x0D', '') private_key = private_key.replace('\r', '') private_key = private_key.replace('\n', '') @@ -204,6 +208,21 @@ def redirect(url, parameters={}, request_data={}): return url + @staticmethod + def abs_url(uri, request_data=None): + """ + get absolute url by local uri + :param uri: the uri + :param request_data: the request data + :return: the absolute url for resource + """ + if uri.startswith('/'): + if request_data is None: + request_data = dict() + uri = '%s%s' % (OneLogin_Saml2_Utils.get_self_url_host(request_data), uri) + + return uri + @staticmethod def get_self_url_host(request_data): """ diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index df1ad661..2855c547 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -75,6 +75,11 @@ def testLoadSettingsFromDict(self): except Exception as e: self.assertIn('Invalid dict settings: idp_sso_url_invalid', str(e)) + settings_info['idp']['singleSignOnService']['url'] = '/valid/uri' + self.assertEqual([], OneLogin_Saml2_Settings(settings_info).get_errors()) + settings_info['sp']['singleLogoutService']['url'] = '/valid/uri' + self.assertEqual([], OneLogin_Saml2_Settings(settings_info).get_errors()) + del settings_info['sp'] del settings_info['idp'] try: From 27ddb5f9494c1979a737e6a835b3b3b9c9b77420 Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Thu, 5 Mar 2015 17:49:58 +0300 Subject: [PATCH 12/35] change name -> python3-saml --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4eca360b..67dc3a98 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( - name='python-saml', + name='python3-saml', version='2.1.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ From 1a476ea54a5d4ce30199287662708cd82e036371 Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Thu, 5 Mar 2015 22:34:04 +0300 Subject: [PATCH 13/35] Revert "support relative uri instead of full-url in for entry-points" This reverts commit 72b4f87ed4490279c881bf9a97afbb45fc2626b6. --- src/onelogin/saml2/auth.py | 12 +++++------ src/onelogin/saml2/authn_request.py | 10 ++++----- src/onelogin/saml2/logout_request.py | 18 +++++++++------- src/onelogin/saml2/logout_response.py | 10 ++++----- src/onelogin/saml2/metadata.py | 13 +++++------- src/onelogin/saml2/response.py | 4 ++-- src/onelogin/saml2/settings.py | 21 +++++++------------ src/onelogin/saml2/utils.py | 19 ----------------- .../src/OneLogin/saml2_tests/settings_test.py | 5 ----- 9 files changed, 38 insertions(+), 74 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 462945a8..c82f397b 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -135,7 +135,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) elif get_data and 'SAMLRequest' in get_data: - logout_request = OneLogin_Saml2_Logout_Request(self.__settings, get_data['SAMLRequest'], request_data=self.__request_data) + logout_request = OneLogin_Saml2_Logout_Request(self.__settings, get_data['SAMLRequest']) if not self.validate_request_signature(get_data): self.__errors.append("invalid_logout_request_signature") elif not logout_request.is_valid(self.__request_data): @@ -147,7 +147,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ in_response_to = OneLogin_Saml2_Logout_Request.get_id(OneLogin_Saml2_Utils.decode_base64_and_inflate(self.__request_data['get_data']['SAMLRequest'])) response_builder = OneLogin_Saml2_Logout_Response(self.__settings) - response_builder.build(in_response_to, self.__request_data) + response_builder.build(in_response_to) logout_response = response_builder.get_response() parameters = {'SAMLResponse': logout_response} @@ -262,7 +262,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False): :returns: Redirection url """ - authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, request_data=self.__request_data) + authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive) saml_request = authn_request.get_request() parameters = {'SAMLRequest': saml_request} @@ -302,7 +302,7 @@ def logout(self, return_to=None, name_id=None, session_index=None): if name_id is None and self.__nameid is not None: name_id = self.__nameid - logout_request = OneLogin_Saml2_Logout_Request(self.__settings, name_id=name_id, session_index=session_index, request_data=self.__request_data) + logout_request = OneLogin_Saml2_Logout_Request(self.__settings, name_id=name_id, session_index=session_index) parameters = {'SAMLRequest': logout_request.get_request()} if return_to is not None: @@ -443,7 +443,7 @@ def __validate_signature(self, data, saml_type): signature = data.get('Signature', None) if signature is None: if self.__settings.is_strict() and self.__settings.get_security_data().get('wantMessagesSigned', False): - self.__errors.append('The %s is not signed. Rejected.' % saml_type) + self._error_reason = 'The %s is not signed. Rejected.' % saml_type return False return True @@ -473,5 +473,5 @@ def __validate_signature(self, data, saml_type): raise Exception('Signature validation failed. %s rejected.' % saml_type) return True except Exception as e: - self.__errors.append(str(e)) + self._error_reason = str(e) return False diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 8671434e..22659b4e 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -22,7 +22,7 @@ class OneLogin_Saml2_Authn_Request(object): """ - def __init__(self, settings, force_authn=False, is_passive=False, request_data=None): + def __init__(self, settings, force_authn=False, is_passive=False): """ Constructs the AuthnRequest object. @@ -34,8 +34,6 @@ def __init__(self, settings, force_authn=False, is_passive=False, request_data=N :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'. :type is_passive: bool - :param request_data: Optional, the request data - :type request_data: dict """ self.__settings = settings @@ -47,7 +45,7 @@ def __init__(self, settings, force_authn=False, is_passive=False, request_data=N self.__id = uid issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) - destination = OneLogin_Saml2_Utils.abs_url(idp_data['singleSignOnService']['url'], request_data) + destination = idp_data['singleSignOnService']['url'] name_id_policy_format = sp_data['NameIDFormat'] if security['wantNameIdEncrypted']: @@ -94,8 +92,8 @@ def __init__(self, settings, force_authn=False, is_passive=False, request_data=N 'is_passive_str': is_passive_str, 'issue_instant': issue_instant, 'destination': destination, - 'assertion_url': OneLogin_Saml2_Utils.abs_url(sp_data['assertionConsumerService']['url'], request_data), - 'entity_id': OneLogin_Saml2_Utils.abs_url(sp_data['entityId'], request_data), + 'assertion_url': sp_data['assertionConsumerService']['url'], + 'entity_id': sp_data['entityId'], 'name_id_policy': name_id_policy_format, 'requested_authn_context_str': requested_authn_context_str, } diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index ef2119c9..8581363b 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -24,7 +24,7 @@ class OneLogin_Saml2_Logout_Request(object): """ - def __init__(self, settings, request=None, name_id=None, session_index=None, request_data=None): + def __init__(self, settings, request=None, name_id=None, session_index=None): """ Constructs the Logout Request object. @@ -39,8 +39,6 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, req :param session_index: SessionIndex that identifies the session of the user. :type session_index: string - :param request_data: the request data - :type request_data: dict """ self.__settings = settings self.__error = None @@ -60,12 +58,12 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, req if name_id is not None: name_id_format = sp_data['NameIDFormat'] else: - name_id = OneLogin_Saml2_Utils.abs_url(idp_data['entityId'], request_data) + name_id = idp_data['entityId'] name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, - OneLogin_Saml2_Utils.abs_url(sp_data['entityId'], request_data), + sp_data['entityId'], name_id_format, cert ) @@ -79,8 +77,8 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, req { 'id': uid, 'issue_instant': issue_instant, - 'single_logout_url': OneLogin_Saml2_Utils.abs_url(idp_data['singleLogoutService']['url'], request_data), - 'entity_id': OneLogin_Saml2_Utils.abs_url(sp_data['entityId'], request_data), + 'single_logout_url': idp_data['singleLogoutService']['url'], + 'entity_id': sp_data['entityId'], 'name_id': name_id_obj, 'session_index': session_index_str, } @@ -212,7 +210,7 @@ def is_valid(self, request_data): root = OneLogin_Saml2_XML.to_etree(self.__logout_request) idp_data = self.__settings.get_idp_data() - idp_entity_id = OneLogin_Saml2_Utils.abs_url(idp_data['entityId'], request_data) + idp_entity_id = idp_data['entityId'] get_data = ('get_data' in request_data and request_data['get_data']) or dict() @@ -249,6 +247,10 @@ def is_valid(self, request_data): 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') + + 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') 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 159823f2..ffdce35f 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -75,7 +75,7 @@ def is_valid(self, request_data, request_id=None): self.__error = None try: idp_data = self.__settings.get_idp_data() - idp_entity_id = OneLogin_Saml2_Utils.abs_url(idp_data['entityId'], request_data) + idp_entity_id = idp_data['entityId'] get_data = request_data['get_data'] if self.__settings.is_strict(): @@ -124,13 +124,11 @@ def __query(self, query): """ return OneLogin_Saml2_XML.query(self.document, query) - def build(self, in_response_to, request_data=None): + def build(self, in_response_to): """ Creates a Logout Response object. :param in_response_to: InResponseTo value for the Logout Response. :type in_response_to: string - :param request_data: the request data - :type request_data: dict """ sp_data = self.__settings.get_sp_data() idp_data = self.__settings.get_idp_data() @@ -142,9 +140,9 @@ def build(self, in_response_to, request_data=None): { 'id': uid, 'issue_instant': issue_instant, - 'destination': OneLogin_Saml2_Utils.abs_url(idp_data['singleLogoutService']['url'], request_data), + 'destination': idp_data['singleLogoutService']['url'], 'in_response_to': in_response_to, - 'entity_id': OneLogin_Saml2_Utils.abs_url(sp_data['entityId'], request_data), + 'entity_id': sp_data['entityId'], 'status': "urn:oasis:names:tc:SAML:2.0:status:Success" } diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 37f4ab31..69fde258 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -30,7 +30,7 @@ class OneLogin_Saml2_Metadata(object): TIME_CACHED = 604800 # 1 week @staticmethod - def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None, request_data=None): + def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None): """ Builds the metadata of the SP @@ -54,9 +54,6 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N :param organization: Organization ingo :type organization: dict - - :param request_data: the request data - :type request_data: dict """ if valid_until is None: valid_until = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_VALID @@ -84,7 +81,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N Location="%(location)s" />\n""" % \ { 'binding': sp['singleLogoutService']['binding'], - 'location': OneLogin_Saml2_Utils.abs_url(sp['singleLogoutService']['url'], request_data) + 'location': sp['singleLogoutService']['url'], } str_authnsign = 'true' if authnsign else 'false' @@ -99,7 +96,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N 'lang': lang, 'name': info['name'], 'display_name': info['displayname'], - 'url': OneLogin_Saml2_Utils.abs_url(info['url'], request_data) + 'url': info['url'], } organization_info.append(org) str_organization = '\n'.join(organization_info) @@ -121,12 +118,12 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N { 'valid': valid_until_time, 'cache': cache_duration_str, - 'entity_id': OneLogin_Saml2_Utils.abs_url(sp['entityId'], request_data), + 'entity_id': sp['entityId'], 'authnsign': str_authnsign, 'wsign': str_wsign, 'name_id_format': sp['NameIDFormat'], 'binding': sp['assertionConsumerService']['binding'], - 'location': OneLogin_Saml2_Utils.abs_url(sp['assertionConsumerService']['url'], request_data), + 'location': sp['assertionConsumerService']['url'], 'sls': sls, 'organization': str_organization, 'contacts': str_contacts, diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index bed74fee..745f683e 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -78,9 +78,9 @@ def is_valid(self, request_data, request_id=None): self.check_status() idp_data = self.__settings.get_idp_data() - idp_entity_id = OneLogin_Saml2_Utils.abs_url(idp_data['entityId'], request_data) + idp_entity_id = idp_data['entityId'] sp_data = self.__settings.get_sp_data() - sp_entity_id = OneLogin_Saml2_Utils.abs_url(sp_data['entityId'], request_data) + sp_entity_id = sp_data['entityId'] sign_nodes = self.__query('//ds:Signature') diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 9f186363..7be05fbd 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -34,13 +34,10 @@ r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) - -uri_regex = re.compile(r'(?:/?|[/?]\S+)$', re.IGNORECASE) - url_schemes = ['http', 'https', 'ftp', 'ftps'] -def validate_uri(url): +def validate_url(url): """ Auxiliary method to validate an urllib :param url: An url to be validated @@ -49,9 +46,6 @@ def validate_uri(url): :rtype: bool """ - if url.startswith("/"): - return bool(uri_regex.match(url)) - scheme = url.split('://')[0].lower() if scheme not in url_schemes: return False @@ -338,13 +332,13 @@ def check_settings(self, settings): 'url' not in idp['singleSignOnService'] or \ len(idp['singleSignOnService']['url']) == 0: errors.append('idp_sso_not_found') - elif not validate_uri(idp['singleSignOnService']['url']): + elif not validate_url(idp['singleSignOnService']['url']): errors.append('idp_sso_url_invalid') if 'singleLogoutService' in idp and \ 'url' in idp['singleLogoutService'] and \ len(idp['singleLogoutService']['url']) > 0 and \ - not validate_uri(idp['singleLogoutService']['url']): + not validate_url(idp['singleLogoutService']['url']): errors.append('idp_slo_url_invalid') if 'sp' not in settings or len(settings['sp']) == 0: @@ -366,13 +360,13 @@ def check_settings(self, settings): 'url' not in sp['assertionConsumerService'] or \ len(sp['assertionConsumerService']['url']) == 0: errors.append('sp_acs_not_found') - elif not validate_uri(sp['assertionConsumerService']['url']): + elif not validate_url(sp['assertionConsumerService']['url']): errors.append('sp_acs_url_invalid') if 'singleLogoutService' in sp and \ 'url' in sp['singleLogoutService'] and \ len(sp['singleLogoutService']['url']) > 0 and \ - not validate_uri(sp['singleLogoutService']['url']): + not validate_url(sp['singleLogoutService']['url']): errors.append('sp_sls_url_invalid') if 'signMetadata' in security and isinstance(security['signMetadata'], dict): @@ -533,7 +527,7 @@ def get_organization(self): """ return self.__organization - def get_sp_metadata(self, request_data=None): + def get_sp_metadata(self): """ Gets the SP metadata. The XML representation. @@ -543,8 +537,7 @@ def get_sp_metadata(self, request_data=None): metadata = OneLogin_Saml2_Metadata.builder( self.__sp, self.__security['authnRequestsSigned'], self.__security['wantAssertionsSigned'], None, None, - self.get_contacts(), self.get_organization(), - request_data=request_data + self.get_contacts(), self.get_organization() ) cert = self.get_sp_cert() metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index b9aeefcb..9684e41c 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -104,8 +104,6 @@ def format_cert(cert, heads=True): :returns: Formated cert :rtype: string """ - - cert = compat.to_string(cert) x509_cert = cert.replace('\x0D', '') x509_cert = x509_cert.replace('\r', '') x509_cert = x509_cert.replace('\n', '') @@ -133,8 +131,6 @@ def format_private_key(key, heads=True): :returns: Formated private key :rtype: string """ - - key = compat.to_string(key) private_key = key.replace('\x0D', '') private_key = private_key.replace('\r', '') private_key = private_key.replace('\n', '') @@ -208,21 +204,6 @@ def redirect(url, parameters={}, request_data={}): return url - @staticmethod - def abs_url(uri, request_data=None): - """ - get absolute url by local uri - :param uri: the uri - :param request_data: the request data - :return: the absolute url for resource - """ - if uri.startswith('/'): - if request_data is None: - request_data = dict() - uri = '%s%s' % (OneLogin_Saml2_Utils.get_self_url_host(request_data), uri) - - return uri - @staticmethod def get_self_url_host(request_data): """ diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index 2855c547..df1ad661 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -75,11 +75,6 @@ def testLoadSettingsFromDict(self): except Exception as e: self.assertIn('Invalid dict settings: idp_sso_url_invalid', str(e)) - settings_info['idp']['singleSignOnService']['url'] = '/valid/uri' - self.assertEqual([], OneLogin_Saml2_Settings(settings_info).get_errors()) - settings_info['sp']['singleLogoutService']['url'] = '/valid/uri' - self.assertEqual([], OneLogin_Saml2_Settings(settings_info).get_errors()) - del settings_info['sp'] del settings_info['idp'] try: From 70515b17a7aa6bfd77cc2d3d969d28b6bec65c6f Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Thu, 5 Mar 2015 22:39:06 +0300 Subject: [PATCH 14/35] revert back error messages on logout request/response signature validation failed --- src/onelogin/saml2/auth.py | 2 ++ tests/src/OneLogin/saml2_tests/auth_test.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index c82f397b..cfe750d4 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -126,6 +126,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ logout_response = OneLogin_Saml2_Logout_Response(self.__settings, get_data['SAMLResponse']) if not self.validate_response_signature(get_data): self.__errors.append('invalid_logout_response_signature') + self.__errors.append('Signature validation failed. Logout Response rejected') elif not logout_response.is_valid(self.__request_data, request_id): self.__errors.append('invalid_logout_response') self.__error_reason = logout_response.get_error() @@ -138,6 +139,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ logout_request = OneLogin_Saml2_Logout_Request(self.__settings, get_data['SAMLRequest']) if not self.validate_request_signature(get_data): self.__errors.append("invalid_logout_request_signature") + self.__errors.append('Signature validation failed. Logout Request rejected') elif not logout_request.is_valid(self.__request_data): self.__errors.append('invalid_logout_request') self.__error_reason = logout_request.get_error() diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index b17e24fe..2cdd1da7 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -911,7 +911,7 @@ def testIsInValidLogoutResponseSign(self): request_data['get_data']['SAMLResponse'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6) auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) auth.process_slo() - self.assertIn('invalid_logout_response_signature', auth.get_errors()) + self.assertIn('Signature validation failed. Logout Response rejected', auth.get_errors()) request_data['get_data']['Signature'] = old_signature settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9' @@ -1000,7 +1000,7 @@ def testIsValidLogoutRequestSign(self): del request_data['get_data']['Signature'] auth = OneLogin_Saml2_Auth(request_data, old_settings=settings) auth.process_slo() - self.assertIn('invalid_logout_request_signature', auth.get_errors()) + self.assertIn('Signature validation failed. Logout Request rejected', auth.get_errors()) request_data['get_data']['Signature'] = old_signature settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9' From ebe65eaadcf6e320530a2c37755f56d6545b1c2b Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Thu, 5 Mar 2015 23:13:20 +0300 Subject: [PATCH 15/35] added dependency link to xmlsec --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 67dc3a98..ffef1d22 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ 'isodate>=0.5.0', 'xmlsec>=0.2.1' ], + dependency_links=['http://github.com/bgaifullin/python-xmlsec/tarball/master'], extras_require={ 'test': ( 'coverage==3.7.1', From 78dc4ecdfc5f31d3ab8eb6ff8e159d4af25ec1f9 Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Sat, 7 Mar 2015 05:00:01 +0300 Subject: [PATCH 16/35] aded paramter to specify nsprefix in xmlsec library. remove workarround --- src/onelogin/saml2/utils.py | 10 ++++------ tests/src/OneLogin/saml2_tests/utils_test.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 9684e41c..9c5cafad 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -549,20 +549,18 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): # Prepare for encryption enc_data = xmlsec.template.encrypted_data_create( - root, xmlsec.Transform.AES128, type=xmlsec.EncryptionType.ELEMENT) + root, xmlsec.Transform.AES128, type=xmlsec.EncryptionType.ELEMENT, ns="xenc") xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) - key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_data) + key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns="dsig") enc_key = xmlsec.template.add_encrypted_key(key_info, xmlsec.Transform.RSA_OAEP) xmlsec.template.encrypted_data_ensure_cipher_value(enc_key) # Encrypt! enc_ctx = xmlsec.EncryptionContext(manager) enc_ctx.key = xmlsec.Key.generate(xmlsec.KeyData.AES, 128, xmlsec.KeyDataType.SESSION) - enc_ctx.encrypt_xml(enc_data, name_id) - new_root = OneLogin_Saml2_XML.make_root(root.tag, nsmap={"dsig": OneLogin_Saml2_Constants.NS_DS, "xenc": OneLogin_Saml2_Constants.NS_XENC}) - new_root[:] = root[:] - return '' + compat.to_string(OneLogin_Saml2_XML.to_string(new_root[0])) + '' + enc_data = enc_ctx.encrypt_xml(enc_data, name_id) + return '' + compat.to_string(OneLogin_Saml2_XML.to_string(enc_data)) + '' else: return OneLogin_Saml2_XML.extract_tag_text(root, "saml:NameID") diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index d8192e88..aa382739 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -476,7 +476,7 @@ def testGenerateNameId(self): key = OneLogin_Saml2_Utils.format_cert(x509cert) name_id_enc = OneLogin_Saml2_Utils.generate_name_id(name_id_value, entity_id, name_id_format, key) - expected_name_id_enc = '\n\n\n\n\n\n' + expected_name_id_enc = '\n\n\n\n\n\n' self.assertIn(expected_name_id_enc, name_id_enc) def testCalculateX509Fingerprint(self): From 15a331e22cb1fc732f3f305f126dc07b5e9b3287 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 14 Apr 2015 13:04:22 +0200 Subject: [PATCH 17/35] Solve HTTPs issue on demos Merge 5226956d1fc7f6990960b32dfa5635b870881145 + 59c0182c0760036b37130280cfc71d55f0d594ef --- demo-django/demo/views.py | 2 ++ demo-flask/index.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index 0c7938e6..b67c5d99 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -15,7 +15,9 @@ def init_saml_auth(req): def prepare_django_request(request): + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields result = { + 'https': 'on' if request.is_secure() else 'off', 'http_host': request.META['HTTP_HOST'], 'script_name': request.META['PATH_INFO'], 'server_port': request.META['SERVER_PORT'], diff --git a/demo-flask/index.py b/demo-flask/index.py index 5121b044..a2eb1cb1 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -20,8 +20,10 @@ def init_saml_auth(req): def prepare_flask_request(request): + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields url_data = urlparse(request.url) return { + 'https': 'on' if request.scheme == 'https' else 'off', 'http_host': request.host, 'server_port': url_data.port, 'script_name': request.path, From 8f00f3c89180803859bac3190ffbd29ffa39dec7 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 14 Apr 2015 14:29:53 +0200 Subject: [PATCH 18/35] Add test. pep8 and pyflakes --- src/onelogin/saml2/auth.py | 4 +- src/onelogin/saml2/compat.py | 44 +++++++++---------- src/onelogin/saml2/logout_response.py | 1 - src/onelogin/saml2/settings.py | 2 +- src/onelogin/saml2/utils.py | 8 ++-- .../saml2_tests/authn_request_test.py | 14 ++++++ .../src/OneLogin/saml2_tests/metadata_test.py | 5 --- .../src/OneLogin/saml2_tests/response_test.py | 7 +-- .../src/OneLogin/saml2_tests/settings_test.py | 5 --- .../saml2_tests/signed_response_test.py | 6 --- 10 files changed, 44 insertions(+), 52 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index ef7b940a..1a28b77d 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -445,7 +445,7 @@ def __validate_signature(self, data, saml_type): 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 + self.__error_reason = 'The %s is not signed. Rejected.' % saml_type return False return True @@ -475,5 +475,5 @@ def __validate_signature(self, data, saml_type): raise Exception('Signature validation failed. %s rejected.' % saml_type) return True except Exception as e: - self._error_reason = str(e) + self.__error_reason = str(e) return False diff --git a/src/onelogin/saml2/compat.py b/src/onelogin/saml2/compat.py index 853824cc..3d2f23cd 100644 --- a/src/onelogin/saml2/compat.py +++ b/src/onelogin/saml2/compat.py @@ -14,40 +14,40 @@ text_types = (basestring,) # noqa str_type = basestring # noqa - def utf8(s): + def utf8(data): """ return utf8-encoded string """ - if isinstance(s, basestring): - return s.decode("utf8") - return unicode(s) + if isinstance(data, basestring): + return data.decode("utf8") + return unicode(data) - def to_string(s): + def to_string(data): """ return string """ - if isinstance(s, unicode): - return s.encode("utf8") - return str(s) + if isinstance(data, unicode): + return data.encode("utf8") + return str(data) - def to_bytes(s): + def to_bytes(data): """ return bytes """ - return str(s) + return str(data) else: # py 3.x text_types = (bytes, str) str_type = str - def utf8(s): + def utf8(data): """ return utf8-encoded string """ - if isinstance(s, bytes): - return s.decode("utf8") - return str(s) + if isinstance(data, bytes): + return data.decode("utf8") + return str(data) - def to_string(s): + def to_string(data): """convert to string""" - if isinstance(s, bytes): - return s.decode("utf8") - return str(s) + if isinstance(data, bytes): + return data.decode("utf8") + return str(data) - def to_bytes(s): + def to_bytes(data): """return bytes""" - if isinstance(s, str): - return s.encode("utf8") - return bytes(s) + if isinstance(data, str): + return data.encode("utf8") + return bytes(data) diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py index ffdce35f..184d5aa9 100644 --- a/src/onelogin/saml2/logout_response.py +++ b/src/onelogin/saml2/logout_response.py @@ -9,7 +9,6 @@ """ -from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates from onelogin.saml2.xml_utils import OneLogin_Saml2_XML diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 828acf8a..de510d8c 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -429,7 +429,7 @@ def check_settings(self, settings): break # Restores the value that had the self.__sp if 'old_sp' in locals(): - self.__sp = locals()['old_sp'] + self.__sp = old_sp return errors diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index e2be7dd7..4ffbf8be 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -52,14 +52,14 @@ def escape_url(url): return quote_plus(url) @staticmethod - def b64encode(s): + def b64encode(data): """base64 encode""" - return compat.to_string(base64.b64encode(compat.to_bytes(s))) + return compat.to_string(base64.b64encode(compat.to_bytes(data))) @staticmethod - def b64decode(s): + def b64decode(data): """base64 decode""" - return base64.b64decode(s) + return base64.b64decode(data) @staticmethod def decode_base64_and_inflate(value, ignore_zip=False): diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 6289912d..50c9c239 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -12,6 +12,8 @@ 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.xml_utils import OneLogin_Saml2_XML + try: from urllib.parse import urlparse, parse_qs @@ -211,3 +213,15 @@ def testCreateEncSAMLRequest(self): self.assertRegexpMatches(inflated, 'http://stuff.com/endpoints/metadata.php') self.assertRegexpMatches(inflated, 'Format="urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"') self.assertRegexpMatches(inflated, 'ProviderName="SP prueba"') + + def testGetID(self): + """ + Tests the get_id method of the OneLogin_Saml2_Authn_Request. + """ + saml_settings = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(saml_settings) + authn_request = OneLogin_Saml2_Authn_Request(settings) + authn_request_encoded = authn_request.get_request() + inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded)) + document = OneLogin_Saml2_XML.to_etree(inflated) + self.assertEqual(authn_request.get_id(), document.get('ID', None)) diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 0e875f51..c8975bce 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -11,11 +11,6 @@ from onelogin.saml2.metadata import OneLogin_Saml2_Metadata from onelogin.saml2.settings import OneLogin_Saml2_Settings -try: - from urllib.parse import urlparse, parse_qs -except ImportError: - from urlparse import urlparse, parse_qs - class OneLogin_Saml2_Metadata_Test(unittest.TestCase): def loadSettingsJSON(self): diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index b08f997d..0ea2ecc9 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -14,11 +14,6 @@ from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils -try: - from urllib.parse import urlparse, parse_qs -except ImportError: - from urlparse import urlparse, parse_qs - class OneLogin_Saml2_Response_Test(unittest.TestCase): data_path = join(dirname(__file__), '..', '..', '..', 'data') @@ -974,7 +969,7 @@ def testIsValid2(self): self.assertTrue(response_2.is_valid(self.get_request_data())) settings_info_3 = self.loadSettingsJSON('settings2.json') - idp_cert = settings_info_3['idp']['x509cert']; + idp_cert = settings_info_3['idp']['x509cert'] settings_info_3['idp']['certFingerprint'] = OneLogin_Saml2_Utils.calculate_x509_fingerprint(idp_cert) settings_info_3['idp']['x509cert'] = '' settings_3 = OneLogin_Saml2_Settings(settings_info_3) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index df1ad661..c24c54e7 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -11,11 +11,6 @@ from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils -try: - from urllib.parse import urlparse, parse_qs -except ImportError: - from urlparse import urlparse, parse_qs - class OneLogin_Saml2_Settings_Test(unittest.TestCase): data_path = join(dirname(__file__), '..', '..', '..', 'data') diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py index 03207d10..ec493014 100644 --- a/tests/src/OneLogin/saml2_tests/signed_response_test.py +++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py @@ -3,7 +3,6 @@ # Copyright (c) 2014, OneLogin, Inc. # All rights reserved. -from base64 import b64encode import json from os.path import dirname, join, exists import unittest @@ -12,11 +11,6 @@ from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils -try: - from urllib.parse import urlparse, parse_qs -except ImportError: - from urlparse import urlparse, parse_qs - class OneLogin_Saml2_SignedResponse_Test(unittest.TestCase): data_path = join(dirname(__file__), '..', '..', '..', 'data') From 93fbb9275c3fb07f4a5a13b85634ec8abc1c792c Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 29 Jul 2015 14:51:18 +0200 Subject: [PATCH 19/35] Update the xmlsec library --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ffef1d22..b3af2c04 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ test_suite='tests', install_requires=[ 'isodate>=0.5.0', - 'xmlsec>=0.2.1' + 'xmlsec>=0.3.1' ], dependency_links=['http://github.com/bgaifullin/python-xmlsec/tarball/master'], extras_require={ From 7711bac2294bdbf1eb1288777b1a6d409e32eec9 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 29 Jul 2015 18:53:33 +0200 Subject: [PATCH 20/35] Fix creation of metadata with no SLS, when using settings.get_sp_metadata() --- src/onelogin/saml2/metadata.py | 2 +- tests/src/OneLogin/saml2_tests/metadata_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 69fde258..24e5fb2b 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -76,7 +76,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N organization = {} sls = '' - if 'singleLogoutService' in sp: + if 'singleLogoutService' in sp and 'url' in sp['singleLogoutService']: sls = """ \n""" % \ { diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index c8975bce..db6bd55c 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -65,7 +65,7 @@ def testBuilder(self): security['authnRequestsSigned'] = True security['wantAssertionsSigned'] = True - del sp_data['singleLogoutService'] + del sp_data['singleLogoutService']['url'] metadata2 = OneLogin_Saml2_Metadata.builder( sp_data, security['authnRequestsSigned'], From 99307976528295a191beffc68ed0dfaebc7b1c79 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 29 Jul 2015 19:22:09 +0200 Subject: [PATCH 21/35] Allow configuration of metadata caching/expiry via settings --- README.md | 7 +++ src/onelogin/saml2/metadata.py | 30 +++++++----- src/onelogin/saml2/settings.py | 10 +++- src/onelogin/saml2/xml_templates.py | 4 +- .../src/OneLogin/saml2_tests/metadata_test.py | 49 ++++++++++++++++++- 5 files changed, 84 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 2fe50fe0..24b37015 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,13 @@ In addition to the required settings data (idp, sp), there is extra information // Set true or don't present thi parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' // Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'), 'requestedAuthnContext': true, + + // In some environment you will need to set how long the published metadata of the Service Provider gonna be valid. + // is possible to not set the 2 following parameters (or set to null) and default values will be set (2 days, 1 week) + // Provide the desire TimeStamp, for example 2015-06-26T20:00:00Z + 'metadataValidUntil': null, + // Provide the desire Duration, for example PT518400S (6 days) + 'metadataCacheDuration': null, }, // Contact information template, it is recommended to suply a diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 24e5fb2b..e7de8dcf 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -18,6 +18,11 @@ from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates from onelogin.saml2.xml_utils import OneLogin_Saml2_XML +try: + basestring +except NameError: + basestring = str + class OneLogin_Saml2_Metadata(object): """ @@ -43,11 +48,11 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N :param wsign: wantAssertionsSigned attribute :type wsign: string - :param valid_until: Metadata's valid time - :type valid_until: string|DateTime + :param valid_until: Metadata's expiry date + :type valid_until: string|DateTime|Timestamp :param cache_duration: Duration of the cache in seconds - :type cache_duration: string|Timestamp + :type cache_duration: int|string :param contacts: Contacts info :type contacts: dict @@ -57,16 +62,19 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N """ if valid_until is None: valid_until = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_VALID - if not isinstance(valid_until, compat.str_type): - valid_until_time = gmtime(valid_until) - valid_until_time = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time) + if not isinstance(valid_until, basestring): + if isinstance(valid_until, datetime): + valid_until_time = valid_until.timetuple() + else: + valid_until_time = gmtime(valid_until) + valid_until_str = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time) else: - valid_until_time = valid_until + valid_until_str = valid_until if cache_duration is None: - cache_duration = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_CACHED + cache_duration = OneLogin_Saml2_Metadata.TIME_CACHED if not isinstance(cache_duration, compat.str_type): - cache_duration_str = 'PT%sS' % cache_duration + cache_duration_str = 'PT%sS' % cache_duration # Period of Time x Seconds else: cache_duration_str = cache_duration @@ -116,8 +124,8 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N metadata = OneLogin_Saml2_Templates.MD_ENTITY_DESCRIPTOR % \ { - 'valid': valid_until_time, - 'cache': cache_duration_str, + 'valid': ('validUntil="%s"' % valid_until_str) if valid_until_str else '', + 'cache': ('cacheDuration="%s"' % cache_duration_str) if cache_duration_str else '', 'entity_id': sp['entityId'], 'authnsign': str_authnsign, 'wsign': str_wsign, diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index de510d8c..1823848a 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -279,6 +279,12 @@ def __add_default_values(self): if 'signMetadata' not in self.__security: self.__security['signMetadata'] = False + # Metadata format + if 'metadataValidUntil' not in self.__security.keys(): + self.__security['metadataValidUntil'] = None # None means use default + if 'metadataCacheDuration' not in self.__security.keys(): + self.__security['metadataCacheDuration'] = None # None means use default + # Sign expected if 'wantMessagesSigned' not in self.__security: self.__security['wantMessagesSigned'] = False @@ -538,7 +544,9 @@ def get_sp_metadata(self): """ metadata = OneLogin_Saml2_Metadata.builder( self.__sp, self.__security['authnRequestsSigned'], - self.__security['wantAssertionsSigned'], None, None, + self.__security['wantAssertionsSigned'], + self.__security['metadataValidUntil'], + self.__security['metadataCacheDuration'], self.get_contacts(), self.get_organization() ) cert = self.get_sp_cert() diff --git a/src/onelogin/saml2/xml_templates.py b/src/onelogin/saml2/xml_templates.py index ed113d77..4c4ac349 100644 --- a/src/onelogin/saml2/xml_templates.py +++ b/src/onelogin/saml2/xml_templates.py @@ -76,8 +76,8 @@ class OneLogin_Saml2_Templates(object): MD_ENTITY_DESCRIPTOR = """\ %(sls)s %(name_id_format)s diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index db6bd55c..12372445 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -5,6 +5,8 @@ import json from os.path import dirname, join, exists +from time import strftime +from datetime import datetime import unittest from onelogin.saml2 import compat @@ -85,15 +87,58 @@ def testBuilder(self): sp_data, security['authnRequestsSigned'], security['wantAssertionsSigned'], '2014-10-01T11:04:29Z', - 'PT1412593469S', + 'P1Y', contacts, organization ) self.assertIsNotNone(metadata3) self.assertIn(' Date: Thu, 30 Jul 2015 01:27:28 +0200 Subject: [PATCH 22/35] Allow metadata signing with SP key specified as config value, not file --- src/onelogin/saml2/settings.py | 61 +++++++++++-------- .../src/OneLogin/saml2_tests/settings_test.py | 28 ++++++++- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 1823848a..c621eb2d 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -538,7 +538,6 @@ def get_organization(self): def get_sp_metadata(self): """ Gets the SP metadata. The XML representation. - :returns: SP metadata (xml) :rtype: string """ @@ -553,11 +552,23 @@ def get_sp_metadata(self): metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert) # Sign metadata - if self.__security['signMetadata'] is not False: + if 'signMetadata' in self.__security and self.__security['signMetadata'] is not False: if self.__security['signMetadata'] is True: - key_file_name = 'sp.key' - cert_file_name = 'sp.crt' + # Use the SP's normal key to sign the metadata: + if not cert: + raise OneLogin_Saml2_Error( + 'Cannot sign metadata: missing SP public key certificate.', + OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND + ) + cert_metadata = cert + key_metadata = self.get_sp_key() + if not key_metadata: + raise OneLogin_Saml2_Error( + 'Cannot sign metadata: missing SP private key.', + OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND + ) else: + # Use a custom key to sign the metadata: if ('keyFileName' not in self.__security['signMetadata'] or 'certFileName' not in self.__security['signMetadata']): raise OneLogin_Saml2_Error( @@ -566,30 +577,28 @@ def get_sp_metadata(self): ) key_file_name = self.__security['signMetadata']['keyFileName'] cert_file_name = self.__security['signMetadata']['certFileName'] - key_metadata_file = self.__paths['cert'] + key_file_name - cert_metadata_file = self.__paths['cert'] + cert_file_name - - if not exists(key_metadata_file): - raise OneLogin_Saml2_Error( - 'Private key file not found: %s', - OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND, - key_metadata_file - ) + key_metadata_file = self.__paths['cert'] + key_file_name + cert_metadata_file = self.__paths['cert'] + cert_file_name - if not exists(cert_metadata_file): - raise OneLogin_Saml2_Error( - 'Public cert file not found: %s', - OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND, - cert_metadata_file - ) - - f_metadata_key = open(key_metadata_file, 'r') - key_metadata = f_metadata_key.read() - f_metadata_key.close() + try: + with open(key_metadata_file, 'r') as f_metadata_key: + key_metadata = f_metadata_key.read() + except IOError: + raise OneLogin_Saml2_Error( + 'Private key file not readable: %s', + OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND, + key_metadata_file + ) - f_metadata_cert = open(cert_metadata_file, 'r') - cert_metadata = f_metadata_cert.read() - f_metadata_cert.close() + try: + with open(cert_metadata_file, 'r') as f_metadata_cert: + cert_metadata = f_metadata_cert.read() + except IOError: + raise OneLogin_Saml2_Error( + 'Public cert file not readable: %s', + OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND, + cert_metadata_file + ) metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key_metadata, cert_metadata) diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index c24c54e7..bd70e0a0 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -8,6 +8,7 @@ import unittest from onelogin.saml2 import compat +from onelogin.saml2.errors import OneLogin_Saml2_Error from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils @@ -372,7 +373,28 @@ def testGetSPMetadataSigned(self): if 'security' not in settings_info: settings_info['security'] = {} settings_info['security']['signMetadata'] = True - settings = OneLogin_Saml2_Settings(settings_info) + self.generateAndCheckMetadata(settings_info) + + # Now try again with SP keys set directly in settings and not from files: + del settings_info['custom_base_path'] + self.generateAndCheckMetadata(settings_info) + + # Now the keys should not be found, so metadata generation won't work: + del settings_info['sp']['x509cert'] + del settings_info['sp']['privateKey'] + with self.assertRaises(OneLogin_Saml2_Error): + OneLogin_Saml2_Settings(settings_info).get_sp_metadata() + # Set the keys in the settings: + settings_info['sp']['x509cert'] = self.file_contents(join(self.data_path, 'customPath', 'certs', 'sp.crt')) + settings_info['sp']['privateKey'] = self.file_contents(join(self.data_path, 'customPath', 'certs', 'sp.key')) + self.generateAndCheckMetadata(settings_info) + + def generateAndCheckMetadata(self, settings): + """ + Helper method: Given some settings, generate metadata and validate it + """ + if not isinstance(settings, OneLogin_Saml2_Settings): + settings = OneLogin_Saml2_Settings(settings) metadata = compat.to_string(settings.get_sp_metadata()) self.assertIn(' Date: Thu, 30 Jul 2015 01:35:50 +0200 Subject: [PATCH 23/35] Modify the KeyInfo (in EncryptedData) and put the EncryptedKey sub-tree as a child element to it --- src/onelogin/saml2/response.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 11fc4bee..9796fd98 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -445,6 +445,25 @@ def __decrypt_assertion(self, xml): 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 Exception('No KeyInfo present, invalid Assertion') + keyinfo = keyinfo[0] + children = keyinfo.getchildren() + if not children: + raise Exception('No child to KeyInfo, invalid Assertion') + 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') + 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="'+uri +'"]') + if encrypted_key: + keyinfo.append(encrypted_key[0]) + encrypted_data = encrypted_data_nodes[0] OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) return xml From 9745ccaf643e1dce0fa20cbdd7ed6125312f2e03 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 02:05:35 +0200 Subject: [PATCH 24/35] Set NAMEID_UNSPECIFIED as default NameIDFormat to prevent conflicts with IdPs that don't support NAMEID_PERSISTENT --- src/onelogin/saml2/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index c621eb2d..4bc64f17 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -265,7 +265,7 @@ def __add_default_values(self): # Related to nameID if 'NameIDFormat' not in self.__sp: - self.__sp['NameIDFormat'] = OneLogin_Saml2_Constants.NAMEID_PERSISTENT + self.__sp['NameIDFormat'] = OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED if 'nameIdEncrypted' not in self.__security: self.__security['nameIdEncrypted'] = False From 950d8533f32e35119b75505868c7a46e1bdca3b4 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 13:14:28 +0200 Subject: [PATCH 25/35] Handle valid but uncommon dsig block with no URI in the reference --- src/onelogin/saml2/utils.py | 8 +++++++- tests/src/OneLogin/saml2_tests/response_test.py | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 4ffbf8be..1c9c6ecc 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -660,7 +660,7 @@ def add_sign(xml, key, cert, debug=False): elem = OneLogin_Saml2_XML.to_etree(xml) xmlsec.enable_debug_trace(debug) xmlsec.tree.add_ids(elem, ["ID"]) - # Sign the metadacta with our private key. + # Sign the metadata with our private key. signature = xmlsec.template.create(elem, xmlsec.Transform.EXCL_C14N, xmlsec.Transform.RSA_SHA1, ns='ds') issuer = OneLogin_Saml2_XML.query(elem, '//saml:Issuer') @@ -737,6 +737,12 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid if cert is None or cert == '': return False + # Check if Reference URI is empty + reference_elem = OneLogin_Saml2_XML.query(signature_node, '//ds:Reference') + if len(reference_elem) > 0: + if reference_elem[0].get('URI') == '': + reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID')) + if validatecert: manager = xmlsec.KeysManager() manager.load_cert_from_memory(cert, xmlsec.KeyFormat.CERT_PEM, xmlsec.KeyDataType.TRUSTED) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 0ea2ecc9..6027f025 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1098,3 +1098,12 @@ def testIsValidSign(self): response_9 = OneLogin_Saml2_Response(settings, xml_9) # Modified message self.assertFalse(response_9.is_valid(self.get_request_data())) + + def testIsValidSignWithEmptyReferenceURI(self): + settings_info = self.loadSettingsJSON() + del settings_info['idp']['x509cert'] + settings_info['idp']['certFingerprint'] = "194d97e4d8c9c8cfa4b721e5ee497fd9660e5213" + settings = OneLogin_Saml2_Settings(settings_info) + xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertTrue(response.is_valid(self.get_request_data())) \ No newline at end of file From d3e12bef1f22ddad8ac2ba840b0f407f5a0d751c Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 13:36:19 +0200 Subject: [PATCH 26/35] Forgot commit this file --- tests/data/responses/response_without_reference_uri.xml.base64 | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/data/responses/response_without_reference_uri.xml.base64 diff --git a/tests/data/responses/response_without_reference_uri.xml.base64 b/tests/data/responses/response_without_reference_uri.xml.base64 new file mode 100644 index 00000000..7ceecf01 --- /dev/null +++ b/tests/data/responses/response_without_reference_uri.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgSUQ9InBmeGQ1OTQzNDdkLTQ5NWYtYjhkMS0wZWUyLTQxY2ZkYTE0ZGQzNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo5MDAxL3YxL3VzZXJzL2F1dGhvcml6ZS9zYW1sIiBDb25zZW50PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y29uc2VudDp1bnNwZWNpZmllZCIgSW5SZXNwb25zZVRvPSJfZWQ5MTVhNDAtNzRmYi0wMTMyLTViMTYtNDhlMGViMTRhMWM3Ij4NCiAgPElzc3VlciB4bWxucz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+aHR0cDovL2V4YW1wbGUuY29tPC9Jc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT5qQ2dlWENQREZsd2pUZ3FnUHAwbVUyVHF3OWc9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPkRmdXByMTh3UityRGFndENQRWZRbFNHSHp3NE5kZlBIWjRIc3pGZTFKUENKWGpmYnlFTTFmZytqemdHYk1NdDZYemdDWGNLSk03RS9DUFNURGt2TWUzRFVKbEh1NERodURPQXovRHN5b0J3V3VWK1JmM1dpTmNGNFhDYzl3QlF6dm4vYXREN3pXNnh3TzdOL2hrQVpKcWZ2SmRkbnBNTUhLR1hxRy9aSFpBdz08L2RzOlNpZ25hdHVyZVZhbHVlPg0KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ3FEQ0NBaEdnQXdJQkFnSUJBREFOQmdrcWhraUc5dzBCQVEwRkFEQnhNUXN3Q1FZRFZRUUdFd0oxY3pFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRWlNQ0FHQTFVRUNnd1pSbXhoZENCWGIzSnNaQ0JMYm05M2JHVmtaMlVzSUVsdVl6RWNNQm9HQTFVRUF3d1RiR1ZoY200dVpteGhkSGR2Y214a0xtTnZiVEVMTUFrR0ExVUVCd3dDUkVNd0hoY05NVFV3TnpBNE1EazFPVEF6V2hjTk1qVXdOekExTURrMU9UQXpXakJ4TVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tWMkZ6YUdsdVozUnZiakVpTUNBR0ExVUVDZ3daUm14aGRDQlhiM0pzWkNCTGJtOTNiR1ZrWjJVc0lFbHVZekVjTUJvR0ExVUVBd3dUYkdWaGNtNHVabXhoZEhkdmNteGtMbU52YlRFTE1Ba0dBMVVFQnd3Q1JFTXdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JBTVBEd3NsNW82eDJRb3VOaTEvRTdJVXFSWWoyWW9jSlJGc3VFR1RldnlVKzJhRkNhQk5WL3R0NnNBYk05V1N1dEx1cWpFL2hmYm5sRWNaMDMrZ24wQ29MbDZZbXdiS0tlUnBrSXplVmhveUoxWVlNUUVBVmhMcmR5OFBvd3U4VUNaMFBiQXorbjlka2lSek01cENDTzc3K2d5Y0ZUQkZLSEFBOXFJcFVaWmtQQWdNQkFBR2pVREJPTUIwR0ExVWREZ1FXQkJRSFU1OGl1R3hGbFp1ckJVSndvbGFsSnIrRlJ6QWZCZ05WSFNNRUdEQVdnQlFIVTU4aXVHeEZsWnVyQlVKd29sYWxKcitGUnpBTUJnTlZIUk1FQlRBREFRSC9NQTBHQ1NxR1NJYjNEUUVCRFFVQUE0R0JBQzZpSGZNbWQraE1TUnpma29zaTNDK3d2cUhDTEVVc2czSEZwa1ptNWp4bVREbEY1cU8rQnQwbjB4bWZvcVdCekJNbE5DOFRzR3JhZmhKM3p1OEdORjBMZW8xMXJmYzFHTUdCdnI1SG9aM1dBQXltbkJFREFBb3N4TjZXWlJtajF4YWdhMTMrNnBXZkdCKysyblB3Y1pXUC84ZGtQY1JvZ2V2VjB4MHA1Njg2PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCg0KICA8QXNzZXJ0aW9uIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iXzcwMGFjMzIwLTc0ZmYtMDEzMi01YjE0LTQ4ZTBlYjE0YTFjNyIgSXNzdWVJbnN0YW50PSIyMDE1LTAxLTAyVDIyOjQ4OjQ4WiIgVmVyc2lvbj0iMi4wIj4NCiAgICA8SXNzdWVyPmh0dHA6Ly9leGFtcGxlLmNvbTwvSXNzdWVyPg0KICAgIDxTdWJqZWN0Pg0KICAgICAgPE5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c2FtbEB1c2VyLmNvbTwvTmFtZUlEPg0KICAgICAgPFN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPFN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iX2VkOTE1YTQwLTc0ZmItMDEzMi01YjE2LTQ4ZTBlYjE0YTFjNyIgTm90T25PckFmdGVyPSIyMDM4LTAxLTAyVDIyOjUxOjQ4WiIgUmVjaXBpZW50PSJodHRwOi8vbG9jYWxob3N0OjkwMDEvdjEvdXNlcnMvYXV0aG9yaXplL3NhbWwiLz4NCiAgICAgIDwvU3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L1N1YmplY3Q+DQogICAgPENvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE1LTAxLTAyVDIyOjQ4OjQzWiIgTm90T25PckFmdGVyPSIyMDM4LTAxLTAyVDIzOjQ4OjQ4WiI+DQogICAgICA8QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPEF1ZGllbmNlPmh0dHA6Ly9sb2NhbGhvc3Q6OTAwMS88L0F1ZGllbmNlPg0KICAgICAgICA8QXVkaWVuY2U+ZmxhdF93b3JsZDwvQXVkaWVuY2U+DQogICAgICA8L0F1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9Db25kaXRpb25zPg0KICAgIDxBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8QXR0cmlidXRlIE5hbWU9Imh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL2VtYWlsYWRkcmVzcyI+DQogICAgICAgIDxBdHRyaWJ1dGVWYWx1ZT5zYW1sQHVzZXIuY29tPC9BdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvQXR0cmlidXRlPg0KICAgIDwvQXR0cmlidXRlU3RhdGVtZW50Pg0KICAgIDxBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBTZXNzaW9uSW5kZXg9Il83MDBhYzMyMC03NGZmLTAxMzItNWIxNC00OGUwZWIxNGExYzciPg0KICAgICAgPEF1dGhuQ29udGV4dD4NCiAgICAgICAgPEF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpmZWRlcmF0aW9uOmF1dGhlbnRpY2F0aW9uOndpbmRvd3M8L0F1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9BdXRobkNvbnRleHQ+DQogICAgPC9BdXRoblN0YXRlbWVudD4NCiAgPC9Bc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file From d680fa30d9b711e224f606c96a1da492286dd952 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 13:37:29 +0200 Subject: [PATCH 27/35] Split the setting check methods. Now 1 method for IdP settings and other for SP settings. Let the setting object to avoid the IdP setting check. required if we want to publish SP SAML Metadata when the IdP data is still not provided. --- README.md | 8 ++ demo-django/demo/views.py | 8 +- src/onelogin/saml2/settings.py | 246 +++++++++++++++++++-------------- 3 files changed, 156 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 24b37015..737c7f3e 100644 --- a/README.md +++ b/README.md @@ -496,6 +496,12 @@ The get_sp_metadata will return the metadata signed or not based on the security Before the XML metadata is exposed, a check takes place to ensure that the info to be provided is valid. +Instead of use the Auth object, you can directly use +``` +saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp_validation_only=True) +``` +to get the settings object and with the sp_validation_only=True parameter we will avoid the IdP Settings validation. + ***Attribute Consumer Service(ACS)*** This code handles the SAML response that the IdP forwards to the SP through the user's client. @@ -787,6 +793,8 @@ Configuration of the OneLogin Python Toolkit * `__init__` Initializes the settings: Sets the paths of the different folders and Loads settings info from settings file or array/object provided. * ***check_settings*** Checks the settings info. +* ***check_idp_settings*** Checks the IdP settings info. +* ***check_sp_settings*** Checks the SP settings info. * ***get_errors*** Returns an array with the errors, the array is empty when the settings is ok. * ***get_sp_metadata*** Gets the SP metadata. The XML representation. * ***validate_metadata*** Validates an XML SP Metadata. diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index b67c5d99..a2277366 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -6,6 +6,7 @@ from django.template import RequestContext from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils @@ -100,9 +101,10 @@ def attrs(request): def metadata(request): - req = prepare_django_request(request) - auth = init_saml_auth(req) - saml_settings = auth.get_settings() + # req = prepare_django_request(request) + # auth = init_saml_auth(req) + # saml_settings = auth.get_settings() + saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=settings.SAML_FOLDER, sp_validation_only=True) metadata = saml_settings.get_sp_metadata() errors = saml_settings.validate_metadata(metadata) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index 4bc64f17..dba684ad 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -61,7 +61,7 @@ class OneLogin_Saml2_Settings(object): """ - def __init__(self, settings=None, custom_base_path=None): + def __init__(self, settings=None, custom_base_path=None, sp_validation_only=False): """ Initializes the settings: - Sets the paths of the different folders @@ -72,7 +72,11 @@ def __init__(self, settings=None, custom_base_path=None): :param custom_base_path: Path where are stored the settings file and the cert folder :type custom_base_path: string + + :param sp_validation_only: Avoid the IdP validation + :type sp_validation_only: boolean """ + self.__sp_validation_only = sp_validation_only self.__paths = {} self.__strict = False self.__debug = False @@ -327,112 +331,148 @@ def check_settings(self, settings): errors = [] if not isinstance(settings, dict) or len(settings) == 0: errors.append('invalid_syntax') - return errors + else: + if not self.__sp_validation_only: + errors += self.check_idp_settings(settings) + sp_errors = self.check_sp_settings(settings) + errors += sp_errors - if 'idp' not in settings or len(settings['idp']) == 0: - errors.append('idp_not_found') + return errors + + def check_idp_settings(self, settings): + """ + Checks the IdP settings info. + :param settings: Dict with settings data + :type settings: dict + :returns: Errors found on the IdP settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or len(settings) == 0: + errors.append('invalid_syntax') else: - idp = settings['idp'] - if 'entityId' not in idp or len(idp['entityId']) == 0: - errors.append('idp_entityId_not_found') - - if 'singleSignOnService' not in idp or \ - 'url' not in idp['singleSignOnService'] or \ - len(idp['singleSignOnService']['url']) == 0: - errors.append('idp_sso_not_found') - elif not validate_url(idp['singleSignOnService']['url']): - errors.append('idp_sso_url_invalid') - - if 'singleLogoutService' in idp and \ - 'url' in idp['singleLogoutService'] and \ - len(idp['singleLogoutService']['url']) > 0 and \ - not validate_url(idp['singleLogoutService']['url']): - errors.append('idp_slo_url_invalid') - - if 'sp' not in settings or len(settings['sp']) == 0: - errors.append('sp_not_found') + if 'idp' not in settings or len(settings['idp']) == 0: + errors.append('idp_not_found') + else: + idp = settings['idp'] + if 'entityId' not in idp or len(idp['entityId']) == 0: + errors.append('idp_entityId_not_found') + + if 'singleSignOnService' not in idp or \ + 'url' not in idp['singleSignOnService'] or \ + len(idp['singleSignOnService']['url']) == 0: + errors.append('idp_sso_not_found') + elif not validate_url(idp['singleSignOnService']['url']): + errors.append('idp_sso_url_invalid') + + if 'singleLogoutService' in idp and \ + 'url' in idp['singleLogoutService'] and \ + len(idp['singleLogoutService']['url']) > 0 and \ + not validate_url(idp['singleLogoutService']['url']): + errors.append('idp_slo_url_invalid') + + if 'security' in settings: + security = settings['security'] + + exists_x509 = ('x509cert' in idp and + len(idp['x509cert']) > 0) + exists_fingerprint = ('certFingerprint' in idp and + len(idp['certFingerprint']) > 0) + + want_assert_sign = 'wantAssertionsSigned' in security.keys() and security['wantAssertionsSigned'] + want_mes_signed = 'wantMessagesSigned' in security.keys() and security['wantMessagesSigned'] + nameid_enc = 'nameIdEncrypted' in security.keys() and security['nameIdEncrypted'] + + if (want_assert_sign or want_mes_signed) and \ + not(exists_x509 or exists_fingerprint): + errors.append('idp_cert_or_fingerprint_not_found_and_required') + if nameid_enc and not exists_x509: + errors.append('idp_cert_not_found_and_required') + return errors + + def check_sp_settings(self, settings): + """ + Checks the SP settings info. + :param settings: Dict with settings data + :type settings: dict + :returns: Errors found on the SP settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or len(settings) == 0: + errors.append('invalid_syntax') else: - # check_sp_certs uses self.__sp so I add it - old_sp = self.__sp - self.__sp = settings['sp'] + if 'sp' not in settings or len(settings['sp']) == 0: + errors.append('sp_not_found') + else: + # check_sp_certs uses self.__sp so I add it + old_sp = self.__sp + self.__sp = settings['sp'] + + sp = settings['sp'] + security = {} + if 'security' in settings: + security = settings['security'] + + if 'entityId' not in sp or len(sp['entityId']) == 0: + errors.append('sp_entityId_not_found') + + if 'assertionConsumerService' not in sp or \ + 'url' not in sp['assertionConsumerService'] or \ + len(sp['assertionConsumerService']['url']) == 0: + errors.append('sp_acs_not_found') + elif not validate_url(sp['assertionConsumerService']['url']): + errors.append('sp_acs_url_invalid') + + if 'singleLogoutService' in sp and \ + 'url' in sp['singleLogoutService'] and \ + len(sp['singleLogoutService']['url']) > 0 and \ + not validate_url(sp['singleLogoutService']['url']): + errors.append('sp_sls_url_invalid') + + if 'signMetadata' in security and isinstance(security['signMetadata'], dict): + if 'keyFileName' not in security['signMetadata'] or \ + 'certFileName' not in security['signMetadata']: + errors.append('sp_signMetadata_invalid') + + authn_sign = 'authnRequestsSigned' in security and security['authnRequestsSigned'] + logout_req_sign = 'logoutRequestSigned' in security and security['logoutRequestSigned'] + logout_res_sign = 'logoutResponseSigned' in security and security['logoutResponseSigned'] + want_assert_enc = 'wantAssertionsEncrypted' in security and security['wantAssertionsEncrypted'] + want_nameid_enc = 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted'] + + if not self.check_sp_certs(): + if authn_sign or logout_req_sign or logout_res_sign or \ + want_assert_enc or want_nameid_enc: + errors.append('sp_cert_not_found_and_required') - sp = settings['sp'] - security = {} - if 'security' in settings: - security = settings['security'] - - if 'entityId' not in sp or len(sp['entityId']) == 0: - errors.append('sp_entityId_not_found') - - if 'assertionConsumerService' not in sp or \ - 'url' not in sp['assertionConsumerService'] or \ - len(sp['assertionConsumerService']['url']) == 0: - errors.append('sp_acs_not_found') - elif not validate_url(sp['assertionConsumerService']['url']): - errors.append('sp_acs_url_invalid') - - if 'singleLogoutService' in sp and \ - 'url' in sp['singleLogoutService'] and \ - len(sp['singleLogoutService']['url']) > 0 and \ - not validate_url(sp['singleLogoutService']['url']): - errors.append('sp_sls_url_invalid') - - if 'signMetadata' in security and isinstance(security['signMetadata'], dict): - if 'keyFileName' not in security['signMetadata'] or \ - 'certFileName' not in security['signMetadata']: - errors.append('sp_signMetadata_invalid') - - authn_sign = 'authnRequestsSigned' in security and security['authnRequestsSigned'] - logout_req_sign = 'logoutRequestSigned' in security and security['logoutRequestSigned'] - logout_res_sign = 'logoutResponseSigned' in security and security['logoutResponseSigned'] - want_assert_enc = 'wantAssertionsEncrypted' in security and security['wantAssertionsEncrypted'] - want_nameid_enc = 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted'] - - if not self.check_sp_certs(): - if authn_sign or logout_req_sign or logout_res_sign or \ - want_assert_enc or want_nameid_enc: - errors.append('sp_cert_not_found_and_required') - - exists_x509 = ('idp' in settings and - 'x509cert' in settings['idp'] and - len(settings['idp']['x509cert']) > 0) - exists_fingerprint = ('idp' in settings and - 'certFingerprint' in settings['idp'] and - len(settings['idp']['certFingerprint']) > 0) - - want_assert_sign = 'wantAssertionsSigned' in security and security['wantAssertionsSigned'] - want_mes_signed = 'wantMessagesSigned' in security and security['wantMessagesSigned'] - nameid_enc = 'nameIdEncrypted' in security and security['nameIdEncrypted'] - - if (want_assert_sign or want_mes_signed) and \ - not(exists_x509 or exists_fingerprint): - errors.append('idp_cert_or_fingerprint_not_found_and_required') - if nameid_enc and not exists_x509: - errors.append('idp_cert_not_found_and_required') - - if 'contactPerson' in settings: - types = settings['contactPerson'] - valid_types = ['technical', 'support', 'administrative', 'billing', 'other'] - for c_type in types: - if c_type not in valid_types: - errors.append('contact_type_invalid') - break - - for c_type in settings['contactPerson']: - contact = settings['contactPerson'][c_type] - if ('givenName' not in contact or len(contact['givenName']) == 0) or \ - ('emailAddress' not in contact or len(contact['emailAddress']) == 0): - errors.append('contact_not_enought_data') - break - - if 'organization' in settings: - for org in settings['organization']: - organization = settings['organization'][org] - if ('name' not in organization or len(organization['name']) == 0) or \ - ('displayname' not in organization or len(organization['displayname']) == 0) or \ - ('url' not in organization or len(organization['url']) == 0): - errors.append('organization_not_enought_data') - break + if 'contactPerson' in settings: + types = settings['contactPerson'] + valid_types = ['technical', 'support', 'administrative', 'billing', 'other'] + for c_type in types: + if c_type not in valid_types: + errors.append('contact_type_invalid') + break + + for c_type in settings['contactPerson']: + contact = settings['contactPerson'][c_type] + if ('givenName' not in contact or len(contact['givenName']) == 0) or \ + ('emailAddress' not in contact or len(contact['emailAddress']) == 0): + errors.append('contact_not_enought_data') + break + + if 'organization' in settings: + for org in settings['organization']: + organization = settings['organization'][org] + if ('name' not in organization or len(organization['name']) == 0) or \ + ('displayname' not in organization or len(organization['displayname']) == 0) or \ + ('url' not in organization or len(organization['url']) == 0): + errors.append('organization_not_enought_data') + break # Restores the value that had the self.__sp if 'old_sp' in locals(): self.__sp = old_sp From 66c46102a309527b780971822985855cba89d8bd Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 16:01:25 +0200 Subject: [PATCH 28/35] Now the SP is able to select the algorithm to be used on signatures. Support sign validation of different kinds of algorithm --- README.md | 8 +++++ demo-django/saml/advanced_settings.json | 3 +- demo-flask/saml/advanced_settings.json | 3 +- src/onelogin/saml2/auth.py | 42 ++++++++++++++++++------- src/onelogin/saml2/constants.py | 24 ++++++++++++-- src/onelogin/saml2/metadata.py | 7 +++-- src/onelogin/saml2/settings.py | 4 +++ src/onelogin/saml2/utils.py | 20 +++++++++--- 8 files changed, 89 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 737c7f3e..6d103217 100644 --- a/README.md +++ b/README.md @@ -324,6 +324,14 @@ In addition to the required settings data (idp, sp), there is extra information 'metadataValidUntil': null, // Provide the desire Duration, for example PT518400S (6 days) 'metadataCacheDuration': null, + + // Algorithm that the toolkit will use on signing process. Options: + // 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + // 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' + 'signatureAlgorithm': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' }, // Contact information template, it is recommended to suply a diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json index e336fe9d..30b61f76 100644 --- a/demo-django/saml/advanced_settings.json +++ b/demo-django/saml/advanced_settings.json @@ -7,7 +7,8 @@ "signMetadata": false, "wantMessagesSigned": false, "wantAssertionsSigned": false, - "wantNameIdEncrypted": false + "wantNameIdEncrypted": false, + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, "contactPerson": { "technical": { diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json index e336fe9d..30b61f76 100644 --- a/demo-flask/saml/advanced_settings.json +++ b/demo-flask/saml/advanced_settings.json @@ -7,7 +7,8 @@ "signMetadata": false, "wantMessagesSigned": false, "wantAssertionsSigned": false, - "wantNameIdEncrypted": false + "wantNameIdEncrypted": false, + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, "contactPerson": { "technical": { diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 1a28b77d..724ff873 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -11,6 +11,8 @@ """ +import xmlsec + from onelogin.saml2 import compat from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.response import OneLogin_Saml2_Response @@ -158,7 +160,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_ security = self.__settings.get_security_data() if security['logoutResponseSigned']: - self.add_response_signature(parameters) + self.add_response_signature(parameters, security['signatureAlgorithm']) return self.redirect_to(self.get_slo_url(), parameters) else: @@ -276,7 +278,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False): security = self.__settings.get_security_data() if security.get('authnRequestsSigned', False): - self.add_request_signature(parameters) + self.add_request_signature(parameters, security['signatureAlgorithm']) return self.redirect_to(self.get_sso_url(), parameters) def logout(self, return_to=None, name_id=None, session_index=None): @@ -314,7 +316,7 @@ def logout(self, return_to=None, name_id=None, session_index=None): security = self.__settings.get_security_data() if security.get('logoutRequestSigned', False): - self.add_request_signature(parameters) + self.add_request_signature(parameters, security['signatureAlgorithm']) return self.redirect_to(slo_url, parameters) def get_sso_url(self): @@ -338,22 +340,28 @@ def get_slo_url(self): if 'url' in idp_data['singleLogoutService']: return idp_data['singleLogoutService']['url'] - def add_request_signature(self, request_data): + def add_request_signature(self, request_data, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ Builds the Signature of the SAML Request. :param request_data: The Request parameters :type request_data: dict + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string """ - return self.__build_signature(request_data, 'SAMLRequest') + return self.__build_signature(request_data, 'SAMLRequest', sign_algorithm) - def add_response_signature(self, response_data): + def add_response_signature(self, response_data, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ Builds the Signature of the SAML Response. :param response_data: The Response parameters :type response_data: dict + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string """ - return self.__build_signature(response_data, 'SAMLResponse') + return self.__build_signature(response_data, 'SAMLResponse', sign_algorithm) @staticmethod def __build_sign_query(saml_data, relay_state, algorithm, saml_type): @@ -379,7 +387,7 @@ def __build_sign_query(saml_data, relay_state, algorithm, saml_type): sign_data.append('SigAlg=%s' % OneLogin_Saml2_Utils.escape_url(algorithm)) return '&'.join(sign_data) - def __build_signature(self, data, saml_type): + def __build_signature(self, data, saml_type, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ Builds the Signature :param data: The Request data @@ -387,6 +395,9 @@ def __build_signature(self, data, saml_type): :param saml_type: The target URL the user should be redirected to :type saml_type: string SAMLRequest | SAMLResponse + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string """ assert saml_type in ('SAMLRequest', 'SAMLResponse') key = self.get_settings().get_sp_key() @@ -399,12 +410,21 @@ def __build_signature(self, data, saml_type): msg = self.__build_sign_query(data[saml_type], data.get('RelayState', None), - OneLogin_Saml2_Constants.RSA_SHA1, + sign_algorithm, saml_type) - signature = OneLogin_Saml2_Utils.sign_binary(msg, key, debug=self.__settings.is_debug_active()) + 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(sign_algorithm, xmlsec.Transform.RSA_SHA1) + + signature = OneLogin_Saml2_Utils.sign_binary(msg, key, sign_algorithm_transform, self.__settings.is_debug_active()) data['Signature'] = OneLogin_Saml2_Utils.b64encode(signature) - data['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1 + data['SigAlg'] = sign_algorithm def validate_request_signature(self, request_data): """ diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py index 99a0c3a5..f8e2ddb3 100644 --- a/src/onelogin/saml2/constants.py +++ b/src/onelogin/saml2/constants.py @@ -76,9 +76,7 @@ class OneLogin_Saml2_Constants(object): STATUS_PARTIAL_LOGOUT = 'urn:oasis:names:tc:SAML:2.0:status:PartialLogout' STATUS_PROXY_COUNT_EXCEEDED = 'urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded' - # Crypto - RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' - + # Namespaces NSMAP = { 'samlp': NS_SAMLP, 'saml': NS_SAML, @@ -86,3 +84,23 @@ class OneLogin_Saml2_Constants(object): 'xenc': NS_XENC, 'md': NS_MD } + + # Sign & Crypto + SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1' + SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256' + SHA384 = 'http://www.w3.org/2001/04/xmlencsha384' + SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512' + + DSA_SHA1 = 'http://www.w3.org/2000/09/xmld/sig#dsa-sha1' + RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + RSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' + RSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' + + # Enc + TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' + AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' + AES192_CBC = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' + AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' + RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' + RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index e7de8dcf..99b1605d 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -140,7 +140,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N return metadata @staticmethod - def sign_metadata(metadata, key, cert): + def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ Signs the metadata with the key/cert provided @@ -155,8 +155,11 @@ def sign_metadata(metadata, key, cert): :returns: Signed Metadata :rtype: string + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string """ - return OneLogin_Saml2_Utils.add_sign(metadata, key, cert) + return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm) @staticmethod def __add_x509_key_descriptors(root, cert, signing): diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index dba684ad..1cb3452d 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -301,6 +301,10 @@ def __add_default_values(self): if 'wantNameIdEncrypted' not in self.__security: self.__security['wantNameIdEncrypted'] = False + # Signature Algorithm + if 'signatureAlgorithm' not in self.__security.keys(): + self.__security['signatureAlgorithm'] = OneLogin_Saml2_Constants.RSA_SHA1 + if 'x509cert' not in self.__idp: self.__idp['x509cert'] = '' if 'certFingerprint' not in self.__idp: diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 1c9c6ecc..0f68e756 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -637,7 +637,7 @@ def decrypt_element(encrypted_data, key, debug=False): return enc_ctx.decrypt(encrypted_data) @staticmethod - def add_sign(xml, key, cert, debug=False): + def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): """ Adds signature key and senders certificate to an element (Message or Assertion). @@ -648,11 +648,14 @@ def add_sign(xml, key, cert, debug=False): :param key: The private key :type: string + :param cert: The public + :type: string + :param debug: Activate the xmlsec debug :type: bool - :param cert: The public - :type: string + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string """ if xml is None or xml == '': raise Exception('Empty string supplied as input') @@ -661,7 +664,16 @@ def add_sign(xml, key, cert, debug=False): xmlsec.enable_debug_trace(debug) xmlsec.tree.add_ids(elem, ["ID"]) # Sign the metadata with our private key. - signature = xmlsec.template.create(elem, xmlsec.Transform.EXCL_C14N, xmlsec.Transform.RSA_SHA1, ns='ds') + 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(sign_algorithm, xmlsec.Transform.RSA_SHA1) + + signature = xmlsec.template.create(elem, xmlsec.Transform.EXCL_C14N, sign_algorithm_transform, ns='ds') issuer = OneLogin_Saml2_XML.query(elem, '//saml:Issuer') if len(issuer) > 0: From 6e0e6180dc51e83adc285ec4bb0a345756fb7c8f Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 16:02:25 +0200 Subject: [PATCH 29/35] Update xmlsec dependency_links --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b3af2c04..acb9c63c 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ 'isodate>=0.5.0', 'xmlsec>=0.3.1' ], - dependency_links=['http://github.com/bgaifullin/python-xmlsec/tarball/master'], + dependency_links=['http://github.com/mehcode/python-xmlsec/tarball/master'], extras_require={ 'test': ( 'coverage==3.7.1', From 6a80f34ff331a6cd87eb0dcce860ff629c929b35 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 16:03:35 +0200 Subject: [PATCH 30/35] Update library info --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index acb9c63c..9780b92b 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='python3-saml', - version='2.1.0', + version='1.0.0', description='Onelogin Python Toolkit. Add SAML support to your Python software using this library', classifiers=[ 'Development Status :: 4 - Beta', @@ -21,7 +21,7 @@ author='OneLogin', author_email='support@onelogin.com', license='MIT', - url='https://github.com/onelogin/python-saml', + url='https://github.com/onelogin/python3-saml', packages=['onelogin', 'onelogin/saml2'], include_package_data=True, package_data={ From ee016f86384717ba255a3e30c14d40e05ac56782 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 17:08:09 +0200 Subject: [PATCH 31/35] Support sign validation of different kinds of algorithm --- src/onelogin/saml2/auth.py | 3 ++- src/onelogin/saml2/utils.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 724ff873..e4ac7f37 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -491,7 +491,8 @@ def __validate_signature(self, data, saml_type): if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, OneLogin_Saml2_Utils.b64decode(signature), x509cert, - debug=self.__settings.is_debug_active()): + sign_alg, + self.__settings.is_debug_active()): raise Exception('Signature validation failed. %s rejected.' % saml_type) return True except Exception as e: diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 0f68e756..701ee2f4 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -801,7 +801,7 @@ def sign_binary(msg, key, algorithm=xmlsec.Transform.RSA_SHA1, debug=False): return dsig_ctx.sign_binary(compat.to_bytes(msg), algorithm) @staticmethod - def validate_binary_sign(signed_query, signature, cert=None, algorithm=xmlsec.Transform.RSA_SHA1, debug=False): + def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False): """ Validates signed bynary data (Used to validate GET Signature). @@ -825,8 +825,18 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=xmlsec.Tr 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), - algorithm, + sign_algorithm_transform, compat.to_bytes(signature)) return True except xmlsec.Error as e: From 7c7b16716b94a4975e1fe091ee751714f8386e51 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 17:10:33 +0200 Subject: [PATCH 32/35] pep8 --- src/onelogin/saml2/response.py | 4 ++-- tests/src/OneLogin/saml2_tests/response_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 9796fd98..9b7a3bcf 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -456,11 +456,11 @@ def __decrypt_assertion(self, xml): if 'RetrievalMethod' in child.tag: if child.attrib['Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey': raise Exception('Unsupported Retrieval Method found') - uri = child.attrib['URI'] + 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="'+uri +'"]') + encrypted_key = OneLogin_Saml2_XML.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id="' + uri + '"]') if encrypted_key: keyinfo.append(encrypted_key[0]) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 6027f025..77a04bc6 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -1106,4 +1106,4 @@ def testIsValidSignWithEmptyReferenceURI(self): settings = OneLogin_Saml2_Settings(settings_info) xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64')) response = OneLogin_Saml2_Response(settings, xml) - self.assertTrue(response.is_valid(self.get_request_data())) \ No newline at end of file + self.assertTrue(response.is_valid(self.get_request_data())) From 29262f8c9a4119d6ae6f5a536955473ab707fdf4 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 17:14:09 +0200 Subject: [PATCH 33/35] Fix server_port can be None --- src/onelogin/saml2/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 701ee2f4..95949976 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -223,7 +223,7 @@ def get_self_url_host(request_data): else: protocol = 'http' - if 'server_port' in request_data: + if 'server_port' in request_data and request_data['server_port'] is not None: port_number = str(request_data['server_port']) port = ':' + port_number From 2c0c043cca01f5cbd1d5f7ac4f5474b8baf124b8 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Thu, 30 Jul 2015 17:23:57 +0200 Subject: [PATCH 34/35] Update the documentation --- README.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6d103217..3d2bdb20 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # OneLogin's SAML Python Toolkit -[![Build Status](https://api.travis-ci.org/onelogin/python-saml.png?branch=master)](http://travis-ci.org/onelogin/python-saml) -[![Coverage Status](https://coveralls.io/repos/onelogin/python-saml/badge.png)](https://coveralls.io/r/onelogin/python-saml) -[![PyPi Version](https://pypip.in/v/python-saml/badge.png)](https://pypi.python.org/pypi/python-saml) -![PyPi Downloads](https://pypip.in/d/python-saml/badge.png) +[![Build Status](https://api.travis-ci.org/onelogin/python3-saml.png?branch=master)](http://travis-ci.org/onelogin/python3-saml) +[![Coverage Status](https://coveralls.io/repos/onelogin/python3-saml/badge.png)](https://coveralls.io/r/onelogin/python3-saml) +[![PyPi Version](https://pypip.in/v/python3-saml/badge.png)](https://pypi.python.org/pypi/python3-saml) +![PyPi Downloads](https://pypip.in/d/python3-saml/badge.png) Add SAML support to your Python software using this library. Forget those complicated libraries and use that open source library provided and supported by OneLogin Inc. +This version supports Python3, exists an alternative version that only support Python2: [python-saml](https://pypi.python.org/pypi/python-saml) Why add SAML support to my software? ------------------------------------ @@ -65,13 +66,11 @@ Installation ### Dependences ### - * python 2.7 - * [M2Crypto](https://pypi.python.org/pypi/M2Crypto) A Python crypto and SSL toolkit (depends on openssl, swig) - * [dm.xmlsec.binding](https://pypi.python.org/pypi/dm.xmlsec.binding) Cython/lxml based binding for the XML security library (depends on python-dev libxml2-dev libxmlsec1-dev) - * [isodate](https://pypi.python.org/pypi/isodate) An ISO 8601 date/time/duration parser and formater - * [defusedxml](https://pypi.python.org/pypi/defusedxml) XML bomb protection for Python stdlib modules + * python 2.7 // python 3.3 + * [xmlsec](https://pypi.python.org/pypi/xmlsec) Python bindings for the XML Security Library. + * [isodate](https://pypi.python.org/pypi/isodate) An ISO 8601 date/time/duration parser and formater -Review the setup.py file to know the version of the library that python-saml is using +Review the setup.py file to know the version of the library that python3-saml is using ### Code ### @@ -79,18 +78,18 @@ Review the setup.py file to know the version of the library that python-saml is The toolkit is hosted on github. You can download it from: - * Lastest release: https://github.com/onelogin/python-saml/releases/latest - * Master repo: https://github.com/onelogin/python-saml/tree/master + * Lastest release: https://github.com/onelogin/python3-saml/releases/latest + * Master repo: https://github.com/onelogin/python3-saml/tree/master Copy the core of the library (src/onelogin/saml2 folder) and merge the setup.py inside the python application. (each application has its structure so take your time to locate the Python SAML toolkit in the best place). #### Option 2. Download from pypi #### -The toolkit is hosted in pypi, you can find the python-saml package at https://pypi.python.org/pypi/python-saml +The toolkit is hosted in pypi, you can find the python3-saml package at https://pypi.python.org/pypi/python3-saml You can install it executing: ``` - pip install python-saml + pip install python3-saml ``` If you want to know how a project can handle python packages review this [guide](https://packaging.python.org/en/latest/tutorial.html) and review this [sampleproject](https://github.com/pypa/sampleproject) From b54b0bd522e44b00b3b4862041e04d0b0d384584 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Wed, 12 Aug 2015 23:19:09 +0200 Subject: [PATCH 35/35] Make SPNameQualifier optional on the generateNameId method. Avoid the use of SPNameQualifier when generating the NameID on the LogoutRequest builder. --- setup.py | 2 +- src/onelogin/saml2/logout_request.py | 4 +++- src/onelogin/saml2/utils.py | 3 ++- tests/src/OneLogin/saml2_tests/utils_test.py | 21 +++++++++++++++++++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 9780b92b..f1daeb7d 100644 --- a/setup.py +++ b/setup.py @@ -45,5 +45,5 @@ 'coveralls==0.4.4', ), }, - keywords='saml saml2 xmlsec django flask', + keywords='saml saml2 xmlsec django flask python3', ) diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py index b18059b2..861b9179 100644 --- a/src/onelogin/saml2/logout_request.py +++ b/src/onelogin/saml2/logout_request.py @@ -60,13 +60,15 @@ def __init__(self, settings, request=None, name_id=None, session_index=None): if name_id is not None: name_id_format = sp_data['NameIDFormat'] + sp_name_qualifier = None else: name_id = idp_data['entityId'] name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY + sp_name_qualifier = sp_data['entityId'] name_id_obj = OneLogin_Saml2_Utils.generate_name_id( name_id, - sp_data['entityId'], + sp_name_qualifier, name_id_format, cert ) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 95949976..38f6a0b8 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -548,7 +548,8 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False): root = OneLogin_Saml2_XML.make_root("{%s}container" % OneLogin_Saml2_Constants.NS_SAML) name_id = OneLogin_Saml2_XML.make_child(root, '{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML) - name_id.set('SPNameQualifier', sp_nq) + if sp_nq is not None: + name_id.set('SPNameQualifier', sp_nq) name_id.set('Format', sp_format) name_id.text = value diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py index d4625210..107ef135 100644 --- a/tests/src/OneLogin/saml2_tests/utils_test.py +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -459,7 +459,7 @@ def testGetExpireTime(self): self.assertNotEqual('3311642371', OneLogin_Saml2_Utils.get_expire_time('PT360000S', '2074-12-10T04:39:31Z')) self.assertNotEqual('3311642371', OneLogin_Saml2_Utils.get_expire_time('PT360000S', 1418186371)) - def testGenerateNameId(self): + def testGenerateNameIdWithSPNameQualifier(self): """ Tests the generateNameId method of the OneLogin_Saml2_Utils """ @@ -479,6 +479,25 @@ def testGenerateNameId(self): expected_name_id_enc = '\n\n\n\n\n\n' self.assertIn(expected_name_id_enc, name_id_enc) + def testGenerateNameIdWithSPNameQualifier(self): + """ + Tests the generateNameId method of the OneLogin_Saml2_Utils + """ + name_id_value = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde' + name_id_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified' + + name_id = OneLogin_Saml2_Utils.generate_name_id(name_id_value, None, name_id_format) + expected_name_id = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde' + self.assertEqual(expected_name_id, name_id) + + settings_info = self.loadSettingsJSON() + x509cert = settings_info['idp']['x509cert'] + key = OneLogin_Saml2_Utils.format_cert(x509cert) + + name_id_enc = OneLogin_Saml2_Utils.generate_name_id(name_id_value, None, name_id_format, key) + expected_name_id_enc = '\n\n\n\n\n\n' + self.assertIn(expected_name_id_enc, name_id_enc) + def testCalculateX509Fingerprint(self): """ Tests the calculateX509Fingerprint method of the OneLogin_Saml2_Utils