Skip to content

Commit 50c3362

Browse files
committed
Refactor logout_request, now is_valid is not a static method (following the php-saml style). Added get_error methods at Logout Request and Logout Response. Refactor tests
1 parent b46f35a commit 50c3362

6 files changed

Lines changed: 214 additions & 135 deletions

File tree

src/onelogin/saml2/auth.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,14 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
4040
"""
4141
Initializes the SP SAML instance.
4242
43-
Arguments are:
44-
* (dict) old_settings. Setting data
43+
:param request_data: Request Data
44+
:type request_data: dict
45+
46+
:param settings: Optional. SAML Toolkit Settings
47+
:type settings: dict|object
48+
49+
:param custom_base_path: Optional. Path where are stored the settings file and the cert folder
50+
:type custom_base_path: string
4551
"""
4652
self.__request_data = request_data
4753
self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path)
@@ -124,14 +130,14 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
124130
OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)
125131

126132
elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']:
127-
request = OneLogin_Saml2_Utils.decode_base64_and_inflate(self.__request_data['get_data']['SAMLRequest'])
128-
if not OneLogin_Saml2_Logout_Request.is_valid(self.__settings, request, self.__request_data):
133+
logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest'])
134+
if not logout_request.is_valid(self.__request_data):
129135
self.__errors.append('invalid_logout_request')
130136
else:
131137
if not keep_local_session:
132138
OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)
133139

134-
in_response_to = OneLogin_Saml2_Logout_Request.get_id(request)
140+
in_response_to = OneLogin_Saml2_Logout_Request.get_id(OneLogin_Saml2_Utils.decode_base64_and_inflate(self.__request_data['get_data']['SAMLRequest']))
135141
response_builder = OneLogin_Saml2_Logout_Response(self.__settings)
136142
response_builder.build(in_response_to)
137143
logout_response = response_builder.get_response()

src/onelogin/saml2/logout_request.py

Lines changed: 107 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
1010
"""
1111

12+
from zlib import decompress
1213
from base64 import b64decode
14+
from lxml import etree
1315
from defusedxml.lxml import fromstring
1416
from urllib import quote_plus
1517
from xml.dom.minidom import Document
16-
from defusedxml.minidom import parseString
1718

1819
from onelogin.saml2.constants import OneLogin_Saml2_Constants
1920
from onelogin.saml2.utils import OneLogin_Saml2_Utils
@@ -28,51 +29,61 @@ class OneLogin_Saml2_Logout_Request(object):
2829
2930
"""
3031

31-
def __init__(self, settings):
32+
def __init__(self, settings, request=None):
3233
"""
3334
Constructs the Logout Request object.
3435
3536
Arguments are:
3637
* (OneLogin_Saml2_Settings) settings. Setting data
3738
"""
3839
self.__settings = settings
40+
self.__error = None
3941

40-
sp_data = self.__settings.get_sp_data()
41-
idp_data = self.__settings.get_idp_data()
42-
security = self.__settings.get_security_data()
43-
44-
uid = OneLogin_Saml2_Utils.generate_unique_id()
45-
name_id_value = OneLogin_Saml2_Utils.generate_unique_id()
46-
issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now())
47-
48-
cert = None
49-
if 'nameIdEncrypted' in security and security['nameIdEncrypted']:
50-
cert = idp_data['x509cert']
51-
52-
name_id = OneLogin_Saml2_Utils.generate_name_id(
53-
name_id_value,
54-
sp_data['entityId'],
55-
sp_data['NameIDFormat'],
56-
cert
57-
)
58-
59-
logout_request = """<samlp:LogoutRequest
60-
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
61-
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
62-
ID="%(id)s"
63-
Version="2.0"
64-
IssueInstant="%(issue_instant)s"
65-
Destination="%(single_logout_url)s">
66-
<saml:Issuer>%(entity_id)s</saml:Issuer>
67-
%(name_id)s
68-
</samlp:LogoutRequest>""" % \
69-
{
70-
'id': uid,
71-
'issue_instant': issue_instant,
72-
'single_logout_url': idp_data['singleLogoutService']['url'],
73-
'entity_id': sp_data['entityId'],
74-
'name_id': name_id,
75-
}
42+
if request is None:
43+
sp_data = self.__settings.get_sp_data()
44+
idp_data = self.__settings.get_idp_data()
45+
security = self.__settings.get_security_data()
46+
47+
uid = OneLogin_Saml2_Utils.generate_unique_id()
48+
name_id_value = OneLogin_Saml2_Utils.generate_unique_id()
49+
issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now())
50+
51+
cert = None
52+
if 'nameIdEncrypted' in security and security['nameIdEncrypted']:
53+
cert = idp_data['x509cert']
54+
55+
name_id = OneLogin_Saml2_Utils.generate_name_id(
56+
name_id_value,
57+
sp_data['entityId'],
58+
sp_data['NameIDFormat'],
59+
cert
60+
)
61+
62+
logout_request = """<samlp:LogoutRequest
63+
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
64+
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
65+
ID="%(id)s"
66+
Version="2.0"
67+
IssueInstant="%(issue_instant)s"
68+
Destination="%(single_logout_url)s">
69+
<saml:Issuer>%(entity_id)s</saml:Issuer>
70+
%(name_id)s
71+
</samlp:LogoutRequest>""" % \
72+
{
73+
'id': uid,
74+
'issue_instant': issue_instant,
75+
'single_logout_url': idp_data['singleLogoutService']['url'],
76+
'entity_id': sp_data['entityId'],
77+
'name_id': name_id,
78+
}
79+
else:
80+
decoded = b64decode(request)
81+
# We try to inflate
82+
try:
83+
inflated = decompress(decoded, -15)
84+
logout_request = inflated
85+
except Exception:
86+
logout_request = decoded
7687

