Skip to content

Commit c45c0f8

Browse files
committed
Improve NameIdFormat support. Be able to provide a NameIDFormat to LogoutRequest
1 parent 476f32f commit c45c0f8

File tree

8 files changed

+242
-11
lines changed

8 files changed

+242
-11
lines changed

demo-flask/saml/advanced_settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"wantNameId" : true,
1111
"wantNameIdEncrypted": false,
1212
"wantAssertionsEncrypted": false,
13-
"signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
13+
"signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
14+
"digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1"
1415
},
1516
"contactPerson": {
1617
"technical": {

src/onelogin/saml2/auth.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
5454
self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path)
5555
self.__attributes = dict()
5656
self.__nameid = None
57+
self.__nameid_format = None
5758
self.__session_index = None
5859
self.__session_expiration = None
5960
self.__authenticated = False
@@ -100,6 +101,7 @@ def process_response(self, request_id=None):
100101
if response.is_valid(self.__request_data, request_id):
101102
self.__attributes = response.get_attributes()
102103
self.__nameid = response.get_nameid()
104+
self.__nameid_format = response.get_nameid_format()
103105
self.__session_index = response.get_session_index()
104106
self.__session_expiration = response.get_session_not_on_or_after()
105107
self.__authenticated = True
@@ -221,6 +223,15 @@ def get_nameid(self):
221223
"""
222224
return self.__nameid
223225

226+
def get_nameid_format(self):
227+
"""
228+
Returns the nameID Format.
229+
230+
:returns: NameID Format
231+
:rtype: string|None
232+
"""
233+
return self.__nameid_format
234+
224235
def get_session_index(self):
225236
"""
226237
Returns the SessionIndex from the AuthnStatement.
@@ -311,7 +322,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_
311322
self.add_request_signature(parameters, security['signatureAlgorithm'])
312323
return self.redirect_to(self.get_sso_url(), parameters)
313324

314-
def logout(self, return_to=None, name_id=None, session_index=None, nq=None):
325+
def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None):
315326
"""
316327
Initiates the SLO process.
317328
@@ -327,6 +338,9 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None):
327338
:param nq: IDP Name Qualifier
328339
:type: string
329340
341+
:param name_id_format: The NameID Format that will be set in the LogoutRequest.
342+
:type: string
343+
330344
:returns: Redirection URL
331345
"""
332346
slo_url = self.get_slo_url()
@@ -339,11 +353,15 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None):
339353
if name_id is None and self.__nameid is not None:
340354
name_id = self.__nameid
341355

356+
if name_id_format is None and self.__nameid_format is not None:
357+
name_id_format = self.__nameid_format
358+
342359
logout_request = OneLogin_Saml2_Logout_Request(
343360
self.__settings,
344361
name_id=name_id,
345362
session_index=session_index,
346-
nq=nq
363+
nq=nq,
364+
name_id_format=name_id_format
347365
)
348366
self.__last_request = logout_request.get_xml()
349367
self.__last_request_id = logout_request.id

src/onelogin/saml2/logout_request.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class OneLogin_Saml2_Logout_Request(object):
2525
2626
"""
2727

28-
def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None):
28+
def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None):
2929
"""
3030
Constructs the Logout Request object.
3131
@@ -43,6 +43,9 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
4343
4444
:param nq: IDP Name Qualifier
4545
:type: string
46+
47+
:param name_id_format: The NameID Format that will be set in the LogoutRequest.
48+
:type: string
4649
"""
4750
self.__settings = settings
4851
self.__error = None
@@ -63,7 +66,8 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
6366
cert = idp_data['x509cert']
6467

6568
if name_id is not None:
66-
name_id_format = sp_data['NameIDFormat']
69+
if name_id_format is None:
70+
name_id_format = sp_data['NameIDFormat']
6771
sp_name_qualifier = None
6872
else:
6973
name_id = idp_data['entityId']
@@ -75,7 +79,8 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
7579
sp_name_qualifier,
7680
name_id_format,
7781
cert,
78-
nq=nq,
82+
False,
83+
nq
7984
)
8085

8186
if session_index:
@@ -194,6 +199,23 @@ def get_nameid(request, key=None):
194199
name_id = OneLogin_Saml2_Logout_Request.get_nameid_data(request, key)
195200
return name_id['Value']
196201

202+
@staticmethod
203+
def get_nameid_format(request, key=None):
204+
"""
205+
Gets the NameID Format of the Logout Request Message
206+
:param request: Logout Request Message
207+
:type request: string|DOMDocument
208+
:param key: The SP key
209+
:type key: string
210+
:return: Name ID Format
211+
:rtype: string
212+
"""
213+
name_id_format = None
214+
name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(request, key)
215+
if name_id_data and 'Format' in name_id_data.keys():
216+
name_id_format = name_id_data['Format']
217+
return name_id_format
218+
197219
@staticmethod
198220
def get_issuer(request):
199221
"""

