Skip to content

Commit b144b06

Browse files
committed
Optionally raise detailed exceptions vs. returning False
1 parent 7ac2640 commit b144b06

File tree

6 files changed

+108
-87
lines changed

6 files changed

+108
-87
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
language: python
22
python:
33
- '2.7.6'
4-
# - '2.7.9'
4+
# - '2.7.9'
55
# - '2.7.12'
6-
- '3.3.4'
6+
# - '3.3.4'
77
# - '3.3.5'
88
# - '3.3.6'
99
- '3.4.3'

src/onelogin/saml2/logout_request.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"""
1111

1212
from onelogin.saml2.constants import OneLogin_Saml2_Constants
13-
from onelogin.saml2.utils import OneLogin_Saml2_Utils
13+
from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception
1414
from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates
1515
from onelogin.saml2.xml_utils import OneLogin_Saml2_XML
1616

@@ -246,7 +246,7 @@ def is_valid(self, request_data, raise_exceptions=False):
246246
if root.get('NotOnOrAfter', None):
247247
na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.get('NotOnOrAfter'))
248248
if na <= OneLogin_Saml2_Utils.now():
249-
raise Exception('Timing issues (please check your clock settings)')
249+
raise Exception('Could not validate timestamp: expired. Check system clock.)')
250250

251251
# Check destination
252252
if root.get('Destination', None):

src/onelogin/saml2/response.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from copy import deepcopy
1313
from onelogin.saml2.constants import OneLogin_Saml2_Constants
14-
from onelogin.saml2.utils import OneLogin_Saml2_Utils
14+
from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception
1515
from onelogin.saml2.xml_utils import OneLogin_Saml2_XML
1616

1717

@@ -124,8 +124,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
124124
raise Exception('The Assertion must include a Conditions element')
125125

126126
# Validates Assertion timestamps
127-
if not self.validate_timestamps():
128-
raise Exception('Timing issues (please check your clock settings)')
127+
self.validate_timestamps(raise_exceptions=True)
129128

130129
# Checks that an AuthnStatement element exists and is unique
131130
if not self.check_one_authnstatement():
@@ -216,11 +215,11 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
216215
fingerprintalg = idp_data.get('certFingerprintAlgorithm', None)
217216

218217
# If find a Signature on the Response, validates it checking the original response
219-
if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH):
218+
if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, raise_exceptions=False):
220219
raise Exception('Signature validation failed. SAML Response rejected')
221220

222221
document_check_assertion = self.decrypted_document if self.encrypted else self.document
223-
if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH):
222+
if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=False):
224223
raise Exception('Signature validation failed. SAML Response rejected')
225224

226225
return True
@@ -502,6 +501,7 @@ def validate_signed_elements(self, signed_elements):
502501

503502
return True
504503

504+
@return_false_on_exception
505505
def validate_timestamps(self):
506506
"""
507507
Verifies that the document is valid according to Conditions Element
@@ -515,9 +515,9 @@ def validate_timestamps(self):
515515
nb_attr = conditions_node.get('NotBefore')
516516
nooa_attr = conditions_node.get('NotOnOrAfter')
517517
if nb_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nb_attr) > OneLogin_Saml2_Utils.now() + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT:
518-
return False
518+
raise Exception('Could not validate timestamp: not yet valid. Check system clock.')
519519
if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT <= OneLogin_Saml2_Utils.now():
520-
return False
520+
raise Exception('Could not validate timestamp: expired. Check system clock.')
521521
return True
522522

523523
def __query_assertion(self, xpath_expr):

src/onelogin/saml2/utils.py

Lines changed: 94 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from isodate import parse_duration as duration_parser
1717
import re
1818
from textwrap import wrap
19+
from functools import wraps
1920
from uuid import uuid4
2021

2122
import zlib
@@ -33,6 +34,24 @@
3334
from urllib import quote_plus # py2
3435

3536

