Skip to content

Commit 9ea32f5

Browse files
committed
Implement a more specific exception class for handling some validation errors. Improve tests
1 parent b144b06 commit 9ea32f5

11 files changed

Lines changed: 384 additions & 132 deletions

File tree

src/onelogin/saml2/auth.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@
1616
from onelogin.saml2 import compat
1717
from onelogin.saml2.settings import OneLogin_Saml2_Settings
1818
from onelogin.saml2.response import OneLogin_Saml2_Response
19-
from onelogin.saml2.errors import OneLogin_Saml2_Error
2019
from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response
2120
from onelogin.saml2.constants import OneLogin_Saml2_Constants
22-
from onelogin.saml2.utils import OneLogin_Saml2_Utils
21+
from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
2322
from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
2423
from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request
2524

@@ -429,7 +428,7 @@ def __build_signature(self, data, saml_type, sign_algorithm=OneLogin_Saml2_Const
429428
if not key:
430429
raise OneLogin_Saml2_Error(
431430
"Trying to sign the %s but can't load the SP private key." % saml_type,
432-
OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND
431+
OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND
433432
)
434433

435434
msg = self.__build_sign_query(data[saml_type],
@@ -472,7 +471,7 @@ def validate_response_signature(self, request_data):
472471

473472
return self.__validate_signature(request_data, 'SAMLResponse')
474473

475-
def __validate_signature(self, data, saml_type):
474+
def __validate_signature(self, data, saml_type, raise_exceptions=False):
476475
"""
477476
Validate Signature
478477
@@ -484,22 +483,30 @@ def __validate_signature(self, data, saml_type):
484483
485484
:param saml_type: The target URL the user should be redirected to
486485
:type saml_type: string SAMLRequest | SAMLResponse
487-
"""
488-
489-
signature = data.get('Signature', None)
490-
if signature is None:
491-
if self.__settings.is_strict() and self.__settings.get_security_data().get('wantMessagesSigned', False):
492-
self.__error_reason = 'The %s is not signed. Rejected.' % saml_type
493-
return False
494-
return True
495-
496-
x509cert = self.get_settings().get_idp_cert()
497-
498-
if x509cert is None:
499-
self.__errors.append("In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type)
500-
return False
501486
487+
:param raise_exceptions: Whether to return false on failure or raise an exception
488+
:type raise_exceptions: Boolean
489+
"""
502490
try:
491+
signature = data.get('Signature', None)
492+
if signature is None:
493+
if self.__settings.is_strict() and self.__settings.get_security_data().get('wantMessagesSigned', False):
494+
raise OneLogin_Saml2_ValidationError(
495+
'The %s is not signed. Rejected.' % saml_type,
496+
OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
497+
)
498+
return True
499+
500+
x509cert = self.get_settings().get_idp_cert()
501+
502+
if not x509cert:
503+
error_msg = "In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type
504+
self.__errors.append(error_msg)
505+
raise OneLogin_Saml2_Error(
506+
error_msg,
507+
OneLogin_Saml2_Error.CERT_NOT_FOUND
508+
)
509+
503510
sign_alg = data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1)
504511
if isinstance(sign_alg, bytes):
505512
sign_alg = sign_alg.decode('utf8')
@@ -520,8 +527,13 @@ def __validate_signature(self, data, saml_type):
520527
x509cert,
521528
sign_alg,
522529
self.__settings.is_debug_active()):
523-
raise Exception('Signature validation failed. %s rejected.' % saml_type)
530+
raise OneLogin_Saml2_ValidationError(
531+
'Signature validation failed. %s rejected.' % saml_type,
532+
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
533+
)
524534
return True
525535
except Exception as e:
526536
self.__error_reason = str(e)
537+
if raise_exceptions:
538+
raise e
527539
return False

src/onelogin/saml2/errors.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class OneLogin_Saml2_Error(Exception):
2525
SETTINGS_INVALID_SYNTAX = 1
2626
SETTINGS_INVALID = 2
2727
METADATA_SP_INVALID = 3
28-
SP_CERTS_NOT_FOUND = 4
28+
CERT_NOT_FOUND = 4
2929
REDIRECT_INVALID_URL = 5
3030
PUBLIC_CERT_FILE_NOT_FOUND = 6
3131
PRIVATE_KEY_FILE_NOT_FOUND = 7
@@ -34,6 +34,8 @@ class OneLogin_Saml2_Error(Exception):
3434
SAML_LOGOUTREQUEST_INVALID = 10
3535
SAML_LOGOUTRESPONSE_INVALID = 11
3636
SAML_SINGLE_LOGOUT_NOT_SUPPORTED = 12
37+
PRIVATE_KEY_NOT_FOUND = 13
38+
UNSUPPORTED_SETTINGS_OBJECT = 14
3739

3840
def __init__(self, message, code=0, errors=None):
3941
"""
@@ -50,3 +52,73 @@ def __init__(self, message, code=0, errors=None):
5052

5153
Exception.__init__(self, message)
5254
self.code = code
55+
56+
57+
class OneLogin_Saml2_ValidationError(Exception):
58+
"""
59+
This class implements another custom Exception handler, related
60+
to exceptions that happens during validation process.
61+
Defines custom error codes .
62+
"""
63+
64+
# Validation Errors
65+
UNSUPPORTED_SAML_VERSION = 0
66+
MISSING_ID = 1
67+
WRONG_NUMBER_OF_ASSERTIONS = 2
68+
MISSING_STATUS = 3
69+
MISSING_STATUS_CODE = 4
70+
STATUS_CODE_IS_NOT_SUCCESS = 5
71+
WRONG_SIGNED_ELEMENT = 6
72+
ID_NOT_FOUND_IN_SIGNED_ELEMENT = 7
73+
DUPLICATED_ID_IN_SIGNED_ELEMENTS = 8
74+
INVALID_SIGNED_ELEMENT = 9
75+
DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS = 10
76+
UNEXPECTED_SIGNED_ELEMENTS = 11
77+
WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE = 12
78+
WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION = 13
79+
INVALID_XML_FORMAT = 14
80+
WRONG_INRESPONSETO = 15
81+
NO_ENCRYPTED_ASSERTION = 16
82+
NO_ENCRYPTED_NAMEID = 17
83+
MISSING_CONDITIONS = 18
84+
ASSERTION_TOO_EARLY = 19
85+
ASSERTION_EXPIRED = 20
86+
WRONG_NUMBER_OF_AUTHSTATEMENTS = 21
87+
NO_ATTRIBUTESTATEMENT = 22
88+
ENCRYPTED_ATTRIBUTES = 23
89+
WRONG_DESTINATION = 24
90+
EMPTY_DESTINATION = 25
91+
WRONG_AUDIENCE = 26
92+
ISSUER_NOT_FOUND_IN_RESPONSE = 27
93+
ISSUER_NOT_FOUND_IN_ASSERTION = 28
94+
WRONG_ISSUER = 29
95+
SESSION_EXPIRED = 30
96+
WRONG_SUBJECTCONFIRMATION = 31
97+
NO_SIGNED_RESPONSE = 32
98+
NO_SIGNED_ASSERTION = 33
99+
NO_SIGNATURE_FOUND = 34
100+
KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35
101+
CHILDREN_NODE_NOT_FOIND_IN_KEYINFO = 36
102+
UNSUPPORTED_RETRIEVAL_METHOD = 37
103+
NO_NAMEID = 38
104+
EMPTY_NAMEID = 39
105+
SP_NAME_QUALIFIER_NAME_MISMATCH = 40
106+
DUPLICATED_ATTRIBUTE_NAME_FOUND = 41
107+
INVALID_SIGNATURE = 42
108+
WRONG_NUMBER_OF_SIGNATURES = 43
109+
RESPONSE_EXPIRED = 44
110+
111+
def __init__(self, message, code=0, errors=None):
112+
"""
113+
Initializes the Exception instance.
114+
Arguments are:
115+
* (str) message. Describes the error.
116+
* (int) code. The code error (defined in the error class).
117+
"""
118+
assert isinstance(code, int)
119+
120+
if errors is not None:
121+
message = message % errors
122+
123+
Exception.__init__(self, message)
124+
self.code = code

src/onelogin/saml2/logout_request.py

Lines changed: 29 additions & 9 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, return_false_on_exception
13+
from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
1414
from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates
1515
from onelogin.saml2.xml_utils import OneLogin_Saml2_XML
1616

@@ -141,7 +141,10 @@ def get_nameid_data(request, key=None):
141141

142142
if len(encrypted_entries) == 1:
143143
if key is None:
144-
raise Exception('Key is required in order to decrypt the NameID')
144+
raise OneLogin_Saml2_Error(
145+
'Private Key is required in order to decrypt the NameID, check settings',
146+
OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND
147+
)
145148

146149
encrypted_data_nodes = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData')
147150
if len(encrypted_data_nodes) == 1:
@@ -153,7 +156,10 @@ def get_nameid_data(request, key=None):
153156
name_id = entries[0]
154157

155158
if name_id is None:
156-
raise Exception('Not NameID found in the Logout Request')
159+
raise OneLogin_Saml2_ValidationError(
160+
'Not NameID found in the Logout Request',
161+
OneLogin_Saml2_ValidationError.NO_NAMEID
162+
)
157163

158164
name_id_data = {
159165
'Value': name_id.text
@@ -236,7 +242,10 @@ def is_valid(self, request_data, raise_exceptions=False):
236242
if self.__settings.is_strict():
237243
res = OneLogin_Saml2_XML.validate_xml(root, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
238244
if isinstance(res, str):
239-
raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd')
245+
raise OneLogin_Saml2_ValidationError(
246+
'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd',
247+
OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
248+
)
240249

241250
security = self.__settings.get_security_data()
242251

@@ -246,30 +255,41 @@ def is_valid(self, request_data, raise_exceptions=False):
246255
if root.get('NotOnOrAfter', None):
247256
na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.get('NotOnOrAfter'))
248257
if na <= OneLogin_Saml2_Utils.now():
249-
raise Exception('Could not validate timestamp: expired. Check system clock.)')
258+
raise OneLogin_Saml2_ValidationError(
259+
'Could not validate timestamp: expired. Check system clock.)',
260+
OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED
261+
)
250262

251263
# Check destination
252264
if root.get('Destination', None):
253265
destination = root.get('Destination')
254266
if destination != '':
255267
if current_url not in destination:
256-
raise Exception(
268+
raise OneLogin_Saml2_ValidationError(
257269
'The LogoutRequest was received at '
258270
'%(currentURL)s instead of %(destination)s' %
259271
{
260272
'currentURL': current_url,
261273
'destination': destination,
262-
}
274+
},
275+
OneLogin_Saml2_ValidationError.WRONG_DESTINATION
263276
)
264277

265278
# Check issuer
266279
issuer = OneLogin_Saml2_Logout_Request.get_issuer(root)
267280
if issuer is not None and issuer != idp_entity_id:
268-
raise Exception('Invalid issuer in the Logout Request')
281+
raise OneLogin_Saml2_ValidationError(
282+
'Invalid issuer in the Logout Request',
283+
OneLogin_Saml2_ValidationError.WRONG_ISSUER
284+
)
269285

270286
if security['wantMessagesSigned']:
271287
if 'Signature' not in get_data:
272-
raise Exception('The Message of the Logout Request is not signed and the SP require it')
288+
raise OneLogin_Saml2_ValidationError(
289+
'The Message of the Logout Request is not signed and the SP require it',
290+
OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
291+
)
292+
273293
return True
274294
except Exception as err:
275295
# pylint: disable=R0801

src/onelogin/saml2/logout_response.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
1010
"""
1111

12-
from onelogin.saml2.utils import OneLogin_Saml2_Utils
12+
from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_ValidationError
1313
from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates
1414
from onelogin.saml2.xml_utils import OneLogin_Saml2_XML
1515

@@ -84,30 +84,45 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
8484
if self.__settings.is_strict():
8585
res = OneLogin_Saml2_XML.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
8686
if isinstance(res, str):
87-
raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd')
87+
raise OneLogin_Saml2_ValidationError(
88+
'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd',
89+
OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
90+
)
8891

8992
security = self.__settings.get_security_data()
9093

9194
# Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided
9295
in_response_to = self.document.get('InResponseTo', None)
9396
if request_id is not None and in_response_to and in_response_to != request_id:
94-
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))
97+
raise OneLogin_Saml2_ValidationError(
98+
'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),
99+
OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
100+
)
95101

96102
# Check issuer
97103
issuer = self.get_issuer()
98104
if issuer is not None and issuer != idp_entity_id:
99-
raise Exception('Invalid issuer in the Logout Request')
105+
raise OneLogin_Saml2_ValidationError(
106+
'Invalid issuer in the Logout Request',
107+
OneLogin_Saml2_ValidationError.WRONG_ISSUER
108+
)
100109

101110
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
102111

103112
# Check destination
104113
destination = self.document.get('Destination', None)
105114
if destination and current_url not in destination:
106-
raise Exception('The LogoutRequest was received at $currentURL instead of $destination')
115+
raise OneLogin_Saml2_ValidationError(
116+
'The LogoutResponse was received at %s instead of %s' % (current_url, destination),
117+
OneLogin_Saml2_ValidationError.WRONG_DESTINATION
118+
)
107119

108120
if security['wantMessagesSigned']:
109121
if 'Signature' not in get_data:
110-
raise Exception('The Message of the Logout Response is not signed and the SP require it')
122+
raise OneLogin_Saml2_ValidationError(
123+
'The Message of the Logout Response is not signed and the SP require it',
124+
OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
125+
)
111126
return True
112127
# pylint: disable=R0801
113128
except Exception as err:

0 commit comments

Comments
 (0)