src/onelogin/saml2/response.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,19 @@ def get_nameid(self):
444444
nameid_value = nameid_data['Value']
445445
return nameid_value
446446

447+
def get_nameid_format(self):
448+
"""
449+
Gets the NameID Format provided by the SAML Response from the IdP
450+
451+
:returns: NameID Format
452+
:rtype: string|None
453+
"""
454+
nameid_format = None
455+
nameid_data = self.get_nameid_data()
456+
if nameid_data and 'Format' in nameid_data.keys():
457+
nameid_format = nameid_data['Format']
458+
return nameid_format
459+
447460
def get_session_not_on_or_after(self):
448461
"""
449462
Gets the SessionNotOnOrAfter from the AuthnStatement

src/onelogin/saml2/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,8 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None):
584584
name_id = OneLogin_Saml2_XML.make_child(root, '{%s}NameID' % OneLogin_Saml2_Constants.NS_SAML)
585585
if sp_nq is not None:
586586
name_id.set('SPNameQualifier', sp_nq)
587-
name_id.set('Format', sp_format)
587+
if sp_format is not None:
588+
name_id.set('Format', sp_format)
588589
if nq is not None:
589590
name_id.set('NameQualifier', nq)
590591
name_id.text = value

tests/src/OneLogin/saml2_tests/auth_test.py

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@
2424
class OneLogin_Saml2_Auth_Test(unittest.TestCase):
2525
data_path = join(dirname(__file__), '..', '..', '..', 'data')
2626

27-
def loadSettingsJSON(self):
28-
filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json')
27+
def loadSettingsJSON(self, filename=None):
28+
if filename:
29+
filename = join(dirname(__file__), '..', '..', '..', 'settings', filename)
30+
else:
31+
filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json')
2932
if exists(filename):
3033
stream = open(filename, 'r')
3134
settings = json.load(stream)
3235
stream.close()
3336
return settings
34-
else:
35-
raise Exception('Settings json file does not exist')
3637

3738
def file_contents(self, filename):
3839
f = open(filename, 'r')
@@ -810,14 +811,29 @@ def testLogoutNameID(self):
810811
auth.process_response()
811812

812813
name_id_from_response = auth.get_nameid()
814+
name_id_format_from_response = auth.get_nameid_format()
813815

814816
target_url = auth.logout()
815817
parsed_query = parse_qs(urlparse(target_url)[4])
816818
self.assertIn('SAMLRequest', parsed_query)
817819
logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])
818820

819821
name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request)
822+
name_id_format_from_request = OneLogin_Saml2_Logout_Request.get_nameid_format(logout_request)
820823
self.assertEqual(name_id_from_response, name_id_from_request)
824+
self.assertEqual(name_id_format_from_response, name_id_format_from_request)
825+
826+
new_name_id = "new_name_id"
827+
new_name_id_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
828+
target_url_2 = auth.logout(name_id=new_name_id, name_id_format=new_name_id_format)
829+
parsed_query = parse_qs(urlparse(target_url_2)[4])
830+
self.assertIn('SAMLRequest', parsed_query)
831+
logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])
832+
833+
name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request)
834+
name_id_format_from_request = OneLogin_Saml2_Logout_Request.get_nameid_format(logout_request)
835+
self.assertEqual(new_name_id, name_id_from_request)
836+
self.assertEqual(new_name_id_format, name_id_format_from_request)
821837

822838
def testSetStrict(self):
823839
"""
@@ -840,6 +856,98 @@ def testSetStrict(self):
840856

841857
self.assertRaises(AssertionError, auth.set_strict, '42')
842858