37+
def return_false_on_exception(func):
38+
"""
39+
Decorator. When applied to a function, it will, by default, suppress any exceptions
40+
raised by that function and return False. It may be overridden by passing a
41+
"raise_exceptions" keyword argument when calling the wrapped function.
42+
"""
43+
@wraps(func)
44+
def exceptfalse(*args, **kwargs):
45+
if not kwargs.pop('raise_exceptions', False):
46+
try:
47+
return func(*args, **kwargs)
48+
except Exception:
49+
return False
50+
else:
51+
return func(*args, **kwargs)
52+
return exceptfalse
53+
54+
3655
class OneLogin_Saml2_Utils(object):
3756
"""
3857
@@ -719,6 +738,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
719738
return OneLogin_Saml2_XML.to_string(elem)
720739

721740
@staticmethod
741+
@return_false_on_exception
722742
def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None):
723743
"""
724744
Validates a signature (Message or Assertion).
@@ -743,36 +763,34 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
743763
744764
:param xpath: The xpath of the signed element
745765
:type: string
746-
"""
747-
try:
748-
if xml is None or xml == '':
749-
raise Exception('Empty string supplied as input')
750-
751-
elem = OneLogin_Saml2_XML.to_etree(xml)
752-
xmlsec.enable_debug_trace(debug)
753-
xmlsec.tree.add_ids(elem, ["ID"])
754766
755-
if xpath:
756-
signature_nodes = OneLogin_Saml2_XML.query(elem, xpath)
757-
else:
758-
signature_nodes = OneLogin_Saml2_XML.query(elem, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH)
767+
:param raise_exceptions: Whether to return false on failure or raise an exception
768+
:type raise_exceptions: Boolean
769+
"""
770+
if xml is None or xml == '':
771+
raise Exception('Empty string supplied as input')
759772

760-
if len(signature_nodes) == 0:
761-
signature_nodes = OneLogin_Saml2_XML.query(elem, OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH)
773+
elem = OneLogin_Saml2_XML.to_etree(xml)
774+
xmlsec.enable_debug_trace(debug)
775+
xmlsec.tree.add_ids(elem, ["ID"])
762776

763-
if len(signature_nodes) == 1:
764-
signature_node = signature_nodes[0]
777+
if xpath:
778+
signature_nodes = OneLogin_Saml2_XML.query(elem, xpath)
779+
else:
780+
signature_nodes = OneLogin_Saml2_XML.query(elem, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH)
765781

766-
return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug)
767-
else:
768-
return False
769-
except xmlsec.Error as e:
770-
if debug:
771-
print(e)
782+
if len(signature_nodes) == 0:
783+
signature_nodes = OneLogin_Saml2_XML.query(elem, OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH)
772784

773-
return False
785+
if len(signature_nodes) == 1:
786+
signature_node = signature_nodes[0]
787+
# Raises expection if invalid
788+
return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
789+
else:
790+
raise Exception('Expected exactly one signature node; got {}.'.format(len(signature_nodes)))
774791

775792
@staticmethod
793+
@return_false_on_exception
776794
def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
777795
"""
778796
Validates a signature of a EntityDescriptor.
@@ -794,35 +812,36 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha
794812
795813
:param debug: Activate the xmlsec debug
796814
:type: bool
815+
816+
:param raise_exceptions: Whether to return false on failure or raise an exception
817+
:type raise_exceptions: Boolean
797818
"""
798-
try:
799-
if xml is None or xml == '':
800-
raise Exception('Empty string supplied as input')
819+
if xml is None or xml == '':
820+
raise Exception('Empty string supplied as input')
801821

802-
elem = OneLogin_Saml2_XML.to_etree(xml)
803-
xmlsec.enable_debug_trace(debug)
804-
xmlsec.tree.add_ids(elem, ["ID"])
822+
elem = OneLogin_Saml2_XML.to_etree(xml)
823+
xmlsec.enable_debug_trace(debug)
824+
xmlsec.tree.add_ids(elem, ["ID"])
805825

806-
signature_nodes = OneLogin_Saml2_XML.query(elem, '/md:EntitiesDescriptor/ds:Signature')
826+
signature_nodes = OneLogin_Saml2_XML.query(elem, '/md:EntitiesDescriptor/ds:Signature')
807827

808-
if len(signature_nodes) == 0:
809-
signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/ds:Signature')
828+
if len(signature_nodes) == 0:
829+
signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/ds:Signature')
810830

811-
if len(signature_nodes) == 0:
812-
signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature')
813-
signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature')
831+
if len(signature_nodes) == 0:
832+
signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature')
833+
signature_nodes += OneLogin_Saml2_XML.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature')
814834

815-
if len(signature_nodes) > 0:
816-
for signature_node in signature_nodes:
817-
if not OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug):
818-
return False
819-
return True
820-
else:
821-
return False
822-
except Exception:
823-
return False
835+
if len(signature_nodes) > 0:
836+
for signature_node in signature_nodes:
837+
# Raises expection if invalid
838+
OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
839+
return True
840+
else:
841+
raise Exception('Could not validate metadata signature: No signature nodes found.')
824842

