Skip to content

Commit

Permalink
Implement a more specific exception class for handling some validatio…
Browse files Browse the repository at this point in the history
…n errors. Improve tests
  • Loading branch information
pitbulk committed Dec 29, 2016
1 parent b144b06 commit 9ea32f5
Show file tree
Hide file tree
Showing 11 changed files with 384 additions and 132 deletions.
50 changes: 31 additions & 19 deletions src/onelogin/saml2/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@
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
from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response
from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.utils import OneLogin_Saml2_Utils
from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request

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

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

return self.__validate_signature(request_data, 'SAMLResponse')

def __validate_signature(self, data, saml_type):
def __validate_signature(self, data, saml_type, raise_exceptions=False):
"""
Validate Signature
Expand All @@ -484,22 +483,30 @@ def __validate_signature(self, data, saml_type):
:param saml_type: The target URL the user should be redirected to
:type saml_type: string SAMLRequest | SAMLResponse
"""

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
:param raise_exceptions: Whether to return false on failure or raise an exception
:type raise_exceptions: Boolean
"""
try:
signature = data.get('Signature', None)
if signature is None:
if self.__settings.is_strict() and self.__settings.get_security_data().get('wantMessagesSigned', False):
raise OneLogin_Saml2_ValidationError(
'The %s is not signed. Rejected.' % saml_type,
OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
)
return True

x509cert = self.get_settings().get_idp_cert()

if not x509cert:
error_msg = "In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type
self.__errors.append(error_msg)
raise OneLogin_Saml2_Error(
error_msg,
OneLogin_Saml2_Error.CERT_NOT_FOUND
)

sign_alg = data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1)
if isinstance(sign_alg, bytes):
sign_alg = sign_alg.decode('utf8')
Expand All @@ -520,8 +527,13 @@ def __validate_signature(self, data, saml_type):
x509cert,
sign_alg,
self.__settings.is_debug_active()):
raise Exception('Signature validation failed. %s rejected.' % saml_type)
raise OneLogin_Saml2_ValidationError(
'Signature validation failed. %s rejected.' % saml_type,
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
)
return True
except Exception as e:
self.__error_reason = str(e)
if raise_exceptions:
raise e
return False
74 changes: 73 additions & 1 deletion src/onelogin/saml2/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class OneLogin_Saml2_Error(Exception):
SETTINGS_INVALID_SYNTAX = 1
SETTINGS_INVALID = 2
METADATA_SP_INVALID = 3
SP_CERTS_NOT_FOUND = 4
CERT_NOT_FOUND = 4
REDIRECT_INVALID_URL = 5
PUBLIC_CERT_FILE_NOT_FOUND = 6
PRIVATE_KEY_FILE_NOT_FOUND = 7
Expand All @@ -34,6 +34,8 @@ class OneLogin_Saml2_Error(Exception):
SAML_LOGOUTREQUEST_INVALID = 10
SAML_LOGOUTRESPONSE_INVALID = 11
SAML_SINGLE_LOGOUT_NOT_SUPPORTED = 12
PRIVATE_KEY_NOT_FOUND = 13
UNSUPPORTED_SETTINGS_OBJECT = 14

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

Exception.__init__(self, message)
self.code = code


class OneLogin_Saml2_ValidationError(Exception):
"""
This class implements another custom Exception handler, related
to exceptions that happens during validation process.
Defines custom error codes .
"""

# Validation Errors
UNSUPPORTED_SAML_VERSION = 0
MISSING_ID = 1
WRONG_NUMBER_OF_ASSERTIONS = 2
MISSING_STATUS = 3
MISSING_STATUS_CODE = 4
STATUS_CODE_IS_NOT_SUCCESS = 5
WRONG_SIGNED_ELEMENT = 6
ID_NOT_FOUND_IN_SIGNED_ELEMENT = 7
DUPLICATED_ID_IN_SIGNED_ELEMENTS = 8
INVALID_SIGNED_ELEMENT = 9
DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS = 10
UNEXPECTED_SIGNED_ELEMENTS = 11
WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE = 12
WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION = 13
INVALID_XML_FORMAT = 14
WRONG_INRESPONSETO = 15
NO_ENCRYPTED_ASSERTION = 16
NO_ENCRYPTED_NAMEID = 17
MISSING_CONDITIONS = 18
ASSERTION_TOO_EARLY = 19
ASSERTION_EXPIRED = 20
WRONG_NUMBER_OF_AUTHSTATEMENTS = 21
NO_ATTRIBUTESTATEMENT = 22
ENCRYPTED_ATTRIBUTES = 23
WRONG_DESTINATION = 24
EMPTY_DESTINATION = 25
WRONG_AUDIENCE = 26
ISSUER_NOT_FOUND_IN_RESPONSE = 27
ISSUER_NOT_FOUND_IN_ASSERTION = 28
WRONG_ISSUER = 29
SESSION_EXPIRED = 30
WRONG_SUBJECTCONFIRMATION = 31
NO_SIGNED_RESPONSE = 32
NO_SIGNED_ASSERTION = 33
NO_SIGNATURE_FOUND = 34
KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35
CHILDREN_NODE_NOT_FOIND_IN_KEYINFO = 36
UNSUPPORTED_RETRIEVAL_METHOD = 37
NO_NAMEID = 38
EMPTY_NAMEID = 39
SP_NAME_QUALIFIER_NAME_MISMATCH = 40
DUPLICATED_ATTRIBUTE_NAME_FOUND = 41
INVALID_SIGNATURE = 42
WRONG_NUMBER_OF_SIGNATURES = 43
RESPONSE_EXPIRED = 44

def __init__(self, message, code=0, errors=None):
"""
Initializes the Exception instance.
Arguments are:
* (str) message. Describes the error.
* (int) code. The code error (defined in the error class).
"""
assert isinstance(code, int)

if errors is not None:
message = message % errors

Exception.__init__(self, message)
self.code = code
38 changes: 29 additions & 9 deletions src/onelogin/saml2/logout_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"""