7788
self.__logout_request = logout_request
7889

@@ -93,11 +104,13 @@ def get_id(request):
93104
:return: string ID
94105
:rtype: str object
95106
"""
96-
if isinstance(request, Document):
97-
dom = request
107+
if isinstance(request, etree._Element):
108+
elem = request
98109
else:
99-
dom = parseString(request)
100-
return dom.documentElement.getAttribute('ID')
110+
if isinstance(request, Document):
111+
request = request.toxml()
112+
elem = fromstring(request)
113+
return elem.get('ID', None)
101114

102115
@staticmethod
103116
def get_nameid_data(request, key=None):
@@ -110,23 +123,26 @@ def get_nameid_data(request, key=None):
110123
:return: Name ID Data (Value, Format, NameQualifier, SPNameQualifier)
111124
:rtype: dict
112125
"""
113-
if isinstance(request, Document):
114-
request = request.toxml()
115-
doc = fromstring(request)
126+
if isinstance(request, etree._Element):
127+
elem = request
128+
else:
129+
if isinstance(request, Document):
130+
request = request.toxml()
131+
elem = fromstring(request)
116132

117133
name_id = None
118-
encrypted_entries = OneLogin_Saml2_Utils.query(doc, '/samlp:LogoutRequest/saml:EncryptedID')
134+
encrypted_entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID')
119135

120136
if len(encrypted_entries) == 1:
121137
if key is None:
122138
raise Exception('Key is required in order to decrypt the NameID')
123139

124-
encrypted_data_nodes = OneLogin_Saml2_Utils.query(doc, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData')
140+
encrypted_data_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData')
125141
if len(encrypted_data_nodes) == 1:
126142
encrypted_data = encrypted_data_nodes[0]
127143
name_id = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key)
128144
else:
129-
entries = OneLogin_Saml2_Utils.query(doc, '/samlp:LogoutRequest/saml:NameID')
145+
entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:NameID')
130146
if len(entries) == 1:
131147
name_id = entries[0]
132148

@@ -165,12 +181,15 @@ def get_issuer(request):
165181
:return: The Issuer
166182
:rtype: string
167183
"""
168-
if isinstance(request, Document):
169-
request = request.toxml()
170-
dom = fromstring(request)
184+
if isinstance(request, etree._Element):
185+
elem = request
186+
else:
187+
if isinstance(request, Document):
188+
request = request.toxml()
189+
elem = fromstring(request)
171190

172191
issuer = None
173-
issuer_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:LogoutRequest/saml:Issuer')
192+
issuer_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:Issuer')
174193
if len(issuer_nodes) == 1:
175194
issuer = issuer_nodes[0].text
176195
return issuer
@@ -184,54 +203,58 @@ def get_session_indexes(request):
184203
:return: The SessionIndex value
185204
:rtype: list
186205
"""
187-
if isinstance(request, Document):
188-
request = request.toxml()
189-
dom = fromstring(request)
206+
if isinstance(request, etree._Element):
207+
elem = request
208+
else:
209+
if isinstance(request, Document):
210+
request = request.toxml()
211+
elem = fromstring(request)
190212

191213
session_indexes = []
192-
session_index_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:LogoutRequest/samlp:SessionIndex')
214+
session_index_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex')
193215
for session_index_node in session_index_nodes:
194216
session_indexes.append(session_index_node.text)
195217
return session_indexes
196218

197-
@staticmethod
198-
def is_valid(settings, request, get_data, debug=False):
219+
def is_valid(self, request_data):
199220
"""
200221
Checks if the Logout Request recieved is valid
201-
:param settings: Settings
202-
:type settings: OneLogin_Saml2_Settings
203-
:param request: Logout Request Message
204-
:type request: string|DOMDocument
222+
:param request_data: Request Data
223+
:type request_data: dict
224+
205225
:return: If the Logout Request is or not valid
206226
:rtype: boolean
207227
"""
228+
self.__error = None
208229
try:
209-
if isinstance(request, Document):
210-
dom = request
211-
else:
212-
dom = parseString(request)
230+
dom = fromstring(self.__logout_request)
213231

