Skip to content

Commit ce0d716

Browse files
authored
Merge pull request #42 from onelogin/improve_debug
Improve debug
2 parents f011fda + c90a62d commit ce0d716

20 files changed

+996
-441
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
language: python
22
python:
33
- '2.7.6'
4-
# - '2.7.9'
4+
# - '2.7.9'
55
# - '2.7.12'
6-
- '3.3.4'
6+
# - '3.3.4'
77
# - '3.3.5'
88
# - '3.3.6'
99
- '3.4.3'

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,10 @@ The login method can recieve 3 more optional parameters:
523523
* is_passive When true the AuthNReuqest will set the Ispassive='true'
524524
* set_nameid_policy When true the AuthNReuqest will set a nameIdPolicy element.
525525

526+
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
527+
528+
auth.get_last_request_id()
529+
526530
#### The SP Endpoints ####
527531

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

713+
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
714+
715+
auth.get_last_request_id()
716+
709717
####Example of a view that initiates the SSO request and handles the response (is the acs target)####
710718

711719
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.
@@ -781,10 +789,13 @@ Main class of OneLogin Python Toolkit
781789
* ***get_last_error_reason*** Returns the reason of the last error
782790
* ***get_sso_url*** Gets the SSO url.
783791
* ***get_slo_url*** Gets the SLO url.
792+
* ***get_last_request_id*** The ID of the last Request SAML message generated (AuthNRequest, LogoutRequest).
784793
* ***build_request_signature*** Builds the Signature of the SAML Request.
785794
* ***build_response_signature*** Builds the Signature of the SAML Response.
786795
* ***get_settings*** Returns the settings info.
787796
* ***set_strict*** Set the strict mode active/disable.
797+
* ***get_last_request_xml*** Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest)
798+
* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse had an encrypted assertion, decrypts it.
788799

789800
####OneLogin_Saml2_Auth - authn_request.py####
790801

@@ -793,7 +804,7 @@ SAML 2 Authentication Request class
793804
* `__init__` This class handles an AuthNRequest. It builds an AuthNRequest object.
794805
* ***get_request*** Returns unsigned AuthnRequest.
795806
* ***get_id*** Returns the AuthNRequest ID.
796-
807+
* ***get_xml*** Returns the XML that will be sent as part of the request.
797808

798809
####OneLogin_Saml2_Response - response.py####
799810

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

816828
####OneLogin_Saml2_LogoutRequest - logout_request.py####
817829

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

830843
####OneLogin_Saml2_LogoutResponse - logout_response.py####
831844

@@ -838,7 +851,7 @@ SAML 2 Logout Response class
838851
* ***build*** Creates a Logout Response object.
839852
* ***get_response*** Returns a Logout Response object.
840853
* ***get_error*** After execute a validation process, if fails this method returns the cause.
841-
854+
* ***get_xml*** Returns the XML that will be sent as part of the response or that was received at the SP
842855

843856
####OneLogin_Saml2_Settings - settings.py####
844857

src/onelogin/saml2/auth.py

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
"""
1313

1414
import xmlsec
15+
from lxml import etree
1516

1617
from onelogin.saml2 import compat
1718
from onelogin.saml2.settings import OneLogin_Saml2_Settings
1819
from onelogin.saml2.response import OneLogin_Saml2_Response
19-
from onelogin.saml2.errors import OneLogin_Saml2_Error
2020
from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response
2121
from onelogin.saml2.constants import OneLogin_Saml2_Constants
22-
from onelogin.saml2.utils import OneLogin_Saml2_Utils
22+
from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
2323
from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
2424
from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request
2525

@@ -59,6 +59,9 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
5959
self.__authenticated = False
6060
self.__errors = []
6161
self.__error_reason = None
62+
self.__last_request_id = None
63+
self.__last_request = None
64+
self.__last_response = None
6265

6366
def get_settings(self):
6467
"""
@@ -92,6 +95,7 @@ def process_response(self, request_id=None):
9295
if 'post_data' in self.__request_data and 'SAMLResponse' in self.__request_data['post_data']:
9396
# AuthnResponse -- HTTP_POST Binding
9497
response = OneLogin_Saml2_Response(self.__settings, self.__request_data['post_data']['SAMLResponse'])
98+
self.__last_response = response.get_xml_document()
9599

96100
if response.is_valid(self.__request_data, request_id):
97101
self.__attributes = response.get_attributes()
@@ -128,6 +132,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
128132
get_data = 'get_data' in self.__request_data and self.__request_data['get_data']
129133
if get_data and 'SAMLResponse' in get_data:
130134
logout_response = OneLogin_Saml2_Logout_Response(self.__settings, get_data['SAMLResponse'])
135+
self.__last_response = logout_response.get_xml()
131136
if not self.validate_response_signature(get_data):
132137
self.__errors.append('invalid_logout_response_signature')
133138
self.__errors.append('Signature validation failed. Logout Response rejected')
@@ -141,6 +146,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
141146

142147
elif get_data and 'SAMLRequest' in get_data:
143148
logout_request = OneLogin_Saml2_Logout_Request(self.__settings, get_data['SAMLRequest'])
149+
self.__last_request = logout_request.get_xml()
144150
if not self.validate_request_signature(get_data):
145151
self.__errors.append("invalid_logout_request_signature")
146152
self.__errors.append('Signature validation failed. Logout Request rejected')
@@ -154,6 +160,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
154160
in_response_to = logout_request.id
155161
response_builder = OneLogin_Saml2_Logout_Response(self.__settings)
156162
response_builder.build(in_response_to)
163+
self.__last_response = response_builder.get_xml()
157164
logout_response = response_builder.get_response()
158165

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

