Skip to content

Commit

Permalink
Add support for Subjects on AuthNRequests by the new name_id_value_re…
Browse files Browse the repository at this point in the history
…q parameter
  • Loading branch information
pitbulk committed Apr 2, 2019
1 parent 02452df commit 5946ea6
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 17 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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``.

Expand All @@ -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 ####
Expand Down Expand Up @@ -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": ""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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.


Expand All @@ -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.

Expand Down Expand Up @@ -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 ####

Expand Down
7 changes: 5 additions & 2 deletions src/onelogin/saml2/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()

Expand Down
14 changes: 13 additions & 1 deletion src/onelogin/saml2/authn_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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 = """
<saml:Subject>
<saml:NameID Format="%s">%s</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"></saml:SubjectConfirmation>
</saml:Subject>""" % (sp_data['NameIDFormat'], name_id_value_req)

nameid_policy_str = ''
if set_nameid_policy:
name_id_policy_format = sp_data['NameIDFormat']
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/onelogin/saml2/xml_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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>
<saml:Issuer>%(entity_id)s</saml:Issuer>%(nameid_policy_str)s
<saml:Issuer>%(entity_id)s</saml:Issuer>%(subject_str)s%(nameid_policy_str)s
%(requested_authn_context_str)s
</samlp:AuthnRequest>"""

Expand Down
46 changes: 43 additions & 3 deletions tests/src/OneLogin/saml2_tests/auth_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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('<samlp:NameIDPolicy', request_3)

def testLoginWithSubject(self):
"""
Tests the login method of the OneLogin_Saml2_Auth class
Case AuthN Request is built with and without Subject
"""
settings_info = self.loadSettingsJSON()
return_to = u'http://example.com/returnto'
sso_url = settings_info['idp']['singleSignOnService']['url']

auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info)
target_url = auth.login(return_to)
parsed_query = parse_qs(urlparse(target_url)[4])
self.assertIn(sso_url, target_url)
self.assertIn('SAMLRequest', parsed_query)
request = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0]))
self.assertNotIn('<saml:Subject>', request)
self.assertNotIn('<saml:NameID', request)
self.assertNotIn('<saml:saml:SubjectConfirmation', request)

auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info)
target_url_2 = auth_2.login(return_to, name_id_value_req='testuser@example.com')
parsed_query_2 = parse_qs(urlparse(target_url_2)[4])
self.assertIn(sso_url, target_url_2)
self.assertIn('SAMLRequest', parsed_query_2)
request_2 = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0]))
self.assertIn('<saml:Subject>', request_2)
self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">testuser@example.com</saml:NameID>', request_2)
self.assertIn('<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">', 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('<saml:Subject>', request_3)
self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">testuser@example.com</saml:NameID>', request_3)
self.assertIn('<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">', request_3)

def testLogout(self):
"""
Tests the logout method of the OneLogin_Saml2_Auth class
Expand Down
31 changes: 31 additions & 0 deletions tests/src/OneLogin/saml2_tests/authn_request_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,37 @@ def testCreateRequestSetNameIDPolicy(self):
self.assertRegex(inflated_3, '^<samlp:AuthnRequest')
self.assertNotIn('<samlp:NameIDPolicy', inflated_3)

def testCreateRequestSubject(self):
"""
Tests the OneLogin_Saml2_Authn_Request Constructor.
The creation of a deflated SAML Request with and without Subject
"""
saml_settings = self.loadSettingsJSON()
settings = OneLogin_Saml2_Settings(saml_settings)
authn_request = OneLogin_Saml2_Authn_Request(settings)
authn_request_encoded = authn_request.get_request()
inflated = compat.to_string(OneLogin_Saml2_Utils.decode_base64_and_inflate(authn_request_encoded))
self.assertRegex(inflated, '^<samlp:AuthnRequest')
self.assertNotIn('<saml:Subject>', 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, '^<samlp:AuthnRequest')
self.assertIn('<saml:Subject>', inflated_2)
self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">testuser@example.com</saml:NameID>', inflated_2)
self.assertIn('<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">', 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, '^<samlp:AuthnRequest')
self.assertIn('<saml:Subject>', inflated_3)
self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">testuser@example.com</saml:NameID>', inflated_3)
self.assertIn('<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">', inflated_3)

def testCreateDeflatedSAMLRequestURLParameter(self):
"""
Tests the OneLogin_Saml2_Authn_Request Constructor.
Expand Down

0 comments on commit 5946ea6

Please sign in to comment.