825843
@staticmethod
844+
@return_false_on_exception
826845
def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
827846
"""
828847
Validates a signature node.
@@ -847,40 +866,42 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
847866
848867
:param debug: Activate the xmlsec debug
849868
:type: bool
869+
870+
:param raise_exceptions: Whether to return false on failure or raise an exception
871+
:type raise_exceptions: Boolean
850872
"""
851-
try:
852-
if (cert is None or cert == '') and fingerprint:
853-
x509_certificate_nodes = OneLogin_Saml2_XML.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate')
854-
if len(x509_certificate_nodes) > 0:
855-
x509_certificate_node = x509_certificate_nodes[0]
856-
x509_cert_value = x509_certificate_node.text
857-
x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg)
858-
if fingerprint == x509_fingerprint_value:
859-
cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
860-
861-
if cert is None or cert == '':
862-
return False
873+
if (cert is None or cert == '') and fingerprint:
874+
x509_certificate_nodes = OneLogin_Saml2_XML.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate')
875+
if len(x509_certificate_nodes) > 0:
876+
x509_certificate_node = x509_certificate_nodes[0]
877+
x509_cert_value = x509_certificate_node.text
878+
x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg)
879+
if fingerprint == x509_fingerprint_value:
880+
cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
863881

864-
# Check if Reference URI is empty
865-
reference_elem = OneLogin_Saml2_XML.query(signature_node, '//ds:Reference')
866-
if len(reference_elem) > 0:
867-
if reference_elem[0].get('URI') == '':
868-
reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
882+
if cert is None or cert == '':
883+
raise Exception('Could not validate node signature: No certificate provided.')
869884

870-
if validatecert:
871-
manager = xmlsec.KeysManager()
872-
manager.load_cert_from_memory(cert, xmlsec.KeyFormat.CERT_PEM, xmlsec.KeyDataType.TRUSTED)
873-
dsig_ctx = xmlsec.SignatureContext(manager)
874-
else:
875-
dsig_ctx = xmlsec.SignatureContext()
876-
dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None)
885+
# Check if Reference URI is empty
886+
reference_elem = OneLogin_Saml2_XML.query(signature_node, '//ds:Reference')
887+
if len(reference_elem) > 0:
888+
if reference_elem[0].get('URI') == '':
889+
reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
890+
891+
if validatecert:
892+
manager = xmlsec.KeysManager()
893+
manager.load_cert_from_memory(cert, xmlsec.KeyFormat.CERT_PEM, xmlsec.KeyDataType.TRUSTED)
894+
dsig_ctx = xmlsec.SignatureContext(manager)
895+
else:
896+
dsig_ctx = xmlsec.SignatureContext()
897+
dsig_ctx.key = xmlsec.Key.from_memory(cert, xmlsec.KeyFormat.CERT_PEM, None)
877898

878-
dsig_ctx.set_enabled_key_data([xmlsec.KeyData.X509])
899+
dsig_ctx.set_enabled_key_data([xmlsec.KeyData.X509])
900+
try:
879901
dsig_ctx.verify(signature_node)
880-
return True
881-
except xmlsec.Error as e:
882-
if debug:
883-
print(e)
902+
except Exception:
903+
raise Exception('Signature validation failed. SAML Response rejected')
904+
return True
884905

885906
@staticmethod
886907
def sign_binary(msg, key, algorithm=xmlsec.Transform.RSA_SHA1, debug=False):

tests/src/OneLogin/saml2_tests/logout_request_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def testIsInvalidNotOnOrAfter(self):
285285

286286
settings.set_strict(True)
287287
logout_request2 = OneLogin_Saml2_Logout_Request(settings, OneLogin_Saml2_Utils.b64encode(request))
288-
with self.assertRaisesRegexp(Exception, 'Timing issues \(please check your clock settings\)'):
288+
with self.assertRaisesRegexp(Exception, 'Could not validate timestamp: expired. Check system clock.'):
289289
logout_request2.is_valid(request_data, raise_exceptions=True)
290290

291291
def testIsValid(self):

tests/src/OneLogin/saml2_tests/response_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ def testIsInValidExpired(self):
533533

534534
settings.set_strict(True)
535535
response_2 = OneLogin_Saml2_Response(settings, xml)
536-
with self.assertRaisesRegexp(Exception, 'Timing issues \(please check your clock settings\)'):
536+
with self.assertRaisesRegexp(Exception, 'Could not validate timestamp: expired. Check system clock.'):
537537
response_2.is_valid(self.get_request_data(), raise_exceptions=True)
538538

539539
def testIsInValidNoStatement(self):
@@ -1043,7 +1043,7 @@ def testIsInValidCert(self):
10431043
xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
10441044
response = OneLogin_Saml2_Response(settings, xml)
10451045

1046-
with self.assertRaisesRegexp(Exception, 'failed to load key'):
1046+
with self.assertRaisesRegexp(Exception, 'Signature validation failed. SAML Response rejected'):
10471047
response.is_valid(self.get_request_data(), raise_exceptions=True)
10481048

10491049
def testIsInValidCert2(self):

0 commit comments

Comments
 (0)