271+
def get_last_request_id(self):
272+
"""
273+
:returns: The ID of the last Request SAML message generated.
274+
:rtype: string
275+
"""
276+
return self.__last_request_id
277+
264278
def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True):
265279
"""
266280
Initiates the SSO process.
@@ -281,6 +295,8 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_
281295
:rtype: string
282296
"""
283297
authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy)
298+
self.__last_request = authn_request.get_xml()
299+
self.__last_request_id = authn_request.get_id()
284300

285301
saml_request = authn_request.get_request()
286302
parameters = {'SAMLRequest': saml_request}
@@ -329,6 +345,8 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None):
329345
session_index=session_index,
330346
nq=nq
331347
)
348+
self.__last_request = logout_request.get_xml()
349+
self.__last_request_id = logout_request.id
332350

333351
parameters = {'SAMLRequest': logout_request.get_request()}
334352
if return_to is not None:
@@ -429,7 +447,7 @@ def __build_signature(self, data, saml_type, sign_algorithm=OneLogin_Saml2_Const
429447
if not key:
430448
raise OneLogin_Saml2_Error(
431449
"Trying to sign the %s but can't load the SP private key." % saml_type,
432-
OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND
450+
OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND
433451
)
434452

435453
msg = self.__build_sign_query(data[saml_type],
@@ -472,7 +490,7 @@ def validate_response_signature(self, request_data):
472490

473491
return self.__validate_signature(request_data, 'SAMLResponse')
474492

475-
def __validate_signature(self, data, saml_type):
493+
def __validate_signature(self, data, saml_type, raise_exceptions=False):
476494
"""
477495
Validate Signature
478496
@@ -484,22 +502,30 @@ def __validate_signature(self, data, saml_type):
484502
485503
:param saml_type: The target URL the user should be redirected to
486504
: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
501505
506+
:param raise_exceptions: Whether to return false on failure or raise an exception
507+
:type raise_exceptions: Boolean
508+
"""
502509
try:
510+
signature = data.get('Signature', None)
511+
if signature is None:
512+
if self.__settings.is_strict() and self.__settings.get_security_data().get('wantMessagesSigned', False):
513+
raise OneLogin_Saml2_ValidationError(
514+
'The %s is not signed. Rejected.' % saml_type,
515+
OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE
516+
)
517+
return True
518+
519+
x509cert = self.get_settings().get_idp_cert()
520+
521+
if not x509cert:
522+
error_msg = "In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type
523+
self.__errors.append(error_msg)
524+
raise OneLogin_Saml2_Error(
525+
error_msg,
526+
OneLogin_Saml2_Error.CERT_NOT_FOUND
527+
)
528+
503529
sign_alg = data.get('SigAlg', OneLogin_Saml2_Constants.RSA_SHA1)
504530
if isinstance(sign_alg, bytes):
505531
sign_alg = sign_alg.decode('utf8')
@@ -520,8 +546,36 @@ def __validate_signature(self, data, saml_type):
520546
x509cert,
521547
sign_alg,
522548
self.__settings.is_debug_active()):
523-
raise Exception('Signature validation failed. %s rejected.' % saml_type)
549+
raise OneLogin_Saml2_ValidationError(
550+
'Signature validation failed. %s rejected.' % saml_type,
551+
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
552+
)
524553
return True
525554
except Exception as e:
526555
self.__error_reason = str(e)
556+
if raise_exceptions:
557+
raise e
527558
return False
559+
560+
def get_last_response_xml(self, pretty_print_if_possible=False):
561+
"""
562+
Retrieves the raw XML (decrypted) of the last SAML response,
563+
or the last Logout Response generated or processed
564+
:returns: SAML response XML
565+
:rtype: string|None
566+
"""
567+
response = None
568+
if self.__last_response is not None:
569+
if isinstance(self.__last_response, basestring):
570+
response = self.__last_response
571+
else:
572+
response = etree.tostring(self.__last_response, pretty_print=pretty_print_if_possible)
573+
return response
574+
575+
def get_last_request_xml(self):
576+
"""
577+
Retrieves the raw XML sent in the last SAML request
578+
:returns: SAML request XML
579+
:rtype: string|None
580+
"""
581+
return self.__last_request or None

src/onelogin/saml2/authn_request.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,11 @@ def get_id(self):
140140
:rtype: string
141141
"""
142142
return self.__id
143+
144+
def get_xml(self):
145+
"""
146+
Returns the XML that will be sent as part of the request
147+
:return: XML request body
148+
:rtype: string
149+
"""
150+
return self.__authn_request

src/onelogin/saml2/errors.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ class OneLogin_Saml2_Error(Exception):
2525
SETTINGS_INVALID_SYNTAX = 1
2626
SETTINGS_INVALID = 2
2727
METADATA_SP_INVALID = 3
28+
# SP_CERTS_NOT_FOUND is deprecated, use CERT_NOT_FOUND instead
2829
SP_CERTS_NOT_FOUND = 4
30+
CERT_NOT_FOUND = 4
2931
REDIRECT_INVALID_URL = 5
3032
PUBLIC_CERT_FILE_NOT_FOUND = 6
3133
PRIVATE_KEY_FILE_NOT_FOUND = 7
@@ -34,6 +36,8 @@ class OneLogin_Saml2_Error(Exception):
3436
SAML_LOGOUTREQUEST_INVALID = 10
3537
SAML_LOGOUTRESPONSE_INVALID = 11
3638
SAML_SINGLE_LOGOUT_NOT_SUPPORTED = 12
39+
PRIVATE_KEY_NOT_FOUND = 13
40+
UNSUPPORTED_SETTINGS_OBJECT = 14
3741

3842
def __init__(self, message, code=0, errors=None):
3943
"""
@@ -50,3 +54,73 @@ def __init__(self, message, code=0, errors=None):
5054

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

0 commit comments

Comments
 (0)