Skip to content

Commit

Permalink
Adjusted acs endpoint to extract NameQualifier and SPNameQualifier fr…
Browse files Browse the repository at this point in the history
…om 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
  • Loading branch information
pitbulk committed Jun 27, 2019
1 parent b6741fd commit bd86f1e
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 36 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -822,17 +822,17 @@ target_url = 'https://example.com'
auth.logout(return_to=target_url)
```

Also there are 4 optional parameters that can be set:
Also there are another 5 optional parameters that can be set:

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

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

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:

Expand All @@ -858,7 +858,12 @@ elif 'sso2' in request.args: # Another SSO init action
return_to = '%sattrs/' % request.host_url # but set a custom RelayState URL
return redirect(auth.login(return_to))
elif 'slo' in request.args: # SLO action. Will sent a Logout Request to IdP
return redirect(auth.logout())
nameid = request.session['samlNameId']
nameid_format = request.session['samlNameIdFormat']
nameid_nq = request.session['samlNameIdNameQualifier']
nameid_spnq = request.session['samlNameIdSPNameQualifier']
session_index = request.session['samlSessionIndex']
return redirect(auth.logout(None, nameid, session_index, nameid_nq, nameid_format, nameid_spnq))
elif 'acs' in request.args: # Assertion Consumer Service
auth.process_response() # Process the Response of the IdP
errors = auth.get_errors() # This method receives an array with the errors
Expand All @@ -867,6 +872,11 @@ elif 'acs' in request.args: # Assertion Consumer Service
msg = "Not authenticated" # data retrieved or not (user authenticated)
else:
request.session['samlUserdata'] = auth.get_attributes() # Retrieves user data
request.session['samlNameId'] = auth.get_nameid()
request.session['samlNameIdFormat'] = auth.get_nameid_format()
request.session['samlNameIdNameQualifier'] = auth.get_nameid_nq()
request.session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq()
request.session['samlSessionIndex'] = auth.get_session_index()
self_url = OneLogin_Saml2_Utils.get_self_url(req)
if 'RelayState' in request.form and self_url != request.form['RelayState']:
return redirect(auth.redirect_to(request.form['RelayState'])) # Redirect if there is a relayState
Expand Down
3 changes: 1 addition & 2 deletions demo-django/demo/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []

ALLOWED_HOSTS = ['pitbulk.no-ip.org']

# Application definition

Expand Down
14 changes: 11 additions & 3 deletions demo-django/demo/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,19 @@ def index(request):
return_to = OneLogin_Saml2_Utils.get_self_url(req) + reverse('attrs')
return HttpResponseRedirect(auth.login(return_to))
elif 'slo' in req['get_data']:
name_id = None
session_index = None
name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None
if 'samlNameId' in request.session:
name_id = request.session['samlNameId']
if 'samlSessionIndex' in request.session:
session_index = request.session['samlSessionIndex']
if 'samlNameIdFormat' in request.session:
name_id_format = request.session['samlNameIdFormat']
if 'samlNameIdNameQualifier' in request.session:
name_id_nq = request.session['samlNameIdNameQualifier']
if 'samlNameIdSPNameQualifier' in request.session:
name_id_spnq = request.session['samlNameIdSPNameQualifier']

return HttpResponseRedirect(auth.logout(name_id=name_id, session_index=session_index))
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))

# If LogoutRequest ID need to be stored in order to later validate it, do instead
# slo_built_url = auth.logout(name_id=name_id, session_index=session_index)
Expand All @@ -77,6 +82,9 @@ def index(request):
del request.session['AuthNRequestID']
request.session['samlUserdata'] = auth.get_attributes()
request.session['samlNameId'] = auth.get_nameid()
request.session['samlNameIdFormat'] = auth.get_nameid_format()
request.session['samlNameIdNameQualifier'] = auth.get_nameid_nq()
request.session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq()
request.session['samlSessionIndex'] = auth.get_session_index()
if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']:
return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState']))
Expand Down
20 changes: 10 additions & 10 deletions demo-django/saml/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,29 @@
"strict": true,
"debug": true,
"sp": {
"entityId": "https://<sp_domain>/metadata/",
"entityId": "http://pitbulk.no-ip.org:8000/metadata/",
"assertionConsumerService": {
"url": "https://<sp_domain>/?acs",
"url": "http://pitbulk.no-ip.org:8000/?acs",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
"singleLogoutService": {
"url": "https://<sp_domain>/?sls",
"url": "http://pitbulk.no-ip.org:8000/?sls",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
"x509cert": "",
"privateKey": ""
"x509cert": "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m",
"privateKey": "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAECgYEA0wDXZPS9hKqMTNh+nnfONioXBjhA6fQ7GVtWKDxa3ofMoPyt7ejGL/Hnvcv13Vn02UAsFx1bKrCstDqVtYwrWrnmywXyH+o9paJnTmd+cRIjWU8mRvCrxzH5I/Bcvbp1qZoASuqZEaGwNjM6JpW2o3QTmHGMALcLUPfEvhApssECQQDy2e65E86HcFhi/Ta8TQ0odDCNbiWA0bI1Iu8B7z+NAy1D1+WnCd7w2u9U6CF/k2nFHCsvxEoeANM0z7h5T/XvAkEA8e4JqKmDrfdiakQT7nf9svU2jXZtxSbPiIRMafNikDvzZ1vJCZkvdmaWYL70GlDZIwc9ad67rHZ/n/fqX1d0MQJAbRpRsJ5gY+KqItbFt3UaWzlP8sowWR5cZJjsLb9RmsV5mYguKYw6t5R0f33GRu1wUFimYlBaR/5w5MIJi57LywJATO1a5uWX+G5MPewNxmsjIY91XEAHIYR4wzkGLz5z3dciS4BVCZdLD0QJlxPA/MkuckPwFET9uhYn+M7VGKHvUQJBANSDwsY+BdCGpi/WRV37HUfwLl07damaFbW3h08PQx8G8SuF7DpN+FPBcI6VhzrIWNRBxWprkgeGioKNfFWzSaM="
},
"idp": {
"entityId": "https://app.onelogin.com/saml/metadata/<onelogin_connector_id>",
"entityId": "https://app.onelogin.com/saml/metadata/3dbd155e-be64-4a4d-8fab-e44788bce74f",
"singleSignOnService": {
"url": "https://app.onelogin.com/trust/saml2/http-post/sso/<onelogin_connector_id>",
"url": "https://sgarcia-us-preprod.onelogin.com/trust/saml2/http-redirect/sso/850162",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"singleLogoutService": {
"url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/<onelogin_connector_id>",
"url": "https://sgarcia-us-preprod.onelogin.com/trust/saml2/http-redirect/slo/850162",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"x509cert": "<onelogin_connector_cert>"
"x509cert": "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw=="
}
}
}
38 changes: 31 additions & 7 deletions demo-flask/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,32 +47,56 @@ def index():

if 'sso' in request.args:
return redirect(auth.login())
# If AuthNRequest ID need to be stored in order to later validate it, do instead
# sso_built_url = auth.login()
# request.session['AuthNRequestID'] = auth.get_last_request_id()
# return redirect(sso_built_url)
elif 'sso2' in request.args:
return_to = '%sattrs/' % request.host_url
return redirect(auth.login(return_to))
elif 'slo' in request.args:
name_id = None
session_index = None
name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None
if 'samlNameId' in session:
name_id = session['samlNameId']
if 'samlSessionIndex' in session:
session_index = session['samlSessionIndex']

return redirect(auth.logout(name_id=name_id, session_index=session_index))
if 'samlNameIdFormat' in session:
name_id_format = session['samlNameIdFormat']
if 'samlNameIdNameQualifier' in session:
name_id_nq = session['samlNameIdNameQualifier']
if 'samlNameIdSPNameQualifier' in session:
name_id_spnq = session['samlNameIdSPNameQualifier']

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))
# If LogoutRequest ID need to be stored in order to later validate it, do instead
# slo_built_url = auth.logout(name_id=name_id, session_index=session_index)
# session['LogoutRequestID'] = auth.get_last_request_id()
#return redirect(slo_built_url)
elif 'acs' in request.args:
auth.process_response()
request_id = None
if 'AuthNRequestID' in session:
request_id = session['AuthNRequestID']

auth.process_response(request_id=request_id)
errors = auth.get_errors()
not_auth_warn = not auth.is_authenticated()
if len(errors) == 0:
if 'AuthNRequestID' in session:
del session['AuthNRequestID']
session['samlUserdata'] = auth.get_attributes()
session['samlNameId'] = auth.get_nameid()
session['samlNameIdFormat'] = auth.get_nameid_format()
session['samlNameIdNameQualifier'] = auth.get_nameid_nq()
session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq()
session['samlSessionIndex'] = auth.get_session_index()
self_url = OneLogin_Saml2_Utils.get_self_url(req)
if 'RelayState' in request.form and self_url != request.form['RelayState']:
return redirect(auth.redirect_to(request.form['RelayState']))
elif 'sls' in request.args:
request_id = None
if 'LogoutRequestID' in session:
request_id = session['LogoutRequestID']
dscb = lambda: session.clear()
url = auth.process_slo(delete_session_cb=dscb)
url = auth.process_slo(request_id=request_id, delete_session_cb=dscb)
errors = auth.get_errors()
if len(errors) == 0:
if url is not None:
Expand Down
30 changes: 28 additions & 2 deletions src/onelogin/saml2/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
self.__attributes = []
self.__nameid = None
self.__nameid_format = None
self.__nameid_nq = None
self.__nameid_spnq = None
self.__session_index = None
self.__session_expiration = None
self.__authenticated = False
Expand Down Expand Up @@ -104,6 +106,8 @@ def process_response(self, request_id=None):
self.__attributes = response.get_attributes()
self.__nameid = response.get_nameid()
self.__nameid_format = response.get_nameid_format()
self.__nameid_nq = response.get_nameid_nq()
self.__nameid_spnq = response.get_nameid_spnq()
self.__session_index = response.get_session_index()
self.__session_expiration = response.get_session_not_on_or_after()
self.__last_message_id = response.get_id()
Expand Down Expand Up @@ -245,6 +249,24 @@ def get_nameid_format(self):
"""
return self.__nameid_format

def get_nameid_nq(self):
"""
Returns the nameID NameQualifier of the Assertion.
:returns: NameID NameQualifier
:rtype: string|None
"""
return self.__nameid_nq

def get_nameid_spnq(self):
"""
Returns the nameID SP NameQualifier of the Assertion.
:returns: NameID SP NameQualifier
:rtype: string|None
"""
return self.__nameid_spnq

def get_session_index(self):
"""
Returns the SessionIndex from the AuthnStatement.
Expand Down Expand Up @@ -362,7 +384,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_
parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm'])
return self.redirect_to(self.get_sso_url(), parameters)

def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None):
def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None):
"""
Initiates the SLO process.
Expand All @@ -381,6 +403,9 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name
:param name_id_format: The NameID Format that will be set in the LogoutRequest.
:type: string
:param spnq: SP Name Qualifier
:type: string
:returns: Redirection url
"""
slo_url = self.get_slo_url()
Expand All @@ -400,7 +425,8 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name
name_id=name_id,
session_index=session_index,
nq=nq,
name_id_format=name_id_format
name_id_format=name_id_format,
spnq=spnq
)
self.__last_request = logout_request.get_xml()
self.__last_request_id = logout_request.id
Expand Down
24 changes: 16 additions & 8 deletions src/onelogin/saml2/logout_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class OneLogin_Saml2_Logout_Request(object):
"""

def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None):
def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None):
"""
Constructs the Logout Request object.
Expand All @@ -50,6 +50,10 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
:param name_id_format: The NameID Format that will be set in the LogoutRequest.
:type: string
:param spnq: SP Name Qualifier
:type: string
"""
self.__settings = settings
self.__error = None
Expand Down Expand Up @@ -79,19 +83,23 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
if not name_id_format and sp_data['NameIDFormat'] != OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED:
name_id_format = sp_data['NameIDFormat']
else:
name_id = idp_data['entityId']
name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY

spNameQualifier = None
if name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY:
name_id = idp_data['entityId']
# From saml-core-2.0-os 8.3.6, when the entity Format is used:
# "The NameQualifier, SPNameQualifier, and SPProvidedID attributes
# MUST be omitted.
if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY:
nq = None
elif nq is not None:
# We only gonna include SPNameQualifier if NameQualifier is provided
spNameQualifier = sp_data['entityId']
spnq = None

# NameID Format UNSPECIFIED omitted
if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED:
name_id_format = None

name_id_obj = OneLogin_Saml2_Utils.generate_name_id(
name_id,
spNameQualifier,
spnq,
name_id_format,
cert,
False,
Expand Down
26 changes: 26 additions & 0 deletions src/onelogin/saml2/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,32 @@ def get_nameid_format(self):
nameid_format = nameid_data['Format']
return nameid_format

def get_nameid_nq(self):
"""
Gets the NameID NameQualifier provided by the SAML Response from the IdP
:returns: NameID NameQualifier
:rtype: string|None
"""
nameid_nq = None
nameid_data = self.get_nameid_data()
if nameid_data and 'NameQualifier' in nameid_data.keys():
nameid_nq = nameid_data['NameQualifier']
return nameid_nq

def get_nameid_spnq(self):
"""
Gets the NameID SP NameQualifier provided by the SAML response from the IdP.
:returns: NameID SP NameQualifier
:rtype: string|None
"""
nameid_spnq = None
nameid_data = self.get_nameid_data()
if nameid_data and 'SPNameQualifier' in nameid_data.keys():
nameid_spnq = nameid_data['SPNameQualifier']
return nameid_spnq

def get_session_not_on_or_after(self):
"""
Gets the SessionNotOnOrAfter from the AuthnStatement
Expand Down
Loading

0 comments on commit bd86f1e

Please sign in to comment.