from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception
from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates
from onelogin.saml2.xml_utils import OneLogin_Saml2_XML

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

if len(encrypted_entries) == 1:
if key is None:
raise Exception('Key is required in order to decrypt the NameID')
raise OneLogin_Saml2_Error(
'Private Key is required in order to decrypt the NameID, check settings',
OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND
)

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

if name_id is None:
raise Exception('Not NameID found in the Logout Request')
raise OneLogin_Saml2_ValidationError(
'Not NameID found in the Logout Request',
OneLogin_Saml2_ValidationError.NO_NAMEID
)

name_id_data = {
'Value': name_id.text
Expand Down Expand Up @@ -236,7 +242,10 @@ def is_valid(self, request_data, raise_exceptions=False):
if self.__settings.is_strict():
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')
raise OneLogin_Saml2_ValidationError(
'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd',
OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
)

security = self.__settings.get_security_data()

Expand All @@ -246,30 +255,41 @@ def is_valid(self, request_data, raise_exceptions=False):
if root.get('NotOnOrAfter', None):
na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.get('NotOnOrAfter'))
if na <= OneLogin_Saml2_Utils.now():
raise Exception('Could not validate timestamp: expired. Check system clock.)')
raise OneLogin_Saml2_ValidationError(
'Could not validate timestamp: expired. Check system clock.)',
OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED
)

# Check destination
if root.get('Destination', None):
destination = root.get('Destination')
if destination != '':
if current_url not in destination:
raise Exception(
raise OneLogin_Saml2_ValidationError(
'The LogoutRequest was received at '
'%(currentURL)s instead of %(destination)s' %
{
'currentURL': current_url,
'destination': destination,
}
},
OneLogin_Saml2_ValidationError.WRONG_DESTINATION
)

# Check issuer
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')
raise OneLogin_Saml2_ValidationError(
'Invalid issuer in the Logout Request',
OneLogin_Saml2_ValidationError.WRONG_ISSUER
)

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')
raise OneLogin_Saml2_ValidationError(
'The Message of the Logout Request is not signed and the SP require it',
OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
)

return True
except Exception as err:
# pylint: disable=R0801
Expand Down
27 changes: 21 additions & 6 deletions src/onelogin/saml2/logout_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"""

from onelogin.saml2.utils import OneLogin_Saml2_Utils
from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_ValidationError
from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates
from onelogin.saml2.xml_utils import OneLogin_Saml2_XML

Expand Down Expand Up @@ -84,30 +84,45 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
if self.__settings.is_strict():
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')
raise OneLogin_Saml2_ValidationError(
'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd',
OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
)

security = self.__settings.get_security_data()

# Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided
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))
raise OneLogin_Saml2_ValidationError(
'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),
OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
)

# Check issuer
issuer = self.get_issuer()
if issuer is not None and issuer != idp_entity_id:
raise Exception('Invalid issuer in the Logout Request')
raise OneLogin_Saml2_ValidationError(
'Invalid issuer in the Logout Request',
OneLogin_Saml2_ValidationError.WRONG_ISSUER
)

current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)

# Check 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')
raise OneLogin_Saml2_ValidationError(
'The LogoutResponse was received at %s instead of %s' % (current_url, destination),
OneLogin_Saml2_ValidationError.WRONG_DESTINATION
)

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')
raise OneLogin_Saml2_ValidationError(
'The Message of the Logout Response is not signed and the SP require it',
OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
)
return True
# pylint: disable=R0801
except Exception as err:
Expand Down
Loading

0 comments on commit 9ea32f5

Please sign in to comment.