Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve debug #175

Merged
merged 11 commits into from Jan 4, 2017
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