859+
def testIsAuthenticated(self):
860+
"""
861+
Tests the is_authenticated method of the OneLogin_Saml2_Auth
862+
"""
863+
request_data = self.get_request()
864+
del request_data['get_data']
865+
message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
866+
request_data['post_data'] = {
867+
'SAMLResponse': message
868+
}
869+
auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON())
870+
auth.process_response()
871+
self.assertFalse(auth.is_authenticated())
872+
873+
message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
874+
request_data['post_data'] = {
875+
'SAMLResponse': message
876+
}
877+
auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON())
878+
auth.process_response()
879+
self.assertTrue(auth.is_authenticated())
880+
881+
def testGetNameId(self):
882+
"""
883+
Tests the get_nameid method of the OneLogin_Saml2_Auth
884+
"""
885+
settings = self.loadSettingsJSON()
886+
request_data = self.get_request()
887+
del request_data['get_data']
888+
message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
889+
request_data['post_data'] = {
890+
'SAMLResponse': message
891+
}
892+
auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
893+
auth.process_response()
894+
self.assertFalse(auth.is_authenticated())
895+
self.assertEqual(auth.get_nameid(), None)
896+
897+
message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
898+
request_data['post_data'] = {
899+
'SAMLResponse': message
900+
}
901+
auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
902+
auth.process_response()
903+
self.assertTrue(auth.is_authenticated())
904+
self.assertEqual("492882615acf31c8096b627245d76ae53036c090", auth.get_nameid())
905+
906+
settings_2 = self.loadSettingsJSON('settings2.json')
907+
message = self.file_contents(join(self.data_path, 'responses', 'signed_message_encrypted_assertion2.xml.base64'))
908+
request_data['post_data'] = {
909+
'SAMLResponse': message
910+
}
911+
auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_2)
912+
auth.process_response()
913+
self.assertTrue(auth.is_authenticated())
914+
self.assertEqual("25ddd7d34a7d79db69167625cda56a320adf2876", auth.get_nameid())
915+
916+
def testGetNameIdFormat(self):
917+
"""
918+
Tests the get_nameid_format method of the OneLogin_Saml2_Auth
919+
"""
920+
settings = self.loadSettingsJSON()
921+
request_data = self.get_request()
922+
del request_data['get_data']
923+
message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
924+
request_data['post_data'] = {
925+
'SAMLResponse': message
926+
}
927+
auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
928+
auth.process_response()
929+
self.assertFalse(auth.is_authenticated())
930+
self.assertEqual(auth.get_nameid_format(), None)
931+
932+
message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
933+
request_data['post_data'] = {
934+
'SAMLResponse': message
935+
}
936+
auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
937+
auth.process_response()
938+
self.assertTrue(auth.is_authenticated())
939+
self.assertEqual("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", auth.get_nameid_format())
940+
941+
settings_2 = self.loadSettingsJSON('settings2.json')
942+
message = self.file_contents(join(self.data_path, 'responses', 'signed_message_encrypted_assertion2.xml.base64'))
943+
request_data['post_data'] = {
944+
'SAMLResponse': message
945+
}
946+
auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_2)
947+
auth.process_response()
948+
self.assertTrue(auth.is_authenticated())
949+
self.assertEqual("urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", auth.get_nameid_format())
950+
843951
def testBuildRequestSignature(self):
844952
"""
845953
Tests the build_request_signature method of the OneLogin_Saml2_Auth

tests/src/OneLogin/saml2_tests/logout_request_test.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,17 @@ def testGetNameIdData(self):
145145
with self.assertRaisesRegexp(Exception, 'NameID not found in the Logout Request'):
146146
OneLogin_Saml2_Logout_Request.get_nameid(inv_request)
147147

148+
idp_data = settings.get_idp_data()
149+
expected_name_id_data = {
150+
'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress',
151+
'NameQualifier': idp_data['entityId'],
152+
'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69'
153+
}
154+
155+
logout_request = OneLogin_Saml2_Logout_Request(settings, None, expected_name_id_data['Value'], None, idp_data['entityId'], expected_name_id_data['Format'])
156+
name_id_data_3 = OneLogin_Saml2_Logout_Request.get_nameid_data(logout_request.get_xml())
157+
self.assertEqual(expected_name_id_data, name_id_data_3)
158+
148159
def testGetNameId(self):
149160
"""
150161
Tests the get_nameid of the OneLogin_Saml2_LogoutRequest

0 commit comments

Comments
 (0)