From 1e8e8e0c8dd3e044a4df67432cd5e45eba179874 Mon Sep 17 00:00:00 2001 From: Seecr Development Team Date: Mon, 5 Mar 2018 09:56:00 +0100 Subject: [PATCH 1/9] TS: pass-in settings as argument: OneLogin_Saml2_Auth; **don't use set_strict**. At most assert it is already set using is_strict. Since set_strict mutates the settings-object. Also: - Caches get_sp_cert and get_sp_key (since these can come from the file-system). --- src/onelogin/saml2/auth.py | 8 ++++++-- src/onelogin/saml2/metadata.py | 9 ++++++++- src/onelogin/saml2/settings.py | 11 +++++++++-- tests/src/OneLogin/saml2_tests/metadata_test.py | 17 +++++++++++++++++ tests/src/OneLogin/saml2_tests/settings_test.py | 2 ++ 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index bb564e4d..5eba6816 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -35,7 +35,7 @@ class OneLogin_Saml2_Auth(object): SAML Response, a Logout Request or a Logout Response). """ - def __init__(self, request_data, old_settings=None, custom_base_path=None): + def __init__(self, request_data, old_settings=None, custom_base_path=None, settings=None): """ Initializes the SP SAML instance. @@ -49,7 +49,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) + + # TS: New hotness: create OneLogin_Saml2_Settings once; add it using the settings argument -- never specify old_settings or custom_base_path. + self.__settings = settings if settings else OneLogin_Saml2_Settings(old_settings, custom_base_path) + self.__attributes = [] self.__nameid = None self.__nameid_format = None @@ -66,6 +69,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None): self.__last_response = None def get_settings(self): + # TS: UNUSED! """ Returns the settings info :return: Setting info diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index c431be48..0cf874dc 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -12,6 +12,7 @@ from time import gmtime, strftime, time from datetime import datetime from defusedxml.minidom import parseString +from xml.sax.saxutils import escape as escapeXml from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.utils import OneLogin_Saml2_Utils @@ -161,13 +162,19 @@ 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(): + surName = info.get('surName') + surNameXml = '' + if surName: + surNameXml = '\n %s' % escapeXml(surName) + contact = """ - %(name)s + %(name)s%(surNameXml)s %(email)s """ % \ { 'type': ctype, 'name': info['givenName'], + 'surNameXml': surNameXml, 'email': info['emailAddress'], } contacts_info.append(contact) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index d5db3a05..e8a19f08 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -128,11 +128,12 @@ def __load_paths(self, base_path=None): self.__paths = { 'base': base_path, 'cert': base_path + 'certs' + sep, - 'lib': base_path + 'lib' + sep, - 'extlib': base_path + 'extlib' + sep, + 'lib': base_path + 'lib' + sep, # TS: Not used (ever)! + 'extlib': base_path + 'extlib' + sep, # TS: Not used (ever)! } def __update_paths(self, settings): + # TS: Broken for a "custom_base_path"; use __init__'s custom_base_path argument instead! """ Set custom paths if necessary """ @@ -154,6 +155,7 @@ def get_base_path(self): return self.__paths['base'] def get_cert_path(self): + # TS: accessor NOT USED! (--> self.__paths['cert'] is :-s) """ Returns cert path @@ -163,6 +165,7 @@ def get_cert_path(self): return self.__paths['cert'] def get_lib_path(self): + # TS: NOT USED! """ Returns lib path @@ -172,6 +175,7 @@ def get_lib_path(self): return self.__paths['lib'] def get_ext_lib_path(self): + # TS: NOT USED! """ Returns external lib path @@ -181,6 +185,7 @@ def get_ext_lib_path(self): return self.__paths['extlib'] def get_schemas_path(self): + # TS: accessor NOT USED! (--> is manually re-computed in OneLogin_Saml2_Utils.validate_xml :-s) """ Returns schema path @@ -517,6 +522,7 @@ def get_sp_key(self): if not key and exists(key_file_name): with open(key_file_name) as f: key = f.read() + self.__sp['privateKey'] = key # TS: use self.__sp settings as a cache. return key or None @@ -533,6 +539,7 @@ def get_sp_cert(self): if not cert and exists(cert_file_name): with open(cert_file_name) as f: cert = f.read() + self.__sp['x509cert'] = cert # TS: use self.__sp settings as a cache. return cert or None diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py index 02e551e7..73c3f2ca 100644 --- a/tests/src/OneLogin/saml2_tests/metadata_test.py +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -36,6 +36,23 @@ def file_contents(self, filename): f.close() return content + def testSurName(self): + settingsDict = self.loadSettingsJSON() + settingsDict['contactPerson']['technical']['surName'] = 'Surname' + settings = OneLogin_Saml2_Settings(settingsDict) + sp_data = settings.get_sp_data() + security = settings.get_security_data() + organization = settings.get_organization() + contacts = settings.get_contacts() + + metadata = OneLogin_Saml2_Metadata.builder( + sp_data, security['authnRequestsSigned'], + security['wantAssertionsSigned'], None, None, contacts, + organization + ) + + self.assertIn('Surname', metadata) + def testBuilder(self): """ Tests the builder method of the OneLogin_Saml2_Metadata diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py index bcdd7e1f..564ceae8 100644 --- a/tests/src/OneLogin/saml2_tests/settings_test.py +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -428,6 +428,8 @@ def testGetSPMetadataSigned(self): # Now try again with SP keys set directly from files that no exists: settings_info['custom_base_path'] = '../path/not/exists/' + del settings_info['sp']['privateKey'] + del settings_info['sp']['x509cert'] with self.assertRaises(OneLogin_Saml2_Error): OneLogin_Saml2_Settings(settings_info).get_sp_metadata() From db627b8eb8a687c6742f69de3689f9fa3c4cd1ed Mon Sep 17 00:00:00 2001 From: Seecr Development Team Date: Mon, 5 Mar 2018 10:02:43 +0100 Subject: [PATCH 2/9] TS: added memory-loading or caching of keys & certs. - Since not all "memory-loading" C functions are exposed in the dm.xmlsec.binding; cache the add_sign signing_key (only used for generating SP metadata). --- src/onelogin/saml2/utils.py | 68 +++++++++++++-------- tests/src/OneLogin/saml2_tests/auth_test.py | 7 +++ 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index dafeaa73..0776872b 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -114,6 +114,7 @@ def deflate_and_base64_encode(value): @staticmethod def validate_xml(xml, schema, debug=False): + # TS: added caching of schema's """ Validates a xml against a schema :param xml: The xml that will be validated @@ -139,11 +140,7 @@ def validate_xml(xml, schema, debug=False): 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) + xmlschema = loadXmlSchemaByFilename(schema) if not xmlschema.validate(dom): if debug: @@ -671,11 +668,8 @@ def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=No # 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) + key_data = xmlsec.Key.loadMemory(cert, xmlsec.KeyDataFormatCertPem, None) mngr.addKey(key_data) - file_cert.close() # Prepare for encryption enc_data = EncData(xmlsec.TransformAes128Cbc, type=xmlsec.TypeEncElement) @@ -814,6 +808,9 @@ def write_temp_file(content): @staticmethod def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1, digest_algorithm=OneLogin_Saml2_Constants.SHA1): + # TS: **Only** called from get_sp_metadata -to-> sign_metadata -to-> add_sign (here) + # So: caching (key, cert) -> xmlsec Key abstraction is OK; because: + # - There is only one (key, cert) combination per configuration. """ Adds signature key and senders certificate to an element (Message or Assertion). @@ -908,13 +905,8 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant key_info.addX509Data() dsig_ctx = xmlsec.DSigCtx() - sign_key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) - - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) - sign_key.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem) - file_cert.close() - dsig_ctx.signKey = sign_key + dsig_ctx.signKey = add_sign_signkey(key=key, cert=cert) dsig_ctx.sign(signature) newdoc = parseString(tostring(elem, encoding='unicode').encode('utf-8')) @@ -1153,17 +1145,13 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger OneLogin_Saml2_Error.CERT_NOT_FOUND ) - file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) - if validatecert: mngr = xmlsec.KeysMngr() - mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted) + mngr.loadCertMemory(cert, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted) dsig_ctx = xmlsec.DSigCtx(mngr) else: dsig_ctx = xmlsec.DSigCtx() - dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) - - file_cert.close() + dsig_ctx.signKey = xmlsec.Key.loadMemory(cert, xmlsec.KeyDataFormatCertPem, None) dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509]) @@ -1209,10 +1197,7 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_ xmlsec.set_error_callback(error_callback_method) 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.signKey = xmlsec.Key.loadMemory(cert, xmlsec.KeyDataFormatCertPem, None) # Sign the metadata with our private key. sign_algorithm_transform_map = { @@ -1254,3 +1239,36 @@ def extract_raw_query_parameter(query_string, parameter, default=''): def case_sensitive_urlencode(to_encode, lowercase=False): encoded = quote_plus(to_encode) return re.sub(r"%[A-F0-9]{2}", lambda m: m.group(0).lower(), encoded) if lowercase else encoded + + +def loadXmlSchemaByFilename(schemaFilename): + schemaFilepath = join(dirname(__file__), 'schemas', schemaFilename) + xmlschema = XML_SCHEMA_CACHE.get(schemaFilepath) + if not xmlschema: + with open(schemaFilepath, 'r') as f: + schema_doc = etree.parse(f) + + xmlschema = etree.XMLSchema(schema_doc) + XML_SCHEMA_CACHE[schemaFilepath] = xmlschema + + return xmlschema + +def add_sign_signkey(key, cert): + cacheKey = (key, cert) + sign_key = ADD_SIGN_SIGNKEY_CACHE.get(cacheKey) + if not sign_key: + sign_key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + sign_key.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem) + file_cert.close() + + ADD_SIGN_SIGNKEY_CACHE[cacheKey] = sign_key + + return sign_key + + + + +ADD_SIGN_SIGNKEY_CACHE = {} # sign_key (constructed from key and cert); by (key, cert) "data" +XML_SCHEMA_CACHE = {} # lxml parsed xml-schema files (by file(base)name). diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index c50fc6e8..a314834f 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -70,6 +70,13 @@ def testGetSSOurl(self): sso_url = settings_info['idp']['singleSignOnService']['url'] self.assertEqual(auth.get_sso_url(), sso_url) + def testAuthInitiatedWithSettings(self): + # TS: a.k.a. does-not-crash-so-must-be-working. + settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) + auth = OneLogin_Saml2_Auth(self.get_request(), settings=settings) + sso_url = settings.get_idp_data()['singleSignOnService']['url'] + self.assertEqual(auth.get_sso_url(), sso_url) + def testGetSLOurl(self): """ Tests the get_slo_url method of the OneLogin_Saml2_Auth class From 181928e386d17f6fc87ec53e1f2549aae1ee3339 Mon Sep 17 00:00:00 2001 From: Seecr Development Team Date: Mon, 5 Mar 2018 10:04:12 +0100 Subject: [PATCH 3/9] TS: added time-mocking hacked-in. - Manipulate onelogin.saml2.time_indirect `mocked_time_fn' Set it to a function that returns a time.time()-like float; when reset to a falsy-value mocking can be turned off again. --- src/onelogin/saml2/metadata.py | 7 ++++--- src/onelogin/saml2/settings.py | 2 +- src/onelogin/saml2/time_indirect.py | 31 +++++++++++++++++++++++++++++ src/onelogin/saml2/utils.py | 9 +++++---- 4 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 src/onelogin/saml2/time_indirect.py diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index 0cf874dc..aed21fdf 100644 --- a/src/onelogin/saml2/metadata.py +++ b/src/onelogin/saml2/metadata.py @@ -9,8 +9,9 @@ """ -from time import gmtime, strftime, time -from datetime import datetime +from time import gmtime, strftime +from datetime import datetime as datetimeClass +from .time_indirect import time from defusedxml.minidom import parseString from xml.sax.saxutils import escape as escapeXml @@ -57,7 +58,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N if valid_until is None: valid_until = int(time()) + OneLogin_Saml2_Metadata.TIME_VALID if not isinstance(valid_until, basestring): - if isinstance(valid_until, datetime): + if isinstance(valid_until, datetimeClass): valid_until_time = valid_until.timetuple() else: valid_until_time = gmtime(valid_until) diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index e8a19f08..ccdf8041 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -11,7 +11,7 @@ import json import re -from time import time +from .time_indirect import time from os.path import dirname, exists, join, sep, abspath from xml.dom.minidom import Document diff --git a/src/onelogin/saml2/time_indirect.py b/src/onelogin/saml2/time_indirect.py new file mode 100644 index 00000000..f3e98363 --- /dev/null +++ b/src/onelogin/saml2/time_indirect.py @@ -0,0 +1,31 @@ +from time import time as _time +from datetime import datetime as _datetime + +# TS: Hackish way to enable time-mocking (to test SAMLResponse's). +# +# Set `mocked_time_value to a truthy-value to enable time-bending powers; like so: +# sys.modules['onelogin.saml2.time_indirect'].mocked_time_fn = . + +mocked_time_fn = None + +def time(): + if mocked_time_fn: + return mocked_time_fn() + return _time() + +def _mocked_datetime_utcnow(): + return _datetime.utcfromtimestamp(mock_time_fn()) + +class datetime(object): + def __getattr__(self, attr): + if mocked_time_fn: + return self._mocked_getattr(attr) + return getattr(_datetime, attr) + + def _mocked_getattr(self, attr): + if attr == 'utcnow': + return _mocked_datetime_utcnow + return getattr(_datetime, attr) + + +datetime = datetime() diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 0776872b..70621b20 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -12,7 +12,8 @@ import base64 from copy import deepcopy from datetime import datetime -import calendar +from .time_indirect import datetime +from calendar import timegm from hashlib import sha1, sha256, sha384, sha512 from isodate import parse_duration as duration_parser from lxml import etree @@ -454,7 +455,7 @@ def parse_SAML_to_time(timestr): data = datetime.strptime(timestr, '%Y-%m-%dT%H:%M:%SZ') except ValueError: data = datetime.strptime(timestr, '%Y-%m-%dT%H:%M:%S.%fZ') - return calendar.timegm(data.utctimetuple()) + return timegm(data.utctimetuple()) @staticmethod def now(): @@ -462,7 +463,7 @@ def now(): :return: unix timestamp of actual time. :rtype: int """ - return calendar.timegm(datetime.utcnow().utctimetuple()) + return timegm(datetime.utcnow().utctimetuple()) @staticmethod def parse_duration(duration, timestamp=None): @@ -487,7 +488,7 @@ def parse_duration(duration, timestamp=None): data = datetime.utcnow() + timedelta else: data = datetime.utcfromtimestamp(timestamp) + timedelta - return calendar.timegm(data.utctimetuple()) + return timegm(data.utctimetuple()) @staticmethod def get_expire_time(cache_duration=None, valid_until=None): From af49d979d1248233bec6244ba63bbcf251ad6b7e Mon Sep 17 00:00:00 2001 From: Seecr Development Team Date: Mon, 5 Mar 2018 10:05:22 +0100 Subject: [PATCH 4/9] TS: mock time & id-generation in indirect_for_mocking module. - Added test-context-managers - Implementing tests that mocking works. --- src/onelogin/saml2/indirect_for_mocking.py | 64 +++++++++++++++++++ src/onelogin/saml2/metadata.py | 2 +- src/onelogin/saml2/settings.py | 2 +- src/onelogin/saml2/time_indirect.py | 31 --------- src/onelogin/saml2/utils.py | 3 +- tests/src/OneLogin/saml2_tests/auth_test.py | 15 +++++ .../saml2_tests/authn_request_test.py | 14 ++++ .../src/OneLogin/saml2_tests/settings_test.py | 54 ++++++++++++++++ tests/src/OneLogin/saml2_tests/utils_test.py | 45 +++++++++++++ 9 files changed, 196 insertions(+), 34 deletions(-) create mode 100644 src/onelogin/saml2/indirect_for_mocking.py delete mode 100644 src/onelogin/saml2/time_indirect.py diff --git a/src/onelogin/saml2/indirect_for_mocking.py b/src/onelogin/saml2/indirect_for_mocking.py new file mode 100644 index 00000000..df1aafa1 --- /dev/null +++ b/src/onelogin/saml2/indirect_for_mocking.py @@ -0,0 +1,64 @@ +from contextlib import contextmanager +from datetime import datetime as _datetime +from hashlib import sha1 +from time import time as _time +from uuid import uuid4 + +# TS: Hackish way to enable random-id and time-mocking (to test SAMLResponse's). +# +# Set `mocked_time_fn to a truthy-value, a function returning UNIX-Time (time.time()'s format). +# +# Set `mocked_random_id_fn' to a truthy-value, a function returning a `random-id-string' when called. + +mocked_time_fn = None +mocked_random_id_fn = None + + +@contextmanager +def mocked_time(time_fn): + global mocked_time_fn + + prev_time_fn = mocked_time_fn + try: + mocked_time_fn = time_fn + yield + finally: + mocked_time_fn = prev_time_fn + +@contextmanager +def mocked_generate_unique_id(random_id_fn): + global mocked_random_id_fn + + prev_random_id_fn = mocked_random_id_fn + try: + mocked_random_id_fn = random_id_fn + yield + finally: + mocked_random_id_fn = prev_random_id_fn + +def time(): + if mocked_time_fn: + return mocked_time_fn() + return _time() + +def _mocked_datetime_utcnow(): + return _datetime.utcfromtimestamp(mocked_time_fn()) + +class datetime(object): + def __getattr__(self, attr): + if mocked_time_fn: + return self._mocked_getattr(attr) + return getattr(_datetime, attr) + + def _mocked_getattr(self, attr): + if attr == 'utcnow': + return _mocked_datetime_utcnow + return getattr(_datetime, attr) + + +datetime = datetime() + +def generate_unique_id(): + if mocked_random_id_fn: + return mocked_random_id_fn() + return 'ONELOGIN_%s' % sha1(uuid4().hex).hexdigest() # Weird, but the original implementation. diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py index aed21fdf..6b47608d 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 as datetimeClass -from .time_indirect import time +from .indirect_for_mocking import time from defusedxml.minidom import parseString from xml.sax.saxutils import escape as escapeXml diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py index ccdf8041..f92615b0 100644 --- a/src/onelogin/saml2/settings.py +++ b/src/onelogin/saml2/settings.py @@ -11,7 +11,7 @@ import json import re -from .time_indirect import time +from .indirect_for_mocking import time from os.path import dirname, exists, join, sep, abspath from xml.dom.minidom import Document diff --git a/src/onelogin/saml2/time_indirect.py b/src/onelogin/saml2/time_indirect.py deleted file mode 100644 index f3e98363..00000000 --- a/src/onelogin/saml2/time_indirect.py +++ /dev/null @@ -1,31 +0,0 @@ -from time import time as _time -from datetime import datetime as _datetime - -# TS: Hackish way to enable time-mocking (to test SAMLResponse's). -# -# Set `mocked_time_value to a truthy-value to enable time-bending powers; like so: -# sys.modules['onelogin.saml2.time_indirect'].mocked_time_fn = . - -mocked_time_fn = None - -def time(): - if mocked_time_fn: - return mocked_time_fn() - return _time() - -def _mocked_datetime_utcnow(): - return _datetime.utcfromtimestamp(mock_time_fn()) - -class datetime(object): - def __getattr__(self, attr): - if mocked_time_fn: - return self._mocked_getattr(attr) - return getattr(_datetime, attr) - - def _mocked_getattr(self, attr): - if attr == 'utcnow': - return _mocked_datetime_utcnow - return getattr(_datetime, attr) - - -datetime = datetime() diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 70621b20..b5882fa7 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -13,6 +13,7 @@ from copy import deepcopy from datetime import datetime from .time_indirect import datetime +from .indirect_for_mocking import datetime, generate_unique_id from calendar import timegm from hashlib import sha1, sha256, sha384, sha512 from isodate import parse_duration as duration_parser @@ -422,7 +423,7 @@ def generate_unique_id(): :return: A unique string :rtype: string """ - return 'ONELOGIN_%s' % sha1(uuid4().hex).hexdigest() + return generate_unique_id() @staticmethod def parse_time_to_SAML(time): diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index a314834f..55101cce 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -70,6 +70,21 @@ def testGetSSOurl(self): sso_url = settings_info['idp']['singleSignOnService']['url'] self.assertEqual(auth.get_sso_url(), sso_url) + def testLoginHasMockedId(self): + settings_info = self.loadSettingsJSON() + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + + redirectStr = auth.login() + + # TODO: Continue Here!! (imports, constants, with :, in/xpath-test for ID). + self.fail('TODO') + + _scheme, _netloc, _path, query, _fragment = urlsplit(redirectStr) + q = parse_qs(query) + self.assertEquals({'SigAlg', 'Signature', 'RelayState', 'SAMLRequest'}, set(q.keys())) + samlRequest = q['SAMLRequest'] + samlRequestXmlStr = decompress(b64decode(samlRequest[0]), -MAX_WBITS) + def testAuthInitiatedWithSettings(self): # TS: a.k.a. does-not-crash-so-must-be-working. settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 6aee012d..3cd50c7c 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -17,6 +17,8 @@ from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.indirect_for_mocking import mocked_generate_unique_id + class OneLogin_Saml2_Authn_Request_Test(unittest.TestCase): def loadSettingsJSON(self, filename='settings1.json'): @@ -64,6 +66,7 @@ def testCreateRequest(self): self.assertRegexpMatches(inflated, '^ Date: Mon, 5 Mar 2018 10:06:40 +0100 Subject: [PATCH 5/9] TS: tested login uses mocked id-generation and time. --- tests/src/OneLogin/saml2_tests/auth_test.py | 61 +++++++++++++++++---- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 55101cce..fdc6c7ef 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -3,13 +3,16 @@ # Copyright (c) 2014, OneLogin, Inc. # All rights reserved. +from StringIO import StringIO from base64 import b64decode, b64encode -import json +from lxml.etree import parse from os.path import dirname, join, exists -import unittest from teamcity import is_running_under_teamcity from teamcity.unittestpy import TeamcityTestRunner -from urlparse import urlparse, parse_qs +from urlparse import urlparse, urlsplit, parse_qs +from zlib import decompress, MAX_WBITS +import json +import unittest from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.constants import OneLogin_Saml2_Constants @@ -18,6 +21,8 @@ from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request from onelogin.saml2.errors import OneLogin_Saml2_Error +from onelogin.saml2.indirect_for_mocking import mocked_generate_unique_id, mocked_time + class OneLogin_Saml2_Auth_Test(unittest.TestCase): data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') @@ -74,16 +79,45 @@ def testLoginHasMockedId(self): settings_info = self.loadSettingsJSON() auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) - redirectStr = auth.login() + def reqIdFromSSOLoginRedirectUrl(url): + _, _, _, query, _ = urlsplit(url) + samlRequestXmlStr = decompress(b64decode(parse_qs(query)['SAMLRequest'][0]), -MAX_WBITS) + lxmlNode = parse(StringIO(samlRequestXmlStr)) + reqId = lxmlNode.xpath('/samlp:AuthnRequest/@ID', namespaces=SAML_NAMESPACES)[0] + return reqId + + _id = [0] + def id_fn(): + _id[0] += 1 + return 'MOCK_ID_{0}'.format(_id[0]) + + self.assertTrue(reqIdFromSSOLoginRedirectUrl(auth.login()).startswith('ONELOGIN_')) + + with mocked_generate_unique_id(id_fn): + self.assertEquals('MOCK_ID_1', reqIdFromSSOLoginRedirectUrl(auth.login())) + self.assertEquals('MOCK_ID_2', reqIdFromSSOLoginRedirectUrl(auth.login())) + + def testLoginHasMockedTime(self): + settings_info = self.loadSettingsJSON() + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + + def instantFromSSOLoginRedirectUrl(url): + _, _, _, query, _ = urlsplit(url) + samlRequestXmlStr = decompress(b64decode(parse_qs(query)['SAMLRequest'][0]), -MAX_WBITS) + lxmlNode = parse(StringIO(samlRequestXmlStr)) + issueInstant = lxmlNode.xpath('/samlp:AuthnRequest/@IssueInstant', namespaces=SAML_NAMESPACES)[0] + return issueInstant + + _time = [0.0] + def time_fn(): + _time[0] += 1.0 + return _time[0] - # TODO: Continue Here!! (imports, constants, with :, in/xpath-test for ID). - self.fail('TODO') + self.assertTrue(instantFromSSOLoginRedirectUrl(auth.login()).startswith('20')) - _scheme, _netloc, _path, query, _fragment = urlsplit(redirectStr) - q = parse_qs(query) - self.assertEquals({'SigAlg', 'Signature', 'RelayState', 'SAMLRequest'}, set(q.keys())) - samlRequest = q['SAMLRequest'] - samlRequestXmlStr = decompress(b64decode(samlRequest[0]), -MAX_WBITS) + with mocked_time(time_fn): + self.assertEquals('1970-01-01T00:00:01Z', instantFromSSOLoginRedirectUrl(auth.login())) + self.assertEquals('1970-01-01T00:00:02Z', instantFromSSOLoginRedirectUrl(auth.login())) def testAuthInitiatedWithSettings(self): # TS: a.k.a. does-not-crash-so-must-be-working. @@ -1187,6 +1221,11 @@ def testGetIdFromLogoutResponse(self): auth.process_slo() self.assertIn(auth.get_last_message_id(), '_f9ee61bd9dbf63606faa9ae3b10548d5b3656fb859') +SAML_NAMESPACES = { + 'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol', + 'saml': 'urn:oasis:names:tc:SAML:2.0:assertion', +} + if __name__ == '__main__': if is_running_under_teamcity(): runner = TeamcityTestRunner() From d3de756ec3f15e3027fad959b260810c5ac9c19a Mon Sep 17 00:00:00 2001 From: Seecr Development Team Date: Mon, 5 Mar 2018 10:07:00 +0100 Subject: [PATCH 6/9] More deps --- deps.txt | 6 ++++++ src/onelogin/saml2/utils.py | 9 +++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 deps.txt diff --git a/deps.txt b/deps.txt new file mode 100644 index 00000000..084fb198 --- /dev/null +++ b/deps.txt @@ -0,0 +1,6 @@ +python-defusedxml (>= 0.4.1) +python-defusedxml (<< 0.5) +python-isodate (>= 0.5.0) +python-isodate (<< 0.6) +python-xmlsec-binding (>= 1.3.2) +python-xmlsec-binding (<< 1.4) \ No newline at end of file diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index b5882fa7..2c930c30 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -21,7 +21,7 @@ from defusedxml.lxml import tostring, fromstring from os.path import basename, dirname, join import re -from sys import stderr +import sys from tempfile import NamedTemporaryFile from textwrap import wrap from urllib import quote_plus @@ -146,11 +146,12 @@ def validate_xml(xml, schema, debug=False): if not xmlschema.validate(dom): if debug: - stderr.write('Errors validating the metadata') - stderr.write(':\n\n') + sys.stderr.write('Errors validating the metadata') + sys.stderr.write(':\n\n') for error in xmlschema.error_log: - stderr.write('%s\n' % error.message) + sys.stderr.write('%s\n' % error.message) + sys.stderr.flush() return 'invalid_xml' return parseString(tostring(dom, encoding='unicode').encode('utf-8')) From 4ef8354e0048c7f65e6fc9a3d3669a49faf4cf5c Mon Sep 17 00:00:00 2001 From: Seecr Development Team Date: Mon, 5 Mar 2018 10:55:19 +0100 Subject: [PATCH 7/9] TS/JJ: removed forgotten merge thingy --- tests/src/OneLogin/saml2_tests/authn_request_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 3cd50c7c..8175c99e 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -66,7 +66,6 @@ def testCreateRequest(self): self.assertRegexpMatches(inflated, '^ Date: Mon, 5 Mar 2018 11:03:14 +0100 Subject: [PATCH 8/9] TS/JJ: fixed test and removed obsolete import --- src/onelogin/saml2/utils.py | 1 - tests/src/OneLogin/saml2_tests/auth_test.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py index 2c930c30..1e461082 100644 --- a/src/onelogin/saml2/utils.py +++ b/src/onelogin/saml2/utils.py @@ -12,7 +12,6 @@ import base64 from copy import deepcopy from datetime import datetime -from .time_indirect import datetime from .indirect_for_mocking import datetime, generate_unique_id from calendar import timegm from hashlib import sha1, sha256, sha384, sha512 diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index fdc6c7ef..35e57a36 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -1033,7 +1033,7 @@ def testBuildRequestSignature(self): valid_signature = 'Pb1EXAX5TyipSJ1SndEKZstLQTsT+1D00IZAhEepBM+OkAZQSToivu3njgJu47HZiZAqgXZFgloBuuWE/+GdcSsRYEMkEkiSDWTpUr25zKYLJDSg6GNo6iAHsKSuFt46Z54Xe/keYxYP03Hdy97EwuuSjBzzgRc5tmpV+KC7+a0=' 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) with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLRequest but can't load the SP private key"): @@ -1052,7 +1052,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) with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLResponse but can't load the SP private key"): From f8bef645cb6de256c0ad2b5a0357d23b945a7be2 Mon Sep 17 00:00:00 2001 From: Seecr Development Team Date: Fri, 15 Mar 2019 09:01:46 +0100 Subject: [PATCH 9/9] JJ: builddeps --- build-deps.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 build-deps.txt diff --git a/build-deps.txt b/build-deps.txt new file mode 100644 index 00000000..532a294d --- /dev/null +++ b/build-deps.txt @@ -0,0 +1,3 @@ +libxmlsec1-dev +libxml2-dev +