214-
idp_data = settings.get_idp_data()
232+
idp_data = self.__settings.get_idp_data()
215233
idp_entity_id = idp_data['entityId']
216234

217-
if settings.is_strict():
218-
res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', debug)
235+
if 'get_data' in request_data.keys():
236+
get_data = request_data['get_data']
237+
else:
238+
get_data = {}
239+
240+
if self.__settings.is_strict():
241+
res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
219242
if not isinstance(res, Document):
220243
raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd')
221244

222-
security = settings.get_security_data()
245+
security = self.__settings.get_security_data()
223246

224-
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(get_data)
247+
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
225248

226249
# Check NotOnOrAfter
227-
if dom.documentElement.hasAttribute('NotOnOrAfter'):
228-
na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.documentElement.getAttribute('NotOnOrAfter'))
250+
if dom.get('NotOnOrAfter', None):
251+
na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.get('NotOnOrAfter'))
229252
if na <= OneLogin_Saml2_Utils.now():
230253
raise Exception('Timing issues (please check your clock settings)')
231254

232255
# Check destination
233-
if dom.documentElement.hasAttribute('Destination'):
234-
destination = dom.documentElement.getAttribute('Destination')
256+
if dom.get('Destination', None):
257+
destination = dom.get('Destination')
235258
if destination != '':
236259
if current_url not in destination:
237260
raise Exception('The LogoutRequest was received at $currentURL instead of $destination')
@@ -268,7 +291,14 @@ def is_valid(settings, request, get_data, debug=False):
268291

269292
return True
270293
except Exception as err:
271-
debug = settings.is_debug_active()
294+
self.__error = err.__str__()
295+
debug = self.__settings.is_debug_active()
272296
if debug:
273-
print err
297+
print err.__str__()
274298
return False
299+
300+
def get_error(self):
301+
"""
302+
After execute a validation process, if fails this method returns the cause
303+
"""
304+
return self.__error

src/onelogin/saml2/logout_response.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def __init__(self, settings, response=None):
3939
response from the IdP.
4040
"""
4141
self.__settings = settings
42+
self.__error = None
43+
4244
if response is not None:
4345
self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response)
4446
self.document = parseString(self.__logout_response)
@@ -75,6 +77,7 @@ def is_valid(self, request_data, request_id=None):
7577
:return: Returns if the SAML LogoutResponse is or not valid
7678
:rtype: boolean
7779
"""
80+
self.__error = None
7881
try:
7982
idp_data = self.__settings.get_idp_data()
8083
idp_entity_id = idp_data['entityId']
@@ -134,9 +137,10 @@ def is_valid(self, request_data, request_id=None):
134137

135138
return True
136139
except Exception as err:
140+
self.__error = err.__str__()
137141
debug = self.__settings.is_debug_active()
138142
if debug:
139-
print err
143+
print err.__str__()
140144
return False
141145

142146
def __query(self, query):
@@ -193,3 +197,9 @@ def get_response(self):
193197
:rtype: string
194198
"""
195199
return OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_response)
200+
201+
def get_error(self):
202+
"""
203+
After execute a validation process, if fails this method returns the cause
204+
"""
205+
return self.__error

src/onelogin/saml2/response.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ def __init__(self, settings, response):
5353

5454
def is_valid(self, request_data, request_id=None):
5555
"""
56-
Constructs the response object.
56+
Validates the response object.
57+
58+
:param request_data: Request Data
59+
:type request_data: dict
5760
5861
:param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP
5962
:type request_id: string

src/onelogin/saml2/utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from hashlib import sha1
1616
from isodate import parse_duration as duration_parser
1717
from lxml import etree
18-
from defusedxml.lxml import fromstring
18+
from defusedxml.lxml import tostring, fromstring
1919
from os.path import basename, dirname, join
2020
import re
2121
from sys import stderr
@@ -97,11 +97,13 @@ def validate_xml(xml, schema, debug=False):
9797
:returns: Error code or the DomDocument of the xml
9898
:rtype: string
9999
"""
100-
assert isinstance(xml, basestring) or isinstance(xml, Document)
100+
assert isinstance(xml, basestring) or isinstance(xml, Document) or isinstance(xml, etree._Element)
101101
assert isinstance(schema, basestring)
102102

103103
if isinstance(xml, Document):
104104
xml = xml.toxml()
105+
elif isinstance(xml, etree._Element):
106+
xml = tostring(xml)
105107

106108
# Switch to lxml for schema validation
107109
try:

0 commit comments

Comments
 (0)