Skip to content

Commit

Permalink
Merge pull request #175 from onelogin/improve_debug
Browse files Browse the repository at this point in the history
Improve debug
  • Loading branch information
pitbulk committed Jan 4, 2017
2 parents d31952f + 74305d5 commit f947feb
Show file tree
Hide file tree
Showing 15 changed files with 837 additions and 721 deletions.
7 changes: 5 additions & 2 deletions README.md
Expand Up @@ -833,7 +833,7 @@ Main class of OneLogin Python Toolkit
* ***get_settings*** Returns the settings info.
* ***set_strict*** Set the strict mode active/disable.
* ***get_last_request_xml*** Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest)
* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse was encrypted, by default tries to return the decrypted XML.
* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse had an encrypted assertion, decrypts it.

####OneLogin_Saml2_Auth - authn_request.py####

Expand All @@ -842,7 +842,7 @@ SAML 2 Authentication Request class
* `__init__` This class handles an AuthNRequest. It builds an AuthNRequest object.
* ***get_request*** Returns unsigned AuthnRequest.
* ***get_id*** Returns the AuthNRequest ID.

* ***get_xml*** Returns the XML that will be sent as part of the request.

####OneLogin_Saml2_Response - response.py####

Expand All @@ -861,6 +861,7 @@ SAML 2 Authentication Response class
* ***validate_num_assertions*** Verifies that the document only contains a single Assertion (encrypted or not)
* ***validate_timestamps*** Verifies that the document is valid according to Conditions Element
* ***get_error*** After execute a validation process, if fails this method returns the cause
* ***get_xml_document*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it).

####OneLogin_Saml2_LogoutRequest - logout_request.py####

Expand All @@ -875,6 +876,7 @@ SAML 2 Logout Request class
* ***get_session_indexes*** Gets the SessionIndexes from the Logout Request.
* ***is_valid*** Checks if the Logout Request recieved is valid.
* ***get_error*** After execute a validation process, if fails this method returns the cause.
* ***get_xml*** Returns the XML that will be sent as part of the request or that was received at the SP

####OneLogin_Saml2_LogoutResponse - logout_response.py####

Expand All @@ -887,6 +889,7 @@ SAML 2 Logout Response class
* ***build*** Creates a Logout Response object.
* ***get_response*** Returns a Logout Response object.
* ***get_error*** After execute a validation process, if fails this method returns the cause.
* ***get_xml*** Returns the XML that will be sent as part of the response or that was received at the SP


####OneLogin_Saml2_Settings - settings.py####
Expand Down
2 changes: 1 addition & 1 deletion src/onelogin/saml2/auth.py
Expand Up @@ -432,7 +432,7 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On
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
)

dsig_ctx = xmlsec.DSigCtx()
Expand Down
78 changes: 78 additions & 0 deletions src/onelogin/saml2/errors.py
Expand Up @@ -25,7 +25,9 @@ class OneLogin_Saml2_Error(Exception):
SETTINGS_INVALID_SYNTAX = 1
SETTINGS_INVALID = 2
METADATA_SP_INVALID = 3
# SP_CERTS_NOT_FOUND is deprecated, use CERT_NOT_FOUND instead
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 +36,82 @@ 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):
"""
Initializes the Exception instance.
Arguments are:
* (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:
message = message % errors

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_MESSAGE = 32
NO_SIGNED_ASSERTION = 33
NO_SIGNATURE_FOUND = 34
KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35
CHILDREN_NODE_NOT_FOUND_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):
"""
Expand Down
53 changes: 41 additions & 12 deletions src/onelogin/saml2/logout_request.py
Expand Up @@ -17,6 +17,7 @@

from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.utils import OneLogin_Saml2_Utils
from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError


class OneLogin_Saml2_Logout_Request(object):
Expand Down Expand Up @@ -179,7 +180,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_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData')
if len(encrypted_data_nodes) == 1:
Expand All @@ -191,7 +195,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(
'NameID not found in the Logout Request',
OneLogin_Saml2_ValidationError.NO_NAMEID
)

