From 5946ea6398b45ae87cf8ab3a3ff3530baed950e3 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Tue, 2 Apr 2019 13:12:22 +0200 Subject: [PATCH] Add support for Subjects on AuthNRequests by the new name_id_value_req parameter --- README.md | 20 ++++---- src/onelogin/saml2/auth.py | 7 ++- src/onelogin/saml2/authn_request.py | 14 +++++- src/onelogin/saml2/xml_templates.py | 2 +- tests/src/OneLogin/saml2_tests/auth_test.py | 46 +++++++++++++++++-- .../saml2_tests/authn_request_test.py | 31 +++++++++++++ 6 files changed, 103 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 2b25f961..404712e2 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ If our environment requires sign or encrypt support, the certs folder may contai * sp.crt The public cert of the SP * sp.key The private key of the SP -Or also we can provide those data in the setting file at the ``X.509cert`` and the ``privateKey`` JSON parameters of the ``sp`` element. +Or also we can provide those data in the setting file at the ``x509cert`` and the ``privateKey`` JSON parameters of the ``sp`` element. Sometimes we could need a signature on the metadata published by the SP, in this case we could use the X.509 cert previously mentioned or use a new X.509 cert: ``metadata.crt`` and ``metadata.key``. @@ -161,7 +161,7 @@ publish that X.509 certificate on Service Provider metadata. If you want to create self-signed certs, you can do it at the https://www.samltool.com/self_signed_certs.php service, or using the command: ```bash -openssl req -new -X.509 -days 3652 -nodes -out sp.crt -keyout saml.key +openssl req -new -x509 -days 3652 -nodes -out sp.crt -keyout saml.key ``` #### demo-flask #### @@ -264,7 +264,7 @@ This is the ``settings.json`` file: // represent the requested subject. // Take a look on src/onelogin/saml2/constants.py to see the NameIdFormat that are supported. "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", - // Usually X.509cert and privateKey of the SP are provided by files placed at + // Usually X.509 cert and privateKey of the SP are provided by files placed at // the certs folder. But we can also provide them with the following parameters "x509cert": "", "privateKey": "" @@ -310,7 +310,7 @@ This is the ``settings.json`` file: * But take in mind that the fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass, * that why we don't recommend it use for production environments. * - * (openssl X.509 -noout -fingerprint -in "idp.crt" to generate it, + * (openssl x509 -noout -fingerprint -in "idp.crt" to generate it, * or add for example the -sha256 , -sha384 or -sha512 parameter) * * If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to @@ -343,7 +343,7 @@ This is the ``settings.json`` file: } ``` -In addition to the required settings data (IdP, SP), extra settings can be defined in `advanced_settings.json`: +In addition to the required settings data (idp, sp), extra settings can be defined in `advanced_settings.json`: ```javascript { @@ -865,7 +865,7 @@ else: ### SP Key rollover ### -If you plan to update the SP ``X.509cert`` and ``privateKey`` you can define the new ``X.509cert`` as ``settings['sp']['X.509certNew']`` and it will be +If you plan to update the SP ``x509cert`` and ``privateKey`` you can define the new ``x509cert`` as ``settings['sp']['x509certNew']`` and it will be published on the SP metadata so Identity Providers can read them and get ready for rollover. @@ -874,11 +874,11 @@ published on the SP metadata so Identity Providers can read them and get ready f In some scenarios the IdP uses different certificates for signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata. -In order to handle that the toolkit offers the ``settings['idp']['X.509certMulti']`` parameter. +In order to handle that the toolkit offers the ``settings['idp']['x509certMulti']`` parameter. -When that parameter is used, ``X.509cert`` and ``certFingerprint`` values will be ignored by the toolkit. +When that parameter is used, ``x509cert`` and ``certFingerprint`` values will be ignored by the toolkit. -The ``X.509certMulti`` is an array with 2 keys: +The ``x509certMulti`` is an array with 2 keys: - ``signing``: An array of certs that will be used to validate IdP signature - ``encryption``: An array with one unique cert that will be used to encrypt data to be sent to the IdP. @@ -1026,7 +1026,7 @@ A class that contains functionality related to the metadata of the SP * ***builder*** Generates the metadata of the SP based on the settings. * ***sign_metadata*** Signs the metadata with the key/cert provided. -* ***add_X.509_key_descriptors*** Adds the X.509 descriptors (sign/encryption) to the metadata +* ***add_x509_key_descriptors*** Adds the X.509 descriptors (sign/encryption) to the metadata #### OneLogin_Saml2_Utils - utils.py #### diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py index 106aabd8..5d6aa84f 100644 --- a/src/onelogin/saml2/auth.py +++ b/src/onelogin/saml2/auth.py @@ -327,7 +327,7 @@ def get_last_authn_contexts(self): """ return self.__last_authn_contexts - def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True): + def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True, name_id_value_req=None): """ Initiates the SSO process. @@ -343,10 +343,13 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_ :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element. :type set_nameid_policy: bool + :param name_id_value_req: Optional argument. Indicates to the IdP the subject that should be authenticated + :type name_id_value_req: string + :returns: Redirection URL :rtype: string """ - authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy) + authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy, name_id_value_req) self.__last_request = authn_request.get_xml() self.__last_request_id = authn_request.get_id() diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py index 2a1eda88..57da1561 100644 --- a/src/onelogin/saml2/authn_request.py +++ b/src/onelogin/saml2/authn_request.py @@ -22,7 +22,7 @@ class OneLogin_Saml2_Authn_Request(object): """ - def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_policy=True): + def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_policy=True, name_id_value_req=None): """ Constructs the AuthnRequest object. @@ -37,6 +37,9 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element. :type set_nameid_policy: bool + + :param name_id_value_req: Optional argument. Indicates to the IdP the subject that should be authenticated + :type name_id_value_req: string """ self.__settings = settings @@ -71,6 +74,14 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol if is_passive is True: is_passive_str = "\n" + ' IsPassive="true"' + subject_str = '' + if name_id_value_req: + subject_str = """ + + %s + + """ % (sp_data['NameIDFormat'], name_id_value_req) + nameid_policy_str = '' if set_nameid_policy: name_id_policy_format = sp_data['NameIDFormat'] @@ -112,6 +123,7 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol 'destination': destination, 'assertion_url': sp_data['assertionConsumerService']['url'], 'entity_id': sp_data['entityId'], + 'subject_str': subject_str, 'nameid_policy_str': nameid_policy_str, 'requested_authn_context_str': requested_authn_context_str, 'attr_consuming_service_str': attr_consuming_service_str, diff --git a/src/onelogin/saml2/xml_templates.py b/src/onelogin/saml2/xml_templates.py index 99025546..ec4f6260 100644 --- a/src/onelogin/saml2/xml_templates.py +++ b/src/onelogin/saml2/xml_templates.py @@ -29,7 +29,7 @@ class OneLogin_Saml2_Templates(object): Destination="%(destination)s" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="%(assertion_url)s"%(attr_consuming_service_str)s> - %(entity_id)s%(nameid_policy_str)s + %(entity_id)s%(subject_str)s%(nameid_policy_str)s %(requested_authn_context_str)s """ diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py index 2a2b557c..02193ee2 100644 --- a/tests/src/OneLogin/saml2_tests/auth_test.py +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -609,7 +609,7 @@ def testLoginSigned(self): def testLoginForceAuthN(self): """ Tests the login method of the OneLogin_Saml2_Auth class - Case Logout with no parameters. A AuthN Request is built with ForceAuthn and redirect executed + Case AuthN Request is built with ForceAuthn and redirect executed """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' @@ -642,7 +642,7 @@ def testLoginForceAuthN(self): def testLoginIsPassive(self): """ Tests the login method of the OneLogin_Saml2_Auth class - Case Logout with no parameters. A AuthN Request is built with IsPassive and redirect executed + Case AuthN Request is built with IsPassive and redirect executed """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' @@ -676,7 +676,7 @@ def testLoginIsPassive(self): def testLoginSetNameIDPolicy(self): """ Tests the login method of the OneLogin_Saml2_Auth class - Case Logout with no parameters. A AuthN Request is built with and without NameIDPolicy + Case AuthN Request is built with and without NameIDPolicy """ settings_info = self.loadSettingsJSON() return_to = u'http://example.com/returnto' @@ -707,6 +707,46 @@ def testLoginSetNameIDPolicy(self): request_3 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])) self.assertNotIn('', request) + self.assertNotIn('', request_2) + self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">testuser@example.com', request_2) + self.assertIn('', request_2) + + settings_info['sp']['NameIDFormat'] = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' + auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url_3 = auth_3.login(return_to, name_id_value_req='testuser@example.com') + parsed_query_3 = parse_qs(urlparse(target_url_3)[4]) + self.assertIn(sso_url, target_url_3) + self.assertIn('SAMLRequest', parsed_query_3) + request_3 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])) + self.assertIn('', request_3) + self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">testuser@example.com', request_3) + self.assertIn('', request_3) + def testLogout(self): """ Tests the logout method of the OneLogin_Saml2_Auth class diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py index 71c99539..3f1262f2 100644 --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -257,6 +257,37 @@ def testCreateRequestSetNameIDPolicy(self): self.assertRegex(inflated_3, '^', inflated) + + authn_request_2 = OneLogin_Saml2_Authn_Request(settings, name_id_value_req='testuser@example.com') + authn_request_encoded_2 = authn_request_2.get_request() + inflated_2 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded_2)) + self.assertRegex(inflated_2, '^', inflated_2) + self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">testuser@example.com', inflated_2) + self.assertIn('', inflated_2) + + saml_settings['sp']['NameIDFormat'] = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' + settings = OneLogin_Saml2_Settings(saml_settings) + authn_request_3 = OneLogin_Saml2_Authn_Request(settings, name_id_value_req='testuser@example.com') + authn_request_encoded_3 = authn_request_3.get_request() + inflated_3 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded_3)) + self.assertRegex(inflated_3, '^', inflated_3) + self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">testuser@example.com', inflated_3) + self.assertIn('', inflated_3) + def testCreateDeflatedSAMLRequestURLParameter(self): """ Tests the OneLogin_Saml2_Authn_Request Constructor.