Skip to content

Commit ae90bac

Browse files
committed
Adjusted acs endpoint to extract NameQualifier and SPNameQualifier from SAMLResponse. Adjusted single logout service to provide NameQualifier and SPNameQualifier to logout method. Add getNameIdNameQualifier to Auth and SamlResponse. Extend logout method from Auth and LogoutRequest constructor to support SPNameQualifier parameter. Align LogoutRequest constructor with SAML specs
1 parent ec9c986 commit ae90bac

File tree

10 files changed

+242
-23
lines changed

10 files changed

+242
-23
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -794,17 +794,17 @@ target_url = 'https://example.com'
794794
auth.logout(return_to=target_url)
795795
```
796796

797-
Also there are 4 optional parameters that can be set:
797+
Also there are another 5 optional parameters that can be set:
798798

799799
* ``name_id``: That will be used to build the ``LogoutRequest``. If no ``name_id`` parameter is set and the auth object processed a
800800
SAML Response with a ``NameId``, then this ``NameId`` will be used.
801801
* ``session_index``: ``SessionIndex`` that identifies the session of the user.
802802
* ``nq``: IDP Name Qualifier.
803803
* ``name_id_format``: The ``NameID`` Format that will be set in the ``LogoutRequest``.
804+
* ``spnq``: The ``NameID SP NameQualifier`` will be set in the ``LogoutRequest``.
804805

805806
If no ``name_id`` is provided, the ``LogoutRequest`` will contain a ``NameID`` with the entity Format.
806807
If ``name_id`` is provided and no ``name_id_format`` is provided, the ``NameIDFormat`` of the settings will be used.
807-
If ``nq`` is provided, the ``SPNameQualifier`` will be also attached to the ``NameId``.
808808

809809
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:
810810

@@ -830,7 +830,12 @@ elif 'sso2' in request.args: # Another SSO init action
830830
return_to = '%sattrs/' % request.host_url # but set a custom RelayState URL
831831
return redirect(auth.login(return_to))
832832
elif 'slo' in request.args: # SLO action. Will sent a Logout Request to IdP
833-
return redirect(auth.logout())
833+
nameid = request.session['samlNameId']
834+
nameid_format = request.session['samlNameIdFormat']
835+
nameid_nq = request.session['samlNameIdNameQualifier']
836+
nameid_spnq = request.session['samlNameIdSPNameQualifier']
837+
session_index = request.session['samlSessionIndex']
838+
return redirect(auth.logout(None, nameid, session_index, nameid_nq, nameid_format, nameid_spnq))
834839
elif 'acs' in request.args: # Assertion Consumer Service
835840
auth.process_response() # Process the Response of the IdP
836841
errors = auth.get_errors() # This method receives an array with the errors
@@ -839,6 +844,11 @@ elif 'acs' in request.args: # Assertion Consumer Service
839844
msg = "Not authenticated" # data retrieved or not (user authenticated)
840845
else:
841846
request.session['samlUserdata'] = auth.get_attributes() # Retrieves user data
847+
request.session['samlNameId'] = auth.get_nameid()
848+
request.session['samlNameIdFormat'] = auth.get_nameid_format()
849+
request.session['samlNameIdNameQualifier'] = auth.get_nameid_nq()
850+
request.session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq()
851+
request.session['samlSessionIndex'] = auth.get_session_index()
842852
self_url = OneLogin_Saml2_Utils.get_self_url(req)
843853
if 'RelayState' in request.form and self_url != request.form['RelayState']:
844854
return redirect(auth.redirect_to(request.form['RelayState'])) # Redirect if there is a relayState

demo-django/demo/views.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,19 @@ def index(request):
4545
return_to = OneLogin_Saml2_Utils.get_self_url(req) + reverse('attrs')
4646
return HttpResponseRedirect(auth.login(return_to))
4747
elif 'slo' in req['get_data']:
48-
name_id = None
49-
session_index = None
48+
name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None
5049
if 'samlNameId' in request.session:
5150
name_id = request.session['samlNameId']
5251
if 'samlSessionIndex' in request.session:
5352
session_index = request.session['samlSessionIndex']
54-
55-
return HttpResponseRedirect(auth.logout(name_id=name_id, session_index=session_index))
53+
if 'samlNameIdFormat' in request.session:
54+
name_id_format = request.session['samlNameIdFormat']
55+
if 'samlNameIdNameQualifier' in request.session:
56+
name_id_nq = request.session['samlNameIdNameQualifier']
57+
if 'samlNameIdSPNameQualifier' in request.session:
58+
name_id_spnq = request.session['samlNameIdSPNameQualifier']
59+
60+
return HttpResponseRedirect(auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq, name_id_format=name_id_format, spnq=name_id_spnq))
5661
elif 'acs' in req['get_data']:
5762
auth.process_response()
5863
errors = auth.get_errors()
@@ -61,6 +66,9 @@ def index(request):
6166
if not errors:
6267
request.session['samlUserdata'] = auth.get_attributes()
6368
request.session['samlNameId'] = auth.get_nameid()
69+
request.session['samlNameIdFormat'] = auth.get_nameid_format()
70+
request.session['samlNameIdNameQualifier'] = auth.get_nameid_nq()
71+
request.session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq()
6472
request.session['samlSessionIndex'] = auth.get_session_index()
6573
if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']:
6674
return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState']))

demo-flask/index.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,28 @@ def index():
5050
return_to = '%sattrs/' % request.host_url
5151
return redirect(auth.login(return_to))
5252
elif 'slo' in request.args:
53-
name_id = None
54-
session_index = None
53+
name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None
5554
if 'samlNameId' in session:
5655
name_id = session['samlNameId']
5756
if 'samlSessionIndex' in session:
5857
session_index = session['samlSessionIndex']
59-
60-
return redirect(auth.logout(name_id=name_id, session_index=session_index))
58+
if 'samlNameIdFormat' in session:
59+
name_id_format = session['samlNameIdFormat']
60+
if 'samlNameIdNameQualifier' in session:
61+
name_id_nq = session['samlNameIdNameQualifier']
62+
if 'samlNameIdSPNameQualifier' in session:
63+
name_id_spnq = session['samlNameIdSPNameQualifier']
64+
65+
return redirect(auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq, name_id_format=name_id_format, spnq=name_id_spnq))
6166
elif 'acs' in request.args:
6267
auth.process_response()
6368
errors = auth.get_errors()
6469
not_auth_warn = not auth.is_authenticated()
6570
if len(errors) == 0:
66-
session['samlUserdata'] = auth.get_attributes()
6771
session['samlNameId'] = auth.get_nameid()
72+
session['samlNameIdFormat'] = auth.get_nameid_format()
73+
session['samlNameIdNameQualifier'] = auth.get_nameid_nq()
74+
session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq()
6875
session['samlSessionIndex'] = auth.get_session_index()
6976
self_url = OneLogin_Saml2_Utils.get_self_url(req)
7077
if 'RelayState' in request.form and self_url != request.form['RelayState']:

src/onelogin/saml2/auth.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
5555
self.__attributes = dict()
5656
self.__nameid = None
5757
self.__nameid_format = None
58+
self.__nameid_nq = None
59+
self.__nameid_spnq = None
5860
self.__session_index = None
5961
self.__session_expiration = None
6062
self.__authenticated = False
@@ -107,6 +109,8 @@ def process_response(self, request_id=None):
107109
self.__attributes = response.get_attributes()
108110
self.__nameid = response.get_nameid()
109111
self.__nameid_format = response.get_nameid_format()
112+
self.__nameid_nq = response.get_nameid_nq()
113+
self.__nameid_spnq = response.get_nameid_spnq()
110114
self.__session_index = response.get_session_index()
111115
self.__session_expiration = response.get_session_not_on_or_after()
112116
self.__last_message_id = response.get_id()
@@ -245,6 +249,24 @@ def get_nameid_format(self):
245249
"""
246250
return self.__nameid_format
247251

252+
def get_nameid_nq(self):
253+
"""
254+
Returns the nameID NameQualifier of the Assertion.
255+
256+
:returns: NameID NameQualifier
257+
:rtype: string|None
258+
"""
259+
return self.__nameid_nq
260+
261+
def get_nameid_spnq(self):
262+
"""
263+
Returns the nameID SP NameQualifier of the Assertion.
264+
265+
:returns: NameID SP NameQualifier
266+
:rtype: string|None
267+
"""
268+
return self.__nameid_spnq
269+
248270
def get_session_index(self):
249271
"""
250272
Returns the SessionIndex from the AuthnStatement.
@@ -366,7 +388,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_
366388
self.add_request_signature(parameters, security['signatureAlgorithm'])
367389
return self.redirect_to(self.get_sso_url(), parameters)
368390

369-
def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None):
391+
def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None):
370392
"""
371393
Initiates the SLO process.
372394
@@ -385,6 +407,9 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name
385407
:param name_id_format: The NameID Format that will be set in the LogoutRequest.
386408
:type: string
387409
410+
:param spnq: SP Name Qualifier
411+
:type: string
412+
388413
:returns: Redirection URL
389414
"""
390415
slo_url = self.get_slo_url()
@@ -405,7 +430,8 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name
405430
name_id=name_id,
406431
session_index=session_index,
407432
nq=nq,
408-
name_id_format=name_id_format
433+
name_id_format=name_id_format,
434+
spnq=spnq
409435
)
410436
self.__last_request = logout_request.get_xml()
411437
self.__last_request_id = logout_request.id

src/onelogin/saml2/logout_request.py

Lines changed: 15 additions & 8 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, name_id_format=None):
28+
def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None):
2929
"""
3030
Constructs the Logout Request object.
3131
@@ -46,6 +46,9 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
4646
4747
:param name_id_format: The NameID Format that will be set in the LogoutRequest.
4848
:type: string
49+
50+
:param spnq: SP Name Qualifier
51+
:type: string
4952
"""
5053
self.__settings = settings
5154
self.__error = None
@@ -75,19 +78,23 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
7578
if not name_id_format and sp_data['NameIDFormat'] != OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED:
7679
name_id_format = sp_data['NameIDFormat']
7780
else:
81+
name_id = idp_data['entityId']
7882
name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY
7983

80-
sp_name_qualifier = None
81-
if name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY:
82-
name_id = idp_data['entityId']
84+
# From saml-core-2.0-os 8.3.6, when the entity Format is used:
85+
# "The NameQualifier, SPNameQualifier, and SPProvidedID attributes
86+
# MUST be omitted.
87+
if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY:
8388
nq = None
84-
elif nq is not None:
85-
# We only gonna include SPNameQualifier if NameQualifier is provided
86-
sp_name_qualifier = sp_data['entityId']
89+
spnq = None
90+
91+
# NameID Format UNSPECIFIED omitted
92+
if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED:
93+
name_id_format = None
8794

8895
name_id_obj = OneLogin_Saml2_Utils.generate_name_id(
8996
name_id,
90-
sp_name_qualifier,
97+
spnq,
9198
name_id_format,
9299
cert,
93100
False,

src/onelogin/saml2/response.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,32 @@ def get_nameid_format(self):
506506
nameid_format = nameid_data['Format']
507507
return nameid_format
508508

509+
def get_nameid_nq(self):
510+
"""
511+
Gets the NameID NameQualifier provided by the SAML Response from the IdP
512+
513+
:returns: NameID NameQualifier
514+
:rtype: string|None
515+
"""
516+
nameid_nq = None
517+
nameid_data = self.get_nameid_data()
518+
if nameid_data and 'NameQualifier' in nameid_data.keys():
519+
nameid_nq = nameid_data['NameQualifier']
520+
return nameid_nq
521+
522+
def get_nameid_spnq(self):
523+
"""
524+
Gets the NameID SP NameQualifier provided by the SAML response from the IdP.
525+
526+
:returns: NameID SP NameQualifier
527+
:rtype: string|None
528+
"""
529+
nameid_spnq = None
530+
nameid_data = self.get_nameid_data()
531+
if nameid_data and 'SPNameQualifier' in nameid_data.keys():
532+
nameid_spnq = nameid_data['SPNameQualifier']
533+
return nameid_spnq
534+
509535
def get_session_not_on_or_after(self):
510536
"""
511537
Gets the SessionNotOnOrAfter from the AuthnStatement

0 commit comments

Comments
 (0)