name_id_data = {
'Value': name_id.text
Expand Down Expand Up @@ -260,12 +267,13 @@ def get_session_indexes(request):
session_indexes.append(session_index_node.text)
return session_indexes

def is_valid(self, request_data):
def is_valid(self, request_data, raise_exceptions=False):
"""
Checks if the Logout Request received is valid
:param request_data: Request Data
:type request_data: dict
:param raise_exceptions: Whether to return false on failure or raise an exception
:type raise_exceptions: Boolean
:return: If the Logout Request is or not valid
:rtype: boolean
"""
Expand All @@ -288,7 +296,10 @@ def is_valid(self, request_data):
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):
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 @@ -298,7 +309,10 @@ def is_valid(self, request_data):
if dom.get('NotOnOrAfter', None):
na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.get('NotOnOrAfter'))
if na <= OneLogin_Saml2_Utils.now():
raise Exception('Timing issues (please check your clock settings)')
raise OneLogin_Saml2_ValidationError(
'Could not validate timestamp: expired. Check system clock.',
OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED
)

# Check destination
if dom.get('Destination', None):
Expand All @@ -311,17 +325,24 @@ def is_valid(self, request_data):
{
'currentURL': current_url,
'destination': destination,
}
},
OneLogin_Saml2_ValidationError.WRONG_DESTINATION
)

# Check issuer
issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom)
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_MESSAGE
)

if 'Signature' in get_data:
if 'SigAlg' not in get_data:
Expand All @@ -334,12 +355,18 @@ def is_valid(self, request_data):
signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))

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')
if 'x509cert' not in idp_data or not idp_data['x509cert']:
raise OneLogin_Saml2_Error(
'In order to validate the sign on the Logout Request, the x509cert of the IdP is required',
OneLogin_Saml2_Error.CERT_NOT_FOUND
)
cert = idp_data['x509cert']

if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
raise Exception('Signature validation failed. Logout Request rejected')
raise OneLogin_Saml2_ValidationError(
'Signature validation failed. Logout Request rejected',
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
)

return True
except Exception as err:
Expand All @@ -348,6 +375,8 @@ def is_valid(self, request_data):
debug = self.__settings.is_debug_active()
if debug:
print err.__str__()
if raise_exceptions:
raise err
return False

def get_error(self):
Expand Down
44 changes: 35 additions & 9 deletions src/onelogin/saml2/logout_response.py
Expand Up @@ -17,6 +17,7 @@

from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.utils import OneLogin_Saml2_Utils
from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError


class OneLogin_Saml2_Logout_Response(object):
Expand Down Expand Up @@ -68,11 +69,13 @@ def get_status(self):
status = entries[0].attrib['Value']
return status

def is_valid(self, request_data, request_id=None):
def is_valid(self, request_data, request_id=None, raise_exceptions=False):
"""
Determines if the SAML LogoutResponse is valid
:param request_id: The ID of the LogoutRequest sent by this SP to the IdP
:type request_id: string
:param raise_exceptions: Whether to return false on failure or raise an exception
:type raise_exceptions: Boolean
:return: Returns if the SAML LogoutResponse is or not valid
:rtype: boolean
"""
Expand All @@ -89,20 +92,29 @@ def is_valid(self, request_data, request_id=None):
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):
raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd')
raise OneLogin_Saml2_ValidationError(
'Invalid SAML Logout Response. 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
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))
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)

Expand All @@ -111,11 +123,17 @@ def is_valid(self, request_data, request_id=None):
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')
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_MESSAGE
)

if 'Signature' in get_data:
if 'SigAlg' not in get_data:
Expand All @@ -128,12 +146,18 @@ def is_valid(self, request_data, request_id=None):
signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))

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')
if 'x509cert' not in idp_data or not idp_data['x509cert']:
raise OneLogin_Saml2_Error(
'In order to validate the sign on the Logout Response, the x509cert of the IdP is required',
OneLogin_Saml2_Error.CERT_NOT_FOUND
)
cert = idp_data['x509cert']

if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
raise Exception('Signature validation failed. Logout Response rejected')
raise OneLogin_Saml2_ValidationError(
'Signature validation failed. Logout Response rejected',
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
)

return True
# pylint: disable=R0801
Expand All @@ -142,6 +166,8 @@ def is_valid(self, request_data, request_id=None):
debug = self.__settings.is_debug_active()
if debug:
print err.__str__()
if raise_exceptions:
raise err
return False

def __query(self, query):
Expand Down

0 comments on commit f947feb

Please sign in to comment.