Skip to content

Commit

Permalink
Merge pull request #42 from onelogin/improve_debug
Browse files Browse the repository at this point in the history
Improve debug
  • Loading branch information
pitbulk authored Jan 4, 2017
2 parents f011fda + c90a62d commit ce0d716
Show file tree
Hide file tree
Showing 20 changed files with 996 additions and 441 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
language: python
python:
- '2.7.6'
# - '2.7.9'
# - '2.7.9'
# - '2.7.12'
- '3.3.4'
# - '3.3.4'
# - '3.3.5'
# - '3.3.6'
- '3.4.3'
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,10 @@ The login method can recieve 3 more optional parameters:
* is_passive When true the AuthNReuqest will set the Ispassive='true'
* set_nameid_policy When true the AuthNReuqest will set a nameIdPolicy element.

If a match on the future SAMLResponse ID and the AuthNRequest ID to be sent is required, that AuthNRequest ID must to be extracted and stored for future validation, we can get that ID by

auth.get_last_request_id()

#### The SP Endpoints ####

Related to the SP there are 3 important endpoints: The metadata view, the ACS view and the SLS view.
Expand Down Expand Up @@ -706,6 +710,10 @@ Also there are 2 optional parameters that can be set:
SAML Response with a NameId, then this NameId will be used.
* session_index. SessionIndex that identifies the session of the user.

If a match on the LogoutResponse ID and the LogoutRequest ID to be sent is required, that LogoutRequest ID must to be extracted and stored for future validation, we can get that ID by

auth.get_last_request_id()

####Example of a view that initiates the SSO request and handles the response (is the acs target)####

We can code a unique file that initiates the SSO process, handle the response, get the attributes, initiate the slo and processes the logout response.
Expand Down Expand Up @@ -781,10 +789,13 @@ Main class of OneLogin Python Toolkit
* ***get_last_error_reason*** Returns the reason of the last error
* ***get_sso_url*** Gets the SSO url.
* ***get_slo_url*** Gets the SLO url.
* ***get_last_request_id*** The ID of the last Request SAML message generated (AuthNRequest, LogoutRequest).
* ***build_request_signature*** Builds the Signature of the SAML Request.
* ***build_response_signature*** Builds the Signature of the SAML Response.
* ***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 had an encrypted assertion, decrypts it.

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

Expand All @@ -793,7 +804,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 @@ -812,6 +823,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 @@ -826,6 +838,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 @@ -838,7 +851,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
92 changes: 73 additions & 19 deletions src/onelogin/saml2/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
"""

import xmlsec
from lxml import etree

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 @@ -59,6 +59,9 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
self.__authenticated = False
self.__errors = []
self.__error_reason = None
self.__last_request_id = None
self.__last_request = None
self.__last_response = None

def get_settings(self):
"""
Expand Down Expand Up @@ -92,6 +95,7 @@ def process_response(self, request_id=None):
if 'post_data' in self.__request_data and 'SAMLResponse' in self.__request_data['post_data']:
# AuthnResponse -- HTTP_POST Binding
response = OneLogin_Saml2_Response(self.__settings, self.__request_data['post_data']['SAMLResponse'])
self.__last_response = response.get_xml_document()

if response.is_valid(self.__request_data, request_id):
self.__attributes = response.get_attributes()
Expand Down Expand Up @@ -128,6 +132,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
get_data = 'get_data' in self.__request_data and self.__request_data['get_data']
if get_data and 'SAMLResponse' in get_data:
logout_response = OneLogin_Saml2_Logout_Response(self.__settings, get_data['SAMLResponse'])
self.__last_response = logout_response.get_xml()
if not self.validate_response_signature(get_data):
self.__errors.append('invalid_logout_response_signature')
self.__errors.append('Signature validation failed. Logout Response rejected')
Expand All @@ -141,6 +146,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_

elif get_data and 'SAMLRequest' in get_data:
logout_request = OneLogin_Saml2_Logout_Request(self.__settings, get_data['SAMLRequest'])
self.__last_request = logout_request.get_xml()
if not self.validate_request_signature(get_data):
self.__errors.append("invalid_logout_request_signature")
self.__errors.append('Signature validation failed. Logout Request rejected')
Expand All @@ -154,6 +160,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
in_response_to = logout_request.id
response_builder = OneLogin_Saml2_Logout_Response(self.__settings)
response_builder.build(in_response_to)
self.__last_response = response_builder.get_xml()
logout_response = response_builder.get_response()

parameters = {'SAMLResponse': logout_response}
Expand Down Expand Up @@ -261,6 +268,13 @@ def get_attribute(self, name):
assert isinstance(name, compat.str_type)
return self.__attributes.get(name)

def get_last_request_id(self):
"""
:returns: The ID of the last Request SAML message generated.
:rtype: string
"""
return self.__last_request_id

def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True):
"""
Initiates the SSO process.
Expand All @@ -281,6 +295,8 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_
:rtype: string
"""
authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy)
self.__last_request = authn_request.get_xml()
self.__last_request_id = authn_request.get_id()

saml_request = authn_request.get_request()
parameters = {'SAMLRequest': saml_request}
Expand Down Expand Up @@ -329,6 +345,8 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None):
session_index=session_index,
nq=nq
)
self.__last_request = logout_request.get_xml()
self.__last_request_id = logout_request.id

parameters = {'SAMLRequest': logout_request.get_request()}
if return_to is not None:
Expand Down Expand Up @@ -429,7 +447,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 +490,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 +502,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_MESSAGE
)
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 +546,36 @@ 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

def get_last_response_xml(self, pretty_print_if_possible=False):
"""
Retrieves the raw XML (decrypted) of the last SAML response,
or the last Logout Response generated or processed
:returns: SAML response XML
:rtype: string|None
"""
response = None
if self.__last_response is not None:
if isinstance(self.__last_response, basestring):
response = self.__last_response
else:
response = etree.tostring(self.__last_response, pretty_print=pretty_print_if_possible)
return response

def get_last_request_xml(self):
"""
Retrieves the raw XML sent in the last SAML request
:returns: SAML request XML
:rtype: string|None
"""
return self.__last_request or None
8 changes: 8 additions & 0 deletions src/onelogin/saml2/authn_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,11 @@ def get_id(self):
:rtype: string
"""
return self.__id

def get_xml(self):
"""
Returns the XML that will be sent as part of the request
:return: XML request body
:rtype: string
"""
return self.__authn_request
74 changes: 74 additions & 0 deletions src/onelogin/saml2/errors.py
Original file line number Diff line number Diff line change
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,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 +54,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_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):
"""
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
Loading

0 comments on commit ce0d716

Please